Bars, lines and areas over shared categories, on one or two value axes
Tags: chart combo dual-axis comparison reporting
The ComboChart draws bars, lines and areas over the same categories, optionally on a second value axis on the right. Use it to show a volume next to a rate, such as monthly closings with average cycle time, or revenue bars with a margin line.
Svelte only (
@classic-homes/charts-svelte); React support is pending.
Import
<script>
import { ComboChart } from '@classic-homes/charts-svelte';
</script>Basic Usage
<script>
import { ComboChart } from '@classic-homes/charts-svelte';
const data = {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
series: [
{ name: 'Closings', type: 'bar', data: [42, 38, 51, 47, 55, 61] },
{
name: 'Avg cycle time',
type: 'line',
axis: 'secondary',
data: [118, 121, 114, 109, 112, 104],
},
],
};
</script>
<ComboChart
title="Closings and Cycle Time"
{data}
valueFormat="integer"
secondaryValueFormat="days"
height={400}
/>Props
Chart-Specific Props
| Prop | Type | Default | Description |
|---|---|---|---|
data |
ComboChartData |
required | Categories and series; each series has a type and an axis |
stacked |
boolean |
false |
Stack all bar series (series with a stack id stack regardless) |
smooth |
boolean |
false |
Use smooth curved lines for line and area series |
showLabels |
boolean |
false |
Show data labels |
connectNulls |
boolean |
false |
Draw lines across missing (null) values instead of leaving a gap |
secondaryValueFormat |
ValueFormat |
- | Value format for series on the secondary axis (defaults to valueFormat) |
labelFormatter |
string | ((params: LabelFormatterParams) => string) |
- | Data label template or function |
xAxisConfig |
AxisConfig |
- | Category axis name, tick formatter, rotation and range |
yAxisConfig |
AxisConfig |
- | Left (primary) value axis customisation |
secondaryYAxisConfig |
AxisConfig |
- | Right (secondary) value axis customisation |
referenceLines |
ReferenceLine[] |
- | Lines marking targets, thresholds or statistics ('average', 'median', 'min', 'max') |
referenceBands |
ReferenceBand[] |
- | Shaded value ranges |
dataZoom |
boolean | 'inside' | 'slider' |
- | Let users zoom and pan the category axis |
legend |
ChartLegendConfig |
- | Built-in legend position, alignment, icon and formatter |
onLegendChange |
(visible: Record<string, boolean>) => void |
- | Called when legend items are shown or hidden |
renderLegend |
Snippet<[LegendRenderProps]> |
- | Custom HTML legend (e.g. ChartLegend) instead of the built-in one |
onClick |
(params: DataPointEventParams) => void |
- | Callback when a bar or point is clicked |
onHover |
(params: DataPointEventParams) => void |
- | Callback when hovering over a bar or point |
Inherited Base Props
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string |
required | Chart title for accessibility (aria-label) |
description |
string |
- | Description for screen readers (generated from the data when omitted) |
height |
number | string |
400 |
Chart height in pixels or CSS value |
minHeight |
number |
- | Minimum height in pixels; also bounds an 'auto' height |
maxHeight |
number |
- | Maximum height in pixels; also bounds an 'auto' height |
loading |
boolean |
false |
Show loading skeleton (an overlay when data is already shown) |
error |
string | null |
- | Error message to display |
onRetry |
() => void |
- | Show a Retry button on the error state, calling this |
emptyMessage |
string |
- | Message shown when data is empty |
theme |
'light' | 'dark' | 'auto' |
'auto' |
Theme preference |
animation |
boolean |
true |
Enable/disable animations (respects prefers-reduced-motion) |
showLegend |
boolean |
true |
Show chart legend |
showTooltip |
boolean |
true |
Show tooltip on hover |
showGrid |
boolean |
true |
Show grid lines |
valueFormat |
ValueFormat |
- | Preset ('number', 'integer', 'compact', 'currency', 'currencyCents', 'percent', 'percentValue', 'days'), Intl.NumberFormatOptions or a function; drives the value axis, tooltip, labels and data table |
locale |
string |
'en-US' |
Locale for valueFormat |
tooltipFormatter |
string | ((items: TooltipFormatterParams[]) => string) |
- | Custom tooltip template or HTML function |
tooltipValueFormatter |
(value: number) => string |
- | Format values in the default tooltip (overrides valueFormat there) |
option |
EChartsOption |
- | Raw ECharts option deep-merged over the generated one |
onReady |
(chart: EChartsType) => void |
- | Called with the ECharts instance when it is created |
events |
ChartEventHandlers |
- | Raw ECharts event handlers keyed by event name ('datazoom', 'legendselectchanged', ...) |
showDataTable |
boolean | 'sr-only' |
'sr-only' |
Data table for screen readers only, a visible "Show data" toggle (true) or none (false) |
exportable |
boolean | ChartExportConfig |
- | Show PNG and CSV download buttons, or { image, csv, filename } |
patterns |
boolean |
false |
Fill series with decal patterns as well as colour |
class |
string |
- | Custom CSS class |
Data Structure
interface ComboChartData {
categories: string[]; // Category labels
series: Array<{
name: string; // Series name for legend
data: (number | null)[]; // Values by category; null leaves a gap
type?: 'bar' | 'line' | 'area'; // How the series is drawn (default 'bar')
axis?: 'primary' | 'secondary'; // Left or right value axis (default 'primary')
stack?: string; // Stack group id
color?: string; // Optional custom color
labels?: (string | null | undefined)[]; // Per-point label text
}>;
}Examples
Dual Axis with Different Formats
Each axis, tooltip row and data label uses the format of the axis its series is on.
<ComboChart
title="Revenue and Gross Margin"
data={{
categories: ['Q1', 'Q2', 'Q3', 'Q4'],
series: [
{ name: 'Revenue', data: [18_400_000, 21_200_000, 19_800_000, 24_600_000] },
{ name: 'Gross margin', type: 'line', axis: 'secondary', data: [0.212, 0.224, 0.208, 0.231] },
],
}}
valueFormat="compact"
secondaryValueFormat="percent"
secondaryYAxisConfig={{ name: 'Margin' }}
/>Stacked Bars with a Total Line
<ComboChart
title="Starts by Product Line"
data={{
categories: ['Jan', 'Feb', 'Mar', 'Apr'],
series: [
{ name: 'Townhomes', stack: 'starts', data: [12, 15, 11, 18] },
{ name: 'Single family', stack: 'starts', data: [28, 31, 26, 34] },
{ name: 'Target', type: 'line', data: [40, 42, 42, 45] },
],
}}
/>From Records
Pivot API rows into categories and series with fromRecords, then set each series' type.
<script>
import { ComboChart, fromRecords } from '@classic-homes/charts-svelte';
let { rows } = $props(); // [{ month: 'Jan', closings: 42, cycleDays: 118 }, ...]
const pivot = $derived(
fromRecords(rows, {
category: 'month',
measures: { Closings: 'closings', 'Cycle time': 'cycleDays' },
})
);
const data = $derived({
categories: pivot.categories,
series: pivot.series.map((s) =>
s.name === 'Cycle time' ? { ...s, type: 'line', axis: 'secondary' } : s
),
});
</script>
<ComboChart title="Closings and Cycle Time" {data} secondaryValueFormat="days" />Target Line and Zoom
<ComboChart
title="Weekly Closings"
{data}
referenceLines={[{ value: 50, label: 'Goal' }]}
dataZoom="slider"
/>Accessibility
- The
titleprop is used asaria-label; a description of each series trend is generated whendescriptionis omitted - The chart data is rendered as a screen-reader table (
showDataTable), with each axis' format - Animations respect
prefers-reduced-motion patternsadds decal fills for colour-blind readers