Chart

Area, line, bar and pie, drawn from a queryset your view already has — no charting library, no npm, and the series follow your theme on their own.

Move across it: the reading follows the pointer. Switch the site to dark and the two series change colour without a line of JavaScript running again.

Installation

uvx django_shadcn@latest add chart

Nothing else. No npm package, no script tag: scripts is empty for this component, and the drawing is done with SVG paths and positioned elements the component writes itself.

Usage

The view serialises; the template passes it on.

import json

def dashboard(request):
    rows = (
        Order.objects
        .values('month')
        .annotate(revenue=Sum('total'), profit=Sum('margin'))
        .order_by('month')
    )
    return render(request, 'dashboard.html', {
        'series': json.dumps(list(rows)),
        'config': json.dumps({
            'revenue': {'label': 'Revenue'},
            'profit': {'label': 'Profit'},
        }),
    })
<c-chart data="{{ series }}" config="{{ config }}">
    <c-chart.plot>
        <c-chart.grid />
        <c-chart.axis-y />
        <c-chart.axis-x key="month" />
        <c-chart.area data_key="revenue" />
    </c-chart.plot>
    <c-chart.tooltip />
    <c-chart.legend />
</c-chart>

Do not put |safe on it. The JSON has to reach the attribute escaped — Django escapes it, the browser decodes it back, and the component reads the decoded string. Marked safe, the first quote ends the attribute and the chart gets nothing.

The config

config maps a key in your rows to how it should be presented. It is the same shape as upstream's:

{
    'revenue': {'label': 'Revenue'},
    'profit': {'label': 'Profit', 'color': 'var(--chart-3)'},
}

Without a colour, a key takes the next of the five chart tokens in order. Those tokens are defined in input.css for light and dark, which is why a series changes colour when the site does and why a project that themes the tokens themes the charts along with everything else.

Upstream writes a <style> block per chart to do this. A component that is only a template cannot inject CSS, so the properties are set on the chart element instead — the result is the same --color-<key> your own CSS can read.

Anatomy

Tag What it is
<c-chart> the data, the config, the colours and the shared scale
<c-chart.plot> the drawing area, measured, and the pointer tracking
<c-chart.grid> a line at each value on the y axis
<c-chart.axis-y> the value ticks; claims its width from the plot
<c-chart.axis-x> the category labels; claims its height from the plot
<c-chart.area> a filled line, stackable
<c-chart.line> a line, with or without dots
<c-chart.bar> columns, grouped or stacked
<c-chart.pie> pie or doughnut; polar, so it replaces the plot
<c-chart.tooltip> the reading under the pointer
<c-chart.legend> one entry per series, or per row for a pie

Order matters inside the plot: the grid goes first so the series sit on top of it.

Bars

<c-chart.plot>
    <c-chart.grid />
    <c-chart.axis-y />
    <c-chart.axis-x key="month" />
    <c-chart.bar data_key="revenue" />
    <c-chart.bar data_key="profit" />
</c-chart.plot>

Two bar series share the category side by side. Add stack="true" to both and they pile up instead, with the axis rescaling to the total:

stack="true" works the same way on <c-chart.area>.

Lines

<c-chart.line> is the stroke without the fill. dots="false" drops the points, which is what you want when the series is dense.

Pie and doughnut

A pie is polar, so it replaces the plot rather than going inside it. Its config is keyed by the row names, and the legend takes name_key to match:

<c-chart data="{{ shares }}" config="{{ palette }}">
    <c-chart.pie data_key="visitors" name_key="browser" donut="true" />
    <c-chart.legend name_key="browser" />
</c-chart>

thickness is how many pixels wide the ring is, measured inwards from the edge.

Sparklines

There is no sparkline component. A sparkline is this one with no grid and no axes, and since each axis claims its own room, dropping them gives the line the whole width:

<c-chart data="{{ series }}" config="{{ config }}" class="aspect-auto h-12">
    <c-chart.plot>
        <c-chart.line data_key="revenue" dots="false" />
    </c-chart.plot>
</c-chart>

That is what sits inside a Metric card.

Numbers follow the site's language

Ticks and readings are formatted with the language Django has active for the request, not the one the visitor's browser is set to. The same page renders 3,000 in English and 3.000 in Portuguese without being told twice.

Props

Prop Where Default Meaning
data <c-chart> [] the rows, as JSON
config <c-chart> {} key to label and colour, as JSON
label <c-chart.plot> Chart names the drawing for a screen reader
key <c-chart.axis-x> the field holding each row's name
height <c-chart.axis-x> 24 pixels reserved at the bottom
width <c-chart.axis-y> 44 pixels reserved at the left
data_key series the field this series draws
stack area, bar false pile onto the series declared before it
dots line true draw a point at each value
donut pie false leave a hole in the middle
thickness pie 60 how wide the ring is, in pixels
name_key pie, legend the field naming each row

Reading it from your own markup

Anything inside the chart can read its scope:

<c-chart data="{{ series }}" config="{{ config }}">
    ...
    <p x-show="hovered >= 0" x-text="reading ? reading.month : ''"></p>
</c-chart>

rows, series, hovered, reading, ticks and format(value) are all there.

What this does not do

  • Nothing is drawn before Alpine starts. The paths are computed from the measured width, and there is no width until the page is laid out. Every charting library works this way; the rest of this library does not, so it is worth knowing.
  • No zoom, brush or pan.
  • No log scale, and no time axis that picks its own ticks. The x axis is categorical: the view sends the label it wants shown.
  • Not for very large series. A few thousand points is where positioned elements start to cost something. A KPI dashboard is nowhere near that.

If one of those is what you need, the honest answer is a charting library, and the registry can declare one per component when the time comes.