const DemoApp = angular.module('DemoApp', ['dx']);
DemoApp.controller('DemoController', ($scope) => {
const store = new DevExpress.data.CustomStore({
load() {
return connection.invoke('getAllData');
},
key: 'date',
});
$scope.chartOptions = {
dataSource: {
store,
},
margin: {
right: 30,
},
loadingIndicator: {
enabled: true,
},
title: 'Stock Price',
series: [{
pane: 'Price',
argumentField: 'date',
type: 'candlestick',
aggregation: {
enabled: true,
method: 'custom',
calculate(e) {
const prices = e.data.map((i) => i.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;
},
},
}, {
pane: 'Volume',
name: 'Volume',
type: 'bar',
argumentField: 'date',
valueField: 'volume',
color: 'red',
aggregation: {
enabled: true,
method: 'sum',
},
}],
panes: [{
name: 'Price',
}, {
name: 'Volume',
height: 80,
}],
customizePoint(arg) {
if (arg.seriesName === 'Volume') {
const point = $('#chart').dxChart('getAllSeries')[0].getPointsByArg(arg.argument)[0].data;
if (point.close >= point.open) {
return { color: '#1db2f5' };
}
}
return null;
},
tooltip: {
enabled: true,
shared: true,
argumentFormat: 'shortDateShortTime',
contentTemplate(pointInfo) {
const volume = pointInfo.points.filter((point) => point.seriesName === 'Volume')[0];
const prices = pointInfo.points.filter((point) => point.seriesName !== 'Volume')[0];
return `<div class='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>`;
},
},
crosshair: {
enabled: true,
horizontalLine: { visible: false },
},
zoomAndPan: {
argumentAxis: 'both',
},
scrollBar: {
visible: true,
},
legend: {
visible: false,
},
argumentAxis: {
argumentType: 'datetime',
minVisualRangeLength: { minutes: 10 },
visualRange: {
length: 'hour',
},
},
valueAxis: {
placeholderSize: 50,
},
};
$scope.connectionStarted = false;
const connection = new signalR.HubConnectionBuilder()
.withUrl('https://js.devexpress.com/Demos/NetCore/stockTickDataHub')
.configureLogging(signalR.LogLevel.Information)
.build();
connection.start()
.then(() => {
connection.on('updateStockPrice', (data) => {
store.push([{ type: 'insert', key: data.date, data }]);
});
$scope.connectionStarted = true;
$scope.$apply();
});
const formatCurrency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2 }).format;
const formatNumber = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format;
});
<!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.1.5/css/dx.light.css" />
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>window.jQuery || document.write(decodeURIComponent('%3Cscript src="js/jquery.min.js"%3E%3C/script%3E'))</script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.8.2/angular.min.js"></script>
<script>window.angular || document.write(decodeURIComponent('%3Cscript src="js/angular.min.js"%3E%3C/script%3E'))</script>
<script src="https://cdn3.devexpress.com/jslib/23.1.5/js/dx.all.js"></script>
<script src="js/signalr.js"></script>
<link rel="stylesheet" type="text/css" href="styles.css" />
<script src="index.js"></script>
</head>
<body class="dx-viewport">
<div class="demo-container" ng-app="DemoApp" ng-controller="DemoController">
<div ng-if="connectionStarted">
<div id="chart" dx-chart="chartOptions"></div>
</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;
}
}
}