depth-chart-hb
(function (global, factory) {
// 检查模块系统并初始化depthChart
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = factory();
} else {
global.depthChart = factory();
}
})(this, function () {
// 判断是否为数组
function isArrayLike(obj, limit) {
if (Array.isArray(obj)) {
return obj;
}
}
// 判断是否为可迭代对象
function isIterable(obj, limit) {
if (Symbol.iterator in Object(obj) || "[object Arguments]" === Object.prototype.toString.call(obj)) {
var result = [];
var done = false;
var error = false;
var errorMsg = void 0;
try {
for (var iterator, step = obj[Symbol.iterator](); !(done = (iterator = step.next()).done) && (result.push(iterator.value), !limit || result.length !== limit); done = true);
} catch (err) {
error = true;
errorMsg = err;
} finally {
try {
if (!done && step.return != null) {
step.return();
}
} finally {
if (error) {
throw errorMsg;
}
}
}
return result;
}
}
// 获取字符串
function getString(value, limit) {
return isArrayLike(value, limit) || isIterable(value, limit);
}
// 深度图主函数
function depthChart(element, options) {
var prototype, originalGetContext;
// 修改canvas的getContext方法,调整canvas的分辨率
(prototype = HTMLCanvasElement.prototype).getContext = (originalGetContext = prototype.getContext, function (contextType) {
var context, pixelRatio, backingStoreRatio;
context = originalGetContext.call(this, contextType);
if (contextType === "2d") {
backingStoreRatio = context.backingStorePixelRatio || context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1;
pixelRatio = (window.devicePixelRatio || 1) / backingStoreRatio;
if (pixelRatio > 1) {
this.style.height = this.height + "px";
this.style.width = this.width + "px";
this.width *= pixelRatio;
this.height *= pixelRatio;
}
}
return context;
});
// 主题设置
var themes = {
"hb-night": {
bidsLineColor: options && options.bidsLineColor || "rgba(0, 141, 115, 1)",
asksLineColor: options && options.asksLineColor || "rgba(236, 49, 75, 1)",
bidsFillColor: options && options.bidsFillColor || "rgba(0, 141, 115, .2)",
asksFillColor: options && options.asksFillColor || "rgba(236, 49, 75, .2)",
axisColor: options && options.axisColor || "rgba(97, 104, 138, .3)",
color: options && options.color || "rgba(81, 128, 159, .8)",
bgColor: options && options.bgColor || "rgba(23, 54, 72, .95)",
dotColor: "rgba(0 , 126, 224, 1)",
tipColor: options && options.tipColor || "#C7CCE6",
tipShadow: 4
},
"hb-day": {
bidsLineColor: options && options.bidsLineColor || "rgba(3, 192, 135, 0)",
asksLineColor: options && options.asksLineColor || "rgba(231, 109, 66, 0)",
bidsFillColor: options && options.bidsFillColor || "rgba(3, 192, 135, .1)",
asksFillColor: options && options.asksFillColor || "rgba(231, 109, 66, .1)",
axisColor: options && options.axisColor || "rgba(180, 188, 227, .3)",
color: options && options.color || "#232A4A",
bgColor: options && options.bgColor || "#ffffff",
dotColor: "rgba(0 , 126, 224, 1)",
tipColor: options && options.tipColor || "#9CA9B5",
tipShadow: 4
}
};
// 配置参数
var config = {
theme: options && options.theme || "hb-night",
ruleHeight: options && options.ruleHeight || 30,
ruleWidth: options && options.ruleWidth || 54,
priceFix: options && options.priceFix || 2,
amountFix: options && options.amountFix || 0,
paddingTop: options && options.paddingTop || 15,
noAmountTick: options && options.noAmountTick || 500,
lang: options && options.lang || "en-us",
langMap: options && options.langMap
};
// 初始化主题
function initializeTheme(theme) {
var selectedTheme = theme || config.theme;
Object.keys(themes["hb-day"]).forEach(function (key) {
config[key] = themes[selectedTheme][key];
});
}
initializeTheme();
// 语言映射
var label, hoveredX, hoveredY, canvasElement = Object.assign({
"zh-cn": {
"委托价": "委托价",
"累计": "累计"
},
"en-us": {
"委托价": "Price",
"累计": "Sum"
}
}, config.langMap || {});
// 数据和画布元素
var askData = [], bidData = [], maxValue = 0, targetElement = typeof element === "string" ? document.querySelector("#" + element.replace("#", "")) : element || document.querySelector("#chart"), mainCanvas = document.createElement("canvas"), containerWidth = targetElement.offsetWidth, containerHeight = targetElement.offsetHeight, overlayCanvas = document.createElement("canvas"), overlayVisible = true, mainContext = mainCanvas.getContext("2d"), overlayContext = overlayCanvas.getContext("2d"), devicePixelRatio = (context = mainContext, (window.devicePixelRatio || 1) / (context.backingStorePixelRatio || context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio || context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || context.backingStorePixelRatio || 1)), chartWidth = containerWidth - config.ruleWidth, chartHeight = containerHeight - config.ruleHeight, padding = ~~(containerHeight * config.paddingTop / 100), offsetY = padding * devicePixelRatio, availableHeight = chartHeight - padding, availableHeightScaled = availableHeight * devicePixelRatio, chartWidthScaled = chartWidth * devicePixelRatio, chartHeightScaled = chartHeight * devicePixelRatio;
// 绘制单个点
function drawPoint(x, y) {
if (x = x || hoveredX) {
if (hoveredX = x, clearCanvas(overlayContext), overlayVisible = false, x > chartWidthScaled - devicePixelRatio) {
overlayVisible = true;
} else {
for (var imageData = mainContext.getImageData(x, 0, 1, chartHeightScaled - devicePixelRatio), i = 0; i < imageData.height; i++) {
var red = imageData.data[4 * i * imageData.width], green = imageData.data[4 * i * imageData.width + 1];
if (red || green) return drawTooltip(x, i, x > chartWidthScaled / 2 ? "asks" : "bids"), void (overlayVisible = true);
}
overlayVisible = true;
}
}
}
// 获取字符串的小数部分长度
function getDecimalLength(value) {
var parts = (value + "").split(".");
return getString(parts, 1)[0].length;
}
// 初始化画布尺寸
mainCanvas.width = overlayCanvas.width = containerWidth * devicePixelRatio;
mainCanvas.height = overlayCanvas.height = containerHeight * devicePixelRatio;
mainCanvas.style.position = overlayCanvas.style.position = "absolute";
mainCanvas.style.width = overlayCanvas.style.width = containerWidth + "px";
mainCanvas.style.height = overlayCanvas.style.height = containerHeight + "px";
targetElement.style.position = "relative";
targetElement.appendChild(mainCanvas);
targetElement.appendChild(overlayCanvas);
// 大数转换
var largeNumberFormat = {
1e3: ["K", "M", "B"],
1e4: ["万", "亿", "兆"]
};
// 绘制标记点
function drawMarker(x, y, type) {
config.dotColor = "bids" === type ? config.bidsLineColor : config.asksLineColor;
overlayContext.beginPath();
overlayContext.arc(x, y, 10 * devicePixelRatio, 0, 2 * Math.PI);
overlayContext.closePath();
overlayContext.fillStyle = config.dotColor.replace("1)", ".3)");
overlayContext.fill();
overlayContext.beginPath();
overlayContext.arc(x, y, 5 * devicePixelRatio, 0, 2 * Math.PI);
overlayContext.closePath();
overlayContext.fillStyle = config.dotColor;
overlayContext.fill();
function drawDashedLine(x, y, type) {
overlayContext.beginPath();
overlayContext.strokeStyle = config[type + "LineColor"];
overlayContext.lineWidth = ~~(2 * devicePixelRatio);
overlayContext.setLineDash([5 * devicePixelRatio, 5 * devicePixelRatio]);
overlayContext.moveTo(x, Math.min(y + 10 * devicePixelRatio, chartHeightScaled));
overlayContext.lineTo(x, chartHeightScaled);
overlayContext.stroke();
overlayContext.closePath();
}
drawDashedLine(x, y, type);
}
// 绘制提示框
function drawTooltip(x, y, type) {
drawMarker(x, y, type);
CanvasRenderingContext2D.prototype.roundRect = function (x, y, width, height, radius) {
var r = Math.min(width, height);
if (radius > r / 2) radius = r / 2;
this.beginPath();
this.moveTo(x + radius, y);
this.arcTo(x + width, y, x + width, y + height, radius);
this.arcTo(x + width, y + height, x, y + height, radius);
this.arcTo(x, y + height, x, y, radius);
this.arcTo(x, y, x + width, y, radius);
this.closePath();
return this;
};
var boxWidth = (6 * (maxValue ? Math.max(maxValue.toFixed(config.amountFix).toString().length - 9, 0) : 0) + 140) * devicePixelRatio;
var boxHeight = 90 * devicePixelRatio;
var boxPadding = 18 * devicePixelRatio;
overlayContext.shadowBlur = config.tipShadow * devicePixelRatio;
overlayContext.shadowOffsetY = config.tipShadow * devicePixelRatio;
var boxX = chartWidthScaled - x > boxWidth ? x + 4 : x - boxWidth - 2;
var boxY = y - boxHeight - boxPadding > overlayContext.shadowBlur ? y - boxHeight - boxPadding : y + boxPadding;
var textY = y - boxHeight - boxPadding > overlayContext.shadowBlur ? y - boxHeight - boxPadding : y + boxPadding;
var pointerY = y - boxHeight - boxPadding > overlayContext.shadowBlur ? y - boxPadding : y + boxHeight + boxPadding;
var scale = getScale();
var price = scale.pTick * x + scale.pMin;
var amount = getAmount(price, type);
overlayContext.beginPath();
overlayContext.moveTo(adjustX(x), adjustY(textY));
overlayContext.lineTo(adjustX(x), adjustY(pointerY));
overlayContext.closePath();
overlayContext.lineWidth = ~~(4 * devicePixelRatio);
overlayContext.strokeStyle = "bids" === type ? config.bidsLineColor : config.asksLineColor;
overlayContext.stroke();
overlayContext.fillStyle = config.bgColor;
overlayContext.roundRect(boxX, boxY, boxWidth, boxHeight, 3 * devicePixelRatio);
overlayContext.fill();
overlayContext.shadowBlur = 0;
overlayContext.shadowOffsetY = 0;
overlayContext.fillStyle = config.tipColor;
overlayContext.font = 12 * devicePixelRatio + "px Arial";
overlayContext.fillText(canvasElement[config.lang]["委托价"], boxX + 16 * devicePixelRatio, boxY + 20 * devicePixelRatio);
overlayContext.fillText(price.toFixed(config.priceFix), boxX + 16 * devicePixelRatio, boxY + 36 * devicePixelRatio);
overlayContext.fillText(canvasElement[config.lang]["累计"], boxX + 16 * devicePixelRatio, boxY + 60 * devicePixelRatio);
overlayContext.fillText(amount.toFixed(config.amountFix), boxX + 16 * devicePixelRatio, boxY + 76 * devicePixelRatio);
}
// 绘制坐标轴
function drawAxis() {
mainContext.strokeStyle = config.axisColor;
mainContext.lineWidth = ~~(1.5 * devicePixelRatio);
mainContext.beginPath();
mainContext.moveTo(adjustX(0), adjustY(chartHeightScaled));
mainContext.lineTo(adjustX(chartWidthScaled), adjustY(chartHeightScaled));
mainContext.lineTo(adjustX(chartWidthScaled), adjustY(0));
mainContext.stroke();
mainContext.closePath();
}
// 绘制买单曲线
function drawBids() {
mainContext.strokeStyle = config.bidsLineColor;
mainContext.lineWidth = ~~(2 * devicePixelRatio);
mainContext.beginPath();
var sortedBids = bidData.sort(function (a, b) {
return a[0] - b[0];
});
var stepX = chartWidthScaled / sortedBids.length / 2;
for (var i = 0; i < sortedBids.length; i++) {
if (i === 0) {
mainContext.moveTo(adjustX(i * stepX), adjustY(getYCoordinate(sortedBids[i][1])));
}
mainContext.lineTo(adjustX(i * stepX), adjustY(getYCoordinate(sortedBids[i][1])));
if (i === sortedBids.length - 1) {
mainContext.lineTo(adjustX(i * stepX), adjustY(chartHeightScaled - devicePixelRatio));
}
}
mainContext.stroke();
mainContext.lineTo(adjustX(0), adjustY(chartHeightScaled));
mainContext.lineTo(adjustX(0), adjustY(0));
mainContext.closePath();
mainContext.fillStyle = config.bidsFillColor;
mainContext.fill();
}
// 绘制卖单曲线
function drawAsks() {
mainContext.strokeStyle = config.asksLineColor;
mainContext.beginPath();
var sortedAsks = askData.sort(function (a, b) {
return a[0] - b[0];
});
var stepX = chartWidthScaled / sortedAsks.length / 2;
var midX = chartWidthScaled / 2 + 2 * devicePixelRatio;
for (var i = 0; i < sortedAsks.length; i++) {
if (i === 0) {
mainContext.lineTo(adjustX(midX), adjustY(chartHeightScaled - devicePixelRatio));
}
mainContext.lineTo(adjustX(i * stepX + midX), adjustY(getYCoordinate(sortedAsks[i][1])));
if (i === sortedAsks.length - 1) {
mainContext.lineTo(adjustX(chartWidthScaled), adjustY(getYCoordinate(sortedAsks[i][1])));
}
}
mainContext.stroke();
mainContext.lineTo(adjustX(chartWidthScaled), adjustY(chartHeightScaled - devicePixelRatio));
mainContext.lineTo(adjustX(midX), adjustY(chartHeightScaled - devicePixelRatio));
mainContext.closePath();
mainContext.fillStyle = config.asksFillColor;
mainContext.fill();
}
// 绘制网格和标签
function drawGrid() {
var step = 32 * devicePixelRatio;
var numStepsY = 1 + ~~(chartHeight / 100);
var stepX = (chartWidthScaled - 2 * step) / (~~(chartWidth / 100) - 1);
var stepY = (chartHeightScaled - 2 * (16 * devicePixelRatio)) / numStepsY;
var xTicks = [];
var yTicks = [];
var xLabels = [];
var yLabels = [];
var yTotal = 0;
var yLabelsTemp = [];
var yLabelsFixed = [];
var scale = getScale();
for (var x = step; x < chartWidthScaled; x += stepX) {
xTicks.push(x);
xLabels.push(scale.pMin + x * scale.pTick);
}
for (var y = chartHeightScaled - devicePixelRatio; y > 0; y -= stepY) {
yTicks.push(y);
yLabels.push((chartHeightScaled - devicePixelRatio - y) * scale.aTick);
}
yLabels.forEach(function (val, i) {
yTotal += val;
yLabelsTemp.push(config.noAmountTick * i);
yLabelsFixed.push(i);
});
if (maxValue < 5 && yTotal !== 0) {
yLabels = yLabelsFixed;
maxValue = (availableHeightScaled - stepY * numStepsY - 1) / stepY + 5;
}
if (yTotal === 0) {
yLabels = yLabelsTemp;
}
yLabels[0] = 0;
drawTicks(xTicks, xLabels, "x");
drawTicks(yTicks, yLabels, "y");
}
// 绘制坐标轴标签
function drawTicks(ticks, labels, axis) {
mainContext.lineWidth = ~~(1.5 * devicePixelRatio);
mainContext.strokeStyle = config.axisColor;
mainContext.font = 12 * devicePixelRatio + "px Arial";
mainContext.fillStyle = config.color;
mainContext.textAlign = axis === "x" ? "center" : "left";
var offset = devicePixelRatio;
if (axis === "x") {
ticks.forEach(function (tick, i) {
mainContext.beginPath();
mainContext.lineTo(adjustX(tick), adjustY(chartHeightScaled + offset));
mainContext.lineTo(adjustX(tick), adjustY((chartHeight + 4) * devicePixelRatio + offset));
mainContext.stroke();
mainContext.closePath();
mainContext.fillText(labels[i].toFixed(config.priceFix), adjustX(tick), adjustY((chartHeight + 20) * devicePixelRatio + offset));
});
} else {
ticks.forEach(function (tick, i) {
var label = labels[i] <= 1e3 ? labels[i].toFixed(0) : formatLargeNumber(labels[i], 2, 1e3);
mainContext.beginPath();
mainContext.lineTo(adjustX(chartWidthScaled + offset), adjustY(tick + offset));
mainContext.lineTo(adjustX((chartWidth + 4) * devicePixelRatio + offset), adjustY(tick + offset));
mainContext.stroke();
mainContext.fillText(label, adjustX((chartWidth + 8) * devicePixelRatio + offset), adjustY(tick + 4 * devicePixelRatio));
mainContext.closePath();
});
}
}
// 获取适当的像素位置
function adjustX(value) {
return .5 + ~~value;
}
// 获取Y轴位置
function adjustY(value) {
if (value === 0) {
return chartHeightScaled - devicePixelRatio;
}
var yPosition = availableHeightScaled - availableHeightScaled * value / maxValue + offsetY;
return yPosition - chartHeightScaled < ~~(mainContext.lineWidth * devicePixelRatio) ? yPosition - ~~(mainContext.lineWidth * devicePixelRatio) : yPosition;
}
// 清空画布
function clearCanvas(context) {
context.clearRect(0, 0, containerWidth * devicePixelRatio, containerHeight * devicePixelRatio);
}
// 获取价格和数量刻度
function getScale() {
var minPrice = bidData[0] && bidData[0][0] || 0;
var maxPrice = askData[askData.length - 1] && askData[askData.length - 1][0] || 0;
return {
pMin: 1 * minPrice,
pMax: 1 * maxPrice,
pTick: (maxPrice - minPrice) / chartWidthScaled,
aTick: maxValue / availableHeightScaled
};
}
// 更新图表
function updateChart() {
clearCanvas(mainContext);
drawAxis();
drawGrid();
drawBids();
drawAsks();
}
// 添加事件监听
overlayCanvas.addEventListener("mousemove", function (event) {
var coordinates = getMouseCoordinates(event);
if (overlayVisible) {
drawPoint(coordinates.x, coordinates.y);
}
}, false);
overlayCanvas.addEventListener("mouseout", function (event) {
setTimeout(function () {
return clearCanvas(overlayContext);
}, 500);
hoveredX = null;
}, false);
// 获取鼠标坐标
function getMouseCoordinates(event) {
var rect = mainCanvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left) * devicePixelRatio,
y: (event.clientY - rect.top) * devicePixelRatio
};
}
// 获取交易量
function getAmount(price, type) {
if (type === "bids") {
var bidDistances = bidData.map(function (item) {
return Math.abs(item[0] - price);
});
var closestIndex = bidDistances.indexOf(Math.min.apply(null, bidDistances));
if (closestIndex > -1) {
label = bidData[closestIndex][1];
}
return label;
}
var askDistances = askData.map(function (item) {
return Math.abs(item[0] - price);
});
var closestIndex = askDistances.indexOf(Math.min.apply(null, askDistances));
if (closestIndex > -1) {
label = askData[closestIndex][1];
}
return label;
}
// 格式化大数
function formatLargeNumber(value, precision, base) {
var symbols = largeNumberFormat[base];
var formattedValue = symbols.reduce(function (result, symbol) {
if (result[0] > base) {
result[0] /= base;
result[1] = symbol;
}
return result;
}, [value, ""]);
var decimalPlaces = precision;
if (getDecimalLength(formattedValue[0]) === getDecimalLength(base) - 1) {
decimalPlaces -= 1;
}
formattedValue[0] = formattedValue[0].toFixed(decimalPlaces);
return formattedValue.join("");
}
return {
update: updateChart,
putData: function (data) {
clearCanvas(mainContext);
clearCanvas(overlayContext);
processChartData(data);
drawAxis();
drawGrid();
drawBids();
drawAsks();
drawPoint();
},
forceUpdate: function () {
containerWidth = targetElement.offsetWidth;
containerHeight = targetElement.offsetHeight;
mainCanvas.width = overlayCanvas.width = containerWidth * devicePixelRatio;
mainCanvas.height = overlayCanvas.height = containerHeight * devicePixelRatio;
mainCanvas.style.width = overlayCanvas.style.width = containerWidth + "px";
mainCanvas.style.height = overlayCanvas.style.height = containerHeight + "px";
chartWidth = containerWidth - config.ruleWidth;
chartHeight = containerHeight - config.ruleHeight;
padding = ~~(containerHeight * config.paddingTop / 100);
offsetY = padding * devicePixelRatio;
availableHeight = chartHeight - padding;
availableHeightScaled = availableHeight * devicePixelRatio;
chartWidthScaled = chartWidth * devicePixelRatio;
chartHeightScaled = chartHeight * devicePixelRatio;
overlayVisible = true;
clearCanvas(overlayContext);
updateChart();
},
initTheme: initializeTheme,
reload: function (newConfig) {
Object.assign(config, newConfig);
}
};
}
return depthChart;
});
公开 最后更新: 2024-07-03 01:34:54 AM
