Bar charts
Eight quarters of revenue for three regions as grouped, stacked, 100% and horizontal bars from one spec: a year tier over the quarters through categoryGroups, a plan line as a pill on the value axis, data labels that step aside when the bars get narrow, and a second chart of variance to plan where negative bars hang from zero in the colour of their sign.
A live, editable Svelte 5 data grid example from the SvGrid gallery (Charts). See the SvGrid documentation for the full API.
About this example
Bar charts every way they are drawn, from one spec of eight quarters and three regions: grouped, stacked, 100% stacked and horizontal are three spec fields, categoryGroups puts the year over its quarters as a second axis tier, referenceLines draws the plan as a pill, dataLabels write the values and step aside when the bars get narrow, and a variance chart hangs negative bars from the zero line coloured by their sign. Free, in @svgrid/grid.
One dataset, eight quarters of revenue for three regions, drawn every way a bar chart is drawn:
stacked,stacked100andorientation: 'horizontal'are spec fields, so the switches below change three words and the chart redraws.categoryGroupsputs the year over its four quarters as a second axis tier, with the group span rather than a repeated label.referenceLinesdraws the plan as a dashed pill on the value axis.dataLabelswrites the value on each bar;hideOverlapkeeps the grouped chart readable when the bars get narrow.- The second chart is the same data as variance to plan: negative bars hang from the zero line and take the colour of their sign through
colors, one per category.
Free, in @svgrid/grid.
Imports, features and API used
Imports: @svgrid/grid, ../shared/ChartDataGrid.svelte, ../shared/ViewSwitch.svelte
Frequently asked questions
How do I stack the bars?
Set stacked: true on the spec. stacked100: true normalises every category to 100 percent, and orientation: horizontal turns the bars sideways. Bars that name a stack group through series.stack pile within their group.
How do I show negative values?
Give the series negative numbers. The bars hang below the zero line, and a colors array on the series, one entry per category, colours them by sign.
How do I group the categories under a second label?
categoryGroups is a list of { label, span }: each group covers that many consecutive categories and is drawn as a second axis tier under them.
Related documentation
Source code (438-chart-bar.svelte)
<!-- Documented in: docs/help/charts/gallery.md -->
<script lang="ts">
/**
* 438. Bar charts
* ---------------
* One dataset, eight quarters of revenue for three regions, drawn every way
* a bar chart is drawn:
*
* - `stacked`, `stacked100` and `orientation: 'horizontal'` are spec fields,
* so the switches below change three words and the chart redraws.
* - `categoryGroups` puts the year over its four quarters as a second axis
* tier, with the group span rather than a repeated label.
* - `referenceLines` draws the plan as a dashed pill on the value axis.
* - `dataLabels` writes the value on each bar; `hideOverlap` keeps the
* grouped chart readable when the bars get narrow.
* - The second chart is the same data as variance to plan: negative bars
* hang from the zero line and take the colour of their sign through
* `colors`, one per category.
*
* Free, in @svgrid/grid.
*/
import { SvChart, type ChartSpec } from '@svgrid/grid'
import ChartDataGrid from '../shared/ChartDataGrid.svelte'
import ViewSwitch from '../shared/ViewSwitch.svelte'
const QUARTERS = ['Q1', 'Q2', 'Q3', 'Q4', 'Q1', 'Q2', 'Q3', 'Q4']
const regions = [
{ label: 'Americas', values: [42, 47, 51, 63, 55, 58, 64, 78] },
{ label: 'EMEA', values: [31, 33, 36, 44, 39, 42, 45, 57] },
{ label: 'APAC', values: [18, 22, 26, 31, 29, 34, 38, 46] },
]
const plan = [95, 100, 110, 130, 125, 132, 145, 170]
let stacked = $state(false)
let percent = $state(false)
let horizontal = $state(false)
let labels = $state(true)
const revenue = $derived<ChartSpec>({
type: 'bar',
categories: QUARTERS,
categoryGroups: [{ label: '2025', span: 4 }, { label: '2026', span: 4 }],
series: regions.map((r) => ({ label: r.label, values: r.values })),
stacked: stacked || percent,
stacked100: percent,
orientation: horizontal ? 'horizontal' : 'vertical',
valueFormat: 'currency',
title: 'Revenue by region',
subtitle: percent ? 'Share of each quarter' : 'USD millions, quarterly',
yAxis: { title: percent ? 'Share' : 'USD (millions)', gridLines: true },
referenceLines: !percent && stacked ? [{ value: 130, label: 'Plan 2026', dashed: true, pill: true }] : undefined,
dataLabels: labels ? { show: true, placement: stacked || percent ? 'inside' : 'top', hideOverlap: true } : { show: false },
height: 340,
})
/** Actual minus plan per quarter: a bar that can hang below zero. */
const variance = $derived<ChartSpec>({
type: 'bar',
categories: QUARTERS,
categoryGroups: [{ label: '2025', span: 4 }, { label: '2026', span: 4 }],
series: [
{
label: 'Actual vs plan',
values: plan.map((p, i) => regions.reduce((sum, r) => sum + r.values[i]!, 0) - p),
colors: plan.map((p, i) => (regions.reduce((sum, r) => sum + r.values[i]!, 0) - p >= 0 ? '#16a34a' : '#dc2626')),
},
],
valueFormat: 'currency',
title: 'Variance to plan',
subtitle: 'Total revenue minus the plan, per quarter',
yAxis: { gridLines: true, title: 'USD (millions)' },
referenceLines: [{ value: 0, label: '', color: '#94a3b8' }],
dataLabels: { show: true, placement: 'top', formatter: (v) => (v > 0 ? `+${v}` : String(v)) },
height: 220,
})
// Chart | Grid: the same spec as the chart or as the rows behind it.
let view = $state<'chart' | 'grid'>('chart')
</script>
<section class="wrap">
<header class="chrome">
<ViewSwitch bind:value={view} options={[['chart', 'Chart'], ['grid', 'Grid']]} />
<label class="chk"><input type="checkbox" bind:checked={stacked} disabled={percent} /> Stacked</label>
<label class="chk"><input type="checkbox" bind:checked={percent} /> 100%</label>
<label class="chk"><input type="checkbox" bind:checked={horizontal} /> Horizontal</label>
<label class="chk"><input type="checkbox" bind:checked={labels} /> Data labels</label>
<span class="note">
Grouped, stacked, normalised and horizontal bars from one spec; a year tier over the quarters, a
plan line, labels that step aside when bars get narrow, and negative bars that hang from zero.
</span>
</header>
<div class="pane">
{#if view === 'chart'}
<SvChart spec={revenue} legend="bottom" selectable autosize />
{:else}
<ChartDataGrid spec={revenue} />
{/if}
</div>
<div class="pane">
{#if view === 'chart'}
<SvChart spec={variance} legend={false} autosize />
{:else}
<ChartDataGrid spec={variance} />
{/if}
</div>
</section>
<style>
.wrap {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
gap: 10px;
overflow: auto;
}
.chrome {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
flex: none;
}
.note {
font-size: 12px;
color: var(--sg-muted, #64748b);
}
.chk {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
color: var(--sg-fg, #0f172a);
white-space: nowrap;
}
.pane {
flex: none;
border: 1px solid var(--sg-border, #e2e8f0);
border-radius: 10px;
background: var(--sg-bg, #fff);
padding: 8px 10px;
min-width: 0;
}
</style>More Charts examples
- Chart a selection (context menu) - With integrated charting enabled, the right-click menu gains a Chart selected range item: select a block of cells, right-click, and the chart panel opens scoped to that range - the Excel chart-this gesture, built in. The item appears only when charting is on and is appended to the default context menu automatically.
- Chart view of the grid - The `chart` prop turns the same <SvGrid> into a chart, driven by the grid’s filtered + sorted rows (search + sort flow through). A view of the grid like board and scheduler, but the renderer is free: the grid lazy-loads a built-in view wrapping the standalone SvChart via rowsToChartSpec. Flip Table <-> Chart (bar / line / area) over one source of truth.
- Candlestick / OHLC - Candlestick and OHLC price marks with an ordinal date axis. ChartSeries.ohlc carries the four prices while values keeps the closes, so the CSV export, the screen-reader table and a 10-day moving average all work with no candle-specific code. Toggle the axis: a real time axis opens a gap over every weekend, an ordinal one spaces sessions evenly and still labels them by date. The strip under the plot is the chart brush: drag its window to pan, drag an edge to resize.
- Box plot + error bars - Distribution rather than average: box plots with the 1.5 IQR whisker rule and individual outliers, next to the same data as a bar chart of the means with error bars. two regions with nearly identical means sit side by side in the bars and look nothing alike in the boxes. boxStats() summarises a raw sample, rowsToBoxSpec() does it per group, and ChartSeries.errors annotates any existing mark without changing its type.
- Axes, titles and styling - The chart hands the decisions back: a numeric x axis that spreads payload sizes the way the numbers do, pinned domains with a fixed tick interval and a formatter per axis, grid lines on or off, a shaded budget band, a vertical reference line, per-point markers and colours, a stepped dashed series on the right axis, a title / subtitle / caption, a legend on any side, and a tooltip snippet in single-series mode. Below it, 50,000 monitoring readings decimated to one point per pixel with LTTB or min / max - toggle it off to see the cost.