core

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

JavaScript Function Support

ECharts relies heavily on JavaScript functions for custom tooltips, axis labels, etc. Since json.dumps turns everything into strings, we need a way to mark certain strings as raw JavaScript. The JSFunc wrapper and EChartsEncoder handle this.


JSFunc


def JSFunc(
    args:VAR_POSITIONAL, kwargs:VAR_KEYWORD
):

Marker class to flag a string as a raw JavaScript function for ECharts options.


EChartsEncoder


def EChartsEncoder(
    skipkeys:bool=False, ensure_ascii:bool=True, check_circular:bool=True, allow_nan:bool=True, sort_keys:bool=False,
    indent:NoneType=None, separators:NoneType=None, default:NoneType=None
):

Custom JSON encoder that flags JSFunc values with a !JS! prefix so the frontend can revive them.

For example, you can use JSFunc to pass a custom tooltip formatter:

opts = {"tooltip": {"formatter": JSFunc("function(p) { return p.name + ': $' + p.value; }")}}
encoded = json.dumps(opts, cls=EChartsEncoder)
assert '!JS!' in encoded
print(encoded)
{"tooltip": {"formatter": "!JS!function(p) { return p.name + ': $' + p.value; }"}}

EChart Component


EChart


def EChart(
    options:dict, chart_id:str=None, width:str='100%', height:str='400px', theme:str=None, hx_get_click:str=None,
    hx_target_click:str=None, hx_click_vals:list=None, hx_click_cb:str=None
):

Render an EChart with support for themes, JS formatters, HTMX click events, and memory cleanup.


preview_echart


def preview_echart(
    echart, height:str='450px'
):

A basic bar chart:

options = {
    "xAxis": {"type": "category", "data": ["Mon", "Tue", "Wed", "Thu", "Fri"]},
    "yAxis": {"type": "value"},
    "series": [{"data": [120, 200, 150, 80, 70], "type": "bar"}]
}
chart = EChart(options, chart_id="demo_bar")
html = to_xml(chart)
assert 'id="demo_bar"' in html
assert 'echarts.init' in html

preview_echart(chart)
/usr/local/lib/python3.12/site-packages/IPython/core/display.py:447: UserWarning: Consider using IPython.display.IFrame instead
  warnings.warn("Consider using IPython.display.IFrame instead")

With dark theme and a JS tooltip formatter:

options_dark = {
    "title": {"text": "Sales"},
    "tooltip": {
        "formatter": JSFunc("function(p) { return '<b>' + p.name + '</b>: $' + p.value; }")
    },
    "xAxis": {"data": ["Shirts", "Sweaters", "Hats"]},
    "yAxis": {},
    "series": [{"type": "bar", "data": [5, 20, 36]}]
}
chart_dark = EChart(options_dark, chart_id="demo_dark", theme="dark")
html_dark = to_xml(chart_dark)
assert "'dark'" in html_dark
assert '!JS!' in html_dark
print('Theme and JSFunc working ✓')

preview_echart(chart_dark)
Theme and JSFunc working ✓

With HTMX click integration:

chart_htmx = EChart(options, chart_id="demo_htmx",
                     hx_get_click="/bar-clicked", hx_target_click="#result")
html_htmx = to_xml(chart_htmx)
assert '/bar-clicked' in html_htmx
assert "#result" in html_htmx
print('HTMX click integration working ✓')
HTMX click integration working ✓

With custom click fields (e.g. for multiple time series):

chart_fields = EChart(options, chart_id="demo_fields",
                      hx_get_click="/clicked",
                      hx_click_vals=["name", "seriesIndex", "dataIndex", "data"])
html_fields = to_xml(chart_fields)
assert 'seriesIndex' in html_fields
assert 'dataIndex' in html_fields
print('Custom click fields working ✓')
Custom click fields working ✓

With a custom JS click callback for full control:

chart_cb = EChart(options, chart_id="demo_cb",
                  hx_get_click="/clicked",
                  hx_click_cb=JSFunc("function(params) { return {x: params.data[0], y: params.data[1], series: params.seriesName}; }"))
html_cb = to_xml(chart_cb)
assert 'params.data[0]' in html_cb
assert 'params.seriesName' in html_cb
print('Custom click callback working ✓')
Custom click callback working ✓

EChartUpdate


EChartUpdate


def EChartUpdate(
    chart_id:str, options:dict, merge:bool=True
):

Update an existing chart instance. Set merge=False to replace all options instead of merging.

update = EChartUpdate("demo_bar", {"series": [{"data": [300, 400, 500, 600, 700]}]})
html_update = to_xml(update)
assert 'getInstanceByDom' in html_update
assert '300' in html_update

EChartJS


EChartJS


def EChartJS(
    chart_id:str, js_func:str
):

Run a JS function against an existing chart instance. js_func receives (chart, el) as arguments.

# Stash data on the DOM element
js_stash = EChartJS("demo_bar", "function(chart, el) { el._loadData = chart.getOption().series[0].data; }")
html_stash = to_xml(js_stash)
assert '_loadData' in html_stash
assert 'getInstanceByDom' in html_stash

# Apply CSS filter
js_blur = EChartJS("demo_bar", "function(chart, el) { el.style.filter = 'blur(4px)'; }")
html_blur = to_xml(js_blur)
assert 'blur' in html_blur
print('EChartJS working ✓')
EChartJS working ✓

EChartOOB


EChartOOB


def EChartOOB(
    scripts:VAR_POSITIONAL, sink_id:str='script-sink'
):

Wrap one or more EChart Script elements in an OOB-swappable Div for HTMX responses.

oob = EChartOOB(
    EChartUpdate("demo_bar", {"series": [{"data": [100]}]}),
    EChartJS("demo_bar", "function(chart, el) { el.style.filter = ''; }")
)
html_oob = to_xml(oob)
assert 'hx-swap-oob="true"' in html_oob
assert 'script-sink' in html_oob
assert html_oob.count('<script>') == 2
print('EChartOOB working ✓')
EChartOOB working ✓