ECharts is a JavaScript charting library (Apache, originally Baidu). It renders charts in the browser by taking a single JSON-like option object that fully describes the chart: data, axes, series, styling, interactions.
Author
Benedict Thekkel
Contents
Setup & how rendering works in Jupyter
The mental model: the option object
Anatomy of a chart (the builder pattern)
Basic charts: bar, line, pie, scatter
Multiple series, stacking, mixed bar+line
Global options (title, legend, tooltip, axis, toolbox)
Series options (labels, markpoints, areastyle, itemstyle)
Theming
JsCode — the escape hatch for callbacks/formatters
Rendering options: - chart.render("out.html") -> standalone HTML file. - chart.render_notebook() -> inline in Jupyter. This is what we use here.
A wrinkle: pyecharts’ built-in render_notebook() for JupyterLab emits markup that calls echarts.init() but never loads the ECharts JS library (it assumes an old lab extension provides a global echarts). In JupyterLab 4, VS Code, and the static Quarto export that global doesn’t exist, so charts come out as a blank box.
The next cell fixes this once: it overrides render_notebook() to embed the self-contained standalone HTML (which carries its own <script src=...echarts.min.js>) inside an iframe. After running it, every chart.render_notebook() below renders correctly everywhere, including the published docs. Run it first.
from pyecharts.globalsimport CurrentConfig, NotebookTypeimport pyechartsprint("pyecharts", pyecharts.__version__)# --- Make render_notebook() render everywhere -----------------------------# pyecharts' JUPYTER_LAB output calls echarts.init() but never loads the# echarts JS library -- it assumes a JupyterLab extension exposes a global# `echarts`. In modern JupyterLab 4, VS Code, and the static Quarto export# that global is absent, so charts render as a blank box.## Fix: override render_notebook() to embed pyecharts' self-contained# standalone HTML (render_embed(), which ships its own <script src=...# echarts.min.js>) inside an iframe. That renders correctly in every# environment, including the published GitHub Pages docs.import html as _htmlfrom pyecharts.charts.base import Basefrom pyecharts.charts.composite_charts.tab import Tab as _Tabfrom pyecharts.charts.composite_charts.page import Page as _Pagefrom pyecharts.render.display import HTMLdef _iframe_height(chart): h =getattr(chart, "height", None)try:returnint(float(str(h).replace("px", ""))) +42except (TypeError, ValueError):return560# composites (Tab/Page) report no single heightdef _render_notebook_iframe(self, height=None): doc = _html.escape(self.render_embed()) px = height or _iframe_height(self)return HTML(f'<iframe srcdoc="{doc}" width="100%" height="{px}" 'f'frameborder="0" scrolling="auto" style="border:none;"></iframe>' )# Base covers single charts, Grid, Timeline; Tab and Page define their own.for _cls in (Base, _Tab, _Page): _cls.render_notebook = _render_notebook_iframe
pyecharts 2.1.0
# Common imports used throughoutfrom pyecharts import options as optsfrom pyecharts.charts import ( Bar, Line, Pie, Scatter, Grid, Tab, Page, Timeline, Kline, HeatMap, Gauge, Radar, Funnel, Graph, Map, Boxplot,)from pyecharts.globalsimport ThemeTypefrom pyecharts.commons.utils import JsCode
2. The mental model: the option object
Every chart is one big config dict. pyecharts gives you a typed, chainable API that produces it. You can always inspect the raw JSON with .dump_options() — this is the single most useful thing to understand what pyecharts is actually doing, and it’s exactly what you’d hand to echarts.setOption() in JS.
Note the keys: series, xAxis, yAxis, legend, tooltip, title. That’s the ECharts schema. pyecharts is camelCase-on-the-wire even though the Python API is snake_case.
bar.render_notebook()
3. Anatomy of a chart (the builder pattern)
pyecharts charts are built by method chaining, and the order matters conceptually but not syntactically:
Step
Method
Purpose
1
Bar(init_opts=...)
construct, set canvas-level options (size, theme, bg)
2
.add_xaxis(categories)
category axis data
3
.add_yaxis(name, data, ...)
a series — call multiple times for multiple series
4
.set_series_opts(...)
per-series styling (labels, markers, area)
5
.set_global_opts(...)
everything not tied to one series (title, legend, tooltip, axes, zoom)
init_opts is special: it configures the canvas/instance, not the option object — width, height, theme, renderer (canvas vs svg), animation.
Pass a built-in theme via InitOpts(theme=...). Themes set the palette, background, and default text styles. Common ones: LIGHT, DARK, WESTEROS, CHALK, ESSOS, MACARONS, ROMA, SHINE, VINTAGE, PURPLE_PASSION.
The Python API can’t express callbacks — and ECharts uses JS functions for formatters, color logic, label content, etc. JsCode("...") injects a raw JS function string into the option. This is how you reach the ~10% of ECharts the typed API doesn’t cover.
Use it for: custom tooltip HTML, conditional colours, dynamic label text, axis formatters with logic.
Caveat:JsCode strings are not validated in Python — a typo fails silently in the browser. Keep them short; if logic gets complex, that’s a signal to move to raw ECharts in JS.
Map colours regions by value. Built-in map names include "china", "world", and country names like "australia". Region names must match the map’s expected labels (English for world/country maps). Map geometry loads from a CDN at render time, so the rendered notebook needs network access.
aus = ( Map() .add("Clinics", [ ("Queensland", 42), ("New South Wales", 30), ("Victoria", 25), ("Western Australia", 12), ("South Australia", 8), ("Tasmania", 3), ("Northern Territory", 2), ("Australian Capital Territory", 5), ], "australia") .set_global_opts( title_opts=opts.TitleOpts(title="Clinics by state"), visualmap_opts=opts.VisualMapOpts(max_=45, is_piecewise=False), ))aus.render_notebook()
12. Composition
Four ways to combine charts:
Tool
Use
.overlap()
superimpose series on the same coordinate system (bar+line) — shown in §5
Grid
multiple separate coordinate systems on one canvas (subplots)
Tab
tabbed pages, one chart each
Page
vertically stacked dashboard of many charts in one HTML
Timeline
one chart animated across a time/category dimension with a play control
Each frame is a full chart; the widget animates between them.
tl = Timeline(init_opts=opts.InitOpts(width="700px"))for year in [2022, 2023, 2024, 2025]: base =100+ (year -2022) *40 frame = ( Bar() .add_xaxis(["Cliniko", "Nookal", "Splose"]) .add_yaxis("clinics", [base, base //2, base //4]) .set_global_opts(title_opts=opts.TitleOpts(title=f"Clinics — {year}"), yaxis_opts=opts.AxisOpts(max_=260)) ) tl.add(frame, str(year))tl.add_schema(is_auto_play=True, play_interval=1200)tl.render_notebook()
13. Interactivity: DataZoom & VisualMap
DataZoom
Lets users zoom/pan an axis. type_="inside" = scroll/drag on the chart; type_="slider" = a draggable bar below. Useful for dense time series.
import mathxs = [f"t{i}"for i inrange(200)]ys = [round(50+30* math.sin(i /8) + random.uniform(-5, 5), 1) for i inrange(200)]zoomed = ( Line() .add_xaxis(xs) .add_yaxis("metric", ys, is_symbol_show=False) .set_global_opts( title_opts=opts.TitleOpts(title="DataZoom on a long series"), datazoom_opts=[ opts.DataZoomOpts(type_="inside"), opts.DataZoomOpts(type_="slider", range_start=0, range_end=30), ], ))zoomed.render_notebook()
VisualMap
Maps a data dimension to a visual channel (colour or size) continuously or piecewise. Common with heatmaps, maps, and scatter; also colours a line by value.
Standalone HTML:chart.render("chart.html") → fully self-contained page (JS from CDN by default).
Embeddable HTML fragment:chart.render_embed() → returns an HTML string you can drop into a template.
Static image (PNG/SVG): requires snapshot-selenium + a browser driver, or snapshot-phantomjs. ECharts is browser-rendered, so there’s no pure-Python image path.
# Demonstrate HTML export (works offline; no browser needed)path = bar.render("/tmp/echarts_demo.html")import osprint("Wrote", path, "-", os.path.getsize(path), "bytes")
Wrote /tmp/echarts_demo.html - 8491 bytes
15. Bridge to ECharts in JS / React
Since you render ECharts in a React 19 SPA, the most valuable thing pyecharts gives you is dump_options_with_quotes() — the exact option object to feed echarts.setOption(). You can prototype a chart in Python, dump the option, and paste it into your React component.
# Get the JS-ready option string (JsCode functions preserved correctly)option_str = bar.dump_options_with_quotes()print(option_str[:600], "...")
pyecharts: server-rendered reports, notebooks, quick exploration, or when Python already holds the data and you want a static HTML artifact. Good for emailed/PDF dashboards.
ECharts directly in React: anything interactive in your SPA. Don’t round-trip through pyecharts at runtime — have your DRF API return data, and build the option object in TypeScript. Use pyecharts/dump_options only as a design-time tool to discover the right option shape.
One honest caveat: pyecharts lags upstream ECharts releases, and its typed API doesn’t cover every newest option. For React, treat the official ECharts docs (echarts.apache.org/en/option.html) as the source of truth; pyecharts is the fast prototyping path, not the spec.
Recap of the whole model: a chart is one option object → series + axes + global config. pyecharts builds it via chained add_*/set_*_opts; JsCode covers callbacks; dump_options reveals the JSON; composition is overlap/Grid/Tab/Page/Timeline; interactivity is DataZoom/VisualMap. The same option shape is exactly what echarts.setOption() consumes in your React app.