架构
本页面向贡献者,介绍内核的组织方式,以及让它保持可维护的规则。
仓库结构
packages/core @kumochart-sdk/core,SDK 本体(无运行时依赖)
packages/vue @kumochart-sdk/vue,Vue 3 适配包
packages/react @kumochart-sdk/react,React 适配包
examples 原生 JS、Vue、React 示例,端到端测试页和性能基准页
e2e Playwright 测试(Chromium、Firefox、WebKit)
scripts 架构检查与包体积检查
docs 文档包之间通过 @kumochart-sdk/source 导出条件解析彼此的源码,因此示例和测试无需构建,直接基于 src 运行。
分层
packages/core/src 中的模块只能导入同层或更低层的模块。npm run check:architecture 会强制执行这条规则,并拒绝运行时的循环导入。
| 层 | 模块 | 职责 |
|---|---|---|
| 0 | utils | 错误、校验、合并、缓存、注册表 |
| 1 | events、data、options、canvas | 事件总线、K 线存储、配置项与主题、画布辅助函数 |
| 2 | format、scales、layout、feed | 数字/日期格式化、时间与价格坐标、刻度、子图布局、周期、交易日历、聚合与 K 线变换 |
| 3 | panes、series、indicators | 子图存储、序列类型与渲染器、指标引擎 |
| 4 | render、interaction、objects | 帧的构建与绘制、输入状态机、对象协调 |
| 5 | drawings、trading | 画线工具与编辑、委托单与标记 |
| 6 | alerts | 预警计算与预警线 |
| 7 | chart | ChartCore、公开 API 门面、状态、插件 |
更新流程
所有变更都遵循同一条路径:
- 公开 API 方法在
core.run()内执行,事件派发会被推迟。 - 方法修改模型(K 线、子图、指标、对象……),并调用
invalidate()(几何信息和所有图层)或invalidateLayers(...)(部分图层)。 FrameScheduler在下一个动画帧中统一绘制一次。如果几何信息已过期,绘制时会先构建帧(frame):布局、可见 K 线、适配后的价格坐标、刻度,以及可直接用于渲染的点缓冲区。三个图层和所有坐标查询读取的都是同一帧。- 过程中产生的事件在操作完成后派发。
需要几何信息的查询(timeToX、getVisibleRange 等)在帧过期时会同步构建帧,但不进行绘制。
数据
BarStore 以 TypedArray 列存储 K 线。每次修改都会递增 revision 并记录变化的内容;使用方调用 changesSince(theirRevision),得到 null、{ kind: 'tail', from } 或 { kind: 'reset' }。这样所有派生结构(指标值、时间轴刻度)都能增量更新,而无需追踪对象引用。
图表显示的 K 线是派生出来的。Feed 保存源数据点(应用推送的 K 线和逐笔成交,按源周期组织)、每个源数据点所属的日历分桶,以及回放的截止时间。显示存储中每个分桶对应一个点,由截止时间之前的源数据点聚合而成。源数据更新时,从第一个变化点所在的分桶开始重新聚合;如果只有最后一根显示 K 线变化或者追加了新 K 线,显示存储会记录一次 tail 变化,因此指标和预警每个 tick 仍是 O(1)。切换周期、修改交易日历、插入历史数据或把回放游标往回移动时,显示存储会以 reset 的方式整体重建。对比序列使用同样的 Feed,只是数据点是数值。
设置了 K 线变换(平均 K 线、砖形图、等幅 K 线或自定义变换)时,Feed 先把显示数据写入一个按时间组织的中间存储,再由 TransformStage 生成图表的 K 线。TransformStage 保留最后一根时间 K 线之前的变换流:tail 变化时在克隆上重跑这一根并接着处理新追加的 K 线,所以逐 tick 更新仍是增量的;reset 时重新开始一条变换流。时间相同的输出会依次错开 1 毫秒,保证显示的时间严格递增。
MarketData(位于 chart)管理源周期、显示周期、交易日历和 K 线变换,先校验主 K 线和所有序列,再一起应用每次写入。Replay 负责移动截止时间,并用窗口的计时器驱动播放。图表之外的代码(插件、自定义画线、chart.data.bars)通过只读的 BarsView 读取 K 线;源数据视图止于回放截止时间。
技术指标
指标是一个流:next(bar) 返回它在一根 K 线上的输出。IndicatorSeries 按列存储这些输出,并保留一份最新 K 线之前的流的克隆。某个 tick 改变最新一根 K 线时,会恢复这份克隆并重放一根 K 线,因此每个指标的更新都是 O(1)。
价格坐标与坐标系
每个子图有一组价格坐标:right、left 和若干叠加坐标,各自适配放在上面的序列和指标。内部以绘图区的左边缘为 x 的 0 点,左侧坐标轴位于负坐标区,所以在绘图区内绘制和做命中测试的代码都不需要关心它。绘制一帧时画布会平移左侧坐标轴的宽度,指针输入在输入控制器处换算,公开 API(layout()、时间轴坐标、指针事件、十字光标、drawings.bounds())报告的 x 都从图表左边缘算起。
渲染
三块画布叠放在一起:scene(网格、序列、指标、坐标轴)、objects(画线、委托单、预警、标记)和 cursor(十字光标、图例、最新价标签)。悬停时只重绘 cursor 层;拖动对象时重绘 objects 层。
序列渲染器接收已转换为像素的 PointBuffer。缓冲区归 BufferPool 所有,并在帧之间复用。
交互
InputController 把 DOM 输入转换为 InteractionHost 上的意图。手势构成一个小型状态机(pressed、pan、drag、pinch、坐标轴拖动、分隔线拖动、触屏十字光标、画线工具)。任何手势都可以被 pointercancel、lostpointercapture、窗口失焦或 Escape 取消;取消对象拖动时会还原该对象。悬停命中测试和滚轮缩放都会合并为每帧一次。
对象
画线、委托单、预警和标记都实现了 ObjectDomain:命中测试、绘制和拖动会话。ObjectsController 在所有域中选出最佳命中(按钮优先于控制点,控制点优先于标签,标签优先于线,线优先于主体;其次是已选中的对象,最后是距离最近的对象),并维护唯一的选中状态和悬停状态。
画线
每种画线都是一个 DrawingTypeDefinition:负责绘制和对锚点做命中测试,并可声明 handles、moveHandle、axisMarks、bounds 和 editableText。内置类型(drawings/built-in)与自定义类型使用同一套约定。DrawingsDomain 本身是通用的:它运行画线工具会话(tool.ts),结合磁吸和 Shift 角度约束解析锚点(anchoring.ts),绘制控制点并做命中测试(handles.ts),绘制坐标轴标记(axis-marks.ts),并打开图上的文字编辑器(text-editor.ts)。锚点以时间和价格存储,因此切换周期和回放时画线保持不变。
插件
PluginSet(位于 chart)保存图表的插件。场景绘制时会对每个子图、在序列的下方和上方各调用一次,传入的上下文已裁剪并平移到绘图区;插件只能看到公开类型和只读的数据视图。大多数用户用不到的可选功能,应基于这个接口做成独立的包。几乎所有用户都需要的功能,或者必须依赖内部实现的功能,放在核心里,例如会改变数据流程的 K 线变换。
设计原则
- API 调用从不触发事件。 只有用户操作和预警才会。应用可以同步图表状态并回传给图表,而不会造成循环。
- 先校验,再修改。 非法输入会在任何改变发生之前抛出指明该值的
ChartError。 - 不做静默回退。 无法计算的值保持为空;不可能出现的状态直接抛出错误。
- 不保留兼容代码。 每件事只有一种做法;API 的变更公开进行。
- 缓存有归属、有上限。 每个缓存都属于某个图表实例,以修订号而非对象引用作为键,并且有大小限制。
质量门禁
npm run verify 会运行架构检查、ESLint(strictTypeChecked)、Prettier、TypeScript(strict、noUncheckedIndexedAccess、exactOptionalPropertyTypes)、单元测试、构建和包体积预算检查。CI 还会在三种浏览器中运行端到端测试。