index.js 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  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. extractMedianTradedVolume,
  16. extractMaxVolume,
  17. zoomTimeFormat,
  18. extractMaxAskBidVolume
  19. } from "./utils";
  20. export const d3 = Object.assign(
  21. Object.assign(
  22. Object.assign({}, d3Scale, d3Array, d3Color)
  23. , d3Format, d3Interpolate, d3Shape
  24. )
  25. , d3Ease, d3Timer
  26. );
  27. /**
  28. * Stock Heatmap
  29. *
  30. */
  31. export default class StockHeatmap extends React.Component {
  32. /** @type {React.RefObject<HTMLCanvasElement>} */
  33. canvasRef = React.createRef();
  34. /** @type {CanvasRenderingContext2D} */
  35. drawingContext = null;
  36. data = [];
  37. windowedData = [];
  38. windowLength = 20;
  39. windowPosition = 0;
  40. isMerged = false; // 是否合并了
  41. isMerge = false; // 是否启用合并
  42. symbol = 'None'
  43. // 鼠标坐标
  44. mouse = {
  45. x: 0,
  46. y: 0
  47. }
  48. // 圆球存放处,事件触发之后需要在这里面取数据进行展示
  49. circles = [];
  50. // 最大的买卖数量
  51. maxBidAskVolume = 0;
  52. // 绘制间隔,60是帧率
  53. drawTimestampDistance = parseInt(1000 / 60);
  54. // 上次绘制时间
  55. prevDrawTimestamp = 0;
  56. orderbookColors = [
  57. '#086892',
  58. '#2f9dd2',
  59. '#fffc19',
  60. '#ff9a01',
  61. ];
  62. /** Default Theme colors and dimensions */
  63. defaults = {
  64. borderPadding: [5, 5, 0, 0],
  65. bidAskWidth: 100,
  66. axisYWidth: 50,
  67. axisXHeight: 50,
  68. buyColor: '#388e3c',
  69. textOnBuyColor: '#ffffff',
  70. sellColor: '#d32f2f',
  71. textOnSellColor: '#ffffff',
  72. textOnBackground: '#000000',
  73. textHighlightOnBackground: '#ff0000',
  74. tradeColor: '#7434eb',
  75. axisTickSize: 6,
  76. axisColor: '#000000',
  77. xAxisTextPadding: 6,
  78. yAxisTextPadding: 6,
  79. bidAskGraphPaddingLeft: 10,
  80. bidAskTransitionDuration: 500,
  81. volumeCircleMaxRadius: 20,
  82. runningRatioSeconds: 5,
  83. hmWidth: () => (this.props.width - this.defaults.borderPadding[1] - this.defaults.borderPadding[3] - this.defaults.bidAskWidth - this.defaults.axisYWidth),
  84. hmHeight: () => (this.props.height - this.defaults.borderPadding[0] - this.defaults.borderPadding[2] - this.defaults.axisXHeight),
  85. clearColor: '#ffffff',
  86. };
  87. shouldComponentUpdate(nextProps, nextState) {
  88. // console.log('shouldComponentUpdate', nextProps);
  89. const shouldUpdate = this.props.width !== nextProps.width
  90. || this.props.height !== nextProps.height;
  91. if (shouldUpdate) {
  92. this.detachMouseListeners();
  93. }
  94. return shouldUpdate;
  95. }
  96. // -------------------START:: Lifecycle methods to retrive 2d context from updated dom-------------------------
  97. componentDidMount() {
  98. // console.log('component mouted');
  99. if (this.canvasRef.current !== null) {
  100. this.drawingContext = this.canvasRef.current.getContext('2d');
  101. this.updateHeatmap();
  102. this.attachMouseListeners();
  103. }
  104. const panel = this
  105. setInterval(() => {
  106. panel.updateHeatmap()
  107. }, 10)
  108. }
  109. componentDidUpdate() {
  110. // console.log('component updtated');
  111. if (this.canvasRef.current !== null) {
  112. this.drawingContext = this.canvasRef.current.getContext('2d');
  113. this.updateHeatmap();
  114. this.attachMouseListeners();
  115. }
  116. }
  117. componentWillUnmount() {
  118. this.detachMouseListeners();
  119. }
  120. // -------------------END:: Lifecycle methods to retrive 2d context from updated dom---------------------------
  121. // ------------------ START:: Mouse Event listeners -------------------
  122. isMouseDown = false;
  123. mouseDownX = 0;
  124. /**
  125. * Attaches mouse interaction event listeners
  126. */
  127. attachMouseListeners = () => {
  128. if (this.canvasRef.current !== null) {
  129. this.canvasRef.current.addEventListener('mousedown', this.eventMouseDown);
  130. this.canvasRef.current.addEventListener('mousemove', this.eventMouseMove);
  131. this.canvasRef.current.addEventListener('mouseup', this.eventMouseUp);
  132. this.canvasRef.current.addEventListener('wheel', this.eventZoomWheel);
  133. this.canvasRef.current.addEventListener('keydown', this.eventKeyDown);
  134. }
  135. }
  136. /**
  137. * Detaches mouse interaction event listeners
  138. */
  139. detachMouseListeners = () => {
  140. if (this.canvasRef.current !== null) {
  141. this.canvasRef.current.removeEventListener('mousedown', this.eventMouseDown);
  142. this.canvasRef.current.removeEventListener('mousemove', this.eventMouseMove);
  143. this.canvasRef.current.removeEventListener('mouseup', this.eventMouseUp);
  144. this.canvasRef.current.removeEventListener('wheel', this.eventZoomWheel);
  145. this.canvasRef.current.removeEventListener('keydown', this.eventKeyDown);
  146. }
  147. }
  148. /**
  149. * Mouse down event on canvas
  150. * @param {MouseEvent} e
  151. */
  152. eventMouseDown = (e) => {
  153. // console.log('eventMouseDown', e);
  154. if (!this.isMouseDown) {
  155. this.isMouseDown = true;
  156. this.mouseDownX = e.clientX;
  157. }
  158. }
  159. /**
  160. * Mouse move event on canvas
  161. * @param {MouseEvent} e
  162. */
  163. eventMouseMove = (e) => {
  164. // 判断鼠标拖拽距离,这个只是判断拖拽是否满足阈值,使其画面滚动暂停
  165. const downDragLength = Math.abs(e.clientX - this.mouseDownX);
  166. if (this.isMouseDown && downDragLength > 200) {
  167. this.props.toggleAutoScroll(false)
  168. }
  169. // 其他事件处理
  170. if (this.isMouseDown && this.xScale) {
  171. // Mouse drag, scroll the time series,距离上一次移动视野的鼠标拖拽距离
  172. let dragX = e.clientX - this.mouse.x;
  173. // 合并数据,拖动范围要增加
  174. if (this.isMerged) {
  175. dragX = dragX * 10;
  176. }
  177. const moveDataPointsCount = Math.floor(Math.abs(dragX) / this.xScale.bandwidth());
  178. if (moveDataPointsCount > 0) this.mouse.x = e.x
  179. // const moveDataPointDirection = dragLength >= 0 ? 'right' : 'left';
  180. // console.log('drag x=', dragLength, moveDataPointsCount, this.windowPosition);
  181. this.moveDataWindow(this.windowPosition + moveDataPointsCount * (dragX >= 0 ? -1 : 1));
  182. } else {
  183. if (e.clientX < this.defaults.hmWidth() && e.clientY < this.defaults.hmHeight()) {
  184. this.mouse = {
  185. x: e.clientX,
  186. y: e.clientY
  187. };
  188. }
  189. }
  190. }
  191. /**
  192. * Mouse up event on canvas
  193. * @param {MouseEvent} e
  194. */
  195. eventMouseUp = (e) => {
  196. // console.log('eventMouseUp',e);
  197. this.isMouseDown = false;
  198. this.mouseDownX = 0;
  199. }
  200. /**
  201. * Wheel event on canvas to zoom
  202. * @param {WheelEvent} e
  203. */
  204. eventZoomWheel = (e) => {
  205. const direction = e.deltaY < 0 ? 'zoom-in' : 'zoom-out';
  206. let l = 0, l2 = 0;
  207. switch (direction) {
  208. case 'zoom-in':
  209. l = Math.max(this.windowLength - 25, 10);
  210. break;
  211. case 'zoom-out':
  212. l = Math.min(this.windowLength + 25, this.data.length);
  213. break;
  214. }
  215. l2 = this.windowLength - l;
  216. this.windowLength = l;
  217. this.moveDataWindow(this.windowPosition + l2);
  218. // console.log('zoom Level=', this.windowLength);
  219. }
  220. /**
  221. * Event to be triggered when keyboard key is pressed
  222. * @param {KeyboardEvent} e
  223. */
  224. eventKeyDown = (e) => {
  225. e.preventDefault();
  226. console.log('key event', e.isComposing, e.key, e.ctrlKey);
  227. switch (e.key) {
  228. case 'ArrowLeft':
  229. this.moveDataWindow(this.windowPosition - (e.ctrlKey ? 10 : 1));
  230. break;
  231. case 'ArrowRight':
  232. this.moveDataWindow(this.windowPosition + (e.ctrlKey ? 10 : 1));
  233. break;
  234. }
  235. }
  236. // ------------------ END:: Mouse Event listeners ---------------------
  237. // ------------------ D3 Variables ---------------------
  238. /** @type {d3Scale.ScaleBand<string>} */
  239. xScale = null;
  240. /** @type {d3Scale.ScaleLinear<number, number>} */
  241. bidAskScale = null;
  242. /** @type {d3Scale.ScaleBand<string>} */
  243. yScale = null;
  244. /** @type {number[]} */
  245. yDomainValues = null;
  246. /** @type {d3Timer.Timer} */
  247. bidAskAnimTimer = null;
  248. // ------------------ D3 Variables ---------------------
  249. /**
  250. * This function will be called if there is any dimension change on heatmap
  251. * This function changes the d3 scales based on windowed data
  252. */
  253. updateHeatmapDimensions = () => {
  254. const { width, height } = this.props;
  255. if (width > 0 && height > 0 && this.windowedData.length > 0) {
  256. // setup x-scale
  257. this.xScale = d3.scaleBand()
  258. .range([0, this.defaults.hmWidth()])
  259. .domain(this.windowedData.map(d => d.ts));
  260. // setup y-scale
  261. this.yDomainValues = extractBidPrices(this.windowedData).sort((a, b) => a - b);
  262. this.yScale = d3.scaleBand()
  263. .range([this.defaults.hmHeight(), 0])
  264. .domain(this.yDomainValues);
  265. // setup bid ask scale
  266. this.bidAskScale = d3.scaleLinear()
  267. .range([0, this.defaults.bidAskWidth])
  268. .domain([0, d3.max(extractBidVolumes(this.windowedData[this.windowedData.length - 1]))]);
  269. }
  270. }
  271. /**
  272. * This method will be called after an update of internal data is performed.
  273. */
  274. updateHeatmap = () => {
  275. // 绘制器有效性判断
  276. if (this.drawingContext === null) {
  277. return;
  278. }
  279. // 绘制间隔判断
  280. if (new Date().getTime() - this.prevDrawTimestamp < this.drawTimestampDistance) {
  281. return;
  282. }
  283. // 1. update scale and dimensions
  284. this.updateHeatmapDimensions();
  285. // 2. Draw the bid ask spread heatmap
  286. this.clearCanvas(this.defaults.borderPadding[3], this.defaults.borderPadding[0],
  287. this.defaults.hmWidth(), this.defaults.hmHeight(), this.defaults.clearColor);
  288. this.drawMainGraph();
  289. // 3. Draw xy Axis
  290. this.drawXAxis();
  291. this.drawYAxisAndBidAskGraph();
  292. // 4. Draw buy-to-sell ratio
  293. this.drawBuy2SellRatio();
  294. // 5. Draw mouse
  295. this.drawMouse();
  296. // 重设绘制间隔
  297. this.prevDrawTimestamp = new Date().getTime()
  298. }
  299. // ------------------------------ START: Canvas draw functions ---------------------------------------
  300. getMousePos = (canvas, x, y) => {
  301. var rect = canvas.getBoundingClientRect();
  302. // 使用容器的内部宽度进行计算,排除 padding 影响
  303. var style = window.getComputedStyle(canvas.parentNode);
  304. var paddingLeft = parseFloat(style.paddingLeft) || 0;
  305. var paddingRight = parseFloat(style.paddingRight) || 0;
  306. var effectiveWidth = rect.width - paddingLeft - paddingRight;
  307. return {
  308. x: (x - rect.left - paddingLeft) * (canvas.width / effectiveWidth) * 1.03,
  309. y: (y - rect.top) * (canvas.height / rect.height)
  310. };
  311. }
  312. drawMouse = () => {
  313. const canvas = this.canvasRef.current;
  314. if (!canvas) return;
  315. const context = canvas.getContext('2d');
  316. if (!context) return;
  317. let mouse = this.getMousePos(canvas, this.mouse.x, this.mouse.y)
  318. const x = mouse.x;
  319. const y = mouse.y;
  320. // 移动事件处理,指上圆球事件处理
  321. let drawPending = []
  322. this.circles.forEach(circle => {
  323. const distance = Math.sqrt(Math.pow(x - circle.x, 2) + Math.pow(y - circle.y, 2));
  324. if (distance <= circle.radius) {
  325. let d = circle.data
  326. drawPending.push(d)
  327. }
  328. });
  329. if (drawPending.length > 0) {
  330. const WIDTH = 100
  331. const HEIGHT = drawPending.length * 45
  332. // 绘制方块
  333. let color = d3.color('#fff').rgb();
  334. color.opacity = 0.7
  335. this.drawingContext.fillStyle = color.toString();
  336. this.drawingContext.fillRect(
  337. x + 2,
  338. y + 2,
  339. WIDTH,
  340. HEIGHT
  341. );
  342. // 绘制方块内数据
  343. drawPending.map((d, index) => {
  344. let depth = d.marketDepth;
  345. let text = depth.lastSellQty !== 0 ? `卖出 ${depth.lastSellQty} 在 ${depth.lastSellPrice}` : `买入 ${depth.lastBuyQty} 在 ${depth.lastBuyPrice}`
  346. context.fillStyle = d3.color(depth.side === 'sell' ? '#222' : '#222').rgb();
  347. context.fillText(`${d.ts}`, x + 10, y + 7 + 15 + index * 45);
  348. context.fillText(text, x + 10, y + 7 + 30 + index * 45);
  349. if (index < drawPending.length - 1) {
  350. context.fillText("--------------", x + 10, y + 7 + 45 + index * 45);
  351. }
  352. })
  353. }
  354. //
  355. // // 清除canvas并重新绘制
  356. // context.clearRect(0, 0, canvas.width, canvas.height);
  357. // 绘制交叉线和文本
  358. context.beginPath();
  359. context.moveTo(x, 30);
  360. context.lineTo(x, this.defaults.hmHeight());
  361. context.moveTo(30, y);
  362. context.lineTo(this.defaults.hmWidth(), y);
  363. context.strokeStyle = '#000';
  364. context.stroke();
  365. }
  366. /**
  367. * Draw buy/sell ratio at bottom right corner
  368. */
  369. drawBuy2SellRatio = () => {
  370. if (this.windowedData.length > 0) {
  371. // dimension
  372. const d = this.windowedData[this.windowedData.length - 1];
  373. const x = this.defaults.borderPadding[3] + this.defaults.hmWidth() + this.defaults.axisTickSize;
  374. const y = this.defaults.borderPadding[0] + this.defaults.hmHeight() + this.defaults.axisTickSize;
  375. const w = this.props.width - x;
  376. const h = this.props.height - y;
  377. this.clearCanvas(x, y, w, h, this.defaults.clearColor);
  378. let textHeight = (h - 10) / 2;
  379. this.drawingContext.save();
  380. this.drawingContext.textAlign = 'center';
  381. this.drawingContext.textBaseline = 'middle';
  382. // this.drawingContext.font = `bold ${textHeight}px sans-serif`;
  383. // this.drawingContext.fillText(, x + w * 1/2, y + textHeight / 2);
  384. // // Runing average ratio
  385. // if(this.windowedData.length >= this.defaults.runningRatioSeconds) {
  386. // let sellT20RunningSum = 0;
  387. // let buyT20RunningSum = 0;
  388. // for (let i = this.windowedData.length - 1; i >= this.windowedData.length - this.defaults.runningRatioSeconds; i--) {
  389. // sellT20RunningSum += (this.windowedData[i].marketDepth.sells || []).reduce((vol, s) => vol + s.qty,0);
  390. // buyT20RunningSum += (this.windowedData[i].marketDepth.buys || []).reduce((vol, s) => vol + s.qty,0);
  391. // }
  392. // const newBSTPFactor = (buyT20RunningSum / sellT20RunningSum);
  393. // this.drawingContext.fillText(newBSTPFactor.toFixed(2), x + w /4, y + textHeight *0.5);
  394. // }
  395. this.drawingContext.font = `bold ${13}px sans-serif`;
  396. this.drawingContext.textBaseline = 'bottom';
  397. this.drawingContext.fillText(`买卖比: ${(d.marketDepth.buyOrderVolume / d.marketDepth.sellOrderVolume).toFixed(2)}`, x + w / 2, y + textHeight * 2 + 5);
  398. this.drawingContext.restore();
  399. }
  400. }
  401. /**
  402. * Draws X Axis
  403. */
  404. drawXAxis = () => {
  405. // clear canvas before axis draw
  406. this.clearCanvas(
  407. this.defaults.borderPadding[3], this.defaults.borderPadding[0] + this.defaults.hmHeight(),
  408. this.defaults.hmWidth(), this.defaults.axisXHeight, this.defaults.clearColor
  409. );
  410. // draw axis
  411. this.drawingContext.save();
  412. this.drawingContext.beginPath();
  413. this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0] + this.defaults.hmHeight());
  414. this.drawingContext.moveTo(0, 0);
  415. this.drawingContext.lineTo(this.defaults.hmWidth(), 0);
  416. this.drawingContext.textAlign = 'center';
  417. this.drawingContext.textBaseline = 'top';
  418. // const assumedTextWidth = this.drawingContext.measureText('77:77:77').width + 20;
  419. // const maxTickInterval = this.defaults.hmWidth() / assumedTextWidth;
  420. const bandInterval = Math.max(1, parseInt(this.windowedData.length / (this.defaults.hmWidth() / 102)));
  421. // console.log('bandInterval=', bandInterval);
  422. let panel = this;
  423. this.windowedData.map((d, i) => {
  424. if (i !=0 && i % bandInterval === 0) {
  425. let x = this.xScale(d.ts);
  426. if (x + d.ts.length * 5 < panel.defaults.hmWidth()) {
  427. this.drawingContext.moveTo(x, 0);
  428. this.drawingContext.lineTo(x, this.defaults.axisTickSize);
  429. this.drawingContext.fillText(d.ts, x, this.defaults.axisTickSize + this.defaults.xAxisTextPadding);
  430. }
  431. }
  432. });
  433. this.drawingContext.textAlign = 'left';
  434. this.drawingContext.font = '12px Arial';
  435. // ========================================= 底部文字绘制 =========================================
  436. // 绘制品种
  437. let symbol = this.symbol
  438. let symbolText = `品种: ${symbol}`
  439. this.drawingContext.fillText(symbolText, 5, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  440. let w = this.drawingContext.measureText(symbolText).width;
  441. // 绘制视域
  442. let zoomLevelText = `当前视域: ${zoomTimeFormat(this.windowedData)} `
  443. this.drawingContext.fillText(zoomLevelText, 20 + w + 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  444. w += this.drawingContext.measureText(zoomLevelText).width;
  445. const maxVolumeInWindowData = extractMaxTradedVolume(this.windowedData);
  446. // 绘制最大交易量
  447. const maxVolumeText = `最近${zoomTimeFormat(this.windowedData)}内最大交易量: `;
  448. this.drawingContext.fillText(maxVolumeText, 20 + w + 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  449. this.drawingContext.fillStyle = this.defaults.textHighlightOnBackground;
  450. w += this.drawingContext.measureText(maxVolumeText).width;
  451. this.drawingContext.font = 'bold 12px Arial';
  452. this.drawingContext.fillText(`${maxVolumeInWindowData}`, 20 + w + 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  453. w += this.drawingContext.measureText(`${maxVolumeInWindowData}`).width;
  454. // 最后交易数据的绘制
  455. let latested = this.windowedData[this.windowedData.length - 1]
  456. if (this.windowedData.length > 0) {
  457. this.drawingContext.fillStyle = this.defaults.textOnBackground;
  458. this.drawingContext.font = '12px Arial';
  459. this.drawingContext.fillText(`
  460. 最后交易价格: ${latested.marketDepth.side === 'buy' ? latested.marketDepth.lastBuyPrice : latested.marketDepth.lastSellPrice}
  461. 最后交易数量: ${latested.marketDepth.side === 'buy' ? latested.marketDepth.lastBuyQty : latested.marketDepth.lastSellQty}
  462. 最后交易时间: ${latested.ts}
  463. `, 20 + w + 40, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
  464. }
  465. this.drawingContext.fillStyle = this.defaults.textOnBackground;
  466. this.drawingContext.lineWidth = 1.2;
  467. this.drawingContext.strokeStyle = this.defaults.axisColor;
  468. this.drawingContext.stroke();
  469. this.drawingContext.restore();
  470. }
  471. /**
  472. * Draws Y Axis and Bid Ask graph at the same time
  473. */
  474. drawYAxisAndBidAskGraph = () => {
  475. if (this.yDomainValues !== null) {
  476. const yh2 = this.yScale.bandwidth() * 0.5;
  477. // clear canvas before axis draw
  478. this.clearCanvas(
  479. this.defaults.borderPadding[3] + this.defaults.hmWidth(), this.defaults.borderPadding[0],
  480. this.defaults.axisYWidth, this.defaults.hmHeight() + this.defaults.axisTickSize, this.defaults.clearColor
  481. );
  482. // translate and draw
  483. this.drawingContext.save();
  484. this.drawingContext.beginPath();
  485. this.drawingContext.translate(this.defaults.borderPadding[3] + this.defaults.hmWidth(), this.defaults.borderPadding[0]);
  486. this.drawingContext.moveTo(0, 0);
  487. this.drawingContext.lineTo(0, this.defaults.hmHeight() + this.defaults.axisTickSize);
  488. this.drawingContext.textAlign = 'start';
  489. this.drawingContext.textBaseline = 'top';
  490. let maxTextWidth = 0;
  491. this.yDomainValues.map(d => {
  492. let y = this.yScale(d) + yh2;
  493. this.drawingContext.moveTo(0, y);
  494. this.drawingContext.lineTo(this.defaults.axisTickSize, y);
  495. // 大于7位,换行绘制
  496. if (String(d).length <= 7) {
  497. y -= 5
  498. this.drawingContext.fillText(d, this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
  499. } else {
  500. y -= 10
  501. let text = String(d)
  502. let [t0, t1] = text.split('.')
  503. this.drawingContext.fillText(t0, this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
  504. this.drawingContext.fillText('.'.concat(t1), this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y + 10, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
  505. }
  506. let tw = this.drawingContext.measureText(d).width;
  507. maxTextWidth = maxTextWidth >= tw ? maxTextWidth : tw;
  508. });
  509. this.drawingContext.lineWidth = 1.2;
  510. this.drawingContext.strokeStyle = this.defaults.axisColor;
  511. this.drawingContext.stroke();
  512. this.drawingContext.restore();
  513. // Now I will draw the bid ask strength graph,
  514. const x = this.defaults.borderPadding[3] + this.defaults.hmWidth() + maxTextWidth + this.defaults.axisTickSize + this.defaults.yAxisTextPadding + this.defaults.bidAskGraphPaddingLeft;
  515. const y = this.defaults.borderPadding[0];
  516. this.drawBidAskGraph(x, y);
  517. }
  518. }
  519. /**
  520. * Draw and animate Bid Ask graph
  521. * @param {number} x
  522. * @param {number} y
  523. */
  524. drawBidAskGraph = (x, y) => {
  525. if (this.windowedData.length > 0) {
  526. if (this.bidAskAnimTimer !== null) {
  527. this.bidAskAnimTimer.stop();
  528. this.bidAskAnimTimer = null;
  529. }
  530. this.bidAskAnimTimer = d3.timer(elapsed => {
  531. // compute how far through the animation we are (0 to 1)
  532. const t = Math.min(1, d3.easeCubic(elapsed / this.defaults.bidAskTransitionDuration));
  533. // ----------------draw--------------------
  534. // console.log('drawing bid ask graph');
  535. this.clearCanvas(
  536. x, y, this.defaults.bidAskWidth, this.defaults.hmHeight() + this.defaults.axisTickSize, this.defaults.clearColor
  537. );
  538. const h = this.yScale.bandwidth() - 2;
  539. const d = this.windowedData[this.windowedData.length - 1];
  540. if (!d) {
  541. return
  542. }
  543. const maxBidAskVol = extractMaxVolume(d);
  544. this.drawingContext.save();
  545. this.drawingContext.translate(x, y);
  546. this.drawingContext.lineWidth = 0;
  547. this.drawingContext.textBaseline = 'middle';
  548. const drawBars = (arr, color, textColor) => {
  549. arr.map(v => {
  550. this.drawingContext.fillStyle = color;
  551. const l = this.defaults.bidAskWidth * (+v.qty / maxBidAskVol);
  552. // save v bars length
  553. this.drawingContext.fillRect(0, this.yScale(v.rate), l, h);
  554. let textWidth = this.drawingContext.measureText(v.qty).width;
  555. if (this.defaults.bidAskWidth - l - textWidth >= textWidth) {
  556. // text outside bar
  557. this.drawingContext.textAlign = 'start';
  558. this.drawingContext.fillStyle = this.defaults.textOnBackground;
  559. this.drawingContext.fillText(v.qty, l + 1, this.yScale(v.rate) + h / 2 + 1);
  560. } else {
  561. this.drawingContext.textAlign = 'end';
  562. this.drawingContext.fillStyle = textColor;
  563. this.drawingContext.fillText(v.qty, l - textWidth, this.yScale(v.rate) + h / 2 + 1);
  564. }
  565. });
  566. }
  567. drawBars(d.marketDepth.buys, this.defaults.buyColor, this.defaults.textOnBuyColor);
  568. drawBars(d.marketDepth.sells, this.defaults.sellColor, this.defaults.textOnSellColor);
  569. this.drawingContext.restore();
  570. // ----------------draw--------------------
  571. // if this animation is over
  572. if (t === 1) this.bidAskAnimTimer.stop();
  573. });
  574. }
  575. }
  576. /**
  577. * Draws background heatmap for both buys and sells
  578. */
  579. drawMainGraph = () => {
  580. this.drawingContext.save();
  581. if (this.xScale && this.yScale && this.bidAskScale && this.drawingContext !== null) {
  582. // const avgTradedVolume = extractAvgTradedVolume(this.data);
  583. const medianTradedVolume = extractMedianTradedVolume(this.windowedData);
  584. const maxBidAskVolume = extractMaxAskBidVolume(this.windowedData);
  585. const xh2 = this.xScale.bandwidth() * 0.5;
  586. const yh2 = this.yScale.bandwidth() * 0.5;
  587. const panel = this
  588. this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0]);
  589. this.windowedData.map(d => {
  590. const marketDepth = d.marketDepth;
  591. const ask1 = marketDepth.sells[0];
  592. const bid1 = marketDepth.buys[0];
  593. const ts = d.ts;
  594. // draw buys
  595. let color = d3.color('#339933').rgb();
  596. if (marketDepth.buys && marketDepth.buys.length > 0) {
  597. marketDepth.buys.map(buy => {
  598. // let rate = buy.qty / maxBidAskVolume;
  599. // let colorIndex = Math.min(parseInt(rate / 0.25), panel.orderbookColors.length - 1);
  600. // let color = d3.color(panel.orderbookColors[colorIndex]).rgb()
  601. //
  602. // color.opacity = 0.5 + 0.5 * (rate % 0.25) / 0.25;
  603. color.opacity = buy.qty / maxBidAskVolume;
  604. this.drawingContext.fillStyle = color.toString();
  605. this.drawingContext.fillRect(
  606. this.xScale(ts),
  607. this.yScale(buy.rate), // 减去半个方块的高度
  608. this.xScale.bandwidth(),
  609. this.yScale.bandwidth()
  610. );
  611. });
  612. }
  613. // draw sells
  614. color = d3.color('#993333').rgb();
  615. if (marketDepth.sells && marketDepth.sells.length > 0) {
  616. marketDepth.sells.map(sell => {
  617. // let rate = sell.qty / maxBidAskVolume;
  618. // let colorIndex = Math.min(parseInt(rate / 0.25), panel.orderbookColors.length - 1);
  619. // let color = d3.color(panel.orderbookColors[colorIndex]).rgb()
  620. //
  621. // color.opacity = 0.5 + 0.5 * (rate % 0.25) / 0.25;
  622. color.opacity = sell.qty / maxBidAskVolume;
  623. this.drawingContext.fillStyle = color.toString();
  624. this.drawingContext.fillRect(
  625. this.xScale(ts),
  626. this.yScale(sell.rate), // 减去半个方块的高度
  627. this.xScale.bandwidth(),
  628. this.yScale.bandwidth()
  629. );
  630. });
  631. }
  632. });
  633. // draw trade size
  634. this.circles = []
  635. this.windowedData.map(d => {
  636. const marketDepth = d.marketDepth;
  637. const maxBidAskVol = extractMaxVolume(d);
  638. const ts = d.ts;
  639. const ask1 = marketDepth.sells[0];
  640. const bid1 = marketDepth.buys[0];
  641. // 绘制买入的圆圈
  642. if (marketDepth.lastBuyQty !== 0) {
  643. let trade_color = d3.color("#44c98b").rgb();
  644. trade_color.opacity = 0.7;
  645. this.drawingContext.fillStyle = trade_color.toString();
  646. const r = 50 - 45 * (2.71 ** (-0.01 * (+marketDepth.lastBuyQty / medianTradedVolume)));
  647. this.drawingContext.beginPath();
  648. this.drawingContext.arc(
  649. this.xScale(ts) + xh2,
  650. this.yScale(+marketDepth.lastBuyPrice) + yh2,
  651. r,
  652. 0,
  653. 2 * Math.PI
  654. );
  655. this.drawingContext.strokeStyle = trade_color;
  656. this.drawingContext.fill();
  657. // 为球添加白色边框
  658. this.drawingContext.lineWidth = 1; // 设置线宽,可以根据需要调整
  659. this.drawingContext.strokeStyle = "white"; // 设置描边颜色为白色
  660. this.drawingContext.stroke(); // 执行描边操作
  661. // 事件触发用
  662. let d_copy = JSON.parse(JSON.stringify(d))
  663. // 只保存需要展示的那边
  664. d_copy.marketDepth.lastSellQty = 0
  665. d_copy.marketDepth.lastSellPrice = 0
  666. this.circles.push({
  667. x: this.xScale(ts) + xh2,
  668. y: this.yScale(+marketDepth.lastBuyPrice) + yh2,
  669. radius: r,
  670. data: d_copy // 存储与圆相关的数据,便于在事件处理时使用
  671. });
  672. }
  673. // 绘制卖出的圆圈
  674. if (marketDepth.lastSellQty !== 0) {
  675. let trade_color = d3.color("#cc5040").rgb();
  676. trade_color.opacity = 0.7;
  677. this.drawingContext.fillStyle = trade_color.toString();
  678. const r = 50 - 45 * (2.71 ** (-0.01 * (+marketDepth.lastSellQty / medianTradedVolume)));
  679. this.drawingContext.beginPath();
  680. this.drawingContext.arc(
  681. this.xScale(ts) + xh2,
  682. this.yScale(+marketDepth.lastSellPrice) + yh2,
  683. r,
  684. 0,
  685. 2 * Math.PI
  686. );
  687. this.drawingContext.strokeStyle = trade_color;
  688. this.drawingContext.fill();
  689. // 为球添加白色边框
  690. this.drawingContext.lineWidth = 1; // 设置线宽,可以根据需要调整
  691. this.drawingContext.strokeStyle = "white"; // 设置描边颜色为白色
  692. this.drawingContext.stroke(); // 执行描边操作
  693. // 事件触发用
  694. let d_copy = JSON.parse(JSON.stringify(d))
  695. // 只保存需要展示的那边
  696. d_copy.marketDepth.lastBuyQty = 0
  697. d_copy.marketDepth.lastBuyPrice = 0
  698. this.circles.push({
  699. x: this.xScale(ts) + xh2,
  700. y: this.yScale(+marketDepth.lastSellPrice) + yh2,
  701. radius: r,
  702. data: d_copy // 存储与圆相关的数据,便于在事件处理时使用
  703. });
  704. }
  705. })
  706. // draw buy line path
  707. let buy_line_color = d3.color(this.defaults.buyColor).rgb();
  708. this.drawingContext.fillStyle = buy_line_color.toString();
  709. this.drawingContext.beginPath();
  710. d3.line()
  711. .x(d => this.xScale(d.ts))
  712. .y(d => this.yScale(d.marketDepth.buys[0].rate) + yh2)
  713. // .curve(d3.curveLinear)
  714. .context(this.drawingContext)
  715. (this.windowedData);
  716. this.drawingContext.lineWidth = 2;
  717. this.drawingContext.strokeStyle = this.defaults.buyColor;
  718. this.drawingContext.stroke();
  719. // draw sell line path
  720. let sell_line_color = d3.color(this.defaults.sellColor).rgb();
  721. this.drawingContext.fillStyle = sell_line_color.toString();
  722. this.drawingContext.beginPath();
  723. d3.line()
  724. .x(d => this.xScale(d.ts))
  725. .y(d => this.yScale(d.marketDepth.sells[0].rate) + yh2)
  726. // .curve(d3.curveLinear)
  727. .context(this.drawingContext)
  728. (this.windowedData);
  729. this.drawingContext.lineWidth = 2;
  730. this.drawingContext.strokeStyle = this.defaults.sellColor;
  731. this.drawingContext.stroke();
  732. }
  733. this.drawingContext.restore();
  734. }
  735. /**
  736. * Clear the canvas area
  737. * @param {number} x x coordinate
  738. * @param {number} y y xoordinate
  739. * @param {number} w width
  740. * @param {number} h height
  741. * @param {string} color color string
  742. */
  743. clearCanvas = (x, y, w, h, color) => {
  744. // console.log('clear canvas area', x, y, w, h, color);
  745. if (this.drawingContext !== null) {
  746. this.drawingContext.save();
  747. this.drawingContext.fillStyle = color || this.defaults.clearColor;
  748. this.drawingContext.fillRect(x, y, w, h);
  749. this.drawingContext.restore();
  750. }
  751. }
  752. // ------------------------------ END: Canvas draw functions ---------------------------------------
  753. /**
  754. * Set Data for the Heatmap to generate
  755. * @param {any[]} data The data to set
  756. */
  757. setData = (data) => {
  758. // console.log('setdata called=', data);
  759. if (data && data.length > 0) {
  760. this.data = data;
  761. this.updateWindowedData();
  762. }
  763. }
  764. /**
  765. * Add as extra data to existing data array.
  766. * @param {any} data
  767. */
  768. addData = (data) => {
  769. if (typeof (data) === 'object') {
  770. this.data.push(data);
  771. this.updateWindowedData();
  772. }
  773. }
  774. /**
  775. * This updates the data in array to be viewed in graph
  776. */
  777. updateWindowedData = () => {
  778. // console.log('window data updated');
  779. if (this.props.autoScroll) {
  780. this.moveDataWindow(this.data.length - this.windowLength - 1);
  781. }
  782. }
  783. isOrderBooksEquals = (orderbooks1, orderbooks2) => {
  784. for (let i = 0; i < orderbooks1.length; i++) {
  785. let orderbook1 = orderbooks1[i]
  786. let orderbook2 = orderbooks2[i]
  787. // 如果d1有,d2没有,那证明不相等
  788. if (!orderbook2) return false
  789. if (orderbook1.rate !== orderbook2.rate) return false
  790. let r1 = (orderbook1.qty / this.maxBidAskVolume);
  791. let o1ColorIndex = Math.min(parseInt(r1 / 0.25), this.orderbookColors.length - 1);
  792. let r2 = (orderbook2.qty / this.maxBidAskVolume);
  793. let o2ColorIndex = Math.min(parseInt(r2 / 0.25), this.orderbookColors.length - 1);
  794. if (o1ColorIndex !== o2ColorIndex) return false
  795. }
  796. return true
  797. }
  798. isDepthEquals = (d1, d2) => {
  799. // buys
  800. if (!this.isOrderBooksEquals(d1.buys, d2.buys)) return false
  801. // sells
  802. if (!this.isOrderBooksEquals(d1.sells, d2.sells)) return false
  803. return true
  804. }
  805. mergeSnapshots = (snapshots) => {
  806. // 初始化合并后的结构
  807. const merged = {
  808. marketDepth: {
  809. avgPrice: 0,
  810. buyOrderVolume: 0,
  811. sellOrderVolume: 0,
  812. lastBuyPrice: 0,
  813. lastBuyQty: 0,
  814. lastSellPrice: 0,
  815. lastSellQty: 0,
  816. lastTradedTS: 0,
  817. open: 0,
  818. high: 0,
  819. low: 0,
  820. close: 0,
  821. priceChangeAmt: 0,
  822. priceChangePct: "0",
  823. buys: [],
  824. sells: []
  825. },
  826. pendingOrders: [],
  827. time: "",
  828. tradingsymbol: "",
  829. ts: ""
  830. };
  831. // 初始化计数器
  832. let totalAvgPrice = 0;
  833. let totalBuyOrderVolume = 0;
  834. let totalSellOrderVolume = 0;
  835. let totalSnapshots = snapshots.length;
  836. // 识别最大的深度档位,取消之前的5档固定的
  837. let maxDepthLevel = snapshots[0].marketDepth.buys.length
  838. snapshots.forEach(snapshot => {
  839. if (maxDepthLevel < snapshot.marketDepth.buys.length) {
  840. maxDepthLevel = snapshot.marketDepth.buys.length
  841. }
  842. if (maxDepthLevel < snapshot.marketDepth.sells.length) {
  843. maxDepthLevel = snapshot.marketDepth.sells.length
  844. }
  845. })
  846. // 初始化买卖数组
  847. let buySums = Array(maxDepthLevel).fill({ rate: 0, orders: 0, qty: 0 });
  848. let sellSums = Array(maxDepthLevel).fill({ rate: 0, orders: 0, qty: 0 });
  849. // 初始化交易数量
  850. let totalLastTradeQtyBuy = 0;
  851. let totalLastTradeQtySell = 0;
  852. // 记录第一个快照的时间和交易符号
  853. merged.time = snapshots[0].time;
  854. merged.tradingsymbol = snapshots[0].tradingsymbol;
  855. merged.ts = snapshots[0].ts;
  856. // 遍历所有快照数据
  857. snapshots.forEach(snapshot => {
  858. // 累加市场深度数据
  859. totalAvgPrice += snapshot.marketDepth.avgPrice;
  860. totalBuyOrderVolume += snapshot.marketDepth.buyOrderVolume;
  861. totalSellOrderVolume += snapshot.marketDepth.sellOrderVolume;
  862. // 合并买单和卖单
  863. snapshot.marketDepth.buys.forEach((buy, index) => {
  864. buySums[index] = {
  865. rate: buy.rate,
  866. orders: buySums[index].orders + buy.orders,
  867. qty: buySums[index].qty + buy.qty
  868. };
  869. });
  870. snapshot.marketDepth.sells.forEach((sell, index) => {
  871. sellSums[index] = {
  872. rate: sell.rate,
  873. orders: sellSums[index].orders + sell.orders,
  874. qty: sellSums[index].qty + sell.qty
  875. };
  876. });
  877. // 合并最后交易的数量和价格
  878. totalLastTradeQtyBuy += snapshot.marketDepth.lastBuyQty;
  879. totalLastTradeQtySell += snapshot.marketDepth.lastSellQty;
  880. // 合并其他字段
  881. merged.marketDepth.close = snapshot.marketDepth.close;
  882. merged.marketDepth.high = snapshot.marketDepth.high;
  883. merged.marketDepth.low = snapshot.marketDepth.low;
  884. merged.marketDepth.open = snapshot.marketDepth.open;
  885. merged.marketDepth.priceChangeAmt = snapshot.marketDepth.priceChangeAmt;
  886. merged.marketDepth.priceChangePct = snapshot.marketDepth.priceChangePct;
  887. merged.marketDepth.lastTradedTS = snapshot.marketDepth.lastTradedTS;
  888. });
  889. // 计算平均市场深度数据
  890. merged.marketDepth.avgPrice = totalAvgPrice / totalSnapshots;
  891. merged.marketDepth.buyOrderVolume = totalBuyOrderVolume / totalSnapshots;
  892. merged.marketDepth.sellOrderVolume = totalSellOrderVolume / totalSnapshots;
  893. // 计算平均买单和卖单
  894. merged.marketDepth.buys = buySums.map(buy => ({
  895. rate: buy.rate,
  896. orders: buy.orders / totalSnapshots,
  897. qty: buy.qty / totalSnapshots
  898. }));
  899. merged.marketDepth.sells = sellSums.map(sell => ({
  900. rate: sell.rate,
  901. orders: sell.orders / totalSnapshots,
  902. qty: sell.qty / totalSnapshots
  903. }));
  904. // 计算最终的最后交易的数量和价格
  905. merged.marketDepth.lastBuyPrice = merged.marketDepth.sells[0].rate;
  906. merged.marketDepth.lastBuyQty = totalLastTradeQtyBuy;
  907. merged.marketDepth.lastSellPrice = merged.marketDepth.buys[0].rate;
  908. merged.marketDepth.lastSellQty = totalLastTradeQtySell;
  909. merged.marketDepth.side = 'both';
  910. return merged;
  911. }
  912. mergeWindowedData = () => {
  913. let windowedData = this.windowedData;
  914. this.maxBidAskVolume = extractMaxAskBidVolume(this.windowedData);
  915. let mergedWindowedData = [];
  916. let panel = this
  917. let prevData = undefined;
  918. let snapshots = [];
  919. windowedData.map((d, i) => {
  920. // 最后一个元素要展示,不然会丢失盘口细节
  921. if (i === windowedData.length - 1) {
  922. mergedWindowedData.push(d)
  923. prevData = d
  924. // 如果是第一个数据,则进行初始化
  925. } else if (!prevData) {
  926. prevData = d
  927. // 如果是中间的数据,则进行逻辑判断,是否需要合并
  928. } else if (!panel.isDepthEquals(prevData.marketDepth, d.marketDepth)) {
  929. let mergedData = panel.mergeSnapshots(snapshots)
  930. mergedWindowedData.push(mergedData)
  931. prevData = d
  932. snapshots = [];
  933. }
  934. snapshots.push(d)
  935. });
  936. return mergedWindowedData;
  937. }
  938. /**
  939. * Move the position of data window within the main data.
  940. * @param {number} position The target position of the window to be moved to.
  941. */
  942. moveDataWindow = (position) => {
  943. if (position !== this.windowPosition && position > -1 && position < this.data.length - this.windowLength) {
  944. // move position only if within valid range
  945. this.windowedData = this.data.slice(position, position + this.windowLength + 1);
  946. // 是否启用合并
  947. if (this.isMerge) {
  948. if (this.windowedData.length > 1000) {
  949. this.windowedData = this.mergeWindowedData();
  950. this.isMerged = true;
  951. } else {
  952. this.isMerged = false;
  953. }
  954. }
  955. // 延迟日志
  956. if (this.windowedData.length > 1) {
  957. let last = this.windowedData[this.windowedData.length - 1]
  958. // console.log(new Date().getTime() - last.time, last)
  959. }
  960. this.windowPosition = position;
  961. if (this.windowPosition === this.data.length - this.windowLength - 1) {
  962. // enable auto scroll
  963. this.props.toggleAutoScroll(true);
  964. }
  965. // console.log('moveDataWindow = ', position, this.windowPosition, this.windowLength, this.data.length, this.autoScroll, this.windowedData);
  966. // update the map
  967. this.updateHeatmap();
  968. }
  969. }
  970. /**
  971. * This sets the Heatmap Zoom level aka. window.
  972. * @param {number} zoom The seconds to zoom into
  973. */
  974. setZoomLevel = (zoom) => {
  975. if (this.data.length == 0) {
  976. return
  977. }
  978. let theoreticalWindowDataLength = 0; // 理论数据条数
  979. let zoomMillsTimestamp = zoom * 1000;
  980. let firstIndex = Math.min(this.data.length - 1, this.windowPosition + this.windowLength)
  981. let first = this.data[firstIndex]
  982. for (let i = firstIndex; i >= 0; i--) {
  983. let d = this.data[i]
  984. if (first.time - d.time > zoomMillsTimestamp) {
  985. break
  986. }
  987. theoreticalWindowDataLength += 1;
  988. }
  989. this.windowLength = Math.max(3, theoreticalWindowDataLength - 1);
  990. let pos = Math.max(0, firstIndex - this.windowLength - 1)
  991. this.moveDataWindow(pos);
  992. }
  993. /**
  994. * Render Function
  995. */
  996. render() {
  997. const { width, height } = this.props;
  998. // console.log('heatmap rendered', width, height, this.data);
  999. return (
  1000. <canvas ref={this.canvasRef} width={width || 300} height={height || 150} tabIndex={1}
  1001. style={{
  1002. display: 'block',
  1003. width: '100%',
  1004. height: '100%',
  1005. cursor: 'crosshair',
  1006. paddingRight: '3%',
  1007. boxSizing: "border-box"
  1008. }}></canvas>
  1009. );
  1010. }
  1011. }