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);
// or remove all:
cv.remove_all_scripts();

查詢腳本

rust
// All scripts currently on the chart
let scripts: Vec<(String, ScriptId)> = cv.scripts();

// Find a script by its tag
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 框架轉發過來:

rust
// Scroll/zoom
cv.on_wheel(delta, mouse_x);

// Pan by dragging
cv.on_drag(dx);  // dx in CSS pixels

// Hover (updates crosshair + cursor)
cv.on_mouse_move(x, y);

// Click (triggers toolbar button actions)
cv.on_mouse_down(x, y);  // returns true if a pane divider was hit
cv.on_mouse_up();
cv.on_click(x, y);

// Resize
cv.on_resize(Size { width: new_w, height: new_h });

// Cursor style (for your UI to update)
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 {
    /// Visual overrides per series graph (colors, styles, visibility).
    pub series_overrides: HashMap<SeriesGraphId, SeriesGraphOverride>,
    /// Input value overrides. Key = input index, value = JSON value.
    pub input_values: HashMap<usize, serde_json::Value>,
}

讀取目前狀態

rust
// Original visual state of all series graphs (before overrides)
let configs: HashMap<SeriesGraphId, SeriesGraphConfig> = cv.graph_configs(id).unwrap();

// Current user overrides
let overrides: HashMap<SeriesGraphId, SeriesGraphOverride> = cv.script_overrides(id).unwrap();

// Full config (overrides + input values)
let config: &ScriptConfig = cv.script_config(id).unwrap();

套用變更

rust
cv.set_script_config(id, new_config);
// or with completion callback:
cv.set_script_config_with_complete(id, new_config, |result| { /* ... */ });

行為:

  • 如果只有 series_overrides 變更:圖表會立即重新渲染,無需重新執行腳本。
  • 如果 input_values 變更:腳本會被中止並以新值重新啟動。

建構設定 UI

本節描述如何為腳本設定建構設定對話框。設計參考 TradingView 的指標設定對話框

設定對話框佈局設定對話框佈局

對話框結構

對話框有兩個分頁:

分頁內容顯示條件
輸入腳本的 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();
// For an Int input at index 0 with value 20:
config.input_values.insert(0, serde_json::json!(20));
// For a Source input, store the integer discriminant:
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>>,          // Distinct color slots
        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>>,       // Up-arrow color slots
        down_colors: Vec<Option<Color>>,     // Down-arrow color slots
    },
    PlotCandle {
        title: Option<String>,
        colors: Vec<Option<Color>>,          // Body color slots
        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,  // defaults to true
}

各圖表類型的 UI 控件

Plot(折線/面積/直方圖):

  • 每個顏色插槽一個色塊。點擊開啟顏色選擇器。
  • 每個插槽的線寬選擇器(1-4 像素按鈕)。
  • 每個插槽的線條樣式選擇器(實線 / 虛線 / 點線)。
  • 全域繪圖樣式下拉選單:Line、StepLine、Diamond、Histogram、Cross、Area、Columns、Circles、LineBr、AreaBr、StepLineBr。
  • 可見性核取方塊。

PlotChar(字元標記):

  • 色塊。
  • 字元文字輸入(單個字元)。
  • 位置下拉選單:AboveBar、BelowBar、Absolute、Top、Bottom。
  • 大小下拉選單:Auto、Tiny、Small、Normal、Large、Huge。

PlotShape(圖形標記):

  • 色塊。
  • 圖形下拉選單:XCross、Cross、Circle、Diamond、Square、TriangleUp、TriangleDown、ArrowUp、ArrowDown、Flag、LabelUp、LabelDown。
  • 位置下拉選單(與 PlotChar 相同)。
  • 大小下拉選單(與 PlotChar 相同)。

PlotArrow(方向箭頭):

  • 分開的色塊群組,標籤為「上升」和「下降」。

PlotCandle(OHLC K 線):

  • 三個色塊群組,標籤為「實體」、「影線」和「邊框」。

PlotBar / BackgroundColor / Fill

  • 僅色塊。

所有圖表類型皆支援透過 visible: bool可見性切換

顏色覆寫編碼

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();

// Override the first color slot to red
ovr.color_overrides = vec![Some(Color::from_rgba(255, 0, 0, 255))];

// Change plot style to histogram
ovr.style = Some(PlotStyle::Histogram);

// Hide this series graph
ovr.visible = false;

cv.set_script_config(id, config);

僅視覺變更會立即重新渲染,無需重新執行腳本。

有效值解析

在 UI 中顯示屬性的目前值時,按三級回退解析:

  1. 覆寫值(來自 SeriesGraphOverride)—— 如已設定,使用它。
  2. 設定預設值(來自 SeriesGraphConfig)—— 腳本的原始值。
  3. 主題預設值 —— 來自圖表主題色彩調色盤的回退顏色。

重設為預設值

要將所有覆寫和輸入重設為原始值:

rust
let mut config = ScriptConfig::default();
// Re-populate input_values with original defaults from InputInfo
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
// Save
let snapshot = cv.save();
let json = serde_json::to_string(&snapshot).unwrap();
// Store `json` in localStorage, a file, or a database

// Restore
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
// Get/set pane height ratios ([main, sub0, sub1, ...], sum ≈ 1.0)
let ratios = cv.pane_ratios();
cv.set_pane_ratios(vec![0.6, 0.2, 0.2]);

// Scroll to a specific bar
cv.scroll_to_bar(42);

// Highlight a bar (e.g., from strategy tester click)
cv.set_highlighted_bar(Some(42));
cv.set_highlighted_bar(None);  // clear

// Read viewport state
let offset = cv.scroll_offset();
let visible_bars = cv.bar_count();
let total = cv.total_bars();

基於 MIT 許可證發佈。