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