Skip to content

画线工具 ​

让用户画线 ​

ts
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 让工具在画完后保持激活,方便连续画多条:

ts
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-line2线段
ray2越过第二个锚点向外延伸
extended-line2向两端延伸
arrow2第二个锚点处为实心箭头
horizontal-line1横跨整个图表;价格常驻显示在价格轴上
horizontal-ray1从锚点向右延伸;价格常驻显示在价格轴上
vertical-line1纵贯整个子图;时间常驻显示在时间轴上
rectangle28 个控制点:四角同时调整两个方向,四边中点只移动一条边
parallel-channel3基线经过锚点 1–2,平行线经过锚点 3;拖动平行线的端点调整宽度
fib-retracement20、0.236、0.382、0.5、0.618、0.786、1 各档,每档有自己的颜色和填充带
fib-extension3把锚点 1 到 2 的涨跌幅从锚点 3 投射出去:0、0.382、0.618、1、1.272、1.618、2、2.618
pitchfork3安德鲁音叉:中线从锚点 1 穿过锚点 2、3 的中点,两条平行线分别经过锚点 2 和 3
price-range2锚点之间的涨跌额、涨跌幅和 K 线数量,箭头使用涨跌色
date-range2两个锚点之间的 K 线数量和时长,例如 24 根 K 线 · 6小时
date-price-range2涨跌额、涨跌幅、K 线数量和时长
long-position3(放置 2 个)开仓价、止盈和止损;见仓位
short-position3(放置 2 个)同上,用于做空
path2 个以上折线,双击或按 Enter 结束
text1一行或多行文字,在图上直接输入和编辑
callout2指向第一个锚点的文字框,文字框位于第二个锚点

仓位 ​

long-position 和 short-position 用来规划一笔交易。第一次点击确定开仓价,第二次点击确定结束时间和到止盈价的距离;止损价初始为止盈价的镜像,所以盈亏比从 1 开始。开仓线以及止盈、止损两条边都可以在任意位置抓住上下拖动;两侧的控制点用来左右拉宽。开仓价始终位于止损和止盈之间,止损和止盈也始终在开仓价的正确一侧。

两块区域分别表示收益和风险。从开仓后的 K 线开始逐根检查,第一根触及止盈或止损的 K 线会平仓(同一根 K 线两者都触及时按止损算);在此之前显示按最新收盘价计算的浮动盈亏。配合行情回放就是一个练习工具:画好仓位,逐根前进,看这笔交易的结果。

ts
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。这个区域会随视图、数据和编辑而变化,所以在画线选中期间,可以在动画帧里读取它:

ts
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 } });

通过代码修改 ​

ts
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 });
ts
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() 会替换全部画线(例如加载文档时),并清空历史记录。

ts
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、选中、控制点、坐标轴标签、拖动、复制、撤销和保存都由图表处理。所有坐标都相对于子图的绘图区域。

ts
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)控制点的外框
editableTextfalse;为 true 时,放置和双击会在原位编辑文字
textAnchor0:文字从哪个锚点开始,也是编辑框打开的位置
place 和 complete(placed)所有锚点都由用户放置
levels(anchors, scale)没有可供预警监控的线

控制点的格式是 { id, x, y, cursor?, line?, marker? }:line 让它可以沿一条线段的任意位置被抓住(例如整条边),marker: false 不画圆点;moveHandle 收到指针下的锚点(已经过磁吸处理),返回新的锚点。下面这个类似矩形的类型,只允许用户拖动它的上边:

ts
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() 计算:

ts
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() 中推算出其余锚点。预览时也会调用它,所以整个画线会跟随指针。仓位工具就是两次点击、保存三个锚点:

ts
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
};

基于 MIT 许可证发布。