|
|
/** * 功能:Canvas绘制方法。 * 作者:洪锡林 * 时间:2025年10月25日 * * @format */
import { utils } from "./util.js";export const HCharts = { // 清除画布
clearCanvas(ctx, width, height) { ctx.clearRect(0, 0, width, height); ctx.setFillStyle("#ffffff"); ctx.fillRect(0, 0, width, height); }, // 设置画布颜色
setCanvasColor(ctx, width, height, color) { ctx.clearRect(0, 0, width, height); ctx.setFillStyle(color); ctx.fillRect(0, 0, width, height); }, // 绘制文本工具函数
drawText(ctx, text, x, y, fontSize = 12, color = "#333", align = "left") { ctx.setFontSize(fontSize); ctx.setFillStyle(color); ctx.setTextAlign(align); ctx.fillText(text, x, y); }, /** * 功能:绘制网格系统。 * 作者:洪锡林 * 时间:2025年10月25日 * * grid:[{ * top:顶部距离 * bottom:底部距离 * left:左侧距离 * right:右侧距离 * lineColor:网格线颜色 * lineWidth:网格线宽度 * horizontalLineNum:水平网格线数量 * verticalLineNum:垂直网格线数量 * label:{ * fontSize:字体大小 * color:字体颜色 * onlyTwo:是否只有两个标签 * text:[{ * value:值标签 * ratio:比例标签 * },{ * value:值标签 * ratio:比例标签 * },...] * },...] */ // 绘制网格系统
drawGrid(ctx, width, height, grid, openTime, closeTime) { // 测试数据
// const preClosePrice = prevClosePrice;
for (let i = 0; i < grid.length; ++i) { const top = grid[i].top; const bottom = grid[i].bottom; const left = grid[i].left; const right = grid[i].right; const lineColor = grid[i].lineColor; const lineWidth = grid[i].lineWidth; const horizontalLineNum = grid[i].horizontalLineNum - 1; const verticalLineNum = grid[i].verticalLineNum - 1; let label; if (grid[i].label) { label = grid[i].label; } ctx.setStrokeStyle(lineColor); ctx.setLineWidth(lineWidth);
// 画图底的开盘收盘时间
if (i == 0 && openTime && closeTime) { HCharts.drawText(ctx, openTime, 6, height - bottom + 12, 14, "#686868", "left"); HCharts.drawText(ctx, closeTime, width - 6, height - bottom + 12, 14, "#686868", "right"); } // 绘制水平网格线
for (let j = 0; j <= horizontalLineNum; j++) { const y = top + (j * (height - bottom - top)) / horizontalLineNum; ctx.beginPath(); if (label.lineStyle[j] == "dash") { ctx.setLineDash([5, 5]); } ctx.moveTo(left, y); ctx.lineTo(width - right, y); ctx.stroke(); ctx.setLineDash([]); } // 绘制垂直网格线
for (let i = 0; i <= verticalLineNum; i++) { const x = ((width - left - right) * i) / verticalLineNum; ctx.beginPath(); ctx.moveTo(x + left, top); ctx.lineTo(x + left, height - bottom); ctx.stroke(); } } }, // 绘制价格标签
drawAxisLabels(ctx, width, height, grid) { for (let i = 0; i < grid.length; ++i) { const top = grid[i].top; const bottom = grid[i].bottom; const left = grid[i].left; const right = grid[i].right; const horizontalLineNum = grid[i].horizontalLineNum - 1; let label; if (grid[i].label) { label = grid[i].label; } // 绘制水平网格线
for (let j = 0; j <= horizontalLineNum; j++) { const y = top + (j * (height - bottom - top)) / horizontalLineNum; // 价格标签
if (label) { let valueXText = left + 1; let ratioXText = width - right - 1; let yText = y + 10; if (j == horizontalLineNum) { yText = y - 1; } let valueAlign = "left"; let ratioAlign = "right"; let fontSize = label.fontSize; let textColor = label.color[j]; if (label.onlyTwo) { if (j == 0) { HCharts.drawText(ctx, label.text[0].value, valueXText, yText, fontSize, label.color[0], valueAlign); } else if (j == horizontalLineNum) { HCharts.drawText(ctx, label.text[1].value, valueXText, yText, fontSize, label.color[1], valueAlign); } } else { HCharts.drawText(ctx, label.text[j].value, valueXText, yText, fontSize, textColor, valueAlign); }
if (typeof label.text[j]?.ratio !== "undefined") { HCharts.drawText(ctx, label.text[j].ratio, ratioXText, yText, fontSize, textColor, ratioAlign); } } } } }, // 绘制价格曲线
drawPriceLine(ctx, width, height, data, grid, priceRange) { if (!data.length) return; // 上下边距1
const top = grid[0].top; const bottom = grid[0].bottom; const left = grid[0].left; const right = grid[0].right; const pointLen = 240; const priceDiff = priceRange.max - priceRange.min; // 绘制价格曲线
ctx.setStrokeStyle("#000"); ctx.setLineWidth(1); ctx.beginPath();
data.forEach((item, index) => { const x = left + (index * (width - left - right)) / pointLen; const y = top + (height - top - bottom) * (1 - (item.price - priceRange.min) / priceDiff); if (index === 0) { ctx.moveTo(x, y); } else { // 使用贝塞尔曲线平滑连接
const prevPoint = data[index - 1]; const prevX = left + ((index - 1) * (width - left - right)) / pointLen; const prevY = top + (height - top + -bottom) * (1 - (prevPoint.price - priceRange.min) / priceDiff); const cp1x = (prevX + x) / 2; const cp1y = prevY; const cp2x = (prevX + x) / 2; const cp2y = y; ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); } }); ctx.stroke(); // 绘制渐变背景
HCharts.drawGradientBackground(ctx, width, height, data, grid, priceRange); },
// 绘制渐变背景
drawGradientBackground(ctx, width, height, data, grid, priceRange) { // 上下边距1
const top = grid[0].top; const bottom = grid[0].bottom; const left = grid[0].left; const right = grid[0].right; const pointLen = 240; const priceDiff = priceRange.max - priceRange.min;
const gradient = ctx.createLinearGradient(0, left, 0, height - top); gradient.addColorStop(0, "rgba(0, 0, 0, 0.3)"); gradient.addColorStop(1, "rgba(0, 0, 0, 0.05)");
ctx.beginPath();
// 绘制价格曲线路径
data.forEach((item, index) => { const x = left + (index * (width - left - right)) / pointLen; const y = top + (height - top - bottom) * (1 - (item.price - priceRange.min) / priceDiff);
if (index === 0) { ctx.moveTo(x, y); } else { const prevPoint = data[index - 1]; const prevX = left + ((index - 1) * (width - left - right)) / pointLen; const prevY = top + (height - top - bottom) * (1 - (prevPoint.price - priceRange.min) / priceDiff);
const cp1x = (prevX + x) / 2; const cp1y = prevY; const cp2x = (prevX + x) / 2; const cp2y = y;
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); } });
// 闭合路径
const lastX = left + ((data.length - 1) * (width - left - right)) / pointLen; ctx.lineTo(lastX, height - bottom); ctx.lineTo(left, height - bottom); ctx.closePath();
ctx.setFillStyle(gradient); ctx.fill(); }, // 绘制成交量
drawVolume(ctx, width, height, data, index, pointLen, grid, volumeRange, offset) { if (!data.length) return;
const top = grid[index - 1].top; const bottom = grid[index - 1].bottom; const left = grid[index - 1].left; const right = grid[index - 1].right;
data.forEach((item, index) => { const x = offset + left + (index * (width - left - right)) / pointLen; const barWidth = (width - left - right) / pointLen - 0.5; const barHeight = (item.volume / volumeRange.max) * (height - bottom - top); // 根据涨跌设置颜色
const isRise = index === 0 || item.price >= data[index - 1].price || item.close >= data[index - 1].close; ctx.setFillStyle(isRise ? "green" : "red");
ctx.fillRect(x - barWidth / 2, height - bottom - barHeight, barWidth, barHeight); }); }, // 字符宽度近似计算(避免使用 measureText)
getApproximateTextWidth(text, fontSize = 10) { // 中文字符约等于 fontSize,英文字符约等于 fontSize * 0.6
let width = 0; for (let char of text) { // 判断是否为中文字符
if (char.match(/[\u4e00-\u9fa5]/)) { width += fontSize; } else { width += fontSize * 0.6; } } return width; }, // 绘制顶部价格显示
drawTopPriceDisplay(ctx, grid, text) { for (let i = 0; i < text.length; i++) { let x = grid[i].left; let y = grid[i].top - 4; for (let j = 0; j < text[i].length; j++) { ctx.setFillStyle(text[i][j].color); ctx.setFontSize(10); ctx.setTextAlign("left"); ctx.fillText(text[i][j].name + ":" + text[i][j].value, x, y); x += HCharts.getApproximateTextWidth(text[i][j].name + ":" + text[i][j].value) + 5; } } }, // 绘制坐标轴标签
drawCrosshairAxisLabels(ctx, width, height, grid, crosshair) { const { x, y } = crosshair; // X轴时间标签
if (crosshair.currentData && (crosshair.currentData.time || crosshair.currentData.date)) { const timeText = crosshair.currentData.time || crosshair.currentData.date; const xBoxWidth = crosshair.currentData.time ? 40 : 70; const xBoxHeight = 15; ctx.setFillStyle("#629AF5");
if (x - xBoxWidth / 2 <= grid[0].left) { ctx.fillRect(grid[0].left, height - grid[0].bottom, xBoxWidth, xBoxHeight); } else if (x + xBoxWidth / 2 < width - grid[0].right) { ctx.fillRect(x - xBoxWidth / 2, height - grid[0].bottom, xBoxWidth, xBoxHeight); } else { ctx.fillRect(width - grid[0].right - xBoxWidth, height - grid[0].bottom, xBoxWidth, xBoxHeight); }
ctx.setFillStyle("#fff"); ctx.setFontSize(12); ctx.setTextAlign("center");
if (x - xBoxWidth / 2 <= grid[0].left) { ctx.fillText(timeText, grid[0].left + xBoxWidth / 2, height - grid[0].bottom + 12); } else if (x + xBoxWidth / 2 < width - grid[0].right) { ctx.fillText(timeText, x, height - grid[0].bottom + 12); } else { ctx.fillText(timeText, width - grid[0].right - xBoxWidth / 2, height - grid[0].bottom + 12); } }
// Y轴价格标签
if (crosshair.currentData) { const priceText = utils.formatPrice(crosshair.currentData.price); const yBoxWidth = 50; const yBoxHeight = 14; ctx.setFillStyle("#629AF5"); if (x < grid[0].left + yBoxWidth + 5) { ctx.fillRect(width - grid[0].right - yBoxWidth, y - yBoxHeight / 2, yBoxWidth, yBoxHeight); } else { ctx.fillRect(grid[0].left, y - yBoxHeight / 2, yBoxWidth, yBoxHeight); }
ctx.setFillStyle("#fff"); ctx.setFontSize(11); ctx.setTextAlign("center"); if (x < grid[0].left + yBoxWidth + 5) { ctx.fillText(priceText, width - grid[0].right - yBoxWidth / 2, y + 3); } else { ctx.fillText(priceText, grid[0].left + yBoxWidth / 2, y + 3); } } }, // 绘制十字准线
drawCrosshair(ctx, width, height, grid, crosshair, text) { if (!ctx) return; const { x, y } = crosshair; if (crosshair.show) { // 每次绘制前先清除整个画布
ctx.clearRect(0, 0, width, height);
// 绘制垂直准线1
ctx.setStrokeStyle("#000"); ctx.setLineWidth(1); // ctx.setLineDash([5, 5]);
for (let i = 0; i < grid.length; i++) { ctx.beginPath(); ctx.moveTo(x, grid[i].top); ctx.lineTo(x, height - grid[i].bottom); ctx.stroke(); } // ctx.beginPath();
// ctx.moveTo(x, grid[0].top);
// ctx.lineTo(x, height - grid[0].bottom);
// ctx.stroke();
// // 绘制垂直准线2
// ctx.beginPath();
// ctx.moveTo(x, grid[1].top);
// ctx.lineTo(x, height - grid[1].bottom);
// ctx.stroke();
// 绘制水平准线
ctx.beginPath(); ctx.moveTo(grid[0].left, y); ctx.lineTo(width - grid[0].right, y); ctx.stroke();
ctx.setLineDash([]);
// 绘制焦点圆点 - 黑边白心(更小)
// 先绘制白色填充
ctx.setFillStyle("#ffffff"); ctx.beginPath(); ctx.arc(x, y, 2, 0, Math.PI * 2); ctx.fill();
// 再绘制黑色边框
ctx.setStrokeStyle("#000000"); ctx.setLineWidth(1); ctx.setLineDash([]); ctx.beginPath(); ctx.arc(x, y, 2, 0, Math.PI * 2); ctx.stroke();
// 绘制坐标轴标签
HCharts.drawCrosshairAxisLabels(ctx, width, height, grid, crosshair); } // 绘制顶部价格显示
HCharts.drawTopPriceDisplay(ctx, grid, text); ctx.draw(false); },};
|