画线工具
让用户画线
chart.drawings.startTool('trend-line');
chart.on('drawingsChange', ({ reason, ids }) => console.log(reason, ids)); // 'create', ['trend-line-1']
chart.on('toolChange', () => toolbar.reset()); // 工具已结束工具激活时,图表会在指针下预览下一个锚点和正在绘制的画线,并在价格轴和时间轴上标出各锚点的位置。用鼠标时每个锚点都是一次点击:点击,移到下一个位置,再点击;按住鼠标键拖动不会画线。触屏没有悬停,所以在触屏上拖动时,手指抬起的位置会放下一个锚点,同时手指上方会出现放大镜,显示落点的位置(interaction.touchMagnifier)。path 可以放置任意多个锚点,双击或按 Enter 结束。画线完成、按 Escape 或调用 cancelTool() 时工具结束,新画线会被选中。
| 绘制或拖动锚点时 | 效果 |
|---|---|
Shift | 以相邻锚点为准,保持水平、垂直或 45° 方向 |
Ctrl / ⌘ | 反转磁吸:关闭时变为强磁吸,弱或强磁吸时变为关闭 |
按下或松开修饰键时,预览会立即更新,不需要移动指针。
startTool() 可以指定新画线的样式和文字;continuous 让工具在画完后保持激活,方便连续画多条:
chart.drawings.startTool('horizontal-line', { continuous: true, style: { color: '#e5484d' } });磁吸
锚点的时间总是对齐到 K 线,最新 K 线之后的区域也一样。options.drawings.magnet 让主图上的锚点价格吸附到指针所在 K 线的开、高、低、收价格:
| 模式 | 吸附方式 |
|---|---|
off | 不吸附(默认) |
weak | 距离四个价格之一 16 像素以内时吸附 |
strong | 总是吸附到最近的一个 |
内置类型
fib-extension、pitchfork、date-range、date-price-range、long-position、short-position 和 callout 是扩展功能:使用前先用 registerExtras(chart)(或逐个)注册。其余类型始终可用。
| 类型 | 锚点数 | 控制点与标签 |
|---|---|---|
trend-line | 2 | 线段 |
ray | 2 | 越过第二个锚点向外延伸 |
extended-line | 2 | 向两端延伸 |
arrow | 2 | 第二个锚点处为实心箭头 |
horizontal-line | 1 | 横跨整个图表;价格常驻显示在价格轴上 |
horizontal-ray | 1 | 从锚点向右延伸;价格常驻显示在价格轴上 |
vertical-line | 1 | 纵贯整个子图;时间常驻显示在时间轴上 |
rectangle | 2 | 8 个控制点:四角同时调整两个方向,四边中点只移动一条边 |
parallel-channel | 3 | 基线经过锚点 1–2,平行线经过锚点 3;拖动平行线的端点调整宽度 |
fib-retracement | 2 | 0、0.236、0.382、0.5、0.618、0.786、1 各档,每档有自己的颜色和填充带 |
fib-extension | 3 | 把锚点 1 到 2 的涨跌幅从锚点 3 投射出去:0、0.382、0.618、1、1.272、1.618、2、2.618 |
pitchfork | 3 | 安德鲁音叉:中线从锚点 1 穿过锚点 2、3 的中点,两条平行线分别经过锚点 2 和 3 |
price-range | 2 | 锚点之间的涨跌额、涨跌幅和 K 线数量,箭头使用涨跌色 |
date-range | 2 | 两个锚点之间的 K 线数量和时长,例如 24 根 K 线 · 6小时 |
date-price-range | 2 | 涨跌额、涨跌幅、K 线数量和时长 |
long-position | 3(放置 2 个) | 开仓价、止盈和止损;见仓位 |
short-position | 3(放置 2 个) | 同上,用于做空 |
path | 2 个以上 | 折线,双击或按 Enter 结束 |
text | 1 | 一行或多行文字,在图上直接输入和编辑 |
callout | 2 | 指向第一个锚点的文字框,文字框位于第二个锚点 |
仓位
long-position 和 short-position 用来规划一笔交易。第一次点击确定开仓价,第二次点击确定结束时间和到止盈价的距离;止损价初始为止盈价的镜像,所以盈亏比从 1 开始。开仓线以及止盈、止损两条边都可以在任意位置抓住上下拖动;两侧的控制点用来左右拉宽。开仓价始终位于止损和止盈之间,止损和止盈也始终在开仓价的正确一侧。
两块区域分别表示收益和风险。从开仓后的 K 线开始逐根检查,第一根触及止盈或止损的 K 线会平仓(同一根 K 线两者都触及时按止损算);在此之前显示按最新收盘价计算的浮动盈亏。配合行情回放就是一个练习工具:画好仓位,逐根前进,看这笔交易的结果。
chart.drawings.add({
type: 'long-position',
points: [
{ time: entryTime, price: 100 }, // 开仓
{ time: endTime, price: 110 }, // 止盈
{ time: endTime, price: 95 } // 止损
]
});编辑
鼠标悬停在画线上时会显示它的控制点;悬停在控制点上时,控制点会放大,光标会提示拖动它的效果。用户点击选中画线,拖动画线移动位置,拖动控制点改变形状;悬停或选中的画线都可以直接拖动控制点。选中的画线会在价格轴和时间轴上标出锚点和覆盖的范围。
| 操作 | 效果 |
|---|---|
按住 Alt 拖动画线 | 拖出一份副本,原画线保持不动 |
Ctrl/⌘ + C、Ctrl/⌘ + V | 复制选中的画线,粘贴到向右 5 根 K 线处 |
| 双击文字 | 在原位编辑 |
Delete / Backspace | 删除选中的画线 |
拖动时按 Escape | 恢复画线 |
锁定的画线可以选中,但不能移动或编辑。
文字
放置 text 画线时,会在文字的位置打开编辑框:Enter 确认,Shift+Enter 换行,按 Escape 或文字为空时不放置,点击别处会保留文字。双击文字或调用 chart.drawings.editText(id) 可以再次编辑。
选中画线的工具栏
工具栏交给应用自己实现,以便和你的产品风格一致。chart.drawings.bounds(id) 返回画线在图表坐标中覆盖的区域(裁剪到所在子图),画线隐藏或不在可视范围内时返回 null。这个区域会随视图、数据和编辑而变化,所以在画线选中期间,可以在动画帧里读取它:
let selected: string | null = null;
chart.on('selectionChange', ({ selection }) => {
selected = selection?.kind === 'drawing' ? selection.id : null;
});
function follow() {
const box = selected ? chart.drawings.bounds(selected) : null;
toolbar.hidden = !box;
if (box) {
toolbar.style.left = `${box.x + box.width / 2}px`;
toolbar.style.top = `${box.y - 48}px`;
}
requestAnimationFrame(follow);
}
requestAnimationFrame(follow);
colorPicker.onchange = (color) => chart.drawings.update(selected!, { style: { color } });通过代码修改
const id = chart.drawings.add({
type: 'horizontal-line',
points: [{ time: Date.now(), price: 104.5 }],
style: { color: '#e5484d', lineStyle: 'dashed' },
locked: true
});
chart.drawings.update(id, { visible: false });
const copy = chart.drawings.duplicate(id);
chart.select({ kind: 'drawing', id: copy });interface DrawingInput {
id?: string;
type: string;
pane?: string; // 默认 'main'
points: Array<{ time: TimeInput; price: number }>;
style?: Partial<{
color: string;
lineWidth: number;
lineStyle: 'solid' | 'dashed' | 'dotted';
fillColor: string;
textColor: string;
fontSize: number;
}>;
text?: string;
locked?: boolean;
visible?: boolean;
}撤销与重做
用户操作(创建、移动、复制、编辑文字、删除)以及 add()、duplicate()、update()、remove()、clear() 都是可撤销的步骤。set() 会替换全部画线(例如加载文档时),并清空历史记录。
chart.drawings.undo();
chart.drawings.redo();
chart.drawings.canUndo();图表获得焦点时,可以使用 Ctrl/⌘ + Z、Ctrl/⌘ + Shift + Z 和 Ctrl + Y。
保存画线
画线包含在 chart.saveState() 中。如果要单独保存,可以监听 drawingsChange 并保存 chart.drawings.list(),再用 chart.drawings.set(saved) 恢复。
自定义画线类型
画线类型只负责绘制自己的形状和报告命中;放置、磁吸与 Shift、选中、控制点、坐标轴标签、拖动、复制、撤销和保存都由图表处理。所有坐标都相对于子图的绘图区域。
import type { DrawingTypeDefinition } from '@kumochart-sdk/core';
const cross: DrawingTypeDefinition = {
points: 1,
defaultStyle: (theme) => ({ color: theme.alert }),
paint: ({ ctx, geometry }) => {
const [anchor] = geometry.points;
if (!anchor) {
return;
}
ctx.strokeStyle = geometry.style.color;
ctx.lineWidth = geometry.style.lineWidth;
ctx.beginPath();
ctx.moveTo(anchor.x - 12, anchor.y);
ctx.lineTo(anchor.x + 12, anchor.y);
ctx.moveTo(anchor.x, anchor.y - 12);
ctx.lineTo(anchor.x, anchor.y + 12);
ctx.stroke();
},
hitTest: (geometry, point, tolerance) => {
const [anchor] = geometry.points;
const distance = anchor ? Math.hypot(point.x - anchor.x, point.y - anchor.y) : Infinity;
return distance <= 12 + tolerance ? { part: 'body', distance } : null;
}
};
chart.drawings.registerType('cross', cross);
chart.drawings.startTool('cross');geometry 提供锚点的像素坐标(points)和数据坐标(anchors)、绘图区尺寸、最终的 style、text、theme、数据(bars 是显示的 K 线,source 是回放已放出的源数据,两者都是 BarsView),以及换算价格(valueToY)、计算 K 线位置(barIndex)、格式化价格(formatValue)和测量文字(measureText)的工具函数。points: 0 表示锚点数量不限。
以下可选成员用来定制编辑体验:
| 成员 | 默认行为 |
|---|---|
handles(geometry) | 每个锚点一个控制点,id 为 '0'、'1'…… |
moveHandle(anchors, handle, to) | 替换该控制点对应的锚点;使用其他 id 的控制点时必须提供 |
axisMarks(anchors) | 选中时标出每个锚点的价格和时间 |
bounds(geometry) | 控制点的外框 |
editableText | false;为 true 时,放置和双击会在原位编辑文字 |
textAnchor | 0:文字从哪个锚点开始,也是编辑框打开的位置 |
place 和 complete(placed) | 所有锚点都由用户放置 |
levels(anchors, scale) | 没有可供预警监控的线 |
控制点的格式是 { id, x, y, cursor?, line?, marker? }:line 让它可以沿一条线段的任意位置被抓住(例如整条边),marker: false 不画圆点;moveHandle 收到指针下的锚点(已经过磁吸处理),返回新的锚点。下面这个类似矩形的类型,只允许用户拖动它的上边:
const band: DrawingTypeDefinition = {
points: 2,
paint, // 同上
hitTest,
handles: ({ points: [a, b] }) =>
a && b ? [{ id: 'top', x: (a.x + b.x) / 2, y: Math.min(a.y, b.y), cursor: 'ns-resize' }] : [],
moveHandle: ([a, b], _handle, to) =>
a && b
? a.price > b.price
? [{ ...a, price: to.price }, b]
: [a, { ...b, price: to.price }]
: [],
axisMarks: (anchors) => ({ prices: anchors.map((anchor) => anchor.price), times: [] })
};在 axisMarks() 中设置 always: true,可以让标签在画线未选中时也保持显示,水平线就是这样做的。
levels() 描述可供预警监控的线,每条线经过两个点:{ key, from, to, extend? },extend 取 none(两点之间)、end(越过 to 继续延伸)或 both;两个点时间相同时表示一条水平线。图表会沿子图的坐标在两点之间插值。如果线之间按画出来的距离偏移(例如通道的平行线),请用 scale.toScale() 和 scale.fromScale() 计算:
levels: ([start, end, through], scale) => {
const offset = scale.toScale(through.price) - scale.toScale(start.price);
const shift = (point) => ({
...point,
price: scale.fromScale(scale.toScale(point.price) + offset)
});
return [
{ key: 'base', from: start, to: end },
{ key: 'parallel', from: shift(start), to: shift(end) }
];
};如果用户放置的锚点比画线保存的少,可以设置 place,再在 complete() 中推算出其余锚点。预览时也会调用它,所以整个画线会跟随指针。仓位工具就是两次点击、保存三个锚点:
const position: DrawingTypeDefinition = {
points: 3,
place: 2,
complete: ([entry, target]) =>
entry && target
? [entry, target, { time: target.time, price: 2 * entry.price - target.price }]
: [],
paint,
hitTest
};