Pārlūkot izejas kodu

Heatmap completed without any interaction.

rongmz 5 gadi atpakaļ
vecāks
revīzija
89c8640076
2 mainītis faili ar 126 papildinājumiem un 15 dzēšanām
  1. 2 0
      package.json
  2. 124 15
      src/index.js

+ 2 - 0
package.json

@@ -69,10 +69,12 @@
   "dependencies": {
     "d3-array": "^2.7.1",
     "d3-color": "^2.0.0",
+    "d3-ease": "^2.0.0",
     "d3-format": "^2.0.0",
     "d3-interpolate": "^2.0.1",
     "d3-scale": "^3.2.2",
     "d3-shape": "^2.0.0",
+    "d3-timer": "^2.0.0",
     "d3-zoom": "^2.0.0"
   }
 }

+ 124 - 15
src/index.js

@@ -7,6 +7,8 @@ import * as d3Format from 'd3-format';
 import * as d3Interpolate from 'd3-interpolate';
 import * as d3Shape from 'd3-shape';
 import * as d3Zoom from 'd3-zoom';
+import * as d3Timer from 'd3-timer';
+import * as d3Ease from 'd3-ease';
 import { extractBidPrices, extractBidVolumes, extractMaxTradedVolume, extractMaxVolume } from './utils';
 
 export const d3 = Object.assign(
@@ -14,7 +16,7 @@ export const d3 = Object.assign(
     Object.assign({}, d3Scale, d3Array, d3Color)
     , d3Format, d3Interpolate, d3Shape
   )
-  , d3Zoom
+  , d3Zoom, d3Ease, d3Timer
 );
 
 /**
@@ -37,19 +39,22 @@ export default class StockHeatmap extends React.Component {
 
   /** Default Theme colors and dimensions */
   defaults = {
-    borderPadding: [0, 0, 0, 0],
-    bidAskWidth: 150,
+    borderPadding: [5, 5, 0, 0],
+    bidAskWidth: 100,
     axisYWidth: 50,
     axisXHeight: 50,
-    buyColor: '#66ed91',
+    buyColor: '#388e3c',
     textOnBuyColor: '#ffffff',
-    sellColor: '#ed6666',
+    sellColor: '#d32f2f',
     textOnSellColor: '#ffffff',
+    textOnBackground: '#000000',
     tradeColor: '#7434eb',
     axisTickSize: 6,
     axisColor: '#000000',
     xAxisTextPadding: 6,
     yAxisTextPadding: 6,
+    bidAskGraphPaddingLeft: 10,
+    bidAskTransitionDuration: 500,
     hmWidth: () => (this.props.width - this.defaults.borderPadding[1] - this.defaults.borderPadding[3] - this.defaults.bidAskWidth - this.defaults.axisYWidth),
     hmHeight: () => (this.props.height - this.defaults.borderPadding[0] - this.defaults.borderPadding[2] - this.defaults.axisXHeight),
     clearColor: '#ffffff',
@@ -59,7 +64,6 @@ export default class StockHeatmap extends React.Component {
     // console.log('shouldComponentUpdate', nextProps);
     const shouldUpdate = this.props.width !== nextProps.width
       || this.props.height !== nextProps.height;
-    if (shouldUpdate) this.updateHeatmap();
     return shouldUpdate;
   }
 
@@ -68,12 +72,14 @@ export default class StockHeatmap extends React.Component {
     // console.log('component mouted');
     if (this.canvasRef.current !== null) {
       this.drawingContext = this.canvasRef.current.getContext('2d');
+      this.updateHeatmap();
     }
   }
   componentDidUpdate() {
     // console.log('component updtated');
     if (this.canvasRef.current !== null) {
       this.drawingContext = this.canvasRef.current.getContext('2d');
+      this.updateHeatmap();
     }
   }
   // -------------------END:: Lifecycle methods to retrive 2d context from updated dom---------------------------
@@ -88,15 +94,18 @@ export default class StockHeatmap extends React.Component {
   yScale = null;
   /** @type {number[]} */
   yDomainValues = null;
+  /** @type {d3Timer.Timer} */
+  bidAskAnimTimer = null;
+  /** @type {{[key:number]:number}} */
+  bidAskBarAnimConfig = {};
   // ------------------ D3 Variables ---------------------
 
-
   /**
    * This function will be called if there is any dimension change on heatmap
    * This function changes the d3 scales based on windowed data
    */
   updateHeatmapDimensions() {
-    console.log('heatmap dimension updated, update scale domains');
+    // console.log('heatmap dimension updated, update scale domains');
     const { width, height } = this.props;
     if (width > 0 && height > 0 && this.windowedData.length > 0) {
       // setup x-scale
@@ -121,6 +130,7 @@ export default class StockHeatmap extends React.Component {
    */
   updateHeatmap() {
     if (this.drawingContext !== null) {
+      // console.log('heatmap update req');
       // 1. update scale and dimensions
       this.updateHeatmapDimensions();
 
@@ -131,8 +141,10 @@ export default class StockHeatmap extends React.Component {
 
       // 3. Draw xy Axis
       this.drawXAxis();
-      this.drawYAxis();
+      this.drawYAxisAndBidAskGraph();
 
+      // 4. Draw buy-to-sell ratio
+      this.drawBuy2SellRatio();
 
       // console.log('heatmap draw update');
       // this.clearCanvas(0, 0, this.defaults.hmWidth(), this.defaults.hmHeight(), '#aaaaaa');
@@ -141,6 +153,29 @@ export default class StockHeatmap extends React.Component {
 
   // ------------------------------ START: Canvas draw functions ---------------------------------------
 
+  /**
+   * Draw buy/sell ratio at bottom right corner
+   */
+  drawBuy2SellRatio() {
+    if (this.windowedData.length > 0) {
+      // dimension
+      const d = this.windowedData[this.windowedData.length - 1];
+      const x = this.defaults.borderPadding[3] + this.defaults.hmWidth() + this.defaults.axisTickSize;
+      const y = this.defaults.borderPadding[0] + this.defaults.hmHeight() + this.defaults.axisTickSize;
+      const w = this.props.width - x;
+      const h = this.props.height - y;
+      this.clearCanvas(x, y, w, h, this.defaults.clearColor);
+      let textHeight = (h - 15) / 2;
+      this.drawingContext.save();
+      this.drawingContext.textAlign = 'center';
+      this.drawingContext.textBaseline = 'middle';
+      this.drawingContext.font = `bold ${textHeight}px sans-serif`;
+      this.drawingContext.fillText((d.marketDepth.buyOrderVolume / d.marketDepth.sellOrderVolume).toFixed(2), x + w / 2, y + textHeight / 2);
+      this.drawingContext.fillText('Buy/Sell', x + w / 2, y + textHeight * 1.5 + 5);
+      this.drawingContext.restore();
+    }
+  }
+
   /**
    * Draws X Axis
    */
@@ -172,9 +207,9 @@ export default class StockHeatmap extends React.Component {
   }
 
   /**
-   * Draws Y Axis
+   * Draws Y Axis and Bid Ask graph at the same time
    */
-  drawYAxis() {
+  drawYAxisAndBidAskGraph() {
     if (this.yDomainValues !== null) {
       // clear canvas before axis draw
       this.clearCanvas(
@@ -189,16 +224,82 @@ export default class StockHeatmap extends React.Component {
       this.drawingContext.lineTo(0, this.defaults.hmHeight() + this.defaults.axisTickSize);
       this.drawingContext.textAlign = 'start';
       this.drawingContext.textBaseline = 'top';
+      let maxTextWidth = 0;
       this.yDomainValues.map(d => {
         let y = this.yScale(d);
         this.drawingContext.moveTo(0, y);
         this.drawingContext.lineTo(this.defaults.axisTickSize, y);
-        this.drawingContext.fillText(d.toFixed(2), this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
+        this.drawingContext.fillText(d.toFixed(2), this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y + 2, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
+        let tw = this.drawingContext.measureText(d.toFixed(2)).width;
+        maxTextWidth = maxTextWidth >= tw ? maxTextWidth : tw;
       });
       this.drawingContext.lineWidth = 1.2;
       this.drawingContext.strokeStyle = this.defaults.axisColor;
       this.drawingContext.stroke();
       this.drawingContext.restore();
+
+      // Now I will draw the bid ask strength graph,
+      const x = this.defaults.borderPadding[3] + this.defaults.hmWidth() + maxTextWidth + this.defaults.axisTickSize + this.defaults.yAxisTextPadding + this.defaults.bidAskGraphPaddingLeft;
+      const y = this.defaults.borderPadding[0];
+      this.drawBidAskGraph(x, y);
+    }
+  }
+
+  /**
+   * Draw and animate Bid Ask graph
+   * @param {number} x 
+   * @param {number} y 
+   */
+  drawBidAskGraph(x, y) {
+    if (this.windowedData.length > 0) {
+      if (this.bidAskAnimTimer !== null) {
+        this.bidAskAnimTimer.stop();
+        this.bidAskAnimTimer = null;
+      }
+      this.bidAskAnimTimer = d3.timer(elapsed => {
+        // compute how far through the animation we are (0 to 1)
+        const t = Math.min(1, d3.easeCubic(elapsed / this.defaults.bidAskTransitionDuration));
+
+        // ----------------draw--------------------
+        // console.log('drawing bid ask graph');
+        this.clearCanvas(
+          x, y, this.defaults.bidAskWidth, this.defaults.hmHeight() + this.defaults.axisTickSize, this.defaults.clearColor
+        );
+        const h = this.yScale.bandwidth() - 2;
+        const d = this.windowedData[this.windowedData.length - 1];
+        const maxBidAskVol = extractMaxVolume(d);
+        this.drawingContext.save();
+        this.drawingContext.translate(x, y);
+        this.drawingContext.lineWidth = 0;
+        this.drawingContext.textBaseline = 'middle';
+        const drawBars = (arr, color, textColor) => {
+          arr.map(v => {
+            this.drawingContext.fillStyle = color;
+            const l = this.defaults.bidAskWidth * (+v.qty / maxBidAskVol);
+            // save v bars length
+            this.bidAskBarAnimConfig[v.rate] = d3.interpolateNumber(this.bidAskBarAnimConfig[v.rate] || 0, l)(t);
+            this.drawingContext.fillRect(0, this.yScale(v.rate), this.bidAskBarAnimConfig[v.rate], h);
+            let tw = this.drawingContext.measureText(v.qty).width;
+            if (this.defaults.bidAskWidth - this.bidAskBarAnimConfig[v.rate] - 2 >= tw) {
+              // text outside bar
+              this.drawingContext.textAlign = 'start';
+              this.drawingContext.fillStyle = this.defaults.textOnBackground;
+              this.drawingContext.fillText(v.qty, this.bidAskBarAnimConfig[v.rate] + 2, this.yScale(v.rate) + h / 2 + 1);
+            } else {
+              this.drawingContext.textAlign = 'end';
+              this.drawingContext.fillStyle = textColor;
+              this.drawingContext.fillText(v.qty, this.bidAskBarAnimConfig[v.rate] - 2, this.yScale(v.rate) + h / 2 + 1);
+            }
+          });
+        }
+        drawBars(d.marketDepth.buys, this.defaults.buyColor, this.defaults.textOnBuyColor);
+        drawBars(d.marketDepth.sells, this.defaults.sellColor, this.defaults.textOnSellColor);
+        this.drawingContext.restore();
+        // ----------------draw--------------------
+
+        // if this animation is over
+        if (t === 1) this.bidAskAnimTimer.stop();
+      });
     }
   }
 
@@ -211,12 +312,11 @@ export default class StockHeatmap extends React.Component {
       const maxTradedVolume = extractMaxTradedVolume(this.windowedData);
       const xh2 = this.xScale.bandwidth() * 0.5;
       const yh2 = this.yScale.bandwidth() * 0.5;
+      this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0]);
       this.windowedData.map(d => {
         const marketDepth = d.marketDepth;
         const ts = d.ts;
         const maxBidAskVol = extractMaxVolume(d);
-        // draw 
-        this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0]);
         // draw buys
         if (marketDepth.buys && marketDepth.buys.length > 0) {
           let color = d3.color(this.defaults.buyColor).rgb();
@@ -317,6 +417,7 @@ export default class StockHeatmap extends React.Component {
     }
   }
 
+  i = 0;
   /**
    * This updates the data in array to be viewed in graph
    */
@@ -325,6 +426,14 @@ export default class StockHeatmap extends React.Component {
       this.windowedData = this.data.slice(-this.windowLength);
     }
     this.updateHeatmap();
+    // -------------------- TEST --------------------
+    // setInterval(() => {
+    //   this.i++;
+    //   this.windowedData = this.data.slice(this.i, this.windowLength + this.i);
+    //   console.log('changed', this.windowedData);
+    //   this.updateHeatmap();
+    // }, 1000);
+    // -------------------- TEST --------------------
   }
 
   /**
@@ -332,7 +441,7 @@ export default class StockHeatmap extends React.Component {
    */
   render() {
     const { width, height } = this.props;
-    console.log('heatmap rendered', width, height, this.data);
+    // console.log('heatmap rendered', width, height, this.data);
     return (
       <canvas ref={this.canvasRef} width={width || 300} height={height || 150} className={styles.mapCanvas}></canvas>
     );