Skip to content

Charts

Golit ships charting that fits its fragment transport: charts are just markup an HTMX swap handles like any other. You get two flavors — static SVG (the default, zero client runtime) and interactive client-side charts (opt-in).

Lets-Plot → static SVG (default)

Lets-Plot is a grammar-of-graphics library — a faithful port of R's ggplot2. Golit runs it in static, no-JavaScript mode and renders to bare SVG server-side. No charting runtime ships to the browser; the SVG is the fragment.

Import the grammar from golit.charts and return a plot spec from a view:

from golit.charts import aes, geom_bar, ggplot, ggsize, labs


@app.view
def chart(by_region: pl.DataFrame):
    return (
        ggplot(by_region, aes("region", "revenue"))
        + geom_bar(stat="identity", fill="#1565C0")
        + labs(x="Region", y="Revenue")
        + ggsize(640, 340)
    )

golit.charts re-exports the full Lets-Plot grammar (ggplot, aes, every geom_*, ggsize, labs, scales, themes, …). View nodes consume Polars frames directly and return specs; Golit compiles a spec to SVG only when the view is dirty.

This is the decisive fit

Because the chart is static SVG, it travels on every path identically — initial load, POST swap, and SSE push — with no per-chart JS bundle and no hydration. It's why static charting is the default.

Interactive charts (Plotly / Altair / Bokeh)

When you want pan/zoom/hover, return a Plotly, Altair, or Bokeh figure. Golit auto-detects the figure type and renders a client-side chart that hydrates on the initial load and across POST/SSE swaps.

Install the extra:

pip install "golit[charts]"
@app.view
def chart(by_region: pl.DataFrame):
    import plotly.express as px
    # plotly.express takes the Polars frame directly (via narwhals).
    fig = px.bar(by_region, x="region", y="revenue", title="Revenue")
    return fig
@app.view
def chart(by_region: pl.DataFrame):
    import altair as alt
    return (
        alt.Chart(alt.Data(values=by_region.to_dicts()))
        .mark_bar()
        .encode(x="region:N", y="revenue:Q")
        .properties(width="container", height=280)
    )
@app.view
def chart(by_region: pl.DataFrame):
    from bokeh.plotting import figure
    regions = by_region["region"].to_list()
    fig = figure(x_range=regions or [""], height=320, sizing_mode="stretch_width")
    fig.vbar(x=regions, top=by_region["revenue"].to_list(), width=0.8)
    return fig

How it works

A view does not return ready-made <script> HTML. Instead Golit emits a library-agnostic mount:

<div class="golit-chart" data-chart-lib="plotly" data-chart-spec="{…json…}"></div>

The page shell registers an htmx.onLoad bootstrap that finds these mounts, lazy-loads the right CDN runtime once, and draws the spec — on the initial render, on every swap, and on SSE pushes alike. There's no inline script to misfire, so the same fragment works on all three paths. (Bokeh is special: its JS must match the installed Python Bokeh, so the version rides on the mount and the loader builds the URLs from it.)

The hot path: chart_spec

Returning a go.Figure (or an Altair/Bokeh figure) is convenient, but constructing the figure object and to_json-ing it costs a few hundred microseconds and ships kilobytes of default template — on every interaction. For a view that rebuilds its chart each update, hand Golit the raw spec dict directly with chart_spec:

from golit.charts import chart_spec


@app.view
def revenue(by_region: pl.DataFrame):
    return chart_spec("plotly", {
        "data": [{"type": "bar",
                  "x": by_region["region"].to_list(),
                  "y": by_region["revenue"].to_list()}],
        "layout": {"margin": {"t": 10}},
    })

It's the same JSON the runtime draws, so the chart is identical — just far cheaper to produce and far smaller on the wire. In the HTTP benchmark (same 16-bar chart, 100K rows on a dev laptop), the spec path runs the per-update round-trip in ~1.5 ms at ~635 B, versus ~2.0 ms at ~6.9 KB for the figure path — and the figure path is exactly what idiomatic Dash does, so chart_spec is ~1.4× faster end-to-end with a ~10× smaller payload, drawing the same chart. lib is any runtime the bootstrap knows (plotly, vega, bokeh, anychart); the spec must already be in that runtime's own format.

Use the figure for convenience, the spec for the hot loop

Returning a figure is the friendly default and perfectly fine for charts that don't rebuild on every keystroke. Reach for chart_spec on the views a slider drives continuously, where shaving the figure build + serialize per interaction matters.

AnyChart

AnyChart has no Python figure object, so Golit gives you a helper that builds a mount from a DataFrame (or [label, value] rows). It loads from a CDN — no Python package, no extra to install:

from golit.charts import anychart


@app.view
def revenue(by_region: pl.DataFrame):
    return anychart(by_region, "region", "revenue", kind="column", title="Revenue")

kind is any AnyChart constructor: column, bar, line, area, spline, pie, donut, funnel.

Bring your own

The chart support isn't a cage. Anything that exports static SVG/PNG/HTML server-side drops straight into the fragment model: a Matplotlib figure (rendered via savefig), a pandas object with _repr_html_, or a raw SVG string all render. See the full resolution order. The only path that needs the opt-in interactive escape hatch is a chart that requires a client-side JS runtime — which is exactly what the Plotly/Altair/Bokeh detection handles.

Full example

The charts_gallery example renders the same reactive aggregation through all four libraries at once — move the slider and every chart fragment swaps from one recompute.

Next

UI components — cards, metrics, tabs, alerts, and more, all server-rendered.