index.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
  1. import React from 'react';
  2. import * as d3Scale from 'd3-scale';
  3. import * as d3Array from 'd3-array';
  4. import * as d3Color from 'd3-color';
  5. import * as d3Format from 'd3-format';
  6. import * as d3Interpolate from 'd3-interpolate';
  7. import * as d3Shape from 'd3-shape';
  8. import * as d3Timer from 'd3-timer';
  9. import * as d3Ease from 'd3-ease';
  10. import { extractBidPrices, extractBidVolumes, extractMaxTradedVolume, extractMaxVolume, zoomTimeFormat } from './utils';
  11. export const d3 = Object.assign(
  12. Object.assign(
  13. Object.assign({}, d3Scale, d3Array, d3Color)
  14. , d3Format, d3Interpolate, d3Shape
  15. )
  16. , d3Ease, d3Timer
  17. );
  18. /**
  19. * Stock Heatmap
  20. * @author Rounak Saha
  21. *
  22. * © Copyright 2020, Rounak Saha
  23. */
  24. export default class StockHeatmap extends React.Component {
  25. /** @type {React.RefObject<HTMLCanvasElement>} */
  26. canvasRef = React.createRef();
  27. /** @type {CanvasRenderingContext2D} */
  28. drawingContext = null;
  29. data = [];
  30. windowedData = [];
  31. windowLength = 40;
  32. windowPosition = 0;
  33. autoScroll = true;
  34. /** Default Theme colors and dimensions */
  35. defaults = {
  36. borderPadding: [5, 5, 0, 0],
  37. bidAskWidth: 100,
  38. axisYWidth: 50,
  39. axisXHeight: 50,
  40. buyColor: '#388e3c',
  41. textOnBuyColor: '#ffffff',
  42. sellColor: '#d32f2f',
  43. textOnSellColor: '#ffffff',
  44. textOnBackground: '#000000',
  45. textHighlightOnBackground: '#ff0000',
  46. tradeColor: '#7434eb',
  47. axisTickSize: 6,
  48. axisColor: '#000000',
  49. xAxisTextPadding: 6,
  50. yAxisTextPadding: 6,
  51. bidAskGraphPaddingLeft: 10,
  52. bidAskTransitionDuration: 500,
  53. volumeCircleMaxRadius: 10,
  54. runningRatioSeconds: 5,
  55. hmWidth: () => (this.props.width - this.defaults.borderPadding[1] - this.defaults.borderPadding[3] - this.defaults.bidAskWidth - this.defaults.axisYWidth),
  56. hmHeight: () => (this.props.height - this.defaults.borderPadding[0] - this.defaults.borderPadding[2] - this.defaults.axisXHeight),
  57. clearColor: '#ffffff',
  58. };
  59. shouldComponentUpdate(nextProps, nextState) {
  60. // console.log('shouldComponentUpdate', nextProps);
  61. const shouldUpdate = this.props.width !== nextProps.width
  62. || this.props.height !== nextProps.height;
  63. if (shouldUpdate) {
  64. this.detachMouseListeners();
  65. }
  66. return shouldUpdate;
  67. }
  68. // -------------------START:: Lifecycle methods to retrive 2d context from updated dom-------------------------
  69. componentDidMount() {
  70. // console.log('component mouted');
  71. if (this.canvasRef.current !== null) {
  72. this.drawingContext = this.canvasRef.current.getContext('2d');
  73. this.updateHeatmap();
  74. this.attachMouseListeners();
  75. }
  76. }
  77. componentDidUpdate() {
  78. // console.log('component updtated');
  79. if (this.canvasRef.current !== null) {
  80. this.drawingContext = this.canvasRef.current.getContext('2d');
  81. this.updateHeatmap();
  82. this.attachMouseListeners();
  83. }
  84. }
  85. componentWillUnmount() {
  86. this.detachMouseListeners();
  87. }
  88. // -------------------END:: Lifecycle methods to retrive 2d context from updated dom---------------------------
  89. // ------------------ START:: Mouse Event listeners -------------------
  90. isMouseDown = false;
  91. mouseDownX = 0;
  92. /**
  93. * Attaches mouse interaction event listeners
  94. */
  95. attachMouseListeners = () => {
  96. if (this.canvasRef.current !== null) {
  97. this.canvasRef.current.addEventListener('mousedown', this.eventMouseDown);
  98. this.canvasRef.current.addEventListener('mousemove', this.eventMouseMove);
  99. this.canvasRef.current.addEventListener('mouseup', this.eventMouseUp);
  100. this.canvasRef.current.addEventListener('wheel', this.eventZoomWheel);
  101. this.canvasRef.current.addEventListener('keydown', this.eventKeyDown);
  102. }
  103. }
  104. /**
  105. * Detaches mouse interaction event listeners
  106. */
  107. detachMouseListeners = () => {
  108. if (this.canvasRef.current !== null) {
  109. this.canvasRef.current.removeEventListener('mousedown', this.eventMouseDown);
  110. this.canvasRef.current.removeEventListener('mousemove', this.eventMouseMove);
  111. this.canvasRef.current.removeEventListener('mouseup', this.eventMouseUp);
  112. this.canvasRef.current.removeEventListener('wheel', this.eventZoomWheel);
  113. this.canvasRef.current.removeEventListener('keydown', this.eventKeyDown);
  114. }
  115. }
  116. /**
  117. * Mouse down event on canvas
  118. * @param {MouseEvent} e
  119. */
  120. eventMouseDown = (e) => {
  121. // console.log('eventMouseDown', e);
  122. this.isMouseDown = true;
  123. this.mouseDownX = e.x;
  124. }
  125. /**
  126. * Mouse move event on canvas
  127. * @param {MouseEvent} e
  128. */
  129. eventMouseMove = (e) => {
  130. if (this.isMouseDown) {
  131. // Mouse drag, scroll the time series
  132. const dragLength = e.x - this.mouseDownX;
  133. const moveDataPointsCount = Math.floor(Math.abs(dragLength) / this.xScale.bandwidth());
  134. if (moveDataPointsCount > 0) this.mouseDownX = e.x;
  135. // const moveDataPointDirection = dragLength >= 0 ? 'right' : 'left';
  136. // console.log('drag x=', dragLength, moveDataPointsCount, this.windowPosition);
  137. this.moveDataWindow(this.windowPosition + moveDataPointsCount * (dragLength >= 0 ? -1 : 1));
  138. }
  139. else {
  140. // normal mouse move
  141. }
  142. }
  143. /**
  144. * Mouse up event on canvas
  145. * @param {MouseEvent} e
  146. */
  147. eventMouseUp = (e) => {
  148. // console.log('eventMouseUp',e);
  149. this.isMouseDown = false;
  150. this.mouseDownX = 0;
  151. }
  152. /**
  153. * Wheel event on canvas to zoom
  154. * @param {WheelEvent} e
  155. */
  156. eventZoomWheel = (e) => {
  157. const direction = e.deltaY < 0 ? 'zoom-in' : 'zoom-out';
  158. let l = 0, l2 = 0;
  159. switch (direction) {
  160. case 'zoom-in':
  161. l = Math.max(this.windowLength - 1, 3);
  162. break;
  163. case 'zoom-out':
  164. l = Math.min(this.windowLength + 1, this.data.length - 1);
  165. break;
  166. }
  167. l2 = this.windowLength - l;
  168. this.windowLength = l;
  169. this.moveDataWindow(this.windowPosition + l2);
  170. // console.log('zoom Level=', this.windowLength);
  171. }
  172. /**
  173. * Event to be triggered when keyboard key is pressed
  174. * @param {KeyboardEvent} e
  175. */
  176. eventKeyDown = (e) => {
  177. e.preventDefault();
  178. console.log('key event', e.isComposing, e.key);
  179. switch(e.key) {
  180. case 'ArrowLeft':
  181. this.moveDataWindow(this.windowPosition - 1);
  182. break;
  183. case 'ArrowRight':
  184. this.moveDataWindow(this.windowPosition + 1);
  185. break;
  186. }
  187. }
  188. // ------------------ END:: Mouse Event listeners ---------------------
  189. // ------------------ D3 Variables ---------------------
  190. /** @type {d3Scale.ScaleBand<string>} */
  191. xScale = null;
  192. /** @type {d3Scale.ScaleLinear<number, number>} */
  193. bidAskScale = null;
  194. /** @type {d3Scale.ScaleBand<string>} */
  195. yScale = null;
  196. /** @type {number[]} */
  197. yDomainValues = null;
  198. /** @type {d3Timer.Timer} */
  199. bidAskAnimTimer = null;
  200. /** @type {{[key:number]:number}} */
  201. bidAskBarAnimConfig = {};
  202. // ------------------ D3 Variables ---------------------
  203. /**
  204. * This function will be called if there is any dimension change on heatmap
  205. * This function changes the d3 scales based on windowed data
  206. */
  207. updateHeatmapDimensions = () => {
  208. // console.log('heatmap dimension updated, update scale domains');
  209. const { width, height } = this.props;
  210. if (width > 0 && height > 0 && this.windowedData.length > 0) {
  211. // setup x-scale
  212. this.xScale = d3.scaleBand()
  213. .range([0, this.defaults.hmWidth()])
  214. .domain(this.windowedData.map(d => d.ts));
  215. // setup y-scale
  216. this.yDomainValues = extractBidPrices(this.windowedData).sort((a, b) => a - b);
  217. this.yScale = d3.scaleBand()
  218. .range([this.defaults.hmHeight(), 0])
  219. .domain(this.yDomainValues);
  220. // setup bid ask scale
  221. this.bidAskScale = d3.scaleLinear()
  222. .range([0, this.defaults.bidAskWidth])
  223. .domain([0, d3.max(extractBidVolumes(this.windowedData[this.windowedData.length - 1]))]);
  224. }
  225. }
  226. /**
  227. * This method will be called after an update of internal data is performed.
  228. */
  229. updateHeatmap = () => {
  230. if (this.drawingContext !== null) {
  231. // console.log('heatmap update req');
  232. // 1. update scale and dimensions
  233. this.updateHeatmapDimensions();
  234. // 2. Draw the bid ask spread heatmap
  235. this.clearCanvas(this.defaults.borderPadding[3], this.defaults.borderPadding[0],
  236. this.defaults.hmWidth(), this.defaults.hmHeight(), this.defaults.clearColor);
  237. this.drawMainGraph();
  238. // 3. Draw xy Axis
  239. this.drawXAxis();
  240. this.drawYAxisAndBidAskGraph();
  241. // 4. Draw buy-to-sell ratio
  242. this.drawBuy2SellRatio();
  243. // console.log('heatmap draw update');
  244. // this.clearCanvas(0, 0, this.defaults.hmWidth(), this.defaults.hmHeight(), '#aaaaaa');
  245. }
  246. }
  247. // ------------------------------ START: Canvas draw functions ---------------------------------------
  248. /**
  249. * Draw buy/sell ratio at bottom right corner
  250. */
  251. drawBuy2SellRatio = () => {
  252. if (this.windowedData.length > 0) {
  253. // dimension
  254. const d = this.windowedData[this.windowedData.length - 1];
  255. const x = this.defaults.borderPadding[3] + this.defaults.hmWidth() + this.defaults.axisTickSize;
  256. const y = this.defaults.borderPadding[0] + this.defaults.hmHeight() + this.defaults.axisTickSize;
  257. const w = this.props.width - x;
  258. const h = this.props.height - y;
  259. this.clearCanvas(x, y, w, h, this.defaults.clearColor);
  260. let textHeight = (h - 10) / 2;
  261. this.drawingContext.save();
  262. this.drawingContext.textAlign = 'center';
  263. this.drawingContext.textBaseline = 'middle';
  264. this.drawingContext.font = `bold ${textHeight}px sans-serif`;
  265. this.drawingContext.fillText((d.marketDepth.buyOrderVolume / d.marketDepth.sellOrderVolume).toFixed(2), x + w *3/4, y + textHeight / 2);
  266. // Runing average ratio
  267. if(this.windowedData.length >= this.defaults.runningRatioSeconds) {
  268. let sellT20RunningSum = 0;
  269. let buyT20RunningSum = 0;
  270. for (let i = this.windowedData.length - 1; i >= this.windowedData.length - this.defaults.runningRatioSeconds; i--) {
  271. sellT20RunningSum += (this.windowedData[i].marketDepth.sells || []).reduce((vol, s) => vol + s.qty,0);
  272. buyT20RunningSum += (this.windowedData[i].marketDepth.buys || []).reduce((vol, s) => vol + s.qty,0);
  273. }
  274. const newBSTPFactor = (buyT20RunningSum / sellT20RunningSum);
  275. this.drawingContext.fillText(newBSTPFactor.toFixed(2), x + w /4, y + textHeight *0.5);
  276. }
  277. this.drawingContext.font = `bold ${13}px sans-serif`;
  278. this.drawingContext.textBaseline = 'bottom';
  279. this.drawingContext.fillText('Buy/Sell', x + w / 2, y + textHeight * 2 + 5);
  280. this.drawingContext.restore();
  281. }
  282. }
  283. /**
  284. * Draws X Axis
  285. */
  286. drawXAxis = () => {
  287. // clear canvas before axis draw
  288. this.clearCanvas(
  289. this.defaults.borderPadding[3], this.defaults.borderPadding[0] + this.defaults.hmHeight(),
  290. this.defaults.hmWidth(), this.defaults.axisXHeight, this.defaults.clearColor
  291. );
  292. // draw axis
  293. this.drawingContext.save();
  294. this.drawingContext.beginPath();
  295. this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0] + this.defaults.hmHeight());
  296. this.drawingContext.moveTo(0, 0);
  297. this.drawingContext.lineTo(this.defaults.hmWidth(), 0);
  298. this.drawingContext.textAlign = 'center';
  299. this.drawingContext.textBaseline = 'top';
  300. const assumedTextWidth = this.drawingContext.measureText('77:77:77').width + 20;
  301. const bandInterval = parseInt(assumedTextWidth / (this.xScale?.bandwidth() || 1)) || 1;
  302. // console.log('bandInterval=', bandInterval);
  303. this.windowedData.map((d, i) => {
  304. if (i % bandInterval === 0) {
  305. let x = this.xScale(d.ts);
  306. this.drawingContext.moveTo(x, 0);
  307. this.drawingContext.lineTo(x, this.defaults.axisTickSize);
  308. this.drawingContext.fillText(d.ts, x, this.defaults.axisTickSize + this.defaults.xAxisTextPadding);
  309. }
  310. });
  311. this.drawingContext.textAlign = 'left';
  312. this.drawingContext.font = '12px Arial';
  313. this.drawingContext.fillText(`Zoom Level: ${zoomTimeFormat(this.windowLength)}`, 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  314. let w = this.drawingContext.measureText(`Zoom Level: ${zoomTimeFormat(this.windowLength)}`).width;
  315. const maxVolumeInWindowData = extractMaxTradedVolume(this.windowedData);
  316. this.drawingContext.fillText(`Max Volume in ${zoomTimeFormat(this.windowLength, 1)}: `, 20 + w + 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  317. this.drawingContext.fillStyle = this.defaults.textHighlightOnBackground;
  318. w += this.drawingContext.measureText(`Max Volume in ${zoomTimeFormat(this.windowLength, 1)}: `).width;
  319. this.drawingContext.font = 'bold 12px Arial';
  320. this.drawingContext.fillText(`${maxVolumeInWindowData}`, 20 + w + 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  321. w += this.drawingContext.measureText(`${maxVolumeInWindowData}`).width;
  322. if (this.windowedData.length > 0) {
  323. this.drawingContext.fillStyle = this.defaults.textOnBackground;
  324. this.drawingContext.fillText(`LTP: ${this.windowedData[this.windowedData.length - 1].marketDepth.lastTradedPrice
  325. } LTQ: ${this.windowedData[this.windowedData.length - 1].marketDepth.lastTradedQty
  326. }`, 20 + w + 40, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  327. }
  328. this.drawingContext.fillStyle = this.defaults.textOnBackground;
  329. this.drawingContext.lineWidth = 1.2;
  330. this.drawingContext.strokeStyle = this.defaults.axisColor;
  331. this.drawingContext.stroke();
  332. this.drawingContext.restore();
  333. }
  334. /**
  335. * Draws Y Axis and Bid Ask graph at the same time
  336. */
  337. drawYAxisAndBidAskGraph = () => {
  338. if (this.yDomainValues !== null) {
  339. // clear canvas before axis draw
  340. this.clearCanvas(
  341. this.defaults.borderPadding[3] + this.defaults.hmWidth(), this.defaults.borderPadding[0],
  342. this.defaults.axisYWidth, this.defaults.hmHeight() + this.defaults.axisTickSize, this.defaults.clearColor
  343. );
  344. // translate and draw
  345. this.drawingContext.save();
  346. this.drawingContext.beginPath();
  347. this.drawingContext.translate(this.defaults.borderPadding[3] + this.defaults.hmWidth(), this.defaults.borderPadding[0]);
  348. this.drawingContext.moveTo(0, 0);
  349. this.drawingContext.lineTo(0, this.defaults.hmHeight() + this.defaults.axisTickSize);
  350. this.drawingContext.textAlign = 'start';
  351. this.drawingContext.textBaseline = 'top';
  352. let maxTextWidth = 0;
  353. this.yDomainValues.map(d => {
  354. let y = this.yScale(d);
  355. this.drawingContext.moveTo(0, y);
  356. this.drawingContext.lineTo(this.defaults.axisTickSize, y);
  357. this.drawingContext.fillText(d.toFixed(2), this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y + 2, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
  358. let tw = this.drawingContext.measureText(d.toFixed(2)).width;
  359. maxTextWidth = maxTextWidth >= tw ? maxTextWidth : tw;
  360. });
  361. this.drawingContext.lineWidth = 1.2;
  362. this.drawingContext.strokeStyle = this.defaults.axisColor;
  363. this.drawingContext.stroke();
  364. this.drawingContext.restore();
  365. // Now I will draw the bid ask strength graph,
  366. const x = this.defaults.borderPadding[3] + this.defaults.hmWidth() + maxTextWidth + this.defaults.axisTickSize + this.defaults.yAxisTextPadding + this.defaults.bidAskGraphPaddingLeft;
  367. const y = this.defaults.borderPadding[0];
  368. this.drawBidAskGraph(x, y);
  369. }
  370. }
  371. /**
  372. * Draw and animate Bid Ask graph
  373. * @param {number} x
  374. * @param {number} y
  375. */
  376. drawBidAskGraph = (x, y) => {
  377. if (this.windowedData.length > 0) {
  378. if (this.bidAskAnimTimer !== null) {
  379. this.bidAskAnimTimer.stop();
  380. this.bidAskAnimTimer = null;
  381. }
  382. this.bidAskAnimTimer = d3.timer(elapsed => {
  383. // compute how far through the animation we are (0 to 1)
  384. const t = Math.min(1, d3.easeCubic(elapsed / this.defaults.bidAskTransitionDuration));
  385. // ----------------draw--------------------
  386. // console.log('drawing bid ask graph');
  387. this.clearCanvas(
  388. x, y, this.defaults.bidAskWidth, this.defaults.hmHeight() + this.defaults.axisTickSize, this.defaults.clearColor
  389. );
  390. const h = this.yScale.bandwidth() - 2;
  391. const d = this.windowedData[this.windowedData.length - 1];
  392. const maxBidAskVol = extractMaxVolume(d);
  393. this.drawingContext.save();
  394. this.drawingContext.translate(x, y);
  395. this.drawingContext.lineWidth = 0;
  396. this.drawingContext.textBaseline = 'middle';
  397. const drawBars = (arr, color, textColor) => {
  398. arr.map(v => {
  399. this.drawingContext.fillStyle = color;
  400. const l = this.defaults.bidAskWidth * (+v.qty / maxBidAskVol);
  401. // save v bars length
  402. this.bidAskBarAnimConfig[v.rate] = d3.interpolateNumber(this.bidAskBarAnimConfig[v.rate] || 0, l)(t);
  403. this.drawingContext.fillRect(0, this.yScale(v.rate), this.bidAskBarAnimConfig[v.rate], h);
  404. let tw = this.drawingContext.measureText(v.qty).width;
  405. if (this.defaults.bidAskWidth - this.bidAskBarAnimConfig[v.rate] - 2 >= tw) {
  406. // text outside bar
  407. this.drawingContext.textAlign = 'start';
  408. this.drawingContext.fillStyle = this.defaults.textOnBackground;
  409. this.drawingContext.fillText(v.qty, this.bidAskBarAnimConfig[v.rate] + 2, this.yScale(v.rate) + h / 2 + 1);
  410. } else {
  411. this.drawingContext.textAlign = 'end';
  412. this.drawingContext.fillStyle = textColor;
  413. this.drawingContext.fillText(v.qty, this.bidAskBarAnimConfig[v.rate] - 2, this.yScale(v.rate) + h / 2 + 1);
  414. }
  415. });
  416. }
  417. drawBars(d.marketDepth.buys, this.defaults.buyColor, this.defaults.textOnBuyColor);
  418. drawBars(d.marketDepth.sells, this.defaults.sellColor, this.defaults.textOnSellColor);
  419. this.drawingContext.restore();
  420. // ----------------draw--------------------
  421. // if this animation is over
  422. if (t === 1) this.bidAskAnimTimer.stop();
  423. });
  424. }
  425. }
  426. /**
  427. * Draws background heatmap for both buys and sells
  428. */
  429. drawMainGraph = () => {
  430. this.drawingContext.save();
  431. if (this.xScale && this.yScale && this.bidAskScale && this.drawingContext !== null) {
  432. const maxTradedVolume = extractMaxTradedVolume(this.windowedData);
  433. const xh2 = this.xScale.bandwidth() * 0.5;
  434. const yh2 = this.yScale.bandwidth() * 0.5;
  435. this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0]);
  436. this.windowedData.map(d => {
  437. const marketDepth = d.marketDepth;
  438. const ts = d.ts;
  439. const maxBidAskVol = extractMaxVolume(d);
  440. // draw buys
  441. if (marketDepth.buys && marketDepth.buys.length > 0) {
  442. let color = d3.color(this.defaults.buyColor).rgb();
  443. marketDepth.buys.map(buy => {
  444. color.opacity = buy.qty / maxBidAskVol;
  445. this.drawingContext.fillStyle = color.toString();
  446. this.drawingContext.fillRect(
  447. this.xScale(ts),
  448. this.yScale(buy.rate),
  449. this.xScale.bandwidth(),
  450. this.yScale.bandwidth()
  451. );
  452. });
  453. }
  454. // draw sells
  455. if (marketDepth.sells && marketDepth.sells.length > 0) {
  456. let color = d3.color(this.defaults.sellColor).rgb();
  457. marketDepth.sells.map(sell => {
  458. color.opacity = sell.qty / maxBidAskVol;
  459. this.drawingContext.fillStyle = color.toString();
  460. this.drawingContext.fillRect(
  461. this.xScale(ts),
  462. this.yScale(sell.rate),
  463. this.xScale.bandwidth(),
  464. this.yScale.bandwidth()
  465. );
  466. });
  467. }
  468. // draw trade line and size
  469. let color = d3.color(this.defaults.tradeColor).rgb();
  470. color.opacity = 1;
  471. this.drawingContext.lineWidth = 1;
  472. this.drawingContext.fillStyle = color.toString();
  473. const r = /*xh2*/ this.defaults.volumeCircleMaxRadius * (+marketDepth.lastTradedQty / maxTradedVolume);
  474. this.drawingContext.beginPath();
  475. this.drawingContext.arc(
  476. this.xScale(ts) /* + xh2*/,
  477. this.yScale(+marketDepth.lastTradedPrice) /* + yh2*/,
  478. r, 0, 2 * Math.PI
  479. );
  480. this.drawingContext.fill();
  481. });
  482. // draw line path
  483. this.drawingContext.beginPath();
  484. d3.line()
  485. .x(d => this.xScale(d.ts))
  486. .y(d => this.yScale(+d.marketDepth.lastTradedPrice))
  487. .curve(d3.curveLinear)
  488. .context(this.drawingContext)
  489. (this.windowedData);
  490. this.drawingContext.lineWidth = 1;
  491. this.drawingContext.strokeStyle = this.defaults.tradeColor;
  492. this.drawingContext.stroke();
  493. }
  494. this.drawingContext.restore();
  495. }
  496. /**
  497. * Clear the canvas area
  498. * @param {number} x x coordinate
  499. * @param {number} y y xoordinate
  500. * @param {number} w width
  501. * @param {number} h height
  502. * @param {string} color color string
  503. */
  504. clearCanvas = (x, y, w, h, color) => {
  505. // console.log('clear canvas area', x, y, w, h, color);
  506. if (this.drawingContext !== null) {
  507. this.drawingContext.save();
  508. this.drawingContext.fillStyle = color || this.defaults.clearColor;
  509. this.drawingContext.fillRect(x, y, w, h);
  510. this.drawingContext.restore();
  511. }
  512. }
  513. // ------------------------------ END: Canvas draw functions ---------------------------------------
  514. /**
  515. * Set Data for the Heatmap to generate
  516. * @param {any[]} data The data to set
  517. */
  518. setData = (data) => {
  519. // console.log('setdata called=', data);
  520. if (data && data.length > 0) {
  521. this.data = data;
  522. this.updateWindowedData();
  523. }
  524. }
  525. /**
  526. * Add as extra data to existing data array.
  527. * @param {any} data
  528. */
  529. addData = (data) => {
  530. if (typeof (data) === 'object') {
  531. this.data.push(data);
  532. this.updateWindowedData();
  533. }
  534. }
  535. /**
  536. * This updates the data in array to be viewed in graph
  537. */
  538. updateWindowedData = () => {
  539. // console.log('window data updated');
  540. this.moveDataWindow(this.data.length - this.windowLength - 1);
  541. }
  542. /**
  543. * Move the position of data window within the main data.
  544. * @param {number} position The target position of the window to be moved to.
  545. */
  546. moveDataWindow = (position) => {
  547. if (position !== this.windowPosition && position > -1 && position < this.data.length - this.windowLength) {
  548. // move position only if within valid range
  549. this.windowedData = this.data.slice(position, position + this.windowLength);
  550. this.windowPosition = position;
  551. if (this.windowPosition === this.data.length - this.windowLength - 1) {
  552. // enable auto scroll
  553. this.autoScroll = true;
  554. }
  555. // console.log('moveDataWindow = ', position, this.windowPosition, this.windowLength, this.data.length, this.autoScroll, this.windowedData);
  556. // update the map
  557. this.updateHeatmap();
  558. }
  559. }
  560. /**
  561. * This sets the Heatmap Zoom level aka. window.
  562. * @param {number} zoom The seconds to zoom into
  563. */
  564. setZoomLevel = (zoom) => {
  565. let l = Math.min(Math.max(zoom, 3), this.data.length - 1);
  566. let l2 = this.windowLength - l;
  567. this.windowLength = l;
  568. this.moveDataWindow(this.windowPosition + l2);
  569. }
  570. /**
  571. * Render Function
  572. */
  573. render() {
  574. const { width, height } = this.props;
  575. // console.log('heatmap rendered', width, height, this.data);
  576. return (
  577. <canvas ref={this.canvasRef} width={width || 300} height={height || 150} tabIndex={1}
  578. style={{
  579. display: 'block',
  580. width: '100%',
  581. height: '100%',
  582. cursor: 'crosshair',
  583. }}></canvas>
  584. );
  585. }
  586. }