Pure-Python charting with pluggable backends and base64 / data-URI output — designed so charts drop straight into HTML emails and into any web framework (Django, Flask, and ASGI apps under uvicorn).
from charthandler import Chart
# A PNG <img> tag you can paste into an HTML email — no external request.
html = Chart.pie({"Chrome": 63, "Firefox": 19, "Safari": 18}).title("Browser share").to_email_img()- The SVG backend is pure Python with zero dependencies — installs instantly anywhere
(slim Docker, Alpine, serverless) and works identically under Django, Flask, FastAPI/uvicorn,
etc. (a chart is just
str/bytes, so it's framework-agnostic). - Raster (PNG/JPEG/GIF/WebP) is an optional extra via Pillow — needed because SVG doesn't render in some email clients (Outlook desktop). Install only if you need it.
- Chart types: pie, donut, bar, stacked bar, line, area, scatter, and combo (mixed bars + line/area with an independently-scaled secondary axis).
- Outputs: raw bytes, file, base64,
data:URI, ready-to-embed<img>tag. - Fully type-hinted (
py.typed), tested, mypy-strict + ruff clean.
pip install pythoncharthandler # SVG only — zero dependencies
pip install "pythoncharthandler[raster]" # + PNG/JPEG/GIF/WebP via Pillowfrom charthandler import Chart, Axis, Series, LegendPosition
# Pie -> SVG string (no dependencies)
Chart.pie({"Chrome": 63, "Firefox": 19, "Safari": 18}).title("Share").to_svg()
# Bar -> PNG bytes (needs the [raster] extra)
Chart.bar([12, 19, 7, 22, 15]).categories(["Jan", "Feb", "Mar", "Apr", "May"]).to_png()
# Multi-series line, saved (format inferred from the extension)
Chart.line([Series.from_values("2024", [10, 14, 9, 18])]) \
.add_series(Series.from_values("2025", [13, 11, 17, 21])) \
.categories(["Q1", "Q2", "Q3", "Q4"]).save("signups.svg")
# Stacked bar
Chart.stacked_bar([
Series.from_values("Direct", [12, 19, 15]),
Series.from_values("Organic", [20, 24, 28]),
]).categories(["Jan", "Feb", "Mar"]).to_png()
# Scatter from (x, y) pairs
Chart.scatter([(1, 5), (2, 9), (4, 3)]).add_points("B", [(1, 2), (3, 6)]).to_svg()
# Combo: grouped bars + a line on an independently-scaled right axis
Chart.combo() \
.add_bar("Revenue", [120, 190, 70, 220]) \
.add_line("Conversion %", [3.2, 4.1, 2.8, 5.0], axis=Axis.RIGHT) \
.title("Revenue vs conversion").categories(["Q1", "Q2", "Q3", "Q4"]) \
.to_email_img()| Method | Returns | Notes |
|---|---|---|
to_svg() |
str |
SVG markup |
to_png() / to_jpeg() |
bytes |
needs the [raster] extra |
to_data_uri(fmt=Format.PNG) |
str |
data:<mime>;base64,… |
to_html_img(fmt=Format.PNG, **attrs) |
str |
<img src="data:…" …> |
to_email_img(**attrs) |
str |
PNG <img> — use this for email |
save(path) |
None |
format inferred from the extension |
render(fmt) |
RenderedChart |
for full control |
FastAPI / uvicorn:
from fastapi import Response
from charthandler import Chart
@app.get("/chart.png")
def chart() -> Response:
png = Chart.bar({"Mon": 8, "Tue": 12, "Wed": 5}).title("This week").to_png()
return Response(content=png, media_type="image/png")Flask:
@app.get("/chart.png")
def chart():
return Chart.bar([8, 12, 5]).to_png(), 200, {"Content-Type": "image/png"}Django:
from django.http import HttpResponse
def chart(request):
return HttpResponse(Chart.pie({"A": 60, "B": 40}).to_png(), content_type="image/png")For inline charts in a template/email, pass chart.to_data_uri() and use
<img src="{{ chart_uri }}">.
Email clients can't fetch external images offline, and several (Outlook desktop, some
Gmail setups) won't render inline SVG. The reliable approach is a base64 PNG embedded
directly in the markup (to_email_img() does exactly this — it needs the [raster] extra):
img = Chart.bar({"Mon": 8, "Tue": 12, "Wed": 5}).title("This week").to_email_img(alt="Activity")
html = f"<h1>Your report</h1>{img}"
# -> <img src="data:image/png;base64,iVBORw0KGgo..." alt="Activity" />A FastAPI gallery is bundled. With Docker it serves on http://localhost:2200/:
docker compose up -d --build
# http://localhost:2200/ — gallery (every type, inline PNG)
# http://localhost:2200/charts/combo.png — single chart as PNG
# http://localhost:2200/charts/pie.svg — single chart as SVGWithout Docker:
pip install "pythoncharthandler[raster,demo]"
uvicorn examples.fastapi_app:app --reload # http://127.0.0.1:8000/python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest # tests
mypy # strict type-check
ruff check . # lintA FastAPI demo lives in examples/fastapi_app.py (run with
uvicorn examples.fastapi_app:app --reload).
MIT — see LICENSE.