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
import React, { useCallback, useEffect, useRef, useState, } from 'react'; import Chart, { ArgumentAxis, ValueAxis, Aggregation, Legend, Series, ScrollBar, ZoomAndPan, LoadingIndicator, Pane, Tooltip, Crosshair, Margin, HorizontalLine, IAggregationProps, } from 'devextreme-react/chart'; import { VisualRange } from 'devextreme-react/common/charts'; import CustomStore from 'devextreme/data/custom_store'; import { HubConnectionBuilder, HttpTransportType } from '@aspnet/signalr'; import TooltipTemplate from './TooltipTemplate.tsx'; const minVisualRangeLength = { minutes: 10 }; const defaultVisualRange: VisualRange = { length: 'hour' }; function App() { const [dataSource, setDataSource] = useState(null); const chartRef = useRef(null); const customizePoint = useCallback((arg) => { if (arg.seriesName === 'Volume') { const point = chartRef.current.instance.getAllSeries()[0] .getPointsByArg(arg.argument)[0].data; if (point && point.close >= point.open) { return { color: '#1db2f5' }; } } return null; }, []); const calculateCandle = useCallback<IAggregationProps['calculate']>((e) => { const prices = 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], }; } return null; }, []); useEffect(() => { 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) => { store.push([{ type: 'insert', key: data.date, data }]); }); setDataSource(store); }); }, []); return ( <div> <Chart id="chart" ref={chartRef} dataSource={dataSource} title="Stock Price" customizePoint={customizePoint}> <Margin right={30} /> <Series pane="Price" argumentField="date" type="candlestick"> <Aggregation enabled={true} method="custom" calculate={calculateCandle} /> </Series> <Series pane="Volume" name="Volume" argumentField="date" valueField="volume" color="red" type="bar"> <Aggregation enabled={true} method="sum" /> </Series> <Pane name="Price"></Pane> <Pane name="Volume" height={80}></Pane> <Legend visible={false} /> <ArgumentAxis argumentType="datetime" minVisualRangeLength={minVisualRangeLength} defaultVisualRange={defaultVisualRange} /> <ValueAxis placeholderSize={50} /> <ZoomAndPan argumentAxis="both" /> <ScrollBar visible={true} /> <LoadingIndicator enabled={true} /> <Tooltip enabled={true} shared={true} argumentFormat="shortDateShortTime" contentRender={TooltipTemplate} /> <Crosshair enabled={true}> <HorizontalLine visible={false} /> </Crosshair> </Chart> </div> ); } export default App;
import React, { useCallback, useEffect, useRef, useState, } from 'react'; import Chart, { ArgumentAxis, ValueAxis, Aggregation, Legend, Series, ScrollBar, ZoomAndPan, LoadingIndicator, Pane, Tooltip, Crosshair, Margin, HorizontalLine, } from 'devextreme-react/chart'; import CustomStore from 'devextreme/data/custom_store'; import { HubConnectionBuilder, HttpTransportType } from '@aspnet/signalr'; import TooltipTemplate from './TooltipTemplate.js'; const minVisualRangeLength = { minutes: 10 }; const defaultVisualRange = { length: 'hour' }; function App() { const [dataSource, setDataSource] = useState(null); const chartRef = useRef(null); const customizePoint = useCallback((arg) => { if (arg.seriesName === 'Volume') { const point = chartRef.current.instance .getAllSeries()[0] .getPointsByArg(arg.argument)[0].data; if (point && point.close >= point.open) { return { color: '#1db2f5' }; } } return null; }, []); const calculateCandle = useCallback((e) => { const prices = 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], }; } return null; }, []); useEffect(() => { 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) => { store.push([{ type: 'insert', key: data.date, data }]); }); setDataSource(store); }); }, []); return ( <div> <Chart id="chart" ref={chartRef} dataSource={dataSource} title="Stock Price" customizePoint={customizePoint} > <Margin right={30} /> <Series pane="Price" argumentField="date" type="candlestick" > <Aggregation enabled={true} method="custom" calculate={calculateCandle} /> </Series> <Series pane="Volume" name="Volume" argumentField="date" valueField="volume" color="red" type="bar" > <Aggregation enabled={true} method="sum" /> </Series> <Pane name="Price"></Pane> <Pane name="Volume" height={80} ></Pane> <Legend visible={false} /> <ArgumentAxis argumentType="datetime" minVisualRangeLength={minVisualRangeLength} defaultVisualRange={defaultVisualRange} /> <ValueAxis placeholderSize={50} /> <ZoomAndPan argumentAxis="both" /> <ScrollBar visible={true} /> <LoadingIndicator enabled={true} /> <Tooltip enabled={true} shared={true} argumentFormat="shortDateShortTime" contentRender={TooltipTemplate} /> <Crosshair enabled={true}> <HorizontalLine visible={false} /> </Crosshair> </Chart> </div> ); } export default App;
import React from 'react'; const formatCurrency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0 }).format; const formatNumber = new Intl.NumberFormat('en-US', { minimumFractionDigits: 0, }).format; export default function TooltipTemplate(pointInfo) { const volume = pointInfo.points.filter((point: { seriesName: string; }) => point.seriesName === 'Volume')[0]; const prices = pointInfo.points.filter((point: { seriesName: string; }) => point.seriesName !== 'Volume')[0]; return ( <div className="tooltip-template"> <div>{ pointInfo.argumentText }</div> <div><span>Open: </span> { formatCurrency(prices.openValue) } </div> <div><span>High: </span> { formatCurrency(prices.highValue) } </div> <div><span>Low: </span> { formatCurrency(prices.lowValue) } </div> <div><span>Close: </span> { formatCurrency(prices.closeValue) } </div> <div><span>Volume: </span> { formatNumber(volume.value) } </div> </div> ); }
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.tsx'; ReactDOM.render( <App />, document.getElementById('app'), );
window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, jsx: 'react', }, meta: { 'react': { 'esModule': true, }, '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/', }, defaultExtension: 'js', map: { 'ts': 'npm:plugin-typescript@4.2.4/lib/plugin.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', 'react': 'npm:react@17.0.2/umd/react.development.js', 'react-dom': 'npm:react-dom@17.0.2/umd/react-dom.development.js', 'prop-types': 'npm:prop-types@15.8.1/prop-types.js', '@aspnet/signalr': 'npm:@aspnet/signalr@1.0.27/dist/cjs', 'tslib': 'npm:tslib@2.6.2/tslib.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-react': 'npm:devextreme-react@23.2.5/cjs', '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/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.51/dist/dx-gantt.js', '@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', 'devextreme-cldr-data': 'npm:devextreme-cldr-data@1.0.3', // SystemJS plugins 'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.4/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.4/parser-html.js', }, packages: { 'devextreme': { defaultExtension: 'js', }, 'devextreme-react': { main: 'index.js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/localization/messages': { format: 'json', defaultExtension: '', }, 'devextreme/events': { main: 'index', }, '@aspnet/signalr': { main: 'index.js', defaultExtension: 'js', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', 'npm:@devextreme/runtime@3.0.12/inferno/package.json', ], babelOptions: { sourceMaps: false, stage0: true, react: true, }, }; System.config(window.config);
import React from 'react'; const formatCurrency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0, }).format; const formatNumber = new Intl.NumberFormat('en-US', { minimumFractionDigits: 0, }).format; export default function TooltipTemplate(pointInfo) { const volume = pointInfo.points.filter((point) => point.seriesName === 'Volume')[0]; const prices = pointInfo.points.filter((point) => point.seriesName !== 'Volume')[0]; return ( <div className="tooltip-template"> <div>{pointInfo.argumentText}</div> <div> <span>Open: </span> {formatCurrency(prices.openValue)} </div> <div> <span>High: </span> {formatCurrency(prices.highValue)} </div> <div> <span>Low: </span> {formatCurrency(prices.lowValue)} </div> <div> <span>Close: </span> {formatCurrency(prices.closeValue)} </div> <div> <span>Volume: </span> {formatNumber(volume.value)} </div> </div> ); }
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.js'; ReactDOM.render(<App />, document.getElementById('app'));
<!DOCTYPE html> <html> <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" /> <link rel="stylesheet" type="text/css" href="styles.css" /> <script src="https://unpkg.com/core-js@2.6.12/client/shim.min.js"></script> <script src="https://unpkg.com/systemjs@0.21.3/dist/system.js"></script> <script type="text/javascript" src="config.js"></script> <script type="text/javascript"> System.import("./index.tsx"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"></div> </div> </body> </html>
#chart { height: 440px; } .tooltip-template span { font-weight: 500; }
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; } } }