index.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import React from 'react';
  2. import styles from './styles.module.css';
  3. import * as d3Scale from 'd3-scale';
  4. import * as d3Array from 'd3-array';
  5. import * as d3Color from 'd3-color';
  6. import * as d3Format from 'd3-format';
  7. import * as d3Interpolate from 'd3-interpolate';
  8. import * as d3Shape from 'd3-shape';
  9. import * as d3Zoom from 'd3-zoom';
  10. import { extractBidPrices, extractBidVolumes, extractMaxTradedVolume, extractMaxVolume } from './utils';
  11. export const d3 = Object.assign(
  12. Object.assign(
  13. Object.assign({}, d3Scale, d3Array, d3Color)
  14. , d3Format, d3Interpolate, d3Shape
  15. )
  16. , d3Zoom
  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. autoScroll = true;
  33. /** Default Theme colors and dimensions */
  34. defaults = {
  35. borderPadding: [0, 0, 0, 0],
  36. bidAskWidth: 150,
  37. axisYWidth: 50,
  38. axisXHeight: 50,
  39. buyColor: '#66ed91',
  40. textOnBuyColor: '#ffffff',
  41. sellColor: '#ed6666',
  42. textOnSellColor: '#ffffff',
  43. tradeColor: '#7434eb',
  44. hmWidth: () => (this.props.width - this.defaults.borderPadding[1] - this.defaults.borderPadding[3] - this.defaults.bidAskWidth - this.defaults.axisYWidth),
  45. hmHeight: () => (this.props.height - this.defaults.borderPadding[0] - this.defaults.borderPadding[2] - this.defaults.axisXHeight),
  46. clearColor: '#ffffff',
  47. };
  48. shouldComponentUpdate(nextProps, nextState) {
  49. // console.log('shouldComponentUpdate', nextProps);
  50. const shouldUpdate = this.props.width !== nextProps.width
  51. || this.props.height !== nextProps.height;
  52. if (shouldUpdate) this.updateHeatmap();
  53. return shouldUpdate;
  54. }
  55. // -------------------START:: Lifecycle methods to retrive 2d context from updated dom-------------------------
  56. componentDidMount() {
  57. // console.log('component mouted');
  58. if (this.canvasRef.current !== null) {
  59. this.drawingContext = this.canvasRef.current.getContext('2d');
  60. }
  61. }
  62. componentDidUpdate() {
  63. // console.log('component updtated');
  64. if (this.canvasRef.current !== null) {
  65. this.drawingContext = this.canvasRef.current.getContext('2d');
  66. }
  67. }
  68. // -------------------END:: Lifecycle methods to retrive 2d context from updated dom---------------------------
  69. // ------------------ D3 Variables ---------------------
  70. /** @type {d3Scale.ScaleBand<string>} */
  71. xScale = null;
  72. /** @type {d3Scale.ScaleLinear<number, number>} */
  73. bidAskScale = null;
  74. /** @type {d3Scale.ScaleBand<string>} */
  75. yScale = null;
  76. // ------------------ D3 Variables ---------------------
  77. /**
  78. * This function will be called if there is any dimension change on heatmap
  79. * This function changes the d3 scales based on windowed data
  80. */
  81. updateHeatmapDimensions() {
  82. console.log('heatmap dimension updated, update scale domains');
  83. const { width, height } = this.props;
  84. if (width > 0 && height > 0 && this.windowedData.length > 0) {
  85. // setup x-scale
  86. this.xScale = d3.scaleBand()
  87. .range([0, this.defaults.hmWidth()])
  88. .domain(this.windowedData.map(d => d.ts));
  89. // setup y-scale
  90. this.yScale = d3.scaleBand()
  91. .range([this.defaults.hmHeight(), 0])
  92. .domain(extractBidPrices(this.windowedData).sort());
  93. // setup bid ask scale
  94. this.bidAskScale = d3.scaleLinear()
  95. .range([0, this.defaults.bidAskWidth])
  96. .domain([0, d3.max(extractBidVolumes(this.windowedData[this.windowedData.length - 1]))]);
  97. }
  98. }
  99. /**
  100. * This method will be called after an update of internal data is performed.
  101. */
  102. updateHeatmap() {
  103. // 1. update scale and dimensions
  104. this.updateHeatmapDimensions();
  105. // 2. Draw the bid ask spread heatmap
  106. this.clearCanvas(this.defaults.borderPadding[3], this.defaults.borderPadding[0],
  107. this.defaults.hmWidth(), this.defaults.hmHeight(), this.defaults.clearColor);
  108. this.drawMainGraph();
  109. // console.log('heatmap draw update');
  110. // this.clearCanvas(0, 0, this.defaults.hmWidth(), this.defaults.hmHeight(), '#aaaaaa');
  111. }
  112. // ------------------------------ START: Canvas draw functions ---------------------------------------
  113. /**
  114. * Draws background heatmap for both buys and sells
  115. */
  116. drawMainGraph() {
  117. this.drawingContext.save();
  118. if (this.xScale && this.yScale && this.bidAskScale && this.drawingContext !== null) {
  119. const maxTradedVolume = extractMaxTradedVolume(this.windowedData);
  120. const xh2 = this.xScale.bandwidth() * 0.5;
  121. const yh2 = this.yScale.bandwidth() * 0.5;
  122. this.windowedData.map(d => {
  123. const marketDepth = d.marketDepth;
  124. const ts = d.ts;
  125. const maxBidAskVol = extractMaxVolume(d);
  126. // draw
  127. this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0]);
  128. // draw buys
  129. if (marketDepth.buys && marketDepth.buys.length > 0) {
  130. let color = d3.color(this.defaults.buyColor).rgb();
  131. marketDepth.buys.map(buy => {
  132. color.opacity = buy.qty / maxBidAskVol;
  133. this.drawingContext.fillStyle = color.toString();
  134. this.drawingContext.fillRect(
  135. this.xScale(ts),
  136. this.yScale(buy.rate),
  137. this.xScale.bandwidth(),
  138. this.yScale.bandwidth()
  139. );
  140. });
  141. }
  142. // draw sells
  143. if (marketDepth.sells && marketDepth.sells.length > 0) {
  144. let color = d3.color(this.defaults.sellColor).rgb();
  145. marketDepth.sells.map(sell => {
  146. color.opacity = sell.qty / maxBidAskVol;
  147. this.drawingContext.fillStyle = color.toString();
  148. this.drawingContext.fillRect(
  149. this.xScale(ts),
  150. this.yScale(sell.rate),
  151. this.xScale.bandwidth(),
  152. this.yScale.bandwidth()
  153. );
  154. });
  155. }
  156. // draw trade line and size
  157. let color = d3.color(this.defaults.tradeColor).rgb();
  158. color.opacity = 1;
  159. this.drawingContext.lineWidth = 1;
  160. this.drawingContext.fillStyle = color.toString();
  161. const r = xh2 * (+marketDepth.lastTradedQty / maxTradedVolume);
  162. this.drawingContext.beginPath();
  163. this.drawingContext.arc(
  164. this.xScale(ts) /* + xh2*/,
  165. this.yScale(+marketDepth.lastTradedPrice) /* + yh2*/,
  166. r, 0, 2 * Math.PI
  167. );
  168. this.drawingContext.fill();
  169. });
  170. // draw line path
  171. this.drawingContext.beginPath();
  172. d3.line()
  173. .x(d => this.xScale(d.ts))
  174. .y(d => this.yScale(+d.marketDepth.lastTradedPrice))
  175. .curve(d3.curveLinear)
  176. .context(this.drawingContext)
  177. (this.windowedData);
  178. this.drawingContext.lineWidth = 1;
  179. this.drawingContext.strokeStyle = this.defaults.tradeColor;
  180. this.drawingContext.stroke();
  181. }
  182. this.drawingContext.restore();
  183. }
  184. /**
  185. * Clear the canvas area
  186. * @param {number} x x coordinate
  187. * @param {number} y y xoordinate
  188. * @param {number} w width
  189. * @param {number} h height
  190. * @param {string} color color string
  191. */
  192. clearCanvas(x, y, w, h, color) {
  193. // console.log('clear canvas area', x, y, w, h, color);
  194. if (this.drawingContext !== null) {
  195. this.drawingContext.save();
  196. this.drawingContext.fillStyle = color || this.defaults.clearColor;
  197. this.drawingContext.fillRect(x, y, w, h);
  198. this.drawingContext.restore();
  199. }
  200. }
  201. // ------------------------------ END: Canvas draw functions ---------------------------------------
  202. /**
  203. * Set Data for the Heatmap to generate
  204. * @param {any[]} data The data to set
  205. */
  206. setData(data) {
  207. console.log('setdata called=', data);
  208. if (data && data.length > 0) {
  209. this.data = data;
  210. this.updateWindowedData();
  211. }
  212. }
  213. /**
  214. * Add as extra data to existing data array.
  215. * @param {any} data
  216. */
  217. addData(data) {
  218. if (typeof (data) === 'object') {
  219. this.data.push(data);
  220. this.updateWindowedData();
  221. }
  222. }
  223. /**
  224. * This updates the data in array to be viewed in graph
  225. */
  226. updateWindowedData() {
  227. if (this.autoScroll || (this.windowedData.length === 0)) {
  228. this.windowedData = this.data.slice(-this.windowLength);
  229. }
  230. this.updateHeatmap();
  231. }
  232. /**
  233. * Render Function
  234. */
  235. render() {
  236. const { width, height } = this.props;
  237. console.log('heatmap rendered', width, height, this.data);
  238. return (
  239. <canvas ref={this.canvasRef} width={width || 300} height={height || 150} className={styles.mapCanvas}></canvas>
  240. );
  241. }
  242. }