图表视图
openpine-chart crate 提供了 ChartView —— 一个自包含的图表组件,用于管理多个 Pine 脚本、处理用户交互,并通过平台无关的绘图接口渲染所有内容。它构建在图表渲染中描述的 Chart 数据模型之上。
openpine-vm 为你提供对脚本执行的低级控制,而 openpine-chart 则处理完整的生命周期:在后台编译脚本、布局窗格、渲染 K 线和绘图、以及响应鼠标事件。
添加依赖
[dependencies]
openpine-chart = { git = "https://github.com/longbridge/openpine.git" }架构
你需要提供两样东西:
DataProvider—— 提供 K 线数据(参见数据提供者)DrawingContext—— 适用于你平台的 2D 绘图后端
ChartView 处理其他一切:脚本编译、执行、布局、渲染和用户交互。
创建 ChartView
use openpine_chart::{ChartView, ChartEvent, Size, Theme};
use openpine_vm::TimeFrame;
let mut cv = ChartView::new(
provider, // impl DataProvider
|fut| { tokio::spawn(fut); }, // async task spawner
"NASDAQ:AAPL", // symbol
TimeFrame::days(1), // timeframe
Size { width: 800.0, height: 600.0 },
move |event| { // event callback
match event {
ChartEvent::Draw => { /* trigger a repaint */ }
ChartEvent::EditScript { tag, id } => { /* open editor */ }
ChartEvent::RemoveScript { tag, id } => { /* confirm removal */ }
_ => {}
}
},
);
cv.set_theme(Theme::dark()); // or Theme::light()
cv.set_locale("en");泛型参数
ChartView<P, T> 有两个泛型参数:
| 参数 | 描述 |
|---|---|
P: DataProvider | 你的 K 线数据源 |
T: Clone + PartialEq | 附加到每个脚本的用户自定义"标签"类型。可使用 String 作为脚本名称、UUID,或者如果不需要标签则使用 ()。 |
标签让你可以在领域模型中标识脚本,而无需直接跟踪 ScriptId 值。
实现 DrawingContext
为你的渲染平台(Canvas 2D、Skia、Cairo 等)实现 DrawingContext trait:
use openpine_chart::*;
use openpine_vm::visuals::{Color, FontFamily, TextFormatting};
struct MyCanvas { /* your platform context */ }
impl DrawingContext for MyCanvas {
// Canvas operations
fn clear_rect(&mut self, rect: Rect) { /* ... */ }
// Colors & styles
fn set_fill_color(&mut self, color: Color) { /* ... */ }
fn set_stroke_color(&mut self, color: Color) { /* ... */ }
fn set_line_width(&mut self, width: f64) { /* ... */ }
fn set_line_dash(&mut self, pattern: &[f64]) { /* ... */ }
fn set_fill_linear_gradient(
&mut self, x0: f64, y0: f64, x1: f64, y1: f64, stops: &[(f64, Color)],
) { /* ... */ }
// Text
fn set_font(&mut self, size: f64, formatting: TextFormatting, family: FontFamily) { /* ... */ }
fn set_text_align(&mut self, align: TextAlign) { /* ... */ }
fn set_text_baseline(&mut self, baseline: TextBaseline) { /* ... */ }
fn fill_text(&mut self, text: &str, x: f64, y: f64) { /* ... */ }
fn measure_text(&self, text: &str) -> f64 { /* ... */ 0.0 }
// Paths
fn begin_path(&mut self) { /* ... */ }
fn move_to(&mut self, x: f64, y: f64) { /* ... */ }
fn line_to(&mut self, x: f64, y: f64) { /* ... */ }
fn close_path(&mut self) { /* ... */ }
fn arc(&mut self, x: f64, y: f64, radius: f64) { /* ... */ }
fn arc_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, radius: f64) { /* ... */ }
fn stroke(&mut self) { /* ... */ }
fn fill(&mut self) { /* ... */ }
// Shapes
fn fill_rect(&mut self, rect: Rect) { /* ... */ }
fn stroke_rect(&mut self, rect: Rect) { /* ... */ }
// Clipping (optional — default is no-op)
fn set_clip(&mut self, rect: Option<Rect>) { /* ... */ }
}set_clip(Some(rect)) 将绘制限制在给定矩形内。后续调用 set_clip(Some(other)) 应替换之前的裁剪区域(而非求交集)。set_clip(None) 完全移除裁剪。
参考实现: 参见 Web Playground 中使用的 Canvas 2D 后端。
管理脚本
添加脚本
let id = cv.add_script(pine_source, "my-indicator".to_string());脚本异步编译和执行。脚本产生输出后,图表会触发 ChartEvent::Draw。使用回调变体来处理错误:
let id = cv.add_script_with_complete(pine_source, tag, |result| {
match result {
Ok(()) => println!("Script ready"),
Err(e) => eprintln!("Script failed: {e}"),
}
});移除脚本
cv.remove_script(id);
// 或移除全部:
cv.remove_all_scripts();查询脚本
// 当前图表上的所有脚本
let scripts: Vec<(String, ScriptId)> = cv.scripts();
// 通过标签查找脚本
if let Some(id) = cv.script_id_for_tag(&"my-indicator".to_string()) {
// ...
}渲染
在你的绑定/绘制回调中调用 render:
fn on_paint(cv: &ChartView<MyProvider, String>, canvas: &mut MyCanvas) {
cv.render(canvas);
}图表在需要重绘时(新数据到达、脚本完成、用户滚动等)会触发 ChartEvent::Draw。你的事件处理器应在收到此事件时触发重绘。
用户交互
将鼠标/滚轮事件从你的 UI 框架转发到 ChartView:
// 滚动/缩放
cv.on_wheel(delta, mouse_x);
// 拖拽平移
cv.on_drag(dx); // dx 单位为 CSS 像素
// 悬停(更新十字线和光标)
cv.on_mouse_move(x, y);
// 点击(触发工具栏按钮操作)
cv.on_mouse_down(x, y); // 如果命中窗格分割线则返回 true
cv.on_mouse_up();
cv.on_click(x, y);
// 调整大小
cv.on_resize(Size { width: new_w, height: new_h });
// 光标样式(供你的 UI 更新)
let cursor: &str = cv.cursor_at(x, y); // "" or "ns-resize"事件
on_event 回调接收 ChartEvent<T>:
| 事件 | 描述 |
|---|---|
Draw | 图表需要重绘 |
EditScript { tag, id } | 用户点击了脚本图例上的 ✎ 编辑按钮 |
RemoveScript { tag, id } | 用户点击了 ✕ 移除按钮 |
ToggleVisibility { tag, id } | 用户切换了脚本的可见性 |
ShowError { tag, error } | 用户点击了 ⚠ 错误图标 |
ConfigureScript { tag, id, inputs } | 用户点击了 ⚙ 配置按钮 |
脚本配置
当用户点击脚本图例上的 ⚙ 按钮时,图表会触发 ConfigureScript 事件,并附带脚本当前的 InputInfo 列表。你的应用程序应呈现配置 UI,并将更改应用回图表。
配置数据模型
脚本的配置由两部分组成:
pub struct ScriptConfig {
/// 每个序列图的视觉覆盖(颜色、样式、可见性)。
pub series_overrides: HashMap<SeriesGraphId, SeriesGraphOverride>,
/// 输入值覆盖。键 = 输入索引,值 = JSON 值。
pub input_values: HashMap<usize, serde_json::Value>,
}读取当前状态
// 所有序列图的原始视觉状态(覆盖前)
let configs: HashMap<SeriesGraphId, SeriesGraphConfig> = cv.graph_configs(id).unwrap();
// 当前用户覆盖
let overrides: HashMap<SeriesGraphId, SeriesGraphOverride> = cv.script_overrides(id).unwrap();
// 完整配置(覆盖 + 输入值)
let config: &ScriptConfig = cv.script_config(id).unwrap();应用更改
cv.set_script_config(id, new_config);
// 或带完成回调:
cv.set_script_config_with_complete(id, new_config, |result| { /* ... */ });行为:
- 如果仅
series_overrides发生变化:图表立即重新渲染,无需重新执行脚本。 - 如果
input_values发生变化:脚本会被中止并使用新值重新启动。
构建配置 UI
本节描述如何为脚本设置构建配置对话框。
对话框结构
对话框包含两个标签页:
| 标签页 | 内容 | 显示条件 |
|---|---|---|
| 输入 | 脚本 input.*() 参数 | inputs.len() > 0 |
| 视觉 | 序列图视觉覆盖(颜色、样式) | graph_configs 非空 |
如果脚本有输入参数,对话框应默认显示输入标签页,否则显示视觉标签页。
底部按钮:
- 恢复默认 —— 清除所有覆盖并将输入重置为原始值。
- 确定 —— 关闭对话框(更改已即时应用,无需确认)。
输入标签页
ConfigureScript 事件提供一个 Vec<InputInfo>:
pub struct InputInfo {
pub idx: usize, // 从零开始的输入索引(input_values 的键)
pub title: Option<String>, // 人类可读的标签
pub tooltip: Option<String>, // 悬停提示文本
pub group: Option<String>, // 分组标题(可选)
pub kind: InputKind, // 类型特定的值和约束
pub default_value: serde_json::Value, // 脚本原始默认值(用于"恢复默认")
}根据 InputKind 变体渲染每个输入:
Int { value, min, max, step, options } —— 数字输入框。如果 options 非空,显示下拉框而非自由输入。遵循 min、max、step 约束。
Float { value, min, max, step, options } —— 带小数步长(默认 0.1)的数字输入框。options、min、max 行为与 Int 相同。
Bool { value } —— 切换开关。
Color { value } —— 色块 + 颜色选择器弹窗。颜色是 RGBA 格式的打包 u32:(R << 24) | (G << 16) | (B << 8) | A。显示为填充矩形;点击时打开包含十六进制输入和透明度滑块的颜色选择器。
Str { value, options } —— 文本输入框。如果 options 非空,显示下拉框。
Source { value } —— 固定选项的下拉框:open、high、low、close、hl2、hlc3、ohlc4、hlcc4。值为 SourceType 枚举。写入 input_values 时,转换为整数判别值:open=0、high=1、low=2、close=3、hl2=4、hlc3=5、ohlc4=6、hlcc4=7。
Enum { value, options } —— 下拉框。options 为 Vec<(i64, String)> ——(判别值,显示标签)对。在 input_values 中存储判别值。
Symbol { value } —— 交易标的代码的文本输入框(如 "AAPL")。
TimeFrame { value, options } —— 下拉框或文本输入。值为 TimeFrame。如果 options 非空,显示下拉框。
Session { value, options } —— 交易时段字符串的文本输入框或下拉框(如 "0930-1600")。
Time { value } —— 日期/时间选择器。值为自纪元以来的毫秒数(i64)。使用脚本的交易所时区(可通过 cv.timezone() 获取)进行日期时间转换。
TextArea { value } —— 多行文本区域。
应用输入更改:
let mut config = cv.script_config(id).cloned().unwrap_or_default();
// 对于索引 0 处值为 20 的 Int 输入:
config.input_values.insert(0, serde_json::json!(20));
// 对于 Source 输入,存储整数判别值:
config.input_values.insert(1, serde_json::json!(3)); // close
cv.set_script_config(id, config);更改应立即应用(实时预览)—— 图表重新执行脚本,完成后触发 ChartEvent::Draw。
视觉标签页
视觉标签页显示每个序列图的视觉覆盖。获取数据:
let graph_configs: HashMap<SeriesGraphId, SeriesGraphConfig> = cv.graph_configs(id).unwrap();
let overrides: HashMap<SeriesGraphId, SeriesGraphOverride> = cv.script_overrides(id).unwrap();SeriesGraphConfig 变体
SeriesGraphConfig 描述原始(覆盖前)的视觉状态。每个变体对应一种绘图类型:
pub enum SeriesGraphConfig {
Plot {
title: Option<String>,
colors: Vec<Option<Color>>, // 不同的颜色槽位
line_width: i32,
style: PlotStyle,
line_style: PlotLineStyle,
},
PlotChar {
title: Option<String>,
colors: Vec<Option<Color>>,
char_value: char,
location: Location,
size: Size,
},
PlotShape {
title: Option<String>,
colors: Vec<Option<Color>>,
style: Shape,
location: Location,
size: Size,
},
PlotArrow {
title: Option<String>,
up_colors: Vec<Option<Color>>, // 上箭头颜色槽位
down_colors: Vec<Option<Color>>, // 下箭头颜色槽位
},
PlotCandle {
title: Option<String>,
colors: Vec<Option<Color>>, // 柱体颜色槽位
wick_colors: Vec<Option<Color>>,
border_colors: Vec<Option<Color>>,
},
PlotBar {
title: Option<String>,
colors: Vec<Option<Color>>,
},
BackgroundColor {
title: Option<String>,
colors: Vec<Option<Color>>,
},
Fill {
title: Option<String>,
colors: Vec<Option<Color>>,
},
}颜色槽位:colors 向量包含脚本使用的不同颜色(按首次出现顺序排列)。None 表示 na / 主题默认值。每个槽位在 SeriesGraphOverride::color_overrides 中有对应的位置。
SeriesGraphOverride
pub struct SeriesGraphOverride {
pub color_overrides: Vec<Option<Color>>,
pub line_width: Option<i32>,
pub style: Option<PlotStyle>,
pub line_style: Option<PlotLineStyle>,
pub char_value: Option<char>,
pub shape: Option<Shape>,
pub location: Option<Location>,
pub size: Option<Size>,
pub line_width_overrides: Vec<Option<i32>>,
pub line_style_overrides: Vec<Option<PlotLineStyle>>,
pub visible: bool, // 默认为 true
}UI 布局
视觉标签页渲染一个垂直的分区列表 —— 每个序列图一个分区,按 SeriesGraphId 排序。每个分区显示:
- 一个标题行,包含图形标题(来自
SeriesGraphConfig::title)或生成的名称如"Plot #1"。 - 色块 —— 每个不同的颜色槽位一个。
- 属性控件 —— 特定于类型(样式、宽度、形状等)。
- 可见性切换 —— 复选框或开关,控制
SeriesGraphOverride::visible。
Plot(线/面积/直方图)
最复杂的类型 —— 每个颜色槽位有自己的线型和宽度:
| 控件 | 字段 | 值 |
|---|---|---|
| 色块(每个槽位) | color_overrides[i] | 颜色选择器。显示为使用有效颜色填充的矩形。None 槽位显示交叉阴影或"自动"指示器。 |
| 线宽(每个槽位) | line_width_overrides[i] | 按钮组:1、2、3、4 像素。未设置时回退到全局 line_width。 |
| 线型(每个槽位) | line_style_overrides[i] | 按钮组或下拉框:Solid、Dashed、Dotted。未设置时回退到全局 line_style。 |
| 绘图样式(全局) | style | 下拉框,包含所有 PlotStyle 值 —— 见下表。 |
| 可见性 | visible | 复选框 / 切换开关。 |
PlotStyle 枚举值:
| 值 | 描述 |
|---|---|
Line | 连接各 K 线值的连续线 |
LineBr | 在 na 值处断开的线(不插值) |
StepLine | 阶梯线 |
StepLineBr | 在 na 处断开的阶梯线 |
Area | 线下方的填充面积 |
AreaBr | 在 na 处断开的面积 |
Histogram | 从零线到值的垂直柱 |
Columns | 从零线到值的填充柱 |
Circles | 每根 K 线处的圆形标记 |
Cross | 每根 K 线处的十字标记 |
Diamond | 每根 K 线处的菱形标记 |
PlotLineStyle 枚举值:
| 值 | 虚线模式 |
|---|---|
Solid | 无虚线 |
Dashed | [6, 3] |
Dotted | [2, 2] |
每个槽位的覆盖布局:line_width_overrides 和 line_style_overrides 是与 color_overrides 相同索引的平行数组。如果数组长度小于颜色槽位数量,缺失的条目使用全局 line_width / line_style 覆盖值(或配置默认值)。
示例:一个有 3 种不同颜色的 plot 渲染 3 行,每行包含一个色块 + 线宽按钮 + 线型按钮:
[■ #2962FF] [1] [2] [3] [4] [━━] [╌╌] [∙∙]
[■ #FF6D00] [1] [2] [3] [4] [━━] [╌╌] [∙∙]
[■ #00BCD4] [1] [2] [3] [4] [━━] [╌╌] [∙∙]
Style: [Line ▾] ☑ VisiblePlotChar(字符标记)
| 控件 | 字段 | 值 |
|---|---|---|
| 色块(每个槽位) | color_overrides[i] | 颜色选择器 |
| 字符 | char_value | 单字符文本输入。在每根 K 线上绘制的字符。 |
| 位置 | location | 下拉框 —— 见下方 Location 表 |
| 大小 | size | 下拉框 —— 见下方 Size 表 |
| 可见性 | visible | 复选框 |
PlotShape(形状标记)
| 控件 | 字段 | 值 |
|---|---|---|
| 色块(每个槽位) | color_overrides[i] | 颜色选择器 |
| 形状 | shape | 下拉框 —— 见下方 Shape 表 |
| 位置 | location | 下拉框 —— 见下方 Location 表 |
| 大小 | size | 下拉框 —— 见下方 Size 表 |
| 可见性 | visible | 复选框 |
PlotArrow(方向箭头)
PlotArrow 有两个独立的颜色组 —— 上箭头和下箭头:
| 控件 | 字段 | 说明 |
|---|---|---|
| 上颜色色块 | color_overrides[0..up_len] | 标签:"上颜色"。每个不同的上箭头颜色一个色块。 |
| 下颜色色块 | color_overrides[up_len..] | 标签:"下颜色"。偏移量为配置中的 up_colors.len()。 |
| 可见性 | visible | 复选框 |
PlotCandle(OHLC K 线)
PlotCandle 有三个颜色组,使用扁平的 color_overrides 布局:
| 控件 | 覆盖偏移量 | 标签 |
|---|---|---|
| 柱体色块 | 0..body_len | "柱体" |
| 影线色块 | body_len..body_len+wick_len | "影线" |
| 边框色块 | body_len+wick_len.. | "边框" |
| 可见性 | visible | 复选框 |
其中 body_len = colors.len(),wick_len = wick_colors.len() 来自配置。
PlotBar
| 控件 | 字段 |
|---|---|
| 色块(每个槽位) | color_overrides[i] |
| 可见性 | visible |
BackgroundColor(bgcolor)
| 控件 | 字段 |
|---|---|
| 色块(每个槽位) | color_overrides[i] |
| 可见性 | visible |
Fill
| 控件 | 字段 |
|---|---|
| 色块(每个槽位) | color_overrides[i] |
| 可见性 | visible |
共享枚举值
Location(用于 PlotChar / PlotShape):
| 值 | 描述 |
|---|---|
AboveBar | K 线上方 |
BelowBar | K 线下方 |
Top | 窗格顶部 |
Bottom | 窗格底部 |
Absolute | Y 位置来自序列浮点值 |
Size(用于 PlotChar / PlotShape):
| 值 | 描述 |
|---|---|
Auto | 自动大小 |
Tiny | 最小 |
Small | 小 |
Normal | 默认 |
Large | 大 |
Huge | 最大 |
Shape(用于 PlotShape):
| 值 | 描述 |
|---|---|
XCross | X 形 |
Cross | + 形 |
Circle | 实心圆 |
Diamond | 菱形 |
Square | 实心方形 |
TriangleUp | 向上三角形 |
TriangleDown | 向下三角形 |
ArrowUp | 向上箭头 |
ArrowDown | 向下箭头 |
Flag | 旗帜 |
LabelUp | 向上标签 |
LabelDown | 向下标签 |
颜色选择器
每个色块点击后应打开颜色选择器,包含:
- 预设调色板 —— 常用颜色网格(如 6x6)。
- 十六进制输入 —— 接受
#RRGGBB格式的文本字段。 - 透明度滑块 —— 0-255 范围,控制 alpha 通道。
颜色编码:颜色是 RGBA 格式的打包 u32 值:(R << 24) | (G << 16) | (B << 8) | A。
// 从 Color 提取分量
let r = color.red(); // 0–255
let g = color.green(); // 0–255
let b = color.blue(); // 0–255
let a = color.alpha(); // 0–255
// 从分量创建
let color = Color::from_rgba(255, 0, 0, 255); // 不透明红色在 CSS 中显示:rgba({r}, {g}, {b}, {a / 255})。
颜色覆盖编码
color_overrides 数组与 SeriesGraphConfig 中的颜色槽位对齐:
- 单段类型(Plot、PlotChar、PlotShape、PlotBar、BackgroundColor、Fill):
color_overrides[i]对应colors[i]。 - PlotArrow:
color_overrides = [..up_colors | ..down_colors](拼接)。前up_colors.len()个条目为上箭头覆盖,其余为下箭头。 - PlotCandle:
color_overrides = [..colors | ..wick_colors | ..border_colors](拼接)。
None 条目表示"保留原始颜色"。Some(color) 条目替换原始值。
应用视觉更改
let mut config = cv.script_config(id).cloned().unwrap_or_default();
let ovr = config.series_overrides.entry(graph_id).or_default();
// 将第一个颜色槽位覆盖为红色
ovr.color_overrides = vec![Some(Color::from_rgba(255, 0, 0, 255))];
// 将绘图样式改为直方图
ovr.style = Some(PlotStyle::Histogram);
// 隐藏此序列图
ovr.visible = false;
cv.set_script_config(id, config);仅视觉更改会立即重新渲染,无需重新执行脚本。
有效值解析
在 UI 中显示属性的当前值时,使用三级回退解析:
- 覆盖值(来自
SeriesGraphOverride)—— 如已设置,使用该值。 - 配置默认值(来自
SeriesGraphConfig)—— 脚本的原始值。 - 主题默认值 —— 来自图表主题调色板的回退颜色。
恢复默认
将所有覆盖和输入重置为原始值:
let mut config = ScriptConfig::default();
// 使用 InputInfo 中的原始默认值重新填充 input_values
for input in &inputs {
config.input_values.insert(input.idx, input.default_value.clone());
}
cv.set_script_config(id, config);这将清除所有 series_overrides 并重置输入,触发完全重新执行。
事件流概览
用户点击图例工具栏上的 ⚙
↓
ChartEvent::ConfigureScript { tag, id, inputs }
↓
你的应用打开配置对话框
↓
获取当前状态:
cv.graph_configs(id) → 每个图形的 SeriesGraphConfig
cv.script_overrides(id) → 当前覆盖
↓
用户编辑输入或视觉属性
↓
构建 ScriptConfig { series_overrides, input_values }
cv.set_script_config(id, config)
↓
图表重新渲染(仅视觉)或重新执行(输入变更)
ChartEvent::Draw 触发 → 重绘
↓
如果输入已更改,重新获取 graph_configs(绘图结构可能已变更)
↓
用户点击确定 → 关闭对话框
保存快照以便持久化参考实现: 参见 Web Playground 中的 ScriptConfigDialog.vue,了解此 UI 模式的完整实现。
切换标的 / 时间周期
cv.set_symbol("NYSE:MSFT", TimeFrame::weeks(1));这会重启数据流并在新的标的/时间周期上重新执行所有脚本。
持久化
保存和恢复整个图表状态(视口位置、缩放级别、窗格比例、所有脚本及其配置):
// 保存
let snapshot = cv.save();
let json = serde_json::to_string(&snapshot).unwrap();
// 将 `json` 存储到 localStorage、文件或数据库
// 恢复
let snapshot: ChartSnapshot<String> = serde_json::from_str(&json).unwrap();
cv.load(snapshot);ChartSnapshot<T> 包含:
| 字段 | 描述 |
|---|---|
scroll_offset | 左侧边缘的小数 K 线索引 |
bar_count | 可见 K 线数量(缩放级别) |
pane_ratios | 主窗格 + 子窗格的高度比例 |
scripts | 所有脚本及其源码、标签、配置和可见性 |
加载快照时,脚本会从保存的源代码重新编译和重新执行。
视口控制
// 获取/设置窗格高度比例([main, sub0, sub1, ...],总和 ≈ 1.0)
let ratios = cv.pane_ratios();
cv.set_pane_ratios(vec![0.6, 0.2, 0.2]);
// 滚动到指定 K 线
cv.scroll_to_bar(42);
// 高亮某根 K 线(例如从策略测试器点击)
cv.set_highlighted_bar(Some(42));
cv.set_highlighted_bar(None); // 清除
// 读取视口状态
let offset = cv.scroll_offset();
let visible_bars = cv.bar_count();
let total = cv.total_bars();