Skip to content

Lightweight Charts as alternative plotting backend #815

@ChadThackray

Description

@ChadThackray

Summary

Add an abstraction layer allowing different plotting backends to be implemented other than plotly.

In particular we look to support TradingView Lightweight Charts (via litecharts). With plotly remaining the default to minimize disruption to current users.

Proposed Approach

Figure Protocol

Introduce a Figure protocol that both Plotly and LWC implement:

The aim would be to have a backend-agnostic vocabulary for creating the most common kinds of charts used in the library, which could be then implemented for each backend.

This would unlock using any number of charting technologies, not just LWC.

Any special charts or methods that can only be implemented using a particular backend can be added as an extra capability, see below for capability reporting.

Example usage

fig = vbt.plotting.create_figure(backend="lwc")
fig.add_ohlc(ohlc_data)
fig.add_line(sma_data, name="SMA 20")
fig.show()

Or

# Global default (optional)
vbt.settings.plotting['default_backend'] = 'lwc'

# Per-call override
df.vbt.ohlcv.plot(backend="lwc")
pf.orders.plot(backend="lwc")

# Plotly remains default, all existing code unchanged
df.vbt.ohlcv.plot()  # Still works exactly as before

Dashboarding

The current system involving PlotsBuilderMixin.plots is tightly coupled with Plotly itself, producing a single plotly figure with sub-plots.

This is problematic for adding additional backends with regards to plotting.

I propose to instead replace this system with a dashboarding system whose job is to collect and compose multiple independent figures using the figure protocol above.

This will allow even using multiple backends in a single dashboard.

Running any .plots() function will return a Dashboard object instead of a plotly go.Figure. Generally speaking users are not manipulating this figure and rather just displaying it with .show(), so actual breakage to end user experience is minimal

Some functionality will be lost such as sharing of x-axis. Although this can potentially be regained with some custom JS subject to further enquiry.

Individual plots can be retrieved and manipulated by the user by indexing into the Dashboard object i.e.

dashboard = pf.plots(subplots=["orders", "trades"])
orders_fig = dashboard["orders"]
native_plotly_fig = orders_fig.native

orders_fig here would be a PlotlyFigure object, with the ability to retrieve the "native" figure object for plotly specific manipulation. Similar for LWC.

Capability-based selection

Not all chart types work with LWC. A capability system declares what each backend supports:

Chart Type Plotly LWC
Candlestick/OHLC
Line/Area
Markers (entry/exit)
Rectangles (zones)
Gauge
Heatmap
Box plots
3D Volume

When LWC is requested for an unsupported chart type, it fails explicitly with a clear error.

Backwards Compatibility

  1. Plotly is default - no behavior change for normal .plot() calls unless backend= is specified. Unlike dashboards we return actual plotly go.Figure objects for this. Potentially could migrate later on.
  2. PlotlyFigure wraps go.Figure - existing code using .update_layout(), .add_shape(), etc. continues to work via __getattr__ proxy
  3. Explicit escape hatch via .native lets users reach the underlying Plotly figure for advanced or Plotly‑specific operations.

Current Vectorbt Plotting Architecture

Path A: Single plot Methods

┌─────────────────────────────────────────────────────────────────┐
│                           USER CALLS                            │
│                                                                 │
│   pf.plot_value()       orders.plot()        df.vbt.plot()      │
│   pf.plot_drawdowns()   trades.plot()        df.vbt.ohlcv.plot()│
│                                                                 │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      PLOT METHOD ENTRY                          │
│                                                                 │
│  Plot methods live on accessors/records/portfolio objects.      │
│  They accept optional fig/add_trace kwargs and often a          │
│  return_fig flag that decides whether to return the wrapper     │
│  or the underlying figure.                                     │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  STEP 1: Create or re-use figure                                │
│  ─────────────────────────────────                              │
│                                                                 │
│  make_figure() or make_subplots() (utils/figure.py)             │
│   ├─ choose Figure vs FigureWidget based on settings            │
│   └─ apply plotting layout/show defaults                        │
│                                                                 │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  STEP 2: Add traces                                             │
│  ─────────────────                                              │
│                                                                 │
│  Two common patterns:                                           │
│   A) Use vectorbt plotting wrappers (generic/plotting.py)       │
│      - build a Plotly trace and call fig.add_trace              │
│      - wrappers keep fig + trace for later update()             │
│   B) Add Plotly traces directly inside the plot method          │
│      - plot functions can bypass wrappers                       │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│  STEP 3: Return wrapper or figure                               │
│  ─────────────────────────                                     │
│                                                                 │
│  return_fig=True  → Figure/FigureWidget                         │
│  return_fig=False → wrapper object with update()                │
└─────────────────────────────────────────────────────────────────┘

Path B: plots() Method (PlotsBuilderMixin)

