fh-echarts

Feature-complete ECharts components for FastHTML — with JS formatters, HTMX click events, theme support, and automatic memory cleanup.

Install

pip install fh-echarts

Or install latest from GitHub:

pip install git+https://github.com/markkvdb/fh-echarts.git

Quick Start

Add echarts_header() to your FastHTML app headers, then use EChart() to render charts:

from fasthtml.common import *
from fh_echarts.core import echarts_header, EChart

app, rt = fast_app(hdrs=(echarts_header(),))

@rt('/')
def get():
    options = {
        "xAxis": {"type": "category", "data": ["Mon", "Tue", "Wed", "Thu", "Fri"]},
        "yAxis": {"type": "value"},
        "series": [{"data": [120, 200, 150, 80, 70], "type": "bar"}]
    }
    return Titled("My Chart", EChart(options))

serve()

Features

Basic Bar Chart

Pass an ECharts option dict to EChart(). Use preview_echart() to preview charts in notebooks.

options = {
    "xAxis": {"type": "category", "data": ["Mon", "Tue", "Wed", "Thu", "Fri"]},
    "yAxis": {"type": "value"},
    "series": [{"data": [120, 200, 150, 80, 70], "type": "bar"}]
}
preview_echart(EChart(options, chart_id="idx_bar"))

Dark Theme

Pass theme="dark" (or "light") to use ECharts’ built-in themes.

options_dark = {
    "title": {"text": "Dark Theme"},
    "xAxis": {"data": ["A", "B", "C", "D"]},
    "yAxis": {},
    "series": [{"type": "bar", "data": [10, 25, 15, 30]}]
}
preview_echart(EChart(options_dark, chart_id="idx_dark", theme="dark"))

JavaScript Functions with JSFunc

ECharts uses JavaScript functions for custom tooltips, axis labels, etc. Normally json.dumps would turn these into literal strings. Wrap them in JSFunc() and they’ll be revived as real JS functions in the browser.

options_fmt = {
    "title": {"text": "Custom Tooltip"},
    "tooltip": {
        "formatter": JSFunc("function(p) { return '<b>' + p.name + '</b>: $' + p.value.toLocaleString(); }")
    },
    "xAxis": {"data": ["Shirts", "Sweaters", "Hats", "Shoes"]},
    "yAxis": {"axisLabel": {
        "formatter": JSFunc("function(v) { return '$' + v; }")
    }},
    "series": [{"type": "bar", "data": [500, 2000, 360, 1200]}]
}
preview_echart(EChart(options_fmt, chart_id="idx_fmt", theme="dark"))

Line and Pie Charts

Any ECharts chart type works — just set "type" in the series.

line_opts = {
    "title": {"text": "Temperature (°C)"},
    "xAxis": {"type": "category", "data": ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]},
    "yAxis": {"type": "value", "axisLabel": {
        "formatter": JSFunc("function(v) { return v + '°C'; }")
    }},
    "series": [{"type": "line", "data": [2, 5, 12, 18, 24, 28], "smooth": True}]
}
preview_echart(EChart(line_opts, chart_id="idx_line"))
pie_opts = {
    "title": {"text": "Browser Share", "left": "center"},
    "tooltip": {
        "formatter": JSFunc("function(p) { return p.name + ': ' + p.percent + '%'; }")
    },
    "series": [{
        "type": "pie", "radius": "60%",
        "data": [
            {"value": 65, "name": "Chrome"},
            {"value": 18, "name": "Safari"},
            {"value": 10, "name": "Firefox"},
            {"value":  7, "name": "Other"}
        ]
    }]
}
preview_echart(EChart(pie_opts, chart_id="idx_pie"))

HTMX Click Integration

Turn chart clicks into server requests with hx_get_click. By default, name, value, and seriesName are sent as query parameters.

EChart(options, hx_get_click="/bar-clicked", hx_target_click="#result")

@rt('/bar-clicked')
def get(name: str, value: int, seriesName: str):
    return P(f"Clicked {name} ({seriesName}): {value}")

Use hx_click_vals to select which fields to extract from the click event (useful for multi-series charts):

