labb 0.5.0 is out

Line Chart

<c-lb.chart.line /> Markdown

Time series and trends in a Django template: line, area, and stacked datasets drawn by Chart.js, themed by daisyUI, rendered from a django-cotton component.

Time series, trends, anything continuous: this is the chart for it. Datasets can be filled to make an area chart, stacked, or mixed with bar datasets in the same component, and they all take their styling from the active daisyUI theme. The live feed example below redraws as new points arrive.

Basic Line Chart

<c-lb.chart.line legend="bottom" data='{
    "labels": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
    "datasets": [
        { "label": "Users",    "data": [120, 180,  95, 210, 170, 230, 160] },
        { "label": "Sessions", "data": [200, 310, 150, 390, 280, 420, 290] }
    ]
}' />

Multiple Datasets

<c-lb.chart.line legend="bottom" data='{
    "labels": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
    "datasets": [
        { "label": "2023", "data": [42, 38, 51, 47, 62, 58, 71, 68, 54, 49, 63, 75] },
        { "label": "2024", "data": [48, 44, 57, 52, 69, 65, 78, 74, 61, 55, 70, 82] }
    ]
}' />

Filled Area

<c-lb.chart.line legend="bottom" data='{
    "labels": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug"],
    "datasets": [
        {
            "label": "Revenue",
            "data": [4200, 3800, 5100, 4700, 6200, 5800, 6900, 7200],
            "backgroundColor": "primary-light",
            "borderColor": "primary",
            "fill": true,
            "tension": 0.4
        },
        {
            "label": "Expenses",
            "data": [2100, 2400, 2800, 2200, 3100, 2700, 3400, 3100],
            "backgroundColor": "secondary-light",
            "borderColor": "secondary",
            "fill": true,
            "tension": 0.4
        },
        {
            "label": "Profit",
            "data": [2100, 1400, 2300, 2500, 3100, 3100, 3500, 4100],
            "backgroundColor": "accent-light",
            "borderColor": "accent",
            "fill": true,
            "tension": 0.4
        }
    ]
}' />

Reactive

<script>
    const lineWeek = {
        data: {
            labels: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
            datasets: [
                { label: 'Users', data: [120, 180, 95, 210, 170, 230, 160] },
                { label: 'Sessions', data: [200, 310, 150, 390, 280, 420, 290] },
            ],
        },
    };
    const lineMonth = {
        data: {
            labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'],
            datasets: [
                { label: 'Users', data: [870, 1020, 940, 1180] },
                { label: 'Sessions', data: [1540, 1830, 1650, 2100] },
            ],
        },
    };
</script>

<c-lbr.signals
    $view="week"
    :$chartData="{
        'data': {
            'labels': ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
            'datasets': [
                {'label': 'Users', 'data': [120, 180, 95, 210, 170, 230, 160]},
                {'label': 'Sessions', 'data': [200, 310, 150, 390, 280, 420, 290]},
            ],
        }
    }"
/>

<div class="flex flex-col gap-3">
    <div class="flex gap-2">
        <c-lb.button
            size="sm"
            btnStyle="ghost"
            data-class="{'btn-active': $view === 'week'}"
            data-on:click="$view = 'week'; $chartData = lineWeek"
        >
            Weekly
        </c-lb.button>
        <c-lb.button
            size="sm"
            btnStyle="ghost"
            data-class="{'btn-active': $view === 'month'}"
            data-on:click="$view = 'month'; $chartData = lineMonth"
        >
            Monthly
        </c-lb.button>
    </div>

    <c-lb.chart.line legend="bottom" data="$chartData" />
</div>

Live Feed

<script>
    let liveTimer = null;

    function nextLivePoint(chart, tick) {
        const pad = n => String(n).padStart(2, '0');
        const label = pad(Math.floor(tick * 5 / 60)) + ':' + pad((tick * 5) % 60);
        const series = chart.data.datasets[0].data;
        const last = series[series.length - 1];
        const next = Math.round(Math.min(95, Math.max(5, last + (Math.random() * 20 - 10))));
        let labels = chart.data.labels.concat([label]);
        let values = series.concat([next]);
        if (labels.length > 12) {
            labels = labels.slice(1);
            values = values.slice(1);
        }
        return { data: { labels, datasets: [{ label: 'Live metric', data: values }] } };
    }
</script>

<c-lbr.signals
    $running="false"
    :$tick="0"
    :$chartData="{
        'data': {
            'labels': ['00:00', '00:05', '00:10', '00:15', '00:20', '00:25', '00:30'],
            'datasets': [{'label': 'Live metric', 'data': [42, 58, 35, 71, 49, 63, 55]}],
        }
    }"
/>

<div class="flex flex-col gap-3">
    <div class="flex gap-2">
        <c-lb.button
            size="sm"
            data-class="{'btn-error': $running, 'btn-primary': !$running}"
            data-on:click="
                $running = !$running;
                if ($running) {
                    liveTimer = setInterval(() => {
                        $tick = $tick + 1;
                        $chartData = nextLivePoint($chartData, $tick);
                    }, 800);
                } else {
                    clearInterval(liveTimer);
                }
            "
        >
            <span data-text="$running ? 'Stop' : 'Start live feed'"></span>
        </c-lb.button>
    </div>

    <c-lb.chart.line data="$chartData" options='{"animation": false}' />
</div>

API Reference

Property Type Default Required Description
class string - No Additional CSS classes applied to the chart wrapper div
height string 300 No Canvas height in pixels
legend
top bottom left right none
top No Legend position; use 'none' to hide the legend
data string {} No JSON string — Chart.js data object with labels and datasets
options string {} No JSON string — Chart.js options object merged on top of labb defaults
Slot Name Description
default Not used — chart is rendered via the data prop