DevExtreme v23.2 is now available.

Explore our newest features/capabilities and share your thoughts with us.

Your search did not match any results.

SignalR Service

This example demonstrates a real-time data update in a financial candlestick chart bound to a SignalR server. Note that data used in this demo is for demonstration purposes only.

To integrate the Chart with a SignalR server, specify a CustomStore. Use the CustomStore's push(changes) method to insert, update, and remove data objects. This method accepts an array and allows you to update data in batches.

To display updated data in real time, use the aggregation configuration object. In this object, set the enabled property to true, the method property to custom, and then implement the calculate function to process the incoming data. In this demo, the calculate function aggregates data into a one point for each interval.

You can also use the contentTemplate function to update the tooltip content with incoming data.

Backend API
<div ng-if="connectionStarted"> <dx-chart id="chart" [dataSource]="dataSource" title="Stock Price" [customizePoint]="customizePoint" > <dxi-series pane="Price" argumentField="date" type="candlestick"> <dxo-aggregation [enabled]="true" method="custom" [calculate]="calculateCandle" > </dxo-aggregation> </dxi-series> <dxi-series pane="Volume" name="Volume" argumentField="date" valueField="volume" color="red" type="bar" > <dxo-aggregation [enabled]="true" method="sum"> </dxo-aggregation> </dxi-series> <dxi-pane name="Price"></dxi-pane> <dxi-pane name="Volume" [height]="80"></dxi-pane> <dxo-tooltip [enabled]="true" [shared]="true" argumentFormat="shortDateShortTime" contentTemplate="tooltipTemplate" ></dxo-tooltip> <dxo-crosshair [enabled]="true" [horizontalLine]="{ visible: false }" ></dxo-crosshair> <dxo-margin [right]="30"></dxo-margin> <dxo-scroll-bar [visible]="true"></dxo-scroll-bar> <dxo-legend [visible]="false"></dxo-legend> <dxi-value-axis [placeholderSize]="50"></dxi-value-axis> <dxo-zoom-and-pan argumentAxis="both"></dxo-zoom-and-pan> <dxo-argument-axis argumentType="datetime" [minVisualRangeLength]="{ minutes: 10 }" [visualRange]="{ length: 'hour' }" ></dxo-argument-axis> <dxo-loading-indicator [enabled]="true"></dxo-loading-indicator> <div *dxTemplate="let pointInfo of 'tooltipTemplate'"> <div class="tooltip-template"> <div>{{ pointInfo.argumentText }}</div> <div> <span>Open: </span> {{ formatPrice(pointInfo.points, "openValue") }} </div> <div> <span>High: </span> {{ formatPrice(pointInfo.points, "highValue") }} </div> <div> <span>Low: </span> {{ formatPrice(pointInfo.points, "lowValue") }} </div> <div> <span>Close: </span> {{ formatPrice(pointInfo.points, "closeValue") }} </div> <div> <span>Volume: </span> {{ formatVolume(pointInfo.points) }} </div> </div> </div> </dx-chart> </div>
import { NgModule, Component, ViewChild, enableProdMode, } from '@angular/core'; import { DecimalPipe, CurrencyPipe } from '@angular/common'; import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { HubConnectionBuilder, HttpTransportType } from '@aspnet/signalr'; import CustomStore from 'devextreme/data/custom_store'; import { DxChartModule, DxChartComponent, DxChartTypes } from 'devextreme-angular/ui/chart'; if (!/localhost/.test(document.location.host)) { enableProdMode(); } @Component({ selector: 'demo-app', templateUrl: 'app/app.component.html', styleUrls: ['app/app.component.css'], providers: [DecimalPipe, CurrencyPipe], }) export class AppComponent { @ViewChild(DxChartComponent, { static: false }) component: DxChartComponent; dataSource: CustomStore; connectionStarted = false; constructor(private decimalPipe: DecimalPipe, private currencyPipe: CurrencyPipe) { const hubConnection = new HubConnectionBuilder() .withUrl('https://js.devexpress.com/Demos/NetCore/stockTickDataHub', { skipNegotiation: true, transport: HttpTransportType.WebSockets, }) .build(); const store = new CustomStore({ load: () => hubConnection.invoke('getAllData'), key: 'date', }); hubConnection .start() .then(() => { hubConnection.on('updateStockPrice', (data: Record<string, unknown>) => { store.push([{ type: 'insert', key: data.date, data }]); }); this.dataSource = store; this.connectionStarted = true; }); } calculateCandle(e: Record<string, Date> & { data: Record<string, unknown>[] }) { const prices: unknown[] = e.data.map((d) => d.price); if (prices.length) { return { date: new Date((e.intervalStart.valueOf() + e.intervalEnd.valueOf()) / 2), open: prices[0], high: Math.max.apply(null, prices), low: Math.min.apply(null, prices), close: prices[prices.length - 1], }; } } customizePoint: DxChartTypes.Properties['customizePoint'] = ({ seriesName, argument }) => { if (seriesName === 'Volume') { const point = this.component.instance.getAllSeries()[0].getPointsByArg(argument)[0].data; if (point.close >= point.open) { return { color: '#1db2f5' }; } } }; formatPrice = (points: { seriesName: string, [key: string]: string }[], field: string) => { const pricePoint = points.find((point) => point.seriesName !== 'Volume'); return this.currencyPipe.transform(pricePoint[field], 'USD', 'symbol', '1.0-0'); }; formatVolume = (points: { seriesName: string, [key: string]: string }[]) => { const volPoint = points.find((point) => point.seriesName === 'Volume'); return this.decimalPipe.transform(volPoint.value, '3.0-0'); }; } @NgModule({ imports: [ BrowserModule, BrowserTransferStateModule, DxChartModule, ], declarations: [AppComponent], bootstrap: [AppComponent], }) export class AppModule { } platformBrowserDynamic().bootstrapModule(AppModule);
::ng-deep #chart { height: 440px; } ::ng-deep .tooltip-template span { font-weight: 500; }
// In real applications, you should not transpile code in the browser. // You can see how to create your own application with Angular and DevExtreme here: // https://js.devexpress.com/Documentation/Guide/Angular_Components/Getting_Started/Create_a_DevExtreme_Application/ window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, }, meta: { 'typescript': { 'exports': 'ts', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, }, paths: { 'npm:': 'https://unpkg.com/', }, map: { 'ts': 'npm:plugin-typescript@4.2.4/lib/plugin.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', '@angular/core': 'npm:@angular/core@12.2.17', '@angular/platform-browser': 'npm:@angular/platform-browser@12.2.17', '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic@12.2.17', '@angular/forms': 'npm:@angular/forms@12.2.17', '@angular/common': 'npm:@angular/common@12.2.17', '@angular/compiler': 'npm:@angular/compiler@12.2.17', 'tslib': 'npm:tslib@2.6.2/tslib.js', 'rxjs': 'npm:rxjs@7.5.3/dist/bundles/rxjs.umd.js', 'rxjs/operators': 'npm:rxjs@7.5.3/dist/cjs/operators/index.js', '@aspnet/signalr': 'npm:@aspnet/signalr@1.0.27/dist/cjs/index.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@1.28.1/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign@1.1.0', 'devextreme': 'npm:devextreme@23.2.5/cjs', 'devextreme/bundles/dx.all': 'npm:devextreme@23.2.5/bundles/dx.all.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', 'devextreme-quill': 'npm:devextreme-quill@1.6.4/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.5', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.51', 'devextreme-angular': 'npm:devextreme-angular@23.2.5', '@devextreme/runtime': 'npm:@devextreme/runtime@3.0.12', 'inferno': 'npm:inferno@7.4.11/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@7.4.11/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate@7.4.11/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.4/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.4/parser-html.js', }, packages: { 'app': { main: './app.component.ts', defaultExtension: 'ts', }, 'devextreme': { defaultExtension: 'js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, 'rxjs': { defaultExtension: 'js', }, 'rxjs/operators': { defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', 'npm:@devextreme/runtime@3.0.12/inferno/package.json', 'npm:@angular/*/package.json', 'npm:@angular/common@12.2.17/*/package.json', 'npm:rxjs@7.5.3/package.json', 'npm:rxjs@7.5.3/operators/package.json', 'npm:devextreme-angular@23.2.5/*/package.json', 'npm:devextreme-angular@23.2.5/ui/*/package.json', 'npm:devextreme-angular@23.2.5/package.json', 'npm:devexpress-diagram@2.2.5/package.json', 'npm:devexpress-gantt@4.1.51/package.json', 'npm:@aspnet/*/package.json', ], }; System.config(window.config);
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" /> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/23.2.5/css/dx.light.css" /> <script src="https://unpkg.com/core-js@2.6.12/client/shim.min.js"></script> <script src="https://unpkg.com/zone.js@0.12.0/dist/zone.js"></script> <script src="https://unpkg.com/reflect-metadata@0.1.13/Reflect.js"></script> <script src="https://unpkg.com/systemjs@0.21.3/dist/system.js"></script> <script src="config.js"></script> <script> System.import("app").catch(console.error.bind(console)); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <demo-app>Loading...</demo-app> </div> </body> </html>
using System.Collections.Generic; using DevExtreme.MVC.Demos.Models.SignalRTickData; using Microsoft.AspNet.SignalR; namespace DevExtreme.MVC.Demos.Hubs { public class StockTickDataHub : Hub { private readonly TickDataService _service; public StockTickDataHub() { _service = TickDataService.Instance; } public IEnumerable<TickItem> GetAllData() { return _service.GetAllData(); } } }
using DevExtreme.MVC.Demos.Hubs; using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; using System; using System.Collections.Generic; using System.Threading; namespace DevExtreme.MVC.Demos.Models.SignalRTickData { public class TickDataService { public readonly static TickDataService Instance = new TickDataService(GlobalHost.ConnectionManager.GetHubContext<StockTickDataHub>().Clients); private IHubConnectionContext<dynamic> Clients { get; set; } private readonly object updateStockPricesLock = new object(); private TickItem[] tickData; private int lastTickIndex; private Timer timer; private TickDataService(IHubConnectionContext<dynamic> clients) { Clients = clients; tickData = GenerateTestData(); lastTickIndex = tickData.Length - 1; timer = new Timer(Update, null, 1000, 1000); } public IEnumerable<TickItem> GetAllData() { var data = new List<TickItem>(); lock(updateStockPricesLock) { for(var i = lastTickIndex; data.Count < 10000; i--) { data.Add(tickData[i]); if(i == 0) { i = tickData.Length - 1; } } } return data; } private void Update(object state) { lock(updateStockPricesLock) { lastTickIndex = (lastTickIndex + 1) % tickData.Length; var tick = tickData[lastTickIndex]; tick.Date = DateTime.Now; BroadcastStockPrice(tick); } } private void BroadcastStockPrice(TickItem item) { Clients.All.updateStockPrice(item); } private TickItem[] GenerateTestData() { var lastPrice = 140m; var random = new Random(); var slowRandomValue = random.NextDouble(); var data = new TickItem[60 * 60 * 20]; var curTime = DateTime.Now; for(var i = 0; i < data.Length / 2; i++) { lastPrice = Math.Round(lastPrice * (decimal)(1 + 0.002 * (-1 + 2 * random.NextDouble())), 2); if(i % 50 == 0) { slowRandomValue = random.NextDouble(); if(slowRandomValue > 0.3 && slowRandomValue <= 0.5) slowRandomValue -= 0.2; if(slowRandomValue > 0.5 && slowRandomValue <= 0.7) slowRandomValue += 0.2; } var volume = (int)(100 + 1900 * random.NextDouble() * slowRandomValue); data[data.Length - 1 - i] = new TickItem(lastPrice, volume, curTime.AddSeconds(-1 * i)); data[i] = new TickItem(lastPrice, volume, curTime.AddSeconds(-1 * (data.Length - 1 - i))); } return data; } } }