Skip to main content

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 with to_html(), to_option(), or browser toDataURL / 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 (no vizly[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 via overlay_geojson (separate from basemap packs)
  • Drill / live update: click events for Streamlit and HTMX; set_data / window.__vizly[id].setOption for 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):

SourceHow
pandas DataFramePass directly
list[dict] (records)Pass directly or vz.from_records(...)
dict[str, sequence] (columnar)Pass directly or vz.from_columnar(...)
CSVvz.from_csv(path_or_text)
TSVvz.from_tsv(...)
JSONvz.from_json(...) (records / columnar / nested data key)
Excelvz.from_excel(path)
SQLvz.from_sql(statement, bind=engine) or url=
Prometheus / CloudWatch / Elasticsearch JSONvz.from_prometheus / from_cloudwatch / from_elasticsearch (from_elk)
Optional Polars / ArrowDuck-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.

TypeRole
flowchart / diagramProcess / dependency boxes (ECharts graph; not Mermaid)
graphGeneral networks
treeSingle-parent hierarchy
sunburst / treemapHierarchical part-to-whole (+ drill breadcrumbs)
sankeyQuantitative 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

FunctionPurpose
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

ExportRole
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

PathAPIRole
Basemap packsregister_map_pack, bundled world / usa, vz.mapChoropleth via echarts.registerMap
Geo overlaysoverlay_geojson / GeoLayer, layers= on vz.map / vz.geoPoints, lines, polygons on a geo coordinate system
Join keysname_field= / id_field=Beyond fragile name-only matching
Introspectionlist_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=False on fragments).
  • SPA / JSON: to_option() / to_json() do not auto-wire drill. Attach chart.on('click', ...) yourself after echarts.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

RuleDetail
Default modeassets.mode = "local" (vendored JS inlined or linked from package)
Optional CDNAllowlist only: cdn.jsdelivr.net, unpkg.com
Banned by defaultChina-primary CDN hosts (bootcdn, npmmirror, assets.pyecharts.org, ...)
Plugin loadingGL, wordcloud, liquidfill load only when the chart needs them
LocaleEnglish-first APIs, docs, and errors (en-US chrome)

Pinned vendored assets

FileVersion
echarts.min.js5.5.1
echarts-gl.min.js2.0.9
echarts-wordcloud.min.js2.1.0
echarts-liquidfill.min.js3.1.0
maps/world.jsonecharts 4.9.0 map pack
maps/usa.jsonapache/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

ModeUse case
Full documentStreamlit page, standalone FastAPI response
FragmentFlask/Django templates, HTMX swaps (include_assets=False when parent loaded assets)
Dashboarddashboard_html / vz.page / st_dashboard (many charts, one ECharts load, linked by default)
JSONto_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.


All examples use the corporate theme. Each chart below shows the complete Python code.

Chart Gallery

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_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).
  • 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