index.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  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 {
  11. extractBidPrices,
  12. extractBidVolumes,
  13. extractMaxTradedVolume,
  14. extractAvgTradedVolume,
  15. extractMaxVolume,
  16. zoomTimeFormat,
  17. extractMaxAskBidVolume
  18. } from "./utils";
  19. export const d3 = Object.assign(
  20. Object.assign(
  21. Object.assign({}, d3Scale, d3Array, d3Color)
  22. , d3Format, d3Interpolate, d3Shape
  23. )
  24. , d3Ease, d3Timer
  25. );
  26. /**
  27. * Stock Heatmap
  28. *
  29. */
  30. export default class StockHeatmap extends React.Component {
  31. /** @type {React.RefObject<HTMLCanvasElement>} */
  32. canvasRef = React.createRef();
  33. /** @type {CanvasRenderingContext2D} */
  34. drawingContext = null;
  35. data = [];
  36. windowedData = [];
  37. windowLength = 20;
  38. windowPosition = 0;
  39. mouse = {
  40. x: 0,
  41. y: 0
  42. }
  43. circles = []
  44. /** Default Theme colors and dimensions */
  45. defaults = {
  46. borderPadding: [5, 5, 0, 0],
  47. bidAskWidth: 100,
  48. axisYWidth: 50,
  49. axisXHeight: 50,
  50. buyColor: '#388e3c',
  51. textOnBuyColor: '#ffffff',
  52. sellColor: '#d32f2f',
  53. textOnSellColor: '#ffffff',
  54. textOnBackground: '#000000',
  55. textHighlightOnBackground: '#ff0000',
  56. tradeColor: '#7434eb',
  57. axisTickSize: 6,
  58. axisColor: '#000000',
  59. xAxisTextPadding: 6,
  60. yAxisTextPadding: 6,
  61. bidAskGraphPaddingLeft: 10,
  62. bidAskTransitionDuration: 500,
  63. volumeCircleMaxRadius: 1,
  64. runningRatioSeconds: 5,
  65. hmWidth: () => (this.props.width - this.defaults.borderPadding[1] - this.defaults.borderPadding[3] - this.defaults.bidAskWidth - this.defaults.axisYWidth),
  66. hmHeight: () => (this.props.height - this.defaults.borderPadding[0] - this.defaults.borderPadding[2] - this.defaults.axisXHeight),
  67. clearColor: '#ffffff',
  68. };
  69. shouldComponentUpdate(nextProps, nextState) {
  70. // console.log('shouldComponentUpdate', nextProps);
  71. const shouldUpdate = this.props.width !== nextProps.width
  72. || this.props.height !== nextProps.height;
  73. if (shouldUpdate) {
  74. this.detachMouseListeners();
  75. }
  76. return shouldUpdate;
  77. }
  78. // -------------------START:: Lifecycle methods to retrive 2d context from updated dom-------------------------
  79. componentDidMount() {
  80. // console.log('component mouted');
  81. if (this.canvasRef.current !== null) {
  82. this.drawingContext = this.canvasRef.current.getContext('2d');
  83. this.updateHeatmap();
  84. this.attachMouseListeners();
  85. }
  86. const panel = this
  87. setInterval(() => {
  88. panel.updateHeatmap()
  89. }, 10)
  90. }
  91. componentDidUpdate() {
  92. // console.log('component updtated');
  93. if (this.canvasRef.current !== null) {
  94. this.drawingContext = this.canvasRef.current.getContext('2d');
  95. this.updateHeatmap();
  96. this.attachMouseListeners();
  97. }
  98. }
  99. componentWillUnmount() {
  100. this.detachMouseListeners();
  101. }
  102. // -------------------END:: Lifecycle methods to retrive 2d context from updated dom---------------------------
  103. // ------------------ START:: Mouse Event listeners -------------------
  104. isMouseDown = false;
  105. mouseDownX = 0;
  106. /**
  107. * Attaches mouse interaction event listeners
  108. */
  109. attachMouseListeners = () => {
  110. if (this.canvasRef.current !== null) {
  111. this.canvasRef.current.addEventListener('mousedown', this.eventMouseDown);
  112. this.canvasRef.current.addEventListener('mousemove', this.eventMouseMove);
  113. this.canvasRef.current.addEventListener('mouseup', this.eventMouseUp);
  114. this.canvasRef.current.addEventListener('wheel', this.eventZoomWheel);
  115. this.canvasRef.current.addEventListener('keydown', this.eventKeyDown);
  116. }
  117. }
  118. /**
  119. * Detaches mouse interaction event listeners
  120. */
  121. detachMouseListeners = () => {
  122. if (this.canvasRef.current !== null) {
  123. this.canvasRef.current.removeEventListener('mousedown', this.eventMouseDown);
  124. this.canvasRef.current.removeEventListener('mousemove', this.eventMouseMove);
  125. this.canvasRef.current.removeEventListener('mouseup', this.eventMouseUp);
  126. this.canvasRef.current.removeEventListener('wheel', this.eventZoomWheel);
  127. this.canvasRef.current.removeEventListener('keydown', this.eventKeyDown);
  128. }
  129. }
  130. /**
  131. * Mouse down event on canvas
  132. * @param {MouseEvent} e
  133. */
  134. eventMouseDown = (e) => {
  135. // console.log('eventMouseDown', e);
  136. if (!this.isMouseDown) {
  137. this.isMouseDown = true;
  138. this.mouseDownX = e.clientX;
  139. }
  140. }
  141. /**
  142. * Mouse move event on canvas
  143. * @param {MouseEvent} e
  144. */
  145. eventMouseMove = (e) => {
  146. // 判断鼠标拖拽距离,这个只是判断拖拽是否满足阈值,使其画面滚动暂停
  147. const downDragLength = Math.abs(e.clientX - this.mouseDownX);
  148. if (this.isMouseDown && downDragLength > 200) {
  149. this.props.toggleAutoScroll(false)
  150. }
  151. // 其他事件处理
  152. if (this.isMouseDown && this.xScale) {
  153. // Mouse drag, scroll the time series,距离上一次移动视野的鼠标拖拽距离
  154. const dragX = e.clientX - this.mouse.x;
  155. const moveDataPointsCount = Math.floor(Math.abs(dragX) / this.xScale.bandwidth());
  156. if (moveDataPointsCount > 0) this.mouse.x = e.x
  157. // const moveDataPointDirection = dragLength >= 0 ? 'right' : 'left';
  158. // console.log('drag x=', dragLength, moveDataPointsCount, this.windowPosition);
  159. this.moveDataWindow(this.windowPosition + moveDataPointsCount * (dragX >= 0 ? -1 : 1));
  160. } else {
  161. if (e.clientX < this.defaults.hmWidth() && e.clientY < this.defaults.hmHeight()) {
  162. this.mouse = {
  163. x: e.clientX,
  164. y: e.clientY
  165. };
  166. }
  167. }
  168. }
  169. /**
  170. * Mouse up event on canvas
  171. * @param {MouseEvent} e
  172. */
  173. eventMouseUp = (e) => {
  174. // console.log('eventMouseUp',e);
  175. this.isMouseDown = false;
  176. this.mouseDownX = 0;
  177. }
  178. /**
  179. * Wheel event on canvas to zoom
  180. * @param {WheelEvent} e
  181. */
  182. eventZoomWheel = (e) => {
  183. const direction = e.deltaY < 0 ? 'zoom-in' : 'zoom-out';
  184. let l = 0, l2 = 0;
  185. switch (direction) {
  186. case 'zoom-in':
  187. l = Math.max(this.windowLength - 16, 3);
  188. break;
  189. case 'zoom-out':
  190. l = Math.min(this.windowLength + 16, this.data.length - 16);
  191. break;
  192. }
  193. l2 = this.windowLength - l;
  194. this.windowLength = l;
  195. this.moveDataWindow(this.windowPosition + l2);
  196. // console.log('zoom Level=', this.windowLength);
  197. }
  198. /**
  199. * Event to be triggered when keyboard key is pressed
  200. * @param {KeyboardEvent} e
  201. */
  202. eventKeyDown = (e) => {
  203. e.preventDefault();
  204. console.log('key event', e.isComposing, e.key, e.ctrlKey);
  205. switch (e.key) {
  206. case 'ArrowLeft':
  207. this.moveDataWindow(this.windowPosition - (e.ctrlKey ? 10 : 1));
  208. break;
  209. case 'ArrowRight':
  210. this.moveDataWindow(this.windowPosition + (e.ctrlKey ? 10 : 1));
  211. break;
  212. }
  213. }
  214. // ------------------ END:: Mouse Event listeners ---------------------
  215. // ------------------ D3 Variables ---------------------
  216. /** @type {d3Scale.ScaleBand<string>} */
  217. xScale = null;
  218. /** @type {d3Scale.ScaleLinear<number, number>} */
  219. bidAskScale = null;
  220. /** @type {d3Scale.ScaleBand<string>} */
  221. yScale = null;
  222. /** @type {number[]} */
  223. yDomainValues = null;
  224. /** @type {d3Timer.Timer} */
  225. bidAskAnimTimer = null;
  226. // ------------------ D3 Variables ---------------------
  227. /**
  228. * This function will be called if there is any dimension change on heatmap
  229. * This function changes the d3 scales based on windowed data
  230. */
  231. updateHeatmapDimensions = () => {
  232. const { width, height } = this.props;
  233. if (width > 0 && height > 0 && this.windowedData.length > 0) {
  234. // setup x-scale
  235. this.xScale = d3.scaleBand()
  236. .range([0, this.defaults.hmWidth()])
  237. .domain(this.windowedData.map(d => d.ts));
  238. // setup y-scale
  239. this.yDomainValues = extractBidPrices(this.windowedData).sort((a, b) => a - b);
  240. this.yScale = d3.scaleBand()
  241. .range([this.defaults.hmHeight(), 0])
  242. .domain(this.yDomainValues);
  243. // setup bid ask scale
  244. this.bidAskScale = d3.scaleLinear()
  245. .range([0, this.defaults.bidAskWidth])
  246. .domain([0, d3.max(extractBidVolumes(this.windowedData[this.windowedData.length - 1]))]);
  247. }
  248. }
  249. /**
  250. * This method will be called after an update of internal data is performed.
  251. */
  252. updateHeatmap = () => {
  253. if (this.drawingContext !== null) {
  254. // 1. update scale and dimensions
  255. this.updateHeatmapDimensions();
  256. // 2. Draw the bid ask spread heatmap
  257. this.clearCanvas(this.defaults.borderPadding[3], this.defaults.borderPadding[0],
  258. this.defaults.hmWidth(), this.defaults.hmHeight(), this.defaults.clearColor);
  259. this.drawMainGraph();
  260. // 3. Draw xy Axis
  261. this.drawXAxis();
  262. this.drawYAxisAndBidAskGraph();
  263. // 4. Draw buy-to-sell ratio
  264. this.drawBuy2SellRatio();
  265. // 5. Draw mouse
  266. this.drawMouse();
  267. // console.log('heatmap draw update');
  268. // this.clearCanvas(0, 0, this.defaults.hmWidth(), this.defaults.hmHeight(), '#aaaaaa');
  269. }
  270. }
  271. // ------------------------------ START: Canvas draw functions ---------------------------------------
  272. getMousePos = (canvas, x, y) => {
  273. var rect = canvas.getBoundingClientRect();
  274. // 使用容器的内部宽度进行计算,排除 padding 影响
  275. var style = window.getComputedStyle(canvas.parentNode);
  276. var paddingLeft = parseFloat(style.paddingLeft) || 0;
  277. var paddingRight = parseFloat(style.paddingRight) || 0;
  278. var effectiveWidth = rect.width - paddingLeft - paddingRight;
  279. return {
  280. x: (x - rect.left - paddingLeft) * (canvas.width / effectiveWidth) * 1.03,
  281. y: (y - rect.top) * (canvas.height / rect.height)
  282. };
  283. }
  284. drawMouse = () => {
  285. const canvas = this.canvasRef.current;
  286. if (!canvas) return;
  287. const context = canvas.getContext('2d');
  288. if (!context) return;
  289. let mouse = this.getMousePos(canvas, this.mouse.x, this.mouse.y)
  290. const x = mouse.x;
  291. const y = mouse.y;
  292. // 移动事件处理,指上圆球事件处理
  293. let drawPending = []
  294. this.circles.forEach(circle => {
  295. const distance = Math.sqrt(Math.pow(x - circle.x, 2) + Math.pow(y - circle.y, 2));
  296. if (distance <= circle.radius) {
  297. let d = circle.data
  298. drawPending.push(d)
  299. }
  300. });
  301. if (drawPending.length > 0) {
  302. const WIDTH = 100
  303. const HEIGHT = drawPending.length * 45
  304. // 绘制方块
  305. let color = d3.color('#fff').rgb();
  306. color.opacity = 0.7
  307. this.drawingContext.fillStyle = color.toString();
  308. this.drawingContext.fillRect(
  309. x + 2,
  310. y + 2,
  311. WIDTH,
  312. HEIGHT
  313. );
  314. // 绘制方块内数据
  315. drawPending.map((d, index) => {
  316. let depth = d.marketDepth;
  317. let text = `${depth.side === 'sell' ? '卖出' : '买入'} ${depth.lastTradedQty} 在 ${depth.lastTradedPrice} `;
  318. context.fillStyle = d3.color(depth.side == 'sell' ? '#222' : '#222').rgb();
  319. context.fillText(`${d.ts}`, x + 10, y + 7 + 15 + index * 45);
  320. context.fillText(text, x + 10, y + 7 + 30 + index * 45);
  321. if (index < drawPending.length - 1) {
  322. context.fillText("--------------", x + 10, y + 7 + 45 + index * 45);
  323. }
  324. })
  325. }
  326. //
  327. // // 清除canvas并重新绘制
  328. // context.clearRect(0, 0, canvas.width, canvas.height);
  329. // 绘制交叉线和文本
  330. context.beginPath();
  331. context.moveTo(x, 30);
  332. context.lineTo(x, this.defaults.hmHeight());
  333. context.moveTo(30, y);
  334. context.lineTo(this.defaults.hmWidth(), y);
  335. context.strokeStyle = '#000';
  336. context.stroke();
  337. }
  338. /**
  339. * Draw buy/sell ratio at bottom right corner
  340. */
  341. drawBuy2SellRatio = () => {
  342. if (this.windowedData.length > 0) {
  343. // dimension
  344. const d = this.windowedData[this.windowedData.length - 1];
  345. const x = this.defaults.borderPadding[3] + this.defaults.hmWidth() + this.defaults.axisTickSize;
  346. const y = this.defaults.borderPadding[0] + this.defaults.hmHeight() + this.defaults.axisTickSize;
  347. const w = this.props.width - x;
  348. const h = this.props.height - y;
  349. this.clearCanvas(x, y, w, h, this.defaults.clearColor);
  350. let textHeight = (h - 10) / 2;
  351. this.drawingContext.save();
  352. this.drawingContext.textAlign = 'center';
  353. this.drawingContext.textBaseline = 'middle';
  354. // this.drawingContext.font = `bold ${textHeight}px sans-serif`;
  355. // this.drawingContext.fillText(, x + w * 1/2, y + textHeight / 2);
  356. // // Runing average ratio
  357. // if(this.windowedData.length >= this.defaults.runningRatioSeconds) {
  358. // let sellT20RunningSum = 0;
  359. // let buyT20RunningSum = 0;
  360. // for (let i = this.windowedData.length - 1; i >= this.windowedData.length - this.defaults.runningRatioSeconds; i--) {
  361. // sellT20RunningSum += (this.windowedData[i].marketDepth.sells || []).reduce((vol, s) => vol + s.qty,0);
  362. // buyT20RunningSum += (this.windowedData[i].marketDepth.buys || []).reduce((vol, s) => vol + s.qty,0);
  363. // }
  364. // const newBSTPFactor = (buyT20RunningSum / sellT20RunningSum);
  365. // this.drawingContext.fillText(newBSTPFactor.toFixed(2), x + w /4, y + textHeight *0.5);
  366. // }
  367. this.drawingContext.font = `bold ${13}px sans-serif`;
  368. this.drawingContext.textBaseline = 'bottom';
  369. this.drawingContext.fillText(`买卖比: ${(d.marketDepth.buyOrderVolume / d.marketDepth.sellOrderVolume).toFixed(2)}`, x + w / 2, y + textHeight * 2 + 5);
  370. this.drawingContext.restore();
  371. }
  372. }
  373. /**
  374. * Draws X Axis
  375. */
  376. drawXAxis = () => {
  377. // clear canvas before axis draw
  378. this.clearCanvas(
  379. this.defaults.borderPadding[3], this.defaults.borderPadding[0] + this.defaults.hmHeight(),
  380. this.defaults.hmWidth(), this.defaults.axisXHeight, this.defaults.clearColor
  381. );
  382. // draw axis
  383. this.drawingContext.save();
  384. this.drawingContext.beginPath();
  385. this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0] + this.defaults.hmHeight());
  386. this.drawingContext.moveTo(0, 0);
  387. this.drawingContext.lineTo(this.defaults.hmWidth(), 0);
  388. this.drawingContext.textAlign = 'center';
  389. this.drawingContext.textBaseline = 'top';
  390. // const assumedTextWidth = this.drawingContext.measureText('77:77:77').width + 20;
  391. // const maxTickInterval = this.defaults.hmWidth() / assumedTextWidth;
  392. const bandInterval = Math.max(1, parseInt(this.windowedData.length / (this.defaults.hmWidth() / 102)));
  393. // console.log('bandInterval=', bandInterval);
  394. let panel = this;
  395. this.windowedData.map((d, i) => {
  396. if (i !=0 && i % bandInterval === 0) {
  397. let x = this.xScale(d.ts);
  398. if (x + d.ts.length * 5 < panel.defaults.hmWidth()) {
  399. this.drawingContext.moveTo(x, 0);
  400. this.drawingContext.lineTo(x, this.defaults.axisTickSize);
  401. this.drawingContext.fillText(d.ts, x, this.defaults.axisTickSize + this.defaults.xAxisTextPadding);
  402. }
  403. }
  404. });
  405. this.drawingContext.textAlign = 'left';
  406. this.drawingContext.font = '12px Arial';
  407. let zoomLevelText = `当前视域: ${zoomTimeFormat(this.windowLength)}`
  408. this.drawingContext.fillText(zoomLevelText, 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  409. let w = this.drawingContext.measureText(zoomLevelText).width;
  410. const maxVolumeInWindowData = extractMaxTradedVolume(this.windowedData);
  411. const maxVolumeText = `最近${zoomTimeFormat(this.windowLength, 1)}内最大交易量: `;
  412. this.drawingContext.fillText(maxVolumeText, 20 + w + 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  413. this.drawingContext.fillStyle = this.defaults.textHighlightOnBackground;
  414. w += this.drawingContext.measureText(maxVolumeText).width;
  415. this.drawingContext.font = 'bold 12px Arial';
  416. this.drawingContext.fillText(`${maxVolumeInWindowData}`, 20 + w + 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  417. w += this.drawingContext.measureText(`${maxVolumeInWindowData}`).width;
  418. if (this.windowedData.length > 0) {
  419. this.drawingContext.fillStyle = this.defaults.textOnBackground;
  420. this.drawingContext.font = '12px Arial';
  421. this.drawingContext.fillText(`
  422. 最后交易价格: ${this.windowedData[this.windowedData.length - 1].marketDepth.lastTradedPrice}
  423. 最后交易数量: ${this.windowedData[this.windowedData.length - 1].marketDepth.lastTradedQty}
  424. 最后交易时间: ${this.windowedData[this.windowedData.length - 1].ts}
  425. `, 20 + w + 40, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  426. }
  427. this.drawingContext.fillStyle = this.defaults.textOnBackground;
  428. this.drawingContext.lineWidth = 1.2;
  429. this.drawingContext.strokeStyle = this.defaults.axisColor;
  430. this.drawingContext.stroke();
  431. this.drawingContext.restore();
  432. }
  433. /**
  434. * Draws Y Axis and Bid Ask graph at the same time
  435. */
  436. drawYAxisAndBidAskGraph = () => {
  437. if (this.yDomainValues !== null) {
  438. const yh2 = this.yScale.bandwidth() * 0.5;
  439. // clear canvas before axis draw
  440. this.clearCanvas(
  441. this.defaults.borderPadding[3] + this.defaults.hmWidth(), this.defaults.borderPadding[0],
  442. this.defaults.axisYWidth, this.defaults.hmHeight() + this.defaults.axisTickSize, this.defaults.clearColor
  443. );
  444. // translate and draw
  445. this.drawingContext.save();
  446. this.drawingContext.beginPath();
  447. this.drawingContext.translate(this.defaults.borderPadding[3] + this.defaults.hmWidth(), this.defaults.borderPadding[0]);
  448. this.drawingContext.moveTo(0, 0);
  449. this.drawingContext.lineTo(0, this.defaults.hmHeight() + this.defaults.axisTickSize);
  450. this.drawingContext.textAlign = 'start';
  451. this.drawingContext.textBaseline = 'top';
  452. let maxTextWidth = 0;
  453. this.yDomainValues.map(d => {
  454. let y = this.yScale(d) + yh2;
  455. this.drawingContext.moveTo(0, y);
  456. this.drawingContext.lineTo(this.defaults.axisTickSize, y);
  457. // 大于7位,换行绘制
  458. if (String(d).length <= 7) {
  459. y -= 5
  460. this.drawingContext.fillText(d, this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
  461. } else {
  462. y -= 10
  463. let text = String(d)
  464. let [t0, t1] = text.split('.')
  465. this.drawingContext.fillText(t0, this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
  466. this.drawingContext.fillText('.'.concat(t1), this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y + 10, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
  467. }
  468. let tw = this.drawingContext.measureText(d).width;
  469. maxTextWidth = maxTextWidth >= tw ? maxTextWidth : tw;
  470. });
  471. this.drawingContext.lineWidth = 1.2;
  472. this.drawingContext.strokeStyle = this.defaults.axisColor;
  473. this.drawingContext.stroke();
  474. this.drawingContext.restore();
  475. // Now I will draw the bid ask strength graph,
  476. const x = this.defaults.borderPadding[3] + this.defaults.hmWidth() + maxTextWidth + this.defaults.axisTickSize + this.defaults.yAxisTextPadding + this.defaults.bidAskGraphPaddingLeft;
  477. const y = this.defaults.borderPadding[0];
  478. this.drawBidAskGraph(x, y);
  479. }
  480. }
  481. /**
  482. * Draw and animate Bid Ask graph
  483. * @param {number} x
  484. * @param {number} y
  485. */
  486. drawBidAskGraph = (x, y) => {
  487. if (this.windowedData.length > 0) {
  488. if (this.bidAskAnimTimer !== null) {
  489. this.bidAskAnimTimer.stop();
  490. this.bidAskAnimTimer = null;
  491. }
  492. this.bidAskAnimTimer = d3.timer(elapsed => {
  493. // compute how far through the animation we are (0 to 1)
  494. const t = Math.min(1, d3.easeCubic(elapsed / this.defaults.bidAskTransitionDuration));
  495. // ----------------draw--------------------
  496. // console.log('drawing bid ask graph');
  497. this.clearCanvas(
  498. x, y, this.defaults.bidAskWidth, this.defaults.hmHeight() + this.defaults.axisTickSize, this.defaults.clearColor
  499. );
  500. const h = this.yScale.bandwidth() - 2;
  501. const d = this.windowedData[this.windowedData.length - 1];
  502. if (!d) {
  503. return
  504. }
  505. const maxBidAskVol = extractMaxVolume(d);
  506. this.drawingContext.save();
  507. this.drawingContext.translate(x, y);
  508. this.drawingContext.lineWidth = 0;
  509. this.drawingContext.textBaseline = 'middle';
  510. const drawBars = (arr, color, textColor) => {
  511. arr.map(v => {
  512. this.drawingContext.fillStyle = color;
  513. const l = this.defaults.bidAskWidth * (+v.qty / maxBidAskVol);
  514. // save v bars length
  515. this.drawingContext.fillRect(0, this.yScale(v.rate), l, h);
  516. let textWidth = this.drawingContext.measureText(v.qty).width;
  517. if (this.defaults.bidAskWidth - l - textWidth >= textWidth) {
  518. // text outside bar
  519. this.drawingContext.textAlign = 'start';
  520. this.drawingContext.fillStyle = this.defaults.textOnBackground;
  521. this.drawingContext.fillText(v.qty, l + 1, this.yScale(v.rate) + h / 2 + 1);
  522. } else {
  523. this.drawingContext.textAlign = 'end';
  524. this.drawingContext.fillStyle = textColor;
  525. this.drawingContext.fillText(v.qty, l - textWidth, this.yScale(v.rate) + h / 2 + 1);
  526. }
  527. });
  528. }
  529. drawBars(d.marketDepth.buys, this.defaults.buyColor, this.defaults.textOnBuyColor);
  530. drawBars(d.marketDepth.sells, this.defaults.sellColor, this.defaults.textOnSellColor);
  531. this.drawingContext.restore();
  532. // ----------------draw--------------------
  533. // if this animation is over
  534. if (t === 1) this.bidAskAnimTimer.stop();
  535. });
  536. }
  537. }
  538. /**
  539. * Draws background heatmap for both buys and sells
  540. */
  541. drawMainGraph = () => {
  542. this.drawingContext.save();
  543. if (this.xScale && this.yScale && this.bidAskScale && this.drawingContext !== null) {
  544. const avgTradedVolume = extractAvgTradedVolume(this.windowedData);
  545. const maxBidAskVolume = extractMaxAskBidVolume(this.windowedData);
  546. const xh2 = this.xScale.bandwidth() * 0.5;
  547. const yh2 = this.yScale.bandwidth() * 0.5;
  548. this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0]);
  549. this.windowedData.map(d => {
  550. const marketDepth = d.marketDepth;
  551. const ask1 = marketDepth.sells[0];
  552. const bid1 = marketDepth.buys[0];
  553. const ts = d.ts;
  554. // draw buys
  555. if (marketDepth.buys && marketDepth.buys.length > 0) {
  556. let color = d3.color('#1a506d').rgb();
  557. marketDepth.buys.map(buy => {
  558. color.opacity = buy.qty / maxBidAskVolume;
  559. this.drawingContext.fillStyle = color.toString();
  560. this.drawingContext.fillRect(
  561. this.xScale(ts),
  562. this.yScale(buy.rate), // 减去半个方块的高度
  563. this.xScale.bandwidth(),
  564. this.yScale.bandwidth()
  565. );
  566. });
  567. }
  568. // draw sells
  569. if (marketDepth.sells && marketDepth.sells.length > 0) {
  570. let color = d3.color('#1a506d').rgb();
  571. marketDepth.sells.map(sell => {
  572. color.opacity = sell.qty / maxBidAskVolume;
  573. this.drawingContext.fillStyle = color.toString();
  574. this.drawingContext.fillRect(
  575. this.xScale(ts),
  576. this.yScale(sell.rate), // 减去半个方块的高度
  577. this.xScale.bandwidth(),
  578. this.yScale.bandwidth()
  579. );
  580. });
  581. }
  582. });
  583. // draw trade size
  584. this.circles = []
  585. this.windowedData.map(d => {
  586. const marketDepth = d.marketDepth;
  587. if (+marketDepth.lastTradedQty === 0) {
  588. return
  589. }
  590. const ask1 = marketDepth.sells[0];
  591. const bid1 = marketDepth.buys[0];
  592. const ts = d.ts;
  593. const maxBidAskVol = extractMaxVolume(d);
  594. let trade_color = d3.color(marketDepth.side == 'sell' ? '#cc5040' : '#44c98b').rgb();
  595. trade_color.opacity = 0.7;
  596. this.drawingContext.fillStyle = trade_color.toString();
  597. const r = /*xh2*/ Math.min(this.defaults.volumeCircleMaxRadius * (+marketDepth.lastTradedQty / avgTradedVolume) + 3, 50);
  598. this.drawingContext.beginPath();
  599. this.drawingContext.arc(
  600. this.xScale(ts) + xh2,
  601. this.yScale(+marketDepth.lastTradedPrice) + yh2,
  602. r, 0, 2 * Math.PI
  603. );
  604. this.drawingContext.strokeStyle = trade_color;
  605. this.drawingContext.fill();
  606. // 为球添加白色边框
  607. this.drawingContext.lineWidth = 1; // 设置线宽,可以根据需要调整
  608. this.drawingContext.strokeStyle = 'white'; // 设置描边颜色为白色
  609. this.drawingContext.stroke(); // 执行描边操作
  610. // 事件
  611. this.circles.push({
  612. x: this.xScale(ts) + xh2,
  613. y: this.yScale(+marketDepth.lastTradedPrice) + yh2,
  614. radius: r,
  615. data: d // 存储与圆相关的数据,便于在事件处理时使用
  616. });
  617. })
  618. // draw buy line path
  619. let buy_line_color = d3.color(this.defaults.buyColor).rgb();
  620. this.drawingContext.fillStyle = buy_line_color.toString();
  621. this.drawingContext.beginPath();
  622. d3.line()
  623. .x(d => this.xScale(d.ts))
  624. .y(d => this.yScale(d.marketDepth.buys[0].rate) + yh2)
  625. // .curve(d3.curveLinear)
  626. .context(this.drawingContext)
  627. (this.windowedData);
  628. this.drawingContext.lineWidth = 2;
  629. this.drawingContext.strokeStyle = this.defaults.buyColor;
  630. this.drawingContext.stroke();
  631. // draw sell line path
  632. let sell_line_color = d3.color(this.defaults.sellColor).rgb();
  633. this.drawingContext.fillStyle = sell_line_color.toString();
  634. this.drawingContext.beginPath();
  635. d3.line()
  636. .x(d => this.xScale(d.ts))
  637. .y(d => this.yScale(d.marketDepth.sells[0].rate) + yh2)
  638. // .curve(d3.curveLinear)
  639. .context(this.drawingContext)
  640. (this.windowedData);
  641. this.drawingContext.lineWidth = 2;
  642. this.drawingContext.strokeStyle = this.defaults.sellColor;
  643. this.drawingContext.stroke();
  644. }
  645. this.drawingContext.restore();
  646. }
  647. /**
  648. * Clear the canvas area
  649. * @param {number} x x coordinate
  650. * @param {number} y y xoordinate
  651. * @param {number} w width
  652. * @param {number} h height
  653. * @param {string} color color string
  654. */
  655. clearCanvas = (x, y, w, h, color) => {
  656. // console.log('clear canvas area', x, y, w, h, color);
  657. if (this.drawingContext !== null) {
  658. this.drawingContext.save();
  659. this.drawingContext.fillStyle = color || this.defaults.clearColor;
  660. this.drawingContext.fillRect(x, y, w, h);
  661. this.drawingContext.restore();
  662. }
  663. }
  664. // ------------------------------ END: Canvas draw functions ---------------------------------------
  665. /**
  666. * Set Data for the Heatmap to generate
  667. * @param {any[]} data The data to set
  668. */
  669. setData = (data) => {
  670. // console.log('setdata called=', data);
  671. if (data && data.length > 0) {
  672. this.data = data;
  673. this.updateWindowedData();
  674. }
  675. }
  676. /**
  677. * Add as extra data to existing data array.
  678. * @param {any} data
  679. */
  680. addData = (data) => {
  681. if (typeof (data) === 'object') {
  682. this.data.push(data);
  683. this.updateWindowedData();
  684. }
  685. }
  686. /**
  687. * This updates the data in array to be viewed in graph
  688. */
  689. updateWindowedData = () => {
  690. // console.log('window data updated');
  691. if (this.props.autoScroll) {
  692. this.moveDataWindow(this.data.length - this.windowLength - 1);
  693. }
  694. }
  695. /**
  696. * Move the position of data window within the main data.
  697. * @param {number} position The target position of the window to be moved to.
  698. */
  699. moveDataWindow = (position) => {
  700. if (position !== this.windowPosition && position > -1 && position < this.data.length - this.windowLength) {
  701. // move position only if within valid range
  702. this.windowedData = this.data.slice(position, position + this.windowLength + 1);
  703. // 延迟日志
  704. if (this.windowedData.length > 1) {
  705. let last = this.windowedData[this.windowedData.length - 1]
  706. console.log(new Date().getTime() - last.time, last)
  707. }
  708. this.windowPosition = position;
  709. if (this.windowPosition === this.data.length - this.windowLength - 1) {
  710. // enable auto scroll
  711. this.props.toggleAutoScroll(true);
  712. }
  713. // console.log('moveDataWindow = ', position, this.windowPosition, this.windowLength, this.data.length, this.autoScroll, this.windowedData);
  714. // update the map
  715. this.updateHeatmap();
  716. }
  717. }
  718. /**
  719. * This sets the Heatmap Zoom level aka. window.
  720. * @param {number} zoom The seconds to zoom into
  721. */
  722. setZoomLevel = (zoom) => {
  723. let l = Math.min(Math.max(zoom * 4, 3), this.data.length - 1);
  724. let l2 = this.windowLength - l;
  725. this.windowLength = l;
  726. this.moveDataWindow(this.windowPosition + l2);
  727. }
  728. /**
  729. * Render Function
  730. */
  731. render() {
  732. const { width, height } = this.props;
  733. // console.log('heatmap rendered', width, height, this.data);
  734. return (
  735. <canvas ref={this.canvasRef} width={width || 300} height={height || 150} tabIndex={1}
  736. style={{
  737. display: 'block',
  738. width: '100%',
  739. height: '100%',
  740. cursor: 'crosshair',
  741. paddingRight: '3%',
  742. boxSizing: "border-box"
  743. }}></canvas>
  744. );
  745. }
  746. }