DevExtreme v23.1 is now available.

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

Your search did not match any results.
Charts

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
Copy to CodeSandBox
Apply
Reset
<template> <div v-if="connectionStarted"> <DxChart id="chart" ref="chart" :data-source="dataSource" :margin="{right: 30}" :customize-point="customizePoint" zooming-mode="all" scrolling-mode="all" title="Stock Price" > <DxSeries argument-field="date" type="candlestick" pane="Price" > <DxAggregation :enabled="true" :calculate="calculateCandle" method="custom" /> </DxSeries> <DxSeries argument-field="date" value-field="volume" type="bar" color="red" pane="Volume" name="Volume" > <DxAggregation :enabled="true" method="sum" /> </DxSeries> <DxPane name="Price"/> <DxPane :height="80" name="Volume" /> <DxLegend :visible="false"/> <DxArgumentAxis :min-visual-range-length="{minutes: 10}" :visual-range="{length: 'hour'}" argument-type="datetime" /> <DxZoomAndPan argument-axis="both"/> <DxValueAxis :placeholder-size="50"/> <DxScrollBar :visible="true"/> <DxLoadingIndicator :enabled="true"/> <DxTooltip :enabled="true" :shared="true" argument-format="shortDateShortTime" content-template="tooltipTemplate" /> <DxCrosshair :enabled="true" :horizontal-line="{visible: false}" /> <template #tooltipTemplate="{ data }"> <TooltipTemplate :point-info="data"/> </template> </DxChart> </div> </template> <script> import { DxChart, DxArgumentAxis, DxValueAxis, DxAggregation, DxSeries, DxLegend, DxScrollBar, DxZoomAndPan, DxLoadingIndicator, DxPane, DxTooltip, DxCrosshair, } from 'devextreme-vue/chart'; import CustomStore from 'devextreme/data/custom_store'; import { HubConnectionBuilder, HttpTransportType } from '@aspnet/signalr'; import TooltipTemplate from './TooltipTemplate.vue'; export default { components: { DxChart, DxArgumentAxis, DxValueAxis, DxAggregation, DxLegend, DxSeries, DxScrollBar, DxZoomAndPan, DxLoadingIndicator, DxPane, DxTooltip, DxCrosshair, TooltipTemplate, }, data() { return { connectionStarted: false, dataSource: null, }; }, mounted() { 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 }]); }); this.dataSource = store; this.connectionStarted = true; }); }, methods: { calculateCandle(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; }, customizePoint(pointInfo) { if (pointInfo.seriesName === 'Volume') { const point = this.$refs.chart.instance .getAllSeries()[0] .getPointsByArg(pointInfo.argument)[0] .data; if (point.close >= point.open) { return { color: '#1db2f5' }; } } return null; }, }, }; </script> <style> #chart { height: 440px; } </style>
<template> <div class="tooltip-template"> <div>{{ pointInfo.argumentText }}</div> <div><span>Open: </span> {{ formatCurrency(prices.openValue, "USD") }} </div> <div><span>High: </span> {{ formatCurrency(prices.highValue, "USD") }} </div> <div><span>Low: </span> {{ formatCurrency(prices.lowValue, "USD") }} </div> <div><span>Close: </span> {{ formatCurrency(prices.closeValue, "USD") }} </div> <div><span>Volume: </span> {{ formatNumber(volume.value, { maximumFractionDigits: 0 }) }} </div> </div> </template> <script> export default { props: { pointInfo: { type: Object, default: () => {}, }, }, data() { return { volume: this.pointInfo.points.filter((point) => point.seriesName === 'Volume')[0], prices: this.pointInfo.points.filter((point) => point.seriesName !== 'Volume')[0], }; }, methods: { formatCurrency: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 0 }).format, formatNumber: new Intl.NumberFormat('en-US', { minimumFractionDigits: 0, }).format, }, }; </script> <style> .tooltip-template span { font-weight: 500; } </style>
import { createApp } from 'vue'; import App from './App.vue'; createApp(App).mount('#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.1.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/systemjs@0.21.3/dist/system.js"></script> <script type="text/javascript" src="config.js"></script> <script type="text/javascript"> System.import("./index.js"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"> </div> </div> </body> </html>
window.config = { transpiler: 'plugin-babel', meta: { '*.vue': { loader: 'vue-loader', }, '*.ts': { loader: 'demo-ts-loader', }, '*.svg': { loader: 'svg-loader', }, 'devextreme/localization.js': { 'esModule': true, }, }, paths: { 'root:': '../../../../../', 'npm:': 'https://unpkg.com/', }, map: { 'vue': 'npm:vue@3.3.4/dist/vue.esm-browser.js', 'vue-loader': 'npm:dx-systemjs-vue-browser@1.1.1/index.js', 'demo-ts-loader': 'root:utils/demo-ts-loader.js', 'svg-loader': 'root:utils/svg-loader.js', '@aspnet/signalr': 'npm:@aspnet/signalr@1.0.27/dist/cjs', 'tslib': 'npm:tslib@2.3.1/tslib.js', 'mitt': 'npm:mitt/dist/mitt.umd.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.1.5/cjs', 'devextreme-vue': 'npm:devextreme-vue@23.1.5', 'jszip': 'npm:jszip@3.7.1/dist/jszip.min.js', 'devextreme-quill': 'npm:devextreme-quill@1.6.2/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.1/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.48/dist/dx-gantt.js', '@devextreme/runtime': 'npm:@devextreme/runtime@3.0.11', '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', '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-vue': { main: 'index.js', }, 'devextreme': { defaultExtension: 'js', }, 'devextreme/events/utils': { main: 'index', }, '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.11/inferno/package.json', ], babelOptions: { sourceMaps: false, stage0: true, }, }; System.config(window.config);
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; } } }