pylightcharts
Python bindings for TradingView Lightweight Charts™, aiming at near-full API coverage with a pythonic interface.
Status: development (P0/P1/P2 hardening — 17 indicators, 13 drawing tools, numeric axes, formatters, CI with JS + Python tests and docs).
How it works
This is not a re-implementation of the charting engine in Python. It embeds the official, battle-tested JavaScript renderer in a WebView (pywebview / Qt WebEngine / wx / Jupyter / Streamlit) and drives it from Python.
Python API ──► JSON / JS bridge ──► Lib.Handler (TypeScript) ──► Lightweight Charts v5
Because the pixels and interactions are produced by the upstream library, visual fidelity and performance match the JS version.
Acknowledgements / License
- Python layer, GUI backends and drawing plugins are derived from lightweight-charts-python (MIT).
- The charting engine is
TradingView Lightweight Charts™ (Apache-2.0),
see
NOTICE.
Migrating from lightweight-charts-python
pylightcharts keeps every class, method and module-level name of the original
package (enforced by tests/test_lwc_compat.py), so existing code runs with a
one-line shim:
from pylightcharts.compat import install_alias
install_alias() # before any `import lightweight_charts`
import lightweight_charts # -> pylightcharts
lightweight_charts.widgets.QWebEngineView = MyPyQt6WebEngineView # patching still works
Or change the imports directly:
- from lightweight_charts.util import Events, JSEmitter
- from lightweight_charts.widgets import QtChart
+ from pylightcharts.util import Events, JSEmitter
+ from pylightcharts.widgets import QtChart
Qt integration
Inside a desktop application, QtChart embeds the chart in a QWebEngineView:
from PyQt6.QtWidgets import QApplication
from pylightcharts.qt import prepare_qt
prepare_qt('PyQt6') # pick the binding *before* QApplication
app = QApplication([])
from pylightcharts.widgets import QtChart
chart = QtChart(toolbox=True)
chart.set(df)
chart.get_webview().show()
app.exec()
QtWebEngineWidgets must be imported before the QApplication exists, which is
why prepare_qt() comes first; $PYLIGHTCHARTS_QT chooses the binding without it.
Full details - multi-chart sync(), screenshots, the Chromium network noise -
are in docs/guide/qt.md.
Examples
examples/ in the repository holds ten self-contained scripts, each with its own
data: loading OHLCV, live bars, tick updates, indicator lines, styling,
callbacks, built-in indicators and panes, headless PNG rendering, custom series
and the PyQt6 window. Start with examples/1_setting_data/setting_data.py.
Development
# build the JS bridge + vendor the engine into pylightcharts/js/
cd jslib && npm install && npm run build
# install the python package in editable mode
cd .. && pip install -e .
# ...or use the requirement files (pyproject.toml stays the source of truth)
pip install -r requirements.txt # runtime only
pip install -r requirements-dev.txt # + tests, e2e, docs, build
Testing
# unit tests: assert on the JS generated by the python bridge (no browser)
python -m pytest -m "not slow"
# e2e: run the built bundle in headless Chromium and check rendered pixels
python tests/e2e/smoke.py
# headless rendering tests (Playwright or a local Chrome/Edge)
python -m pytest tests/test_headless.py
# full stack: real pywebview window + real chart engine (needs a desktop session)
python tests/e2e/full_stack.py
# performance: JSON vs binary bulk data transfer
python tests/e2e/benchmark.py 100000
CI (.github/workflows/ci.yml) builds the bundle, checks the committed artifacts
are up to date, runs the unit tests on Linux/Windows/macOS, renders in headless
Chromium and verifies the wheel ships the JS assets.
Packaging
cd jslib && npm run build # regenerate pylightcharts/js/*
cd .. && python -m build # sdist + wheel (JS assets are package-data)
twine upload dist/*
The built pylightcharts/js/ files are committed on purpose so that
pip install works without Node. Always run npm run build before committing.
Bridge rules
- Fire-and-forget calls go through
Window.invoke(the transport appends;undefinedso pywebview never serialises a live chart object). - Methods that return an object must use
store_as=...or, for primitives,invoke_get. Returning a live object throughinvoke_getwould explode. - Round-trips are serialised and correlated by request id; JS errors raise
pylightcharts.util.BridgeErrorinstead of returningNone. - Chart and series share method names (
apply_options); they are deliberately kept apart (series_options,data_points) to avoid MRO shadowing. - Upgrading lightweight-charts:
npm pack lightweight-charts@X, migrate the breaking changes,npm run build, then run the e2e suite.
The generic bridge
New features should not require touching TypeScript. Any method on any live chart object can be called from Python:
chart._invoke('addSeries', 'Area', 'my area', {'lineWidth': 2}, handle)
chart.win.invoke(f'{series.id}.series', 'applyOptions', {'lineWidth': 3})
Handles are either registered via Lib.register(...) or dotted window paths
(window.abcdefgh.chart). A {'$ref': handle} argument is resolved to the live
JS object, so object-taking APIs work too:
series.attach_primitive('myPrimitive') # -> attachPrimitive({"$ref": "myPrimitive"})
See jslib/src/general/rpc.ts.
Full option coverage
apply_options(**kwargs) accepts any option of the underlying API and converts
snake_case keys to camelCase automatically:
chart.apply_options(auto_size=True, localization={'price_format': {'precision': 4}})
chart.time_scale_options(right_offset=5, bar_spacing=8) # time scale
series.apply_options(line_width=3, crosshair_marker_visible=False)
get = chart.get_price_scale('left'); get.set_mode('logarithmic')
Indicators
Pure-pandas implementations, no extra dependencies. Overlays go on the price pane, oscillators get their own pane automatically:
chart.add_sma('close', 20)
chart.add_ema('close', 50, color='#2196F3')
chart.add_bollinger('close', 20, 2) # -> (upper, middle, lower)
chart.add_donchian(20)
chart.add_vwap()
chart.add_rsi(14) # own pane + 70/30 guide lines
chart.add_macd() # own pane -> (macd, signal, histogram)
chart.add_stochastic() # own pane -> (%K, %D)
chart.add_atr(14)
chart.add_adx(14) # own pane -> (adx, +DI, -DI)
chart.add_obv() # own pane (needs volume)
chart.add_cci(20)
chart.add_williams_r(14)
chart.add_mfi(14)
chart.add_roc(12)
chart.add_keltner(20, 2.0) # overlay -> (upper, middle, lower)
Raw values: from pylightcharts import indicators; indicators.rsi(close, 14).
RSI, OBV, CCI, %R, ROC, MFI and Bollinger match TA-Lib to floating-point
precision; ATR and ADX match within ~1e-4 and ~0.1 respectively (Wilder
accumulation rounding).
Indicators follow the data
Every indicator is recomputed automatically: a full re-send on chart.set(),
and a single-point update on chart.update(bar) (recomputed from a warm-up
window so recursive indicators stay numerically identical to a full run).
add_computed_series(compute, name, ...) adds your own.
Batching ticks
Each bridge call is one webview round-trip, so wrap bursts in chart.batch():
with chart.batch():
for tick in ticks:
chart.update(tick) # flushed as a single script
Conflation (very large data)
Conflation is a plain series option, so the bridge already exposes it:
chart.apply_options(enable_conflation=True, precompute_conflation_on_init=True)
series.apply_options(enable_conflation=True)
Custom series (declarative rendering)
The tricky part of a custom series is draw(), which runs synchronously every
frame - python cannot take part in that. So the protocol splits the work:
python computes the shapes once per data update, one generic JS renderer draws
them every frame.
from pylightcharts import shapes
series = chart.add_custom_series('range bars', pane_index='new')
series.set(df, shapes=lambda row: shapes.range_bar(row['low'], row['high']))
Shape builders: rect band line circle text polyline, plus presets
range_bar box_plot error_bar. Vertical anchors are prices, horizontal
offsets are in bar units (-0.4..0.4 spans a bar).
shapes.box_plot(low, q1, median, q3, high)
shapes.polyline([(0.0, 1.0), (0.4, 2.0)], fill_color='rgba(0,0,0,0.2)')
shapes.text(price, 'label')
Each data item may carry value, low, high (autoscale + last value),
color, and shapes.
Price lines, data readback, events
line = series.create_price_line(105.0, color='#00e676', title='entry')
line.apply_options(line_visible=False)
line.remove()
series.price_to_coordinate(105) # <-> series.coordinate_to_price(y)
series.data_by_index(10) # individual bar
series.data_points() # everything the chart holds
series.pop(2); series.last_value_data(); series.bars_in_logical_range(0, 50)
series.series_type(); series.get_pane_index()
series.move_to_pane(2); series.series_order(); series.set_series_order(3)
chart.scroll_to_real_time(); chart.scroll_to_position(3, animated=False)
chart.reset_time_scale(); chart.set_visible_logical_range(0, 60)
chart.get_visible_range(); chart.get_visible_logical_range()
chart.time_to_coordinate(t); chart.coordinate_to_time(x)
chart.logical_to_coordinate(l); chart.coordinate_to_logical(x)
chart.pane_height(0); chart.set_pane_height(300, 0)
chart.pane_stretch_factor(0); chart.pane_size(); chart.pane_series_count(0)
chart.version(); chart.auto_size_active(); chart.remove()
chart.set_crosshair_position(100.0, some_time); chart.clear_crosshair_position()
chart.get_price_scale('right').get_visible_range()
chart.get_price_scale('right').set_auto_scale(False)
chart.pane_price_scale(0, 'left')
chart.events.crosshair_move += handler # handler(chart, time, price)
chart.events.dblclick += handler
chart.events.click.unsubscribe()
Multi-chart sync
price = Chart(); volume = Chart()
price.sync(volume) # volume follows price pan/zoom
price.sync(volume, crosshairs_only=True)
Native panes (lightweight-charts v5)
chart.add_pane()
chart.create_area(name='close', pane_index=1)
chart.add_series('Line', 'ma20', pane_index=1, color='#0ff')
chart.set_pane_stretch(1, 1.5)
chart.pane_count()
pane_index='new' creates a fresh pane, and passing an index beyond the
current pane count creates the panes in between.
Drawing tools
Trend line / horizontal line / vertical line / ray / box, plus:
chart.fibonacci(t1, p1, t2, p2, levels=(0, 0.382, 0.5, 0.618, 1))
chart.measure(t1, p1, t2, p2) # price %, bar count
chart.parallel_channel(t1, p1, t2, p2, offset=5) # second line 5 price units away
chart.position(t1, entry, t2, target, risk_ratio=1.5) # profit + stop zones
chart.long_position(t1, entry, t2, target) # aliases
chart.short_position(t1, entry, t2, target)
chart.andrews_pitchfork(t1, p1, t2, p2, t3, p3) # 3 points
chart.triangle(t1, p1, t2, p2, t3, p3) # 3 points
chart.fibonacci_extension(t1, p1, t2, p2, t3, p3) # impulse + retracement
chart.gann_fan(t1, p1, t2, p2) # 1x1 plus steeper/shallower rays
With toolbox=True these are also available interactively
(Alt+F / M / C / P / A / G / X / N).
Numeric axes and formatters
from pylightcharts import YieldCurveChart, OptionsChart
curve = YieldCurveChart() # x axis = duration in months
series = curve.add_series('Line', 'rate')
series.set(pd.DataFrame({'time': [1, 3, 12], 'rate': [4.2, 4.0, 3.6]}))
surface = OptionsChart() # x axis = strike
chart.set_price_formatter(decimals=2, thousands=True, prefix='$')
chart.set_time_formatter('YYYY-MM-DD HH:mm')
chart.register_js_formatter('eur', "value => '\u20ac' + value.toFixed(2)")
chart.set_price_formatter(name='eur')
Large data
DataFrames with 2000+ rows are transferred as a base64 column-major Float64
buffer and rebuilt in the browser (Lib.decodeData) instead of being shipped as
JavaScript source. Measured 100k bars: 1.56s -> 0.54s (~3x);
500k bars: 8.9s -> 3.2s. Tune with pylightcharts.util.BINARY_DATA_THRESHOLD.
Headless / server-side rendering
from pylightcharts.headless import HeadlessChart
chart = HeadlessChart(width=1200, height=700)
chart.set(df)
chart.add_sma('close', 20)
chart.add_rsi(14)
chart.render('report.png') # PNG bytes, no window required
chart.to_html() # or grab the standalone HTML
Uses Playwright when installed, otherwise a local Chrome/Edge in headless mode
(PYLIGHTCHARTS_CHROME overrides the browser path).
Metadata
Release files for pylightcharts 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pylightcharts-0.1.0.tar.gz | 151.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pylightcharts-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 304.3 kB
Release files / pylightcharts-0.1.0.tar.gz
| Download URL | pylightcharts-0.1.0.tar.gz |
|---|---|
| Size | 151.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
19f980b774a6a6a7b5a4bc7c8a053a112a5708f22a0fdb1f1d79a0ed5e90a37d
|
|
BLAKE2b-256 checksum How to use checksums |
b13273be49b3fecc65f8ee69ea1c4b7a02a914a3bd92e9e8097658f95abd08c1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.13
|
Release files / pylightcharts-0.1.0-py3-none-any.whl
| Download URL | pylightcharts-0.1.0-py3-none-any.whl |
|---|---|
| Size | 152.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0c98b49dd5b35757edb12fac78bea906fc075dc18c86505c0b67de5072a0c33f
|
|
BLAKE2b-256 checksum How to use checksums |
4b57fce59f8ca9d1c56b9c14d7a3e5a82a1c5c462cf3b66ca4c3e8b9e20519ba
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.13
|