BarChart
Compare categorical values in a responsive SVG chart, including positive and negative data.
BarChartPlayground
Change the dataset, edit each daily value, and see the chart update immediately. Try an all-negative balance, set scale bounds, switch colors and units, or remove the data entirely. Reset restores every control.
<RegorApp id="bar-chart-demo" src="./playground.ts"/> import {
type BarChartItem,
defineBarChartComponents,
defineButtonComponents,
defineFlexComponents,
defineFormComponents,
defineFormInputField,
defineFormSelectField,
defineGridComponents,
defineIconComponents,
definePanelComponents,
type FormSelectOption,
} from '@purestack/ts-components'
import { getThemePaletteVar } from '@purestack/ts-style'
import { lucide_chevron_down } from '@purestack/ts-svg-icons'
import {
batch,
type ComputedRef,
computed,
createApp,
defineComponent,
html,
type Ref,
ref,
type SRef,
sref,
} from 'regor'
export type BarChartPreset = 'mixed' | 'positive' | 'negative' | 'zero'
export type BarChartPalette = 'balance' | 'categorical' | 'accent'
export interface BarChartEditorRow {
id: string
label: string
value: Ref<number | string>
}
export interface BarChartPlayground {
rows: SRef<BarChartEditorRow[]>
chartItems: ComputedRef<BarChartItem[]>
summary: ComputedRef<string>
preset: Ref<BarChartPreset>
palette: Ref<BarChartPalette>
suffix: Ref<string>
minimum: Ref<number | string>
maximum: Ref<number | string>
height: Ref<number | string>
valuesVisible: Ref<boolean>
axisVisible: Ref<boolean>
labelsVisible: Ref<boolean>
motionEnabled: Ref<boolean>
presets: FormSelectOption[]
palettes: FormSelectOption[]
units: FormSelectOption[]
applyPreset: (event: Event) => void
clear: () => void
reset: () => void
}
const barChartPlaygroundTemplate = html`<Flex direction="column">
<Grid columns="1" columnsMd="3">
<FormSelectField id="bar-preset" label="Dataset" :model="preset" :options="presets" @change="applyPreset" />
<FormSelectField id="bar-palette" label="Bar colors" :model="palette" :options="palettes" />
<FormSelectField id="bar-unit" label="Value suffix" :model="suffix" :options="units" />
</Grid>
<Panel variant="surfaceAlt" bodyClass="p-3 min-w-0">
<p class="text-eyebrow mt-0">LIVE PREVIEW · WEEKLY BALANCE</p>
<div class="overflow-x-auto" tabindex="0" role="region" aria-label="Scrollable chart preview">
<BarChart
:items="chartItems"
title="Weekly balance"
description="Illustrative daily changes. Edit the exact values in the fields below."
:ariaLabel="summary"
emptyLabel="No days to compare"
:valueSuffix="suffix"
:minValue="minimum"
:maxValue="maximum"
:height="height"
:showValues="valuesVisible"
:showAxis="axisVisible"
:showLabels="labelsVisible"
:animated="motionEnabled"
style="min-width: 440px"
/>
</div>
<FormStatus>{{ summary }}</FormStatus>
</Panel>
<Flex wrap="true">
<FormCheck id="bar-values" label="Values" :checked="valuesVisible" />
<FormCheck id="bar-axis" label="Grid and zero line" :checked="axisVisible" />
<FormCheck id="bar-labels" label="Category labels" :checked="labelsVisible" />
<FormCheck id="bar-motion" label="Entrance animation" :checked="motionEnabled" />
</Flex>
<Flex justify="between" align="center" wrap="true">
<h3 class="m-0">Edit the data</h3>
<Flex wrap="true">
<Btn variant="outline" @click="clear">Empty data</Btn>
<Btn tone="accent" variant="surface" @click="reset">Reset playground</Btn>
</Flex>
</Flex>
<Grid columns="2" columnsSm="3" columnsLg="5">
<FormInputField
r-for="row in rows"
:id="row.id"
:label="row.label"
type="number"
step="1"
:model="row.value"
/>
</Grid>
<Grid columns="1" columnsMd="3">
<FormInputField id="bar-min" label="Minimum bound" type="number" placeholder="Automatic" :model="minimum" />
<FormInputField id="bar-max" label="Maximum bound" type="number" placeholder="Automatic" :model="maximum" />
<FormInputField id="bar-height" label="Chart height (px)" type="number" min="180" step="20" :model="height" />
</Grid>
<p class="m-0 text-muted">Blank bounds use the data range. The scale always includes zero and every value. On narrow screens, scroll the preview horizontally to keep its labels readable.</p>
</Flex>`
const presetValues: Record<BarChartPreset, number[]> = {
mixed: [24, 38, -12, 46, 32],
positive: [18, 32, 25, 48, 60],
negative: [-18, -32, -8, -24, -12],
zero: [0, 0, 0, 0, 0],
}
function createBarChartRows(preset: BarChartPreset): BarChartEditorRow[] {
return ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'].map((label, index) => ({
id: `bar-value-${index}`,
label,
value: ref<number | string>(presetValues[preset][index]),
}))
}
function createBarChartPlayground(): BarChartPlayground {
const preset = ref<BarChartPreset>('mixed')
const palette = ref<BarChartPalette>('balance')
const suffix = ref('')
const minimum = ref<number | string>('')
const maximum = ref<number | string>('')
const height = ref<number | string>(280)
const valuesVisible = ref(true)
const axisVisible = ref(true)
const labelsVisible = ref(true)
const motionEnabled = ref(true)
const rows = sref(createBarChartRows('mixed'))
const accent = getThemePaletteVar('semanticTone.accent.button.rest.bgcolor')
const danger = getThemePaletteVar('semanticTone.danger.button.rest.bgcolor')
const chartItems = computed<BarChartItem[]>(() =>
rows().map((row) => {
const value = Number(row.value())
return {
label: row.label,
value,
color:
palette() === 'categorical'
? undefined
: palette() === 'balance' && value < 0
? danger
: accent,
}
}),
)
return {
rows,
chartItems,
summary: computed(() => {
if (!rows().length)
return 'No data. Choose a dataset or reset the playground.'
const values = rows().map(
(row) => `${row.label}: ${Number(row.value())}${suffix()}`,
)
return values.join(' · ')
}),
preset,
palette,
suffix,
minimum,
maximum,
height,
valuesVisible,
axisVisible,
labelsVisible,
motionEnabled,
presets: [
{ label: 'Mixed gains and losses', value: 'mixed' },
{ label: 'All positive', value: 'positive' },
{ label: 'All negative', value: 'negative' },
{ label: 'All zero', value: 'zero' },
],
palettes: [
{ label: 'Gains / losses', value: 'balance' },
{ label: 'One color per category', value: 'categorical' },
{ label: 'Single accent', value: 'accent' },
],
units: [
{ label: 'No suffix', value: '' },
{ label: 'Percent (%)', value: '%' },
{ label: 'Milliseconds (ms)', value: 'ms' },
{ label: 'Credits (cr)', value: 'cr' },
],
applyPreset: (event) => {
const next = (event.target as HTMLSelectElement).value as BarChartPreset
rows(createBarChartRows(next))
},
clear: () => rows([]),
reset: () =>
batch(() => {
preset('mixed')
palette('balance')
suffix('')
minimum('')
maximum('')
height(280)
valuesVisible(true)
axisVisible(true)
labelsVisible(true)
motionEnabled(true)
rows(createBarChartRows('mixed'))
}),
}
}
const barChartPlayground = defineComponent<BarChartPlayground>(
barChartPlaygroundTemplate,
{ context: createBarChartPlayground },
)
const icons: Record<string, string> = {
'lucide:chevron-down': lucide_chevron_down,
}
createApp(
{
components: {
BarChartPlayground: barChartPlayground,
...defineBarChartComponents(),
...defineButtonComponents(),
...defineFlexComponents(),
...defineFormComponents(),
...defineFormInputField(),
...defineFormSelectField(),
...defineGridComponents(),
...defineIconComponents((name) => icons[name] ?? ''),
...definePanelComponents(),
},
},
{ selector: 'app#bar-chart-demo', template: html`<BarChartPlayground />` },
) A fixed comparison scale
Use explicit bounds to compare values against a stable range. Here, both categories use a zero-to-100 scale. Bounds expand when needed to include the data; they do not clip outlying values.
<BarChart
title="Example completion"
ariaLabel="Completion: content 80 percent, review 55 percent"
:items="[{label:'Content',value:80},{label:'Review',value:55}]"
minValue="0"
maxValue="100"
height="280"
valueSuffix="%"
:animated="false"
/> Positive and negative values
Every bar starts at zero. Losses extend downward, with their values below the bar and category names in a separate row. A symmetric scale makes the direction and magnitude easy to compare.
<BarChart
title="Change since the previous release"
ariaLabel="Change: API plus 24 percent, jobs minus 32 percent, sync plus 18 percent, cache minus 8 percent"
:items="[{label:'API',value:24},{label:'Jobs',value:-32},{label:'Sync',value:18},{label:'Cache',value:-8}]"
minValue="-40"
maxValue="40"
height="280"
valueSuffix="%"
:animated="false"
/> A compact trend
Hide the grid and numeric labels when the shape is the useful signal. Keep an accessible description with the exact values. The same component fits a small dashboard card.
76 deployments this week
Daily activity across the release pipeline.
<Panel tone="accent" variant="surface" bodyClass="p-3">
<h3 class="mt-0">76 deployments this week</h3>
<p>Daily activity across the release pipeline.</p>
<BarChart
title="Daily deployments"
ariaLabel="Deployments: Monday 8, Tuesday 13, Wednesday 11, Thursday 19, Friday 16, Saturday 6, Sunday 3"
:items="[{label:'Mon',value:8},{label:'Tue',value:13},{label:'Wed',value:11},{label:'Thu',value:19},{label:'Fri',value:16},{label:'Sat',value:6},{label:'Sun',value:3}]"
height="180"
:showAxis="false"
:showValues="false"
:animated="false"
/>
</Panel> Behavior and accessibility
Each valid item is a category. Non-finite values are removed; negative values extend below zero. SVG title and description help describe the chart, but include nearby text or a data table for exact values. Turn animation off when comparing rapid updates.
API reference
BarChart contract
Props below are the public template API. RefOrValue accepts a literal or a reactive ref; bind refs with a colon-prefixed attribute. Native attributes and events can be passed through the component root.
items
RefOrValue<Array<RefOrValue<BarChartItem>>> - Default
[]
Array of BarChartItem objects with optional label, numeric value (or numeric string), and CSS color. Missing labels become Item 1, Item 2 and so on.
title
RefOrValue<string> - Default
not set
SVG title text; not a separate visible heading.
description
RefOrValue<string> - Default
not set
SVG desc text explaining the data or takeaway.
ariaLabel
RefOrValue<string> - Default
title, then Bar chart
Accessible name on the SVG image.
emptyLabel
RefOrValue<string> - Default
No data
Visible fallback when no usable data remains.
valueSuffix
RefOrValue<string> - Default
not set
Unit appended to formatted values, such as ms or %.
width
RefOrValue<number | string> - Default
100%
CSS width. Numeric values become pixel lengths; strings can use CSS units.
height
RefOrValue<number | string> - Default
automatic
CSS chart height; omitted height preserves the SVG’s intrinsic aspect ratio.
minValue
RefOrValue<number | string> - Default
automatic
Requested lower domain bound. The resolved minimum also includes zero and the smallest data value. Omit it to use the data range automatically.
maxValue
RefOrValue<number | string> - Default
automatic
Requested upper domain bound. The resolved maximum also includes zero and the largest data value, so bars are never clipped by a tighter bound.
animated
RefOrValue<boolean | string> - Default
true
Enables the SVG entrance animation when bars mount. Bind false for immediate rendering, especially while editing values. This is not an interpolated transition between datasets.
showValues
RefOrValue<boolean | string> - Default
true
Draws numeric values beside marks. Enable only when labels have enough room.
showLabels
RefOrValue<boolean | string> - Default
true
Shows category labels along the horizontal axis.
showAxis
RefOrValue<boolean | string> - Default
true
Shows the axis grid and value labels.
tone
RefOrValue<SemanticTone> - Default
inherited
Semantic intent: neutral, accent, secondary, info, success, warning, danger, feature, custom or ghost. The active skin supplies the colors.
variant
RefOrValue<ComponentVariant> - Default
none
Visual treatment. Accepts solid, surface, surfaceAlt, spotlight, glass, flat, flatAlt, flatSolid, outlineFill, outline, subtle, subtleBtn, link, sheen, underline, rail, bracket or none.
variantMode
RefOrValue<ComponentVariantMode> - Default
stateless
Use stateless for content surfaces; stateful enables the treatment’s hover, focus and active styles. It does not add interaction handlers.
Composition
No slots. Register defineBarChartComponents. Bind a ref of BarChartItem[] to update the data.