vizly
High-performance, low-boilerplate, fully themable Python charting over Apache ECharts. Ship production charts in a few lines of Python: DataFrame, records, columnar dict, file, or SQL in; HTML, JSON, or browser image out. No nested option builders. Built for speed (local assets, one ECharts load per page) and for native embeds in the stacks you already use.
- Easy to use: set a theme, call
vz.line/vz.bar/ ..., export withto_html(),to_option(), or browsertoDataURL/downloadImage - Data without forced DataFrames: pass
list[dict],dict[list], loader output, or pandas (pandas remains a dependency, not a required call-site API) - SQL and files in base:
from_sql,from_csv,from_tsv,from_json,from_excel(novizly[sql]extra) - Highly performant: vendored JS by default; GL and plugins load only when a chart needs them
- Worldwide maps by default: bundled world atlas plus
usa; GeoJSON overlays viaoverlay_geojson(separate from basemap packs) - Drill / live update: click events for Streamlit and HTMX;
set_data/window.__vizly[id].setOptionfor refresh without full re-embed - Trusted asset defaults: no Chinese CDN forced (unlike many other Python ECharts wrappers)
- Wide chart coverage: 40 registered chart types including cartesian, statistical, geo, flowchart, graph, 3D, and compose (
page/tab/timeline) - Native integrations: Streamlit, FastAPI, Flask, Django, HTMX, and Jupyter
Install
Requires Python 3.9 or later.
pip install vizly
Base runtime includes pandas, numpy, sqlalchemy, and openpyxl. Install DBAPI drivers yourself for the databases you use.
Framework extras (install only what you use): vizly[streamlit], vizly[fastapi], vizly[flask], vizly[django].
HTMX helpers (vizly.integrations.htmx) ship in the base package. No vizly[htmx] extra.
30-second example
import vizly as vz
vz.set_theme("corporate")
# No DataFrame required:
chart = vz.line(
{"date": ["2026-01-01", "2026-01-02"], "revenue": [10, 20]},
x="date",
y="revenue",
title="Revenue",
)
chart.to_html() # self-contained local ECharts
chart.to_option() # plain dict for APIs / agents
# Browser image (after embed): window.__vizly[id].downloadImage("revenue.png")
Maps
vz.map(df) # world (default)
vz.map(df, map="usa") # US states
vz.register_map_pack(...) # basemap GeoJSON you supply
vz.overlay_geojson(...) # overlay layer (not a basemap pack)
Data ingest
Every chart factory accepts these shapes (and loader output):
| Source | How |
|---|---|
pandas DataFrame | Pass directly |
list[dict] (records) | Pass directly or vz.from_records(...) |
dict[str, sequence] (columnar) | Pass directly or vz.from_columnar(...) |
| CSV | vz.from_csv(path_or_text) |
| TSV | vz.from_tsv(...) |
| JSON | vz.from_json(...) (records / columnar / nested data key) |
| Excel | vz.from_excel(path) |
| SQL | vz.from_sql(statement, bind=engine) or url= |
| Prometheus / CloudWatch / Elasticsearch JSON | vz.from_prometheus / from_cloudwatch / from_elasticsearch (from_elk) |
| Optional Polars / Arrow | Duck-typed when installed (not hard dependencies) |
import vizly as vz
from sqlalchemy import create_engine
table = vz.from_csv("sales.csv")
vz.bar(table, x="region", y="sales")
engine = create_engine("postgresql+psycopg://...")
sql_table = vz.from_sql("SELECT day, value FROM metrics", bind=engine)
vz.line(sql_table, x="day", y="value")
SQLAlchemy coverage: vizly charts any result set SQLAlchemy can return. vizly does not bundle every DB driver or external dialect package. If SQLAlchemy can connect, vizly can chart the rows.
Chart inventory
vz.list_chart_types()
vz.list_unavailable_chart_types() # e.g. chord (upstream-unavailable)
40 registered types: line, bar, area, scatter, pie, donut, boxplot, heatmap, candlestick, kline, radar, funnel, gauge, sankey, treemap, map, grid, mix, combo, effect_scatter, waterfall, polar, parallel, sunburst, tree, graph, flowchart, wordcloud, geo, bar3d, line3d, scatter3d, page, tab, timeline, pictorial_bar, theme_river, liquid, surface3d
Alias: diagram -> flowchart.
| Type | Role |
|---|---|
flowchart / diagram | Process / dependency boxes (ECharts graph; not Mermaid) |
graph | General networks |
tree | Single-parent hierarchy |
sunburst / treemap | Hierarchical part-to-whole (+ drill breadcrumbs) |
sankey | Quantitative flow |
Extra map packs and geo layers are not chart types.
Upstream unavailable
chord: ECharts 5.x no longer ships a first-class chord series; use sankey or graph instead.
Compose charts
grid, page, tab, and timeline take charts= instead of data=. page / tab to_option() returns a compose descriptor under _vizly_compose (child options), including connect when linked. Use HTML embeds (dashboard_html / to_html) for browser layout.
Theming
vz.set_theme("corporate")
# Product: default, light, dark, corporate, minimal, contrast
# Ops-inspired: ops_grafana, ops_cloudwatch, ops_kibana
# Editor-inspired: editor_monokai, editor_tokyo_night, editor_dracula,
# editor_nord, editor_solarized_light, editor_solarized_dark, editor_one_dark
vz.set_theme({"palette": ["#0B1F33", "#2F6FED"]}) # deep-merge override
vz.register_theme("acme", {"background": "#FFFFFF", "palette": ["#111111"]})
vz.load_theme("examples/themes/atlantic.json", activate=True, register=True)
vz.export_theme("acme", "acme.json")
vz.bar(df, x="region", y="sales", theme="editor_tokyo_night")
Ops and editor theme IDs are visual inspiration only (not affiliated with Grafana Labs, AWS, Elastic, Monokai, Tokyo Night, Dracula, Nord, Solarized, or One Dark).
Theme API
| Function | Purpose |
|---|---|
vz.set_theme(name_or_dict) | Session theme (dict deep-merges) |
vz.get_theme() / resolve_theme() | Read or resolve theme |
vz.register_theme(name, dict) | Register a custom preset |
vz.load_theme(path, activate=, register=) | Load theme from JSON file |
vz.export_theme(name, path) | Export a registered theme |
vz.list_themes() | List available theme names |
Per-chart theme= kwarg overrides without mutating session theme. Theme locale defaults to en-US.
Title / legend layout (chrome)
Defaults keep chrome clear of the plot: title left, legend top-right, and cartesian grid padding with containLabel. These are defaults only. Override any time:
chart.merge_option({
"title": {"left": "center", "top": 0},
"legend": {"top": "bottom", "left": "center"},
"grid": {"top": 40, "bottom": 72, "containLabel": True},
})
Core API
Every chart factory lives on the vizly namespace (import vizly as vz).
import vizly as vz
# Sugar constructors
chart = vz.line(df, x='date', y='revenue', title='Revenue')
chart = vz.bar(df, x='region', y='sales', title='Sales')
chart = vz.scatter(df, x='height', y='weight', title='Height vs weight')
# Universal constructor by type name
chart = vz.chart('area', df, x='date', y='revenue', title='Area')
# Wrap a raw ECharts option
chart = vz.from_option({...})
# Export
chart.to_html() # self-contained HTML with local ECharts
chart.to_option() # plain dict for APIs / SPAs
chart.to_json() # JSON string
chart.render("file.html") # write to file
# Escape hatches
chart.update(...)
chart.merge_option({...})
chart.set_data(...) # live data update
chart.live_update_script(chart_id) # partial option push
Export paths
| Export | Role |
|---|---|
chart.to_html() | Self-contained HTML with local ECharts (vendored JS inlined) |
chart.to_option() | Plain dict for APIs, SPAs, or agents |
chart.to_json() | JSON string of the option |
chart.render("file.html") | Write self-contained HTML to a file |
Base lifecycle: data standardization -> _build() -> theme apply -> user merge -> export.
Ops metric helpers
Normalize observability JSON you already have - no live API calls.
import vizly as vz
table = vz.from_prometheus(prom_api_json) # timestamp, value, series
table = vz.from_cloudwatch(cw_datapoints) # timestamp, value [, unit|series]
table = vz.from_elasticsearch(es_search_json) # timestamp, value (alias: from_elk)
vz.set_theme("ops_grafana")
vz.line(table, x="timestamp", y="value")
Bad rows are dropped with a warning and DataFrame.attrs['vizly_skipped'] set.
Maps and geography
| Path | API | Role |
|---|---|---|
| Basemap packs | register_map_pack, bundled world / usa, vz.map | Choropleth via echarts.registerMap |
| Geo overlays | overlay_geojson / GeoLayer, layers= on vz.map / vz.geo | Points, lines, polygons on a geo coordinate system |
| Join keys | name_field= / id_field= | Beyond fragile name-only matching |
| Introspection | list_bundled_maps(), list_opt_in_maps() | Bundled vs opt-in geography |
Default atlas is world. Bundled regional pack: usa. Extra regional packs are not bundled by default. China administrative packs remain opt-in via register_map_pack only. No Baidu Map defaults.
SPA / JSON note: to_option() / json_response return the ECharts option only. They do not embed GeoJSON. HTML rendering calls echarts.registerMap for you; SPA clients must register map packs themselves (or use HTML embeds).
Drilldowns, events, and live update
HTML embeds emit a structured vizly:event CustomEvent and postMessage payload on click (name, value, region, breadcrumb for sunburst/treemap/tree).
from vizly.events import filter_by_click
# Host receives payload -> filter sibling chart data
child = vz.bar(filter_by_click(detail_table, payload), x="category", y="sales")
# Live refresh without full page reload (host already has ECharts):
chart.set_data(new_table)
# In the browser: window.__vizly[chartId].setOption(partialOption)
- Streamlit:
st_vizly(..., events=True)returns the last click payload via a small declared component. - HTMX:
htmx_event_listener_js("/detail")POSTs clicks without reloading ECharts (include_assets=Falseon fragments). - SPA / JSON:
to_option()/to_json()do not auto-wire drill. Attachchart.on('click', ...)yourself afterecharts.init.
Multi-chart pages (vz.page / dashboard_html / st_dashboard) use echarts.connect by default for linked tooltip/brush (connect=False to opt out).
Image export (PNG / JPEG / SVG)
Images come from the browser that already rendered the chart (ECharts getDataURL). vizly does not ship Chromium.
// After any HTML embed: chart id is on the root .vizly-chart element
const id = document.querySelector(".vizly-chart").id;
window.__vizly[id].toDataURL({ type: "png", pixelRatio: 2 });
window.__vizly[id].downloadImage("chart.png");
Optional toolbox button (no custom JS):
chart.merge_option({"toolbox": {"feature": {"saveAsImage": {"type": "png"}}}})
For headless batch PNG/PDF, run Playwright (or similar) on chart.to_html() yourself.
Trust and assets
| Rule | Detail |
|---|---|
| Default mode | assets.mode = "local" (vendored JS inlined or linked from package) |
| Optional CDN | Allowlist only: cdn.jsdelivr.net, unpkg.com |
| Banned by default | China-primary CDN hosts (bootcdn, npmmirror, assets.pyecharts.org, ...) |
| Plugin loading | GL, wordcloud, liquidfill load only when the chart needs them |
| Locale | English-first APIs, docs, and errors (en-US chrome) |
Pinned vendored assets
| File | Version |
|---|---|
echarts.min.js | 5.5.1 |
echarts-gl.min.js | 2.0.9 |
echarts-wordcloud.min.js | 2.1.0 |
echarts-liquidfill.min.js | 3.1.0 |
maps/world.json | echarts 4.9.0 map pack |
maps/usa.json | apache/echarts-examples |
Smart asset loading: assets_html(charts=...) and Django {% vizly_assets charts=... %} auto-include GL and plugin scripts when child charts need them.
Integrations
Shared embed contract in vizly.integrations. Do not fork embed logic per framework.
Embed modes
| Mode | Use case |
|---|---|
| Full document | Streamlit page, standalone FastAPI response |
| Fragment | Flask/Django templates, HTMX swaps (include_assets=False when parent loaded assets) |
| Dashboard | dashboard_html / vz.page / st_dashboard (many charts, one ECharts load, linked by default) |
| JSON | to_option() / to_json() for SPAs and agents |
Multi-chart / dashboard
from vizly.integrations import assets_html, chart_html, dashboard_html
head = assets_html(charts=[c1, c2])
a = chart_html(c1, fragment=True, include_assets=False)
b = chart_html(c2, fragment=True, include_assets=False)
html = dashboard_html([c1, c2], title="Ops") # connect=True by default
Streamlit
from vizly.integrations.streamlit import st_vizly, st_dashboard
event = st_vizly(chart, height=420) # last click payload or None
st_dashboard([c1, c2], height=900) # many charts, ECharts once
FastAPI / Flask / Django
Same embed contract. Django uses {% vizly_assets charts=... %}, {% vizly_chart ... %}, {% vizly_dashboard ... %}, {{ chart|vizly_html }}.
HTMX
from vizly.integrations.htmx import htmx_chart_fragment, htmx_event_listener_js
# Parent page: assets once + htmx_event_listener_js("/detail")
# Fragments: include_assets=False
Jupyter
Charts display via _repr_html_() on chart objects.
Example gallery
All examples use the corporate theme. Each chart below shows the complete Python code.
See all chart types rendered live with theme switching in the Vizly Chart Gallery -- no Python needed.
area
df = pd.DataFrame({
'date': ['2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04', '2026-01-05', '2026-01-06', '2026-01-07', '2026-01-08', '2026-01-09', '2026-01-10', '2026-01-11', '2026-01-12', '2026-01-13', '2026-01-14'],
'revenue': [42, 48, 45, 61, 58, 72, 68, 81, 76, 90, 84, 95, 88, 102],
})
chart = vz.area(df, x='date', y='revenue', title='Revenue trend', theme='corporate')
# chart.render("chart.html") # or chart.to_html() / st_vizly(chart)
bar
df = pd.DataFrame({
'region': ['Northeast', 'Southeast', 'Midwest', 'Southwest', 'West', 'Canada'],
'sales': [86, 64, 71, 58, 93, 47],
})
chart = vz.bar(df, x='region', y='sales', title='Sales by region', theme='corporate')
scatter
df = pd.DataFrame({
'height': [1.86, 1.726, 1.893, 1.829, 1.588, 1.94, 1.854, 1.864, 1.601, 1.73, 1.698, 1.921, 1.808, 1.879, 1.727, 1.641, 1.772, 1.576, 1.881, 1.803, 1.853, 1.692, 1.938, 1.907, 1.861, 1.628, 1.737, 1.568, 1.612, 1.823, 1.848, 1.937, 1.68, 1.698, 1.738, 1.626],
'weight': [58.2, 74.8, 62.9, 84.2, 73.0, 92.0, 85.6, 67.0, 91.9, 90.6, 70.6, 65.8, 84.8, 58.7, 61.6, 52.4, 89.8, 83.9, 85.8, 89.5, 74.0, 79.3, 58.7, 57.5, 84.1, 74.6, 79.1, 88.7, 82.5, 78.6, 78.8, 66.6, 53.5, 73.0, 62.3, 71.6],
})
chart = vz.scatter(df, x='height', y='weight', title='Height vs weight', theme='corporate')
pie
df = pd.DataFrame({
'name': ['Enterprise', 'Mid-market', 'SMB', 'Startup', 'Partner'],
'value': [34, 26, 18, 12, 10],
})
chart = vz.pie(df, names='name', values='value', title='Segment mix', theme='corporate')
map
df = pd.DataFrame({
'name': ['United States', 'Canada', 'Brazil', 'Germany', 'United Kingdom', 'France', 'India', 'Japan', 'Australia', 'South Africa'],
'value': [92, 54, 61, 48, 57, 44, 73, 66, 39, 28],
})
chart = vz.map(df, title='Global demand', theme='corporate')
candlestick
df = pd.DataFrame({
'date': ['2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04', '2026-01-05', '2026-01-06', '2026-01-07', '2026-01-08', '2026-01-09', '2026-01-10', '2026-01-11', '2026-01-12'],
'open': [100, 104, 101, 108, 112, 109, 115, 118, 114, 121, 125, 122],
'close': [104, 101, 108, 112, 109, 115, 118, 114, 121, 125, 122, 129],
'low': [98, 99, 100, 106, 107, 108, 113, 112, 113, 119, 120, 121],
'high': [106, 105, 110, 114, 114, 117, 120, 119, 123, 127, 126, 131],
})
chart = vz.candlestick(df, x='date', title='Price action', theme='corporate')
combo
df = pd.DataFrame({
'date': ['2026-01-01', '2026-01-02', '2026-01-03', '2026-01-04', '2026-01-05', '2026-01-06', '2026-01-07', '2026-01-08', '2026-01-09', '2026-01-10', '2026-01-11', '2026-01-12', '2026-01-13', '2026-01-14'],
'revenue': [42, 48, 45, 61, 58, 72, 68, 81, 76, 90, 84, 95, 88, 102],
'cost': [18, 20, 19, 24, 22, 28, 26, 31, 29, 34, 32, 37, 35, 40],
})
chart = vz.combo(df, x='date', bar='cost', line='revenue', title='Revenue vs cost', theme='corporate')
page (dashboard)
vz.set_theme('corporate')
chart = vz.page(charts=[
vz.line(df_line, x='date', y=['revenue', 'cost'], title='Revenue vs cost'),
vz.pie(df_pie, names='name', values='value', title='Segment mix'),
vz.map(df_map, title='Global demand'),
], title="Dashboard")
Known limitations
- SPA / JSON + maps:
to_option()/json_responsereturn the ECharts option only. They do not embed GeoJSON. HTML rendering callsecharts.registerMapfor you; SPA clients must register map packs themselves (or use HTML embeds). - page / tab JSON:
to_option()returns a compose descriptor under_vizly_compose(child options). Use HTML embeds (dashboard_html/to_html) for browser layout. - Flowchart: process/dependency diagrams, not Mermaid syntax, BPMN, swimlanes, or sequence diagrams.
- Export: PNG/JPEG/SVG via browser
toDataURL/downloadImage. No server-side Chromium in vizly. - SQL drivers: vizly depends on SQLAlchemy; install the DBAPI / dialect for your database yourself.
License
MIT