EChart(options, hx_get_click="/clicked",
       hx_click_vals=["name", "seriesIndex", "dataIndex", "data"])

For full control, pass a JS callback via hx_click_cb that receives params and returns a values dict:

EChart(options, hx_get_click="/clicked",
       hx_click_cb=JSFunc("function(params) { return {x: params.data[0], y: params.data[1]}; }"))

### Dynamic Updates with `EChartUpdate`

Update an existing chart without re-creating it. Return an `EChartUpdate` from a route to merge new options into the chart.

```python
import random

@rt('/')
def get():
    options = {
        "xAxis": {"data": ["A", "B", "C"]},
        "yAxis": {},
        "series": [{"type": "bar", "data": [10, 20, 30]}]
    }
    return Div(
        EChart(options, chart_id="updatable"),
        Button("Randomize", hx_get="/randomize", hx_target="#update-slot"),
        Div(id="update-slot")
    )

@rt('/randomize')
def get():
    new_data = [random.randint(5, 100) for _ in range(3)]
    return EChartUpdate("updatable", {"series": [{"data": new_data}]})

Set merge=False to replace all options instead of merging.

Run Arbitrary JS with EChartJS

Execute any JavaScript against a chart instance. The callback receives (chart, el) — the ECharts instance and the DOM element.

# Blur a chart
EChartJS("mychart", "function(chart, el) { el.style.filter = 'blur(4px)'; }")

# Stash current data on the DOM element
EChartJS("mychart", "function(chart, el) { el._loadData = chart.getOption().series[0].data; }")

# Append data to a streaming time series
EChartJS("mychart", """function(chart, el) {
    var opt = chart.getOption();
    opt.xAxis[0].data.push('6s');
    opt.series[0].data.push(42);
    chart.setOption(opt);
}""")

OOB Updates with EChartOOB

Wrap chart scripts in an HTMX out-of-band swap container. Useful when returning chart updates alongside other HTML from a route.

@rt('/update')
def get():
    return Div(
        P("Data updated!"),
        EChartOOB(
            EChartUpdate("mychart", {"series": [{"data": new_data}]}),
            EChartJS("mychart", "function(chart, el) { el.style.filter = ''; }")
        )
    )

Memory Cleanup

When HTMX removes a chart from the DOM (e.g. navigating tabs or swapping content), the ECharts instance and ResizeObserver are automatically disposed via the htmx:beforeCleanupElement event. No extra code needed.

Pandas Time Series with ts_options

Use ts_options from fh_echarts.helpers to turn a Pandas DataFrame into a time-series chart with minimal code. It auto-detects datetime columns, numeric series, and handles NaNs. The result is a plain dict you can customise before passing to EChart.

from fh_echarts.helpers import ts_options
import pandas as pd, numpy as np

dates = pd.date_range("2024-01-01", periods=30, freq="D")
df = pd.DataFrame({"date": dates,
                    "temperature": 20 + 5 * np.random.randn(30).cumsum() * 0.1,
                    "humidity": 60 + 3 * np.random.randn(30).cumsum() * 0.1})

opts = ts_options(df, x="date")
opts["title"] = {"text": "Weather Station"}
opts["legend"] = {"show": True}
EChart(opts)

Works with DataFrames, DatetimeIndex, individual Series, and you can select specific y columns:

ts_options(df, x="date", y="temperature")
ts_options(df.set_index("date"))
ts_options(df.set_index("date")["temperature"])

API Reference

Function Description
echarts_header(version) CDN script tag for ECharts (default v5.5.0)
EChart(options, ...) Render a chart with optional theme, hx_get_click, hx_target_click, hx_click_vals, hx_click_cb
EChartUpdate(chart_id, options, merge) Update an existing chart instance
EChartJS(chart_id, js_func) Run arbitrary JS against a chart instance; callback receives (chart, el)
EChartOOB(*scripts, sink_id) Wrap scripts in an OOB-swappable Div for HTMX responses
JSFunc(js_string) Mark a string as raw JavaScript (for formatters, callbacks, etc.)
preview_echart(echart, height) Preview a chart in a notebook via iframe
ts_options(data, x, y, kind) Convert a Pandas DataFrame/Series into an ECharts time-series options dict