Metadata-Version: 2.4
Name: pythoncharthandler
Version: 0.1.0
Summary: Pure-Python charting library with pluggable backends (SVG core, optional Pillow raster) and base64/data-URI output for HTML email.
Author-email: Hassan Bagheri <bagheri.h@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/hbagheri/PythonChartHandler
Keywords: chart,graph,svg,png,base64,email
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Multimedia :: Graphics
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: raster
Requires-Dist: pillow>=10.0; extra == "raster"
Provides-Extra: demo
Requires-Dist: fastapi>=0.110; extra == "demo"
Requires-Dist: uvicorn>=0.29; extra == "demo"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: mypy>=1.8; extra == "dev"
Requires-Dist: ruff>=0.5; extra == "dev"
Requires-Dist: pillow>=10.0; extra == "dev"
Requires-Dist: fastapi>=0.110; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Dynamic: license-file

# ChartHandler (Python)

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

```python
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()
```

## Why this design

- **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.

## Features

- **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.

## Install

```bash
pip install pythoncharthandler            # SVG only — zero dependencies
pip install "pythoncharthandler[raster]"  # + PNG/JPEG/GIF/WebP via Pillow
```

## Quickstart

```python
from 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()
```

## Output methods

| 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 |

## Using it in web frameworks

**FastAPI / uvicorn:**

```python
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:**

```python
@app.get("/chart.png")
def chart():
    return Chart.bar([8, 12, 5]).to_png(), 200, {"Content-Type": "image/png"}
```

**Django:**

```python
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 }}">`.

## Charts in HTML email

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):

```python
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" />
```

## Run the demo (Docker)

A FastAPI gallery is bundled. With Docker it serves on **http://localhost:2200/**:

```bash
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 SVG
```

Without Docker:

```bash
pip install "pythoncharthandler[raster,demo]"
uvicorn examples.fastapi_app:app --reload   # http://127.0.0.1:8000/
```

## Development

```bash
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest        # tests
mypy          # strict type-check
ruff check .  # lint
```

A FastAPI demo lives in `examples/fastapi_app.py` (run with
`uvicorn examples.fastapi_app:app --reload`).

## License

MIT — see [LICENSE](LICENSE).
