|
|
@@ -7,7 +7,7 @@ import * as d3Interpolate from 'd3-interpolate';
|
|
|
import * as d3Shape from 'd3-shape';
|
|
|
import * as d3Timer from 'd3-timer';
|
|
|
import * as d3Ease from 'd3-ease';
|
|
|
-import { extractBidPrices, extractBidVolumes, extractMaxTradedVolume, extractMaxVolume, zoomTimeFormat } from './utils';
|
|
|
+import { extractBidPrices, extractBidVolumes, extractMaxTradedVolume, extractAvgTradedVolume, extractMaxVolume, zoomTimeFormat } from './utils';
|
|
|
|
|
|
export const d3 = Object.assign(
|
|
|
Object.assign(
|
|
|
@@ -36,6 +36,13 @@ export default class StockHeatmap extends React.Component {
|
|
|
windowPosition = 0;
|
|
|
autoScroll = true;
|
|
|
|
|
|
+ mouse = {
|
|
|
+ x: 0,
|
|
|
+ y: 0
|
|
|
+ }
|
|
|
+
|
|
|
+ circles = []
|
|
|
+
|
|
|
/** Default Theme colors and dimensions */
|
|
|
defaults = {
|
|
|
borderPadding: [5, 5, 0, 0],
|
|
|
@@ -55,7 +62,7 @@ export default class StockHeatmap extends React.Component {
|
|
|
yAxisTextPadding: 6,
|
|
|
bidAskGraphPaddingLeft: 10,
|
|
|
bidAskTransitionDuration: 500,
|
|
|
- volumeCircleMaxRadius: 10,
|
|
|
+ volumeCircleMaxRadius: 1,
|
|
|
runningRatioSeconds: 5,
|
|
|
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),
|
|
|
@@ -80,6 +87,11 @@ export default class StockHeatmap extends React.Component {
|
|
|
this.updateHeatmap();
|
|
|
this.attachMouseListeners();
|
|
|
}
|
|
|
+
|
|
|
+ const panel = this
|
|
|
+ setInterval(() => {
|
|
|
+ panel.updateHeatmap()
|
|
|
+ }, 10)
|
|
|
}
|
|
|
componentDidUpdate() {
|
|
|
// console.log('component updtated');
|
|
|
@@ -149,37 +161,16 @@ export default class StockHeatmap extends React.Component {
|
|
|
// console.log('drag x=', dragLength, moveDataPointsCount, this.windowPosition);
|
|
|
this.moveDataWindow(this.windowPosition + moveDataPointsCount * (dragLength >= 0 ? -1 : 1));
|
|
|
|
|
|
- this.autoScroll = false
|
|
|
+ if (dragLength > 60) {
|
|
|
+ this.autoScroll = false
|
|
|
+ }
|
|
|
} else {
|
|
|
- // const canvas = this.canvasRef.current;
|
|
|
- // if (!canvas) return;
|
|
|
- //
|
|
|
- // const context = canvas.getContext('2d');
|
|
|
- // if (!context) return;
|
|
|
- //
|
|
|
- // // 获取鼠标位置相对于 canvas 的坐标
|
|
|
- // const rect = canvas.getBoundingClientRect();
|
|
|
- // const x = event.clientX - rect.left;
|
|
|
- // const y = event.clientY - rect.top;
|
|
|
- //
|
|
|
- // // 清除canvas并重新绘制
|
|
|
- // context.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
- // // this.drawChart(); // 确保drawChart方法可以访问到context或者在方法内使用this.canvasRef获取
|
|
|
- //
|
|
|
- // this.updateHeatmap();
|
|
|
- //
|
|
|
- // // 绘制交叉线和文本
|
|
|
- // context.beginPath();
|
|
|
- // context.moveTo(x, 0);
|
|
|
- // context.lineTo(x, canvas.height);
|
|
|
- // context.moveTo(0, y);
|
|
|
- // context.lineTo(canvas.width, y);
|
|
|
- // context.strokeStyle = '#eeeeee';
|
|
|
- // context.stroke();
|
|
|
- //
|
|
|
- // context.fillStyle = '#000';
|
|
|
- // context.fillText(`X: ${x.toFixed(2)}`, x + 5, y + 15);
|
|
|
- // context.fillText(`Y: ${y.toFixed(2)}`, x + 5, y + 30);
|
|
|
+ if (e.clientX < this.defaults.hmWidth() && e.clientY < this.defaults.hmHeight()) {
|
|
|
+ this.mouse = {
|
|
|
+ x: e.clientX,
|
|
|
+ y: e.clientY
|
|
|
+ };
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -295,12 +286,78 @@ export default class StockHeatmap extends React.Component {
|
|
|
// 4. Draw buy-to-sell ratio
|
|
|
this.drawBuy2SellRatio();
|
|
|
|
|
|
+ // 5. Draw mouse
|
|
|
+ this.drawMouse();
|
|
|
+
|
|
|
// console.log('heatmap draw update');
|
|
|
// this.clearCanvas(0, 0, this.defaults.hmWidth(), this.defaults.hmHeight(), '#aaaaaa');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// ------------------------------ START: Canvas draw functions ---------------------------------------
|
|
|
+ getMousePos = (canvas, x, y) => {
|
|
|
+ var rect = canvas.getBoundingClientRect();
|
|
|
+
|
|
|
+ // 使用容器的内部宽度进行计算,排除 padding 影响
|
|
|
+ var style = window.getComputedStyle(canvas.parentNode);
|
|
|
+ var paddingLeft = parseFloat(style.paddingLeft) || 0;
|
|
|
+ var paddingRight = parseFloat(style.paddingRight) || 0;
|
|
|
+ var effectiveWidth = rect.width - paddingLeft - paddingRight;
|
|
|
+
|
|
|
+ return {
|
|
|
+ x: (x - rect.left - paddingLeft) * (canvas.width / effectiveWidth) * 1.03,
|
|
|
+ y: (y - rect.top) * (canvas.height / rect.height)
|
|
|
+ };
|
|
|
+ }
|
|
|
+ drawMouse = () => {
|
|
|
+ const canvas = this.canvasRef.current;
|
|
|
+ if (!canvas) return;
|
|
|
+
|
|
|
+ const context = canvas.getContext('2d');
|
|
|
+ if (!context) return;
|
|
|
+
|
|
|
+ let mouse = this.getMousePos(canvas, this.mouse.x, this.mouse.y)
|
|
|
+
|
|
|
+ const x = mouse.x;
|
|
|
+ const y = mouse.y;
|
|
|
+
|
|
|
+ // 移动事件处理
|
|
|
+ this.circles.forEach(circle => {
|
|
|
+ const distance = Math.sqrt(Math.pow(x - circle.x, 2) + Math.pow(y - circle.y, 2));
|
|
|
+ if (distance <= circle.radius) {
|
|
|
+ let d = circle.data
|
|
|
+ let depth = d.marketDepth;
|
|
|
+ let text = `${depth.side} ${depth.lastTradedQty} at ${depth.lastTradedPrice} `;
|
|
|
+
|
|
|
+ let color = d3.color('#fff').rgb();
|
|
|
+ color.opacity = 0.7
|
|
|
+ this.drawingContext.fillStyle = color.toString();
|
|
|
+ this.drawingContext.fillRect(
|
|
|
+ x + 2,
|
|
|
+ y + 2,
|
|
|
+ text.length * 10,
|
|
|
+ 35
|
|
|
+ );
|
|
|
+
|
|
|
+ // 在这里执行更多的操作,如显示详情、更新数据等
|
|
|
+ context.fillStyle = d3.color(depth.side == 'sell' ? '#222' : '#222').rgb();
|
|
|
+ context.fillText(`${d.ts}`, x + 5, y + 15);
|
|
|
+ context.fillText(text, x + 5, y + 30);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ //
|
|
|
+ // // 清除canvas并重新绘制
|
|
|
+ // context.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
+
|
|
|
+ // 绘制交叉线和文本
|
|
|
+ context.beginPath();
|
|
|
+ context.moveTo(x, 30);
|
|
|
+ context.lineTo(x, this.defaults.hmHeight());
|
|
|
+ context.moveTo(30, y);
|
|
|
+ context.lineTo(this.defaults.hmWidth(), y);
|
|
|
+ context.strokeStyle = '#fff';
|
|
|
+ context.stroke();
|
|
|
+ }
|
|
|
|
|
|
/**
|
|
|
* Draw buy/sell ratio at bottom right corner
|
|
|
@@ -357,7 +414,7 @@ export default class StockHeatmap extends React.Component {
|
|
|
this.drawingContext.textBaseline = 'top';
|
|
|
// const assumedTextWidth = this.drawingContext.measureText('77:77:77').width + 20;
|
|
|
// const maxTickInterval = this.defaults.hmWidth() / assumedTextWidth;
|
|
|
- const bandInterval = Math.max(1, parseInt(this.windowedData.length / 18));
|
|
|
+ const bandInterval = Math.max(1, parseInt(this.windowedData.length / (this.defaults.hmWidth() / 102)));
|
|
|
// console.log('bandInterval=', bandInterval);
|
|
|
this.windowedData.map((d, i) => {
|
|
|
if (i !=0 && i % bandInterval === 0) {
|
|
|
@@ -415,8 +472,21 @@ export default class StockHeatmap extends React.Component {
|
|
|
let y = this.yScale(d) + yh2;
|
|
|
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);
|
|
|
- let tw = this.drawingContext.measureText(d.toFixed(2)).width;
|
|
|
+
|
|
|
+ // 大于7位,换行绘制
|
|
|
+ if (String(d).length <= 7) {
|
|
|
+ y -= 5
|
|
|
+ this.drawingContext.fillText(d, this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
|
|
|
+ } else {
|
|
|
+ y -= 10
|
|
|
+ let text = String(d)
|
|
|
+ let [t0, t1] = text.split('.')
|
|
|
+
|
|
|
+ this.drawingContext.fillText(t0, this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
|
|
|
+ this.drawingContext.fillText('.'.concat(t1), this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y + 10, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
|
|
|
+ }
|
|
|
+
|
|
|
+ let tw = this.drawingContext.measureText(d).width;
|
|
|
maxTextWidth = maxTextWidth >= tw ? maxTextWidth : tw;
|
|
|
});
|
|
|
this.drawingContext.lineWidth = 1.2;
|
|
|
@@ -453,6 +523,11 @@ export default class StockHeatmap extends React.Component {
|
|
|
);
|
|
|
const h = this.yScale.bandwidth() - 2;
|
|
|
const d = this.windowedData[this.windowedData.length - 1];
|
|
|
+
|
|
|
+ if (!d) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
const maxBidAskVol = extractMaxVolume(d);
|
|
|
this.drawingContext.save();
|
|
|
this.drawingContext.translate(x, y);
|
|
|
@@ -496,7 +571,7 @@ export default class StockHeatmap extends React.Component {
|
|
|
drawMainGraph = () => {
|
|
|
this.drawingContext.save();
|
|
|
if (this.xScale && this.yScale && this.bidAskScale && this.drawingContext !== null) {
|
|
|
- const maxTradedVolume = extractMaxTradedVolume(this.windowedData);
|
|
|
+ const avgTradedVolume = extractAvgTradedVolume(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]);
|
|
|
@@ -534,11 +609,21 @@ export default class StockHeatmap extends React.Component {
|
|
|
);
|
|
|
});
|
|
|
}
|
|
|
+ });
|
|
|
+
|
|
|
+ // draw trade size
|
|
|
+ this.circles = []
|
|
|
+ this.windowedData.map(d => {
|
|
|
+ const marketDepth = d.marketDepth;
|
|
|
+ const ask1 = marketDepth.sells[0];
|
|
|
+ const bid1 = marketDepth.buys[0];
|
|
|
+ const ts = d.ts;
|
|
|
+ const maxBidAskVol = extractMaxVolume(d);
|
|
|
|
|
|
- // draw trade size
|
|
|
- let trade_color = marketDepth.side == 'sell' ? this.defaults.sellColor : this.defaults.buyColor;
|
|
|
+ let trade_color = d3.color(marketDepth.side == 'sell' ? '#cc5040' : '#44c98b').rgb();
|
|
|
+ trade_color.opacity = 0.7;
|
|
|
this.drawingContext.fillStyle = trade_color.toString();
|
|
|
- const r = /*xh2*/ this.defaults.volumeCircleMaxRadius * (+marketDepth.lastTradedQty / maxTradedVolume) + 3;
|
|
|
+ const r = /*xh2*/ Math.min(this.defaults.volumeCircleMaxRadius * (+marketDepth.lastTradedQty / avgTradedVolume) + 3, 50);
|
|
|
this.drawingContext.beginPath();
|
|
|
this.drawingContext.arc(
|
|
|
this.xScale(ts) + xh2,
|
|
|
@@ -552,7 +637,15 @@ export default class StockHeatmap extends React.Component {
|
|
|
this.drawingContext.lineWidth = 1; // 设置线宽,可以根据需要调整
|
|
|
this.drawingContext.strokeStyle = 'white'; // 设置描边颜色为白色
|
|
|
this.drawingContext.stroke(); // 执行描边操作
|
|
|
- });
|
|
|
+
|
|
|
+ // 事件
|
|
|
+ this.circles.push({
|
|
|
+ x: this.xScale(ts) + xh2,
|
|
|
+ y: this.yScale(+marketDepth.lastTradedPrice) + yh2,
|
|
|
+ radius: r,
|
|
|
+ data: d // 存储与圆相关的数据,便于在事件处理时使用
|
|
|
+ });
|
|
|
+ })
|
|
|
|
|
|
// draw buy line path
|
|
|
let buy_line_color = d3.color(this.defaults.buyColor).rgb();
|
|
|
@@ -650,7 +743,7 @@ export default class StockHeatmap extends React.Component {
|
|
|
// 延迟日志
|
|
|
if (this.windowedData.length > 1) {
|
|
|
let last = this.windowedData[this.windowedData.length - 1]
|
|
|
- console.log(new Date().getTime() - last.time, last)
|
|
|
+ console.log(new Date().getTime() - last.time, last.marketDepth.lastTradedPrice, last.marketDepth.lastTradedQty)
|
|
|
}
|
|
|
|
|
|
this.windowPosition = position;
|