Skip to content

图表视图

openpine-chart crate 提供了 ChartView —— 一个自包含的图表组件,用于管理多个 Pine 脚本、处理用户交互,并通过平台无关的绘图接口渲染所有内容。它构建在图表渲染中描述的 Chart 数据模型之上。

openpine-vm 为你提供对脚本执行的低级控制,而 openpine-chart 则处理完整的生命周期:在后台编译脚本、布局窗格、渲染 K 线和绘图、以及响应鼠标事件。

添加依赖

toml
[dependencies]
openpine-chart = { git = "https://github.com/longbridge/openpine.git" }

架构

ChartView 架构ChartView 架构

你需要提供两样东西:

  1. DataProvider —— 提供 K 线数据(参见数据提供者
  2. DrawingContext —— 适用于你平台的 2D 绘图后端

ChartView 处理其他一切:脚本编译、执行、布局、渲染和用户交互。

创建 ChartView

rust
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:

rust
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 后端

管理脚本

添加脚本

rust
let id = cv.add_script(pine_source, "my-indicator".to_string());

脚本异步编译和执行。脚本产生输出后,图表会触发 ChartEvent::Draw。使用回调变体来处理错误:

rust
let id = cv.add_script_with_complete(pine_source, tag, |result| {
    match result {
        Ok(()) => println!("Script ready"),
        Err(e) => eprintln!("Script failed: {e}"),
    }
});

移除脚本

rust
cv.remove_script(id);
// 或移除全部:
cv.remove_all_scripts();

查询脚本

rust
// 当前图表上的所有脚本
let scripts: Vec<(String, ScriptId)> = cv.scripts();

// 通过标签查找脚本
if let Some(id) = cv.script_id_for_tag(&"my-indicator".to_string()) {
    // ...
}

渲染

在你的绑定/绘制回调中调用 render

rust
fn on_paint(cv: &ChartView<MyProvider, String>, canvas: &mut MyCanvas) {
    cv.render(canvas);
}

图表在需要重绘时(新数据到达、脚本完成、用户滚动等)会触发 ChartEvent::Draw。你的事件处理器应在收到此事件时触发重绘。

用户交互

将鼠标/滚轮事件从你的 UI 框架转发到 ChartView:

rust
// 滚动/缩放
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,并将更改应用回图表。

配置数据模型

脚本的配置由两部分组成:

rust
pub struct ScriptConfig {
    /// 每个序列图的视觉覆盖(颜色、样式、可见性)。
    pub series_overrides: HashMap<SeriesGraphId, SeriesGraphOverride>,
    /// 输入值覆盖。键 = 输入索引,值 = JSON 值。
    pub input_values: HashMap<usize, serde_json::Value>,
}

读取当前状态

rust
// 所有序列图的原始视觉状态(覆盖前)
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();

应用更改

rust
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>

rust
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 非空,显示下拉框而非自由输入。遵循 minmaxstep 约束。

Float { value, min, max, step, options } —— 带小数步长(默认 0.1)的数字输入框。optionsminmax 行为与 Int 相同。

Bool { value } —— 切换开关。

Color { value } —— 色块 + 颜色选择器弹窗。颜色是 RGBA 格式的打包 u32(R << 24) | (G << 16) | (B << 8) | A。显示为填充矩形;点击时打开包含十六进制输入和透明度滑块的颜色选择器。

Str { value, options } —— 文本输入框。如果 options 非空,显示下拉框。

Source { value } —— 固定选项的下拉框:openhighlowclosehl2hlc3ohlc4hlcc4。值为 SourceType 枚举。写入 input_values 时,转换为整数判别值:open=0、high=1、low=2、close=3、hl2=4、hlc3=5、ohlc4=6、hlcc4=7。

Enum { value, options } —— 下拉框。optionsVec<(i64, String)> ——(判别值,显示标签)对。在 input_values 中存储判别值。

Symbol { value } —— 交易标的代码的文本输入框(如 "AAPL")。

TimeFrame { value, options } —— 下拉框或文本输入。值为 TimeFrame。如果 options 非空,显示下拉框。

Session { value, options } —— 交易时段字符串的文本输入框或下拉框(如 "0930-1600")。

Time { value } —— 日期/时间选择器。值为自纪元以来的毫秒数(i64)。使用脚本的交易所时区(可通过 cv.timezone() 获取)进行日期时间转换。

TextArea { value } —— 多行文本区域。

应用输入更改:

rust
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

视觉标签页

视觉标签页显示每个序列图的视觉覆盖。获取数据:

rust
let graph_configs: HashMap<SeriesGraphId, SeriesGraphConfig> = cv.graph_configs(id).unwrap();
let overrides: HashMap<SeriesGraphId, SeriesGraphOverride> = cv.script_overrides(id).unwrap();

SeriesGraphConfig 变体

SeriesGraphConfig 描述原始(覆盖前)的视觉状态。每个变体对应一种绘图类型:

rust
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

rust
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 排序。每个分区显示:

  1. 一个标题行,包含图形标题(来自 SeriesGraphConfig::title)或生成的名称如"Plot #1"。
  2. 色块 —— 每个不同的颜色槽位一个。
  3. 属性控件 —— 特定于类型(样式、宽度、形状等)。
  4. 可见性切换 —— 复选框或开关,控制 SeriesGraphOverride::visible

Plot(线/面积/直方图)

最复杂的类型 —— 每个颜色槽位有自己的线型和宽度:

控件字段
色块(每个槽位)color_overrides[i]颜色选择器。显示为使用有效颜色填充的矩形。None 槽位显示交叉阴影或"自动"指示器。
线宽(每个槽位)line_width_overrides[i]按钮组:1、2、3、4 像素。未设置时回退到全局 line_width
线型(每个槽位)line_style_overrides[i]按钮组或下拉框:SolidDashedDotted。未设置时回退到全局 line_style
绘图样式(全局)style下拉框,包含所有 PlotStyle 值 —— 见下表。
可见性visible复选框 / 切换开关。

PlotStyle 枚举值:

描述
Line连接各 K 线值的连续线
LineBrna 值处断开的线(不插值)
StepLine阶梯线
StepLineBrna 处断开的阶梯线
Area线下方的填充面积
AreaBrna 处断开的面积
Histogram从零线到值的垂直柱
Columns从零线到值的填充柱
Circles每根 K 线处的圆形标记
Cross每根 K 线处的十字标记
Diamond每根 K 线处的菱形标记

PlotLineStyle 枚举值:

虚线模式
Solid无虚线
Dashed[6, 3]
Dotted[2, 2]

每个槽位的覆盖布局line_width_overridesline_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 ▾]                    ☑ Visible

PlotChar(字符标记)

控件字段
色块(每个槽位)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):

描述
AboveBarK 线上方
BelowBarK 线下方
Top窗格顶部
Bottom窗格底部
AbsoluteY 位置来自序列浮点值

Size(用于 PlotChar / PlotShape):

描述
Auto自动大小
Tiny最小
Small
Normal默认
Large
Huge最大

Shape(用于 PlotShape):

描述
XCrossX 形
Cross+ 形
Circle实心圆
Diamond菱形
Square实心方形
TriangleUp向上三角形
TriangleDown向下三角形
ArrowUp向上箭头
ArrowDown向下箭头
Flag旗帜
LabelUp向上标签
LabelDown向下标签

颜色选择器

每个色块点击后应打开颜色选择器,包含:

  1. 预设调色板 —— 常用颜色网格(如 6x6)。
  2. 十六进制输入 —— 接受 #RRGGBB 格式的文本字段。
  3. 透明度滑块 —— 0-255 范围,控制 alpha 通道。

颜色编码:颜色是 RGBA 格式的打包 u32 值:(R << 24) | (G << 16) | (B << 8) | A

rust
// 从 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]
  • PlotArrowcolor_overrides = [..up_colors | ..down_colors](拼接)。前 up_colors.len() 个条目为上箭头覆盖,其余为下箭头。
  • PlotCandlecolor_overrides = [..colors | ..wick_colors | ..border_colors](拼接)。

None 条目表示"保留原始颜色"。Some(color) 条目替换原始值。

应用视觉更改

rust
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 中显示属性的当前值时,使用三级回退解析:

  1. 覆盖值(来自 SeriesGraphOverride)—— 如已设置,使用该值。
  2. 配置默认值(来自 SeriesGraphConfig)—— 脚本的原始值。
  3. 主题默认值 —— 来自图表主题调色板的回退颜色。

恢复默认

将所有覆盖和输入重置为原始值:

rust
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 模式的完整实现。


切换标的 / 时间周期

rust
cv.set_symbol("NYSE:MSFT", TimeFrame::weeks(1));

这会重启数据流并在新的标的/时间周期上重新执行所有脚本。

持久化

保存和恢复整个图表状态(视口位置、缩放级别、窗格比例、所有脚本及其配置):

rust
// 保存
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所有脚本及其源码、标签、配置和可见性

加载快照时,脚本会从保存的源代码重新编译和重新执行。

视口控制

rust
// 获取/设置窗格高度比例([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();

基于 MIT 许可证发布。