┌─────────────────────────────────────────────────────────────────┐
│                      USER CALLS                                 │
│                                                                 │
│   pf.plots()           trades.plots()        indicators.plots() │
│                                                                 │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                          PLOTS BUILDER                          │
│                      generic/plots_builder.py                   │
│                                                                 │
│   1) Resolve defaults from settings['plots_builder']            │
│      + per-object overrides (plots_defaults).                   │
│   2) Apply template mapping, tags, filters, and column/group    │
│      selection to decide which subplots to include.             │
│   3) Resolve each subplot's plot_func and arguments             │
│      (fig, add_trace_kwargs, axis refs, domains, etc.).         │
│   4) Create figure via make_subplots(...) with layout defaults. │
│   5) Call each subplot plot_func to add traces to the figure.   │
│   6) Return the populated Figure/FigureWidget.                  │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
                        ┌───────────┐
                        │ Figure or │
                        │ FigureWidget │
                        └───────────┘

Proposed Vectorbt Plotting Architecture

Path A: Single plot Methods

┌─────────────────────────────────────────────────────────────────┐
│                           USER CALLS                            │
│                                                                 │
│   pf.plot_value()       orders.plot()        df.vbt.ohlcv.plot()│
│   (optional backend="lwc" for time-series use cases)            │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      PLOT METHOD ENTRY                          │
│                                                                 │
│  Plot methods create or reuse a Figure that implements the      │
│  Figure protocol (backend-agnostic surface).                    │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                   FIGURE PROTOCOL (NEW)                         │
│                                                                 │
│  add_ohlc / add_line / add_marker / add_hline / show / to_html  │
│  defines what a Figure can do, regardless of backend.           │
└─────────────────────────────┬───────────────────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
┌───────────────────────────┐   ┌───────────────────────────────┐
│        PlotlyFigure       │   │          LWCFigure            │
│                           │   │                               │
│  Wraps go.Figure          │   │  Wraps litecharts.Chart        │
│  proxy to Plotly API      │   │  build → render on show        │
└───────────────────────────┘   └───────────────────────────────┘
              │                               │
              ▼                               ▼
┌───────────────────────────┐   ┌───────────────────────────────┐
│  plotly.graph_objects     │   │     Lightweight Charts JS     │
└───────────────────────────┘   └───────────────────────────────┘
  • Plotly is no longer the only surface; a Figure protocol removes hard coupling.
  • Plotly remains for complex chart types not supported by LWC.

Path B: plots() Returns a Dashboard

┌─────────────────────────────────────────────────────────────────┐
│                    PlotsBuilderMixin.plots()                    │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                       DASHBOARD (NEW)                           │
│                                                                 │
│  Collects multiple Figures (one per subplot).                   │
│  Handles layout and optional time/crosshair sync.               │
└─────────────────────────────┬───────────────────────────────────┘
                              │
              ┌───────────────┼───────────────┐
              ▼               ▼               ▼
        ┌───────────┐   ┌───────────┐   ┌───────────┐
        │  Figure   │   │  Figure   │   │  Figure   │
        │ (Orders)  │   │ (Trades)  │   │ (Value)   │
        └───────────┘   └───────────┘   └───────────┘
              │               │               │
              ▼               ▼               ▼
   [ PlotlyFigure or LWCFigure, depending on backend ]
  • plots() no longer returns a single multi-row Plotly figure.
  • Dashboard separates multi-pane charts (inside a Figure) from dashboards
    (multiple independent Figures).
  • Synchronization across figures is explicit rather than an implicit Plotly subplot feature.

Capability-Gated Backends

┌─────────────────────────────────────────────────────────────────┐
│                     CAPABILITY FLAGS (NEW)                      │
│                                                                 │
│  TIME_SERIES: OHLC, LINE, AREA, HISTOGRAM, MARKERS, HLINE        │
│  PLOTLY-ONLY: GAUGE, HEATMAP, BOX, SCATTER_XY, VOLUME_3D         │
└─────────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
┌───────────────────────────┐   ┌───────────────────────────────┐
│        Plotly backend      │  │         LWC backend           │
│  full capability set       │  │  time-series subset           │
└───────────────────────────┘   └───────────────────────────────┘
  • Unsupported chart types are detected early instead of failing mid-render.
  • LWC can be offered safely without breaking non-time-series plots.

Composition Hierarchy

┌─────────────────────────────────────────────────────────────────┐
│                         DASHBOARD                               │
│   Layout of independent Figures (grid / columns / rows)         │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                          FIGURE                                 │
│   One chart element with synchronized panes (e.g. price + volume) │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                           PANE                                  │
│   One visualization area with overlaid series                   │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                          SERIES                                 │
│   OHLC / line / area / histogram / markers                      │
└─────────────────────────────────────────────────────────────────┘
  • Fixes the Plotly conflation of multi-pane charts and dashboards.
  • Aligns vectorbt composition with LWC's native chart + pane model.

Next Steps

If the overall architecture is approved I will create a roadmap for implementing this refactor over a series of PRs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions