Skip to content
ShineUI
Install

Combo chart

Bars with a line on a second axis.

<sui-chart-combo>ChartsStable

About

Combo chart: columns on a start axis and rate lines on an end axis over the same periods, for revenue against gross margin. It takes any number of each; every series walks the brand ramp, the accent marks the flagged one, and the end axis is in accent ink so the scales read apart. Legend keys toggle a series.

Usage

<sui-chart-combo heading="Revenue and gross margin" subtitle="Columns on the left money axis, line on the right percent axis · Jan to Jul 2026" caption="Dual axes: money left, percentage right in accent ink" money></sui-chart-combo> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-chart-combo"); el.labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; /* string[] */ el.bars = { label: "Revenue", data: [148200, 152900, 161400, 155100, 166800, 168900, 166200] }; /* SuiComboSeries | SuiComboSeries[] */ el.addEventListener("suiHover", (e) => console.log(e.detail)); </script>
import { SuiChartCombo } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; /* string[] */ const bars = { label: "Revenue", data: [148200, 152900, 161400, 155100, 166800, 168900, 166200] }; /* SuiComboSeries | SuiComboSeries[] */ return <SuiChartCombo heading="Revenue and gross margin" subtitle="Columns on the left money axis, line on the right percent axis · Jan to Jul 2026" caption="Dual axes: money left, percentage right in accent ink" money labels={labels} bars={bars} onSuiHover={(e) => console.log(e.detail)} />; }
<script setup> import { SuiChartCombo } from "@shineui/vue"; import "@shineui/core/shineui.css"; const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; // string[] const bars = { label: "Revenue", data: [148200, 152900, 161400, 155100, 166800, 168900, 166200] }; // SuiComboSeries | SuiComboSeries[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiChartCombo heading="Revenue and gross margin" subtitle="Columns on the left money axis, line on the right percent axis · Jan to Jul 2026" caption="Dual axes: money left, percentage right in accent ink" money :labels="labels" :bars="bars" @sui-hover="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiChartCombo } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiChartCombo], template: `<sui-chart-combo heading="Revenue and gross margin" subtitle="Columns on the left money axis, line on the right percent axis · Jan to Jul 2026" caption="Dual axes: money left, percentage right in accent ink" money [labels]="labels" [bars]="bars" (suiHover)="onChange($event)"></sui-chart-combo>`, }) export class Example { labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; // string[] bars = { label: "Revenue", data: [148200, 152900, 161400, 155100, 166800, 168900, 166200] }; // SuiComboSeries | SuiComboSeries[] onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The plot is one tab stop named by an aria-label on the host, else by the heading and every series, and the arrow keys reach each column and point. Columns take --viz-1 onward and a lone line --accent. money formats the start axis and the column figures; formatEnd words the end axis.

<sui-chart-combo heading="Revenue and gross margin" subtitle="Columns on the left money axis, line on the right percent axis · Jan to Jul 2026" caption="Dual axes: money left, percentage right in accent ink" money></sui-chart-combo>
// setup(root): properties and events setup(root) { fill(root.querySelector('sui-chart-combo')); }

Several series

bars and line each take one series or a list. Columns group side by side inside the period and walk --viz-1 to --viz-4 in order, the lines carry on from where the columns stopped, and --accent goes to the series marked accent.

<sui-chart-combo heading="Revenue, cost of sales and two margins" subtitle="Two column series on the left money axis, two rate lines on the right" caption="Net margin is the flagged series" money></sui-chart-combo>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-combo'); el.labels = M; el.bars = [REVENUE, { label: 'Cost of sales', data: k([79.1, 81.3, 85.2, 83.0, 86.9, 87.5, 87.2]) }]; el.line = [MARGIN, { label: 'Net margin', data: [12.1, 12.8, 13.4, 12.9, 14.1, 14.4, 13.8], accent: true }]; }

End axis format

formatEnd formats the end axis ticks and every line row in the tooltip, so the second measure can be days, a ratio or a count. It defaults to a percentage here, because this chart's end axis is always the rate.

<sui-chart-combo heading="Revenue and days to pay" subtitle="Columns on the left money axis, line on the right in days" caption="Collections improved four days while revenue held" money></sui-chart-combo>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-combo'); el.labels = M; el.bars = REVENUE; el.line = { label: 'Days to pay', data: [41, 40, 38, 39, 37, 36, 37] }; el.formatEnd = (v) => v + 'd'; }

Curved

curve draws each line as a smooth path through the same points. The curve never overshoots a point, so it shows no rate the series does not hold; the columns and the hover are unchanged.

<sui-chart-combo curve heading="Revenue and gross margin" subtitle="Smoothed rate line" money></sui-chart-combo>
// setup(root): properties and events setup(root) { fill(root.querySelector('sui-chart-combo')); }

Events

suiHover fires whenever the hovered period changes and reports { index }, with index -1 on leave. hoverIndex is mutable: the buttons write it directly, which moves the readout but emits nothing, and that is the property sui-chart-synced sets to keep stacked panels on one period.

Idle.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-chart-combo id="cb-events" heading="Revenue and gross margin" subtitle="Hover a period, or use the buttons" money></sui-chart-combo> <div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center"> <sui-button id="cb-may" size="sm" variant="outline" label="Show May"></sui-button> <sui-button id="cb-clear" size="sm" variant="ghost" label="Clear"></sui-button> <span id="cb-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#cb-events'); const log = root.querySelector('#cb-log'); fill(el); el.addEventListener('suiHover', (e) => { const i = e.detail.index; log.textContent = i < 0 ? 'Idle.' : `${M[i]}: revenue $${REVENUE.data[i].toLocaleString('en-US')}, margin ${MARGIN.data[i]}%.`; }); root.querySelector('#cb-may').addEventListener('click', () => { el.hoverIndex = 4; }); root.querySelector('#cb-clear').addEventListener('click', () => { el.hoverIndex = -1; }); }

Empty and loading

loading stands a shimmer in for the plot and marks the host aria-busy while the figures are on their way; the frame and its heading stay. A chart with nothing to plot keeps its frame and says so, and emptyMessage is the sentence. Both are the same on every chart.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-chart-combo heading="Revenue and gross margin" subtitle="Fetching the year" height="150" loading></sui-chart-combo> <sui-chart-combo heading="Revenue and gross margin" subtitle="This year" height="150" empty-message="No revenue has been booked this year."></sui-chart-combo> </div>

Keyboard

KeyAction
TabMoves to the plot, then through the legend keys, one button per series.
ArrowRight ArrowLeftMoves to the next or the previous period and shows its tooltip; a reader hears the period, the series and the figure.
ArrowDown ArrowUpMoves to the next or the previous series at the same period, the columns first and then the lines.
Home EndMoves to the first or the last period of the series.
Ctrl+Home Ctrl+EndMoves to the first point of the first series or the last point of the last.
EscapeLeaves the point and hides the tooltip; the plot keeps focus.
Enter SpaceOn a legend key, hides its series and redraws the chart without it; press again to bring it back.

API

<sui-chart-combo>

Properties

PropertyAttributeTypeDefaultDescription
barsproperty onlySuiComboSeriesInputSuiComboSeries | SuiComboSeries[]{ label: '', data: [] }Column series on the start axis: one series or a list. Several columns group side by side inside each period. Property, not attribute.
captioncaptionstringstring | undefinedFootnote under the legend.
currencycurrencystring'USD'The ISO 4217 code money prints in: USD by default, EUR, JPY. The page's locale (setLocale(), else <html lang>) places the symbol and sets the grouping and the decimal mark, so EUR on a German page reads 1,2 Mio. € on the axis and 121.695 € in the tooltip. An unknown code prints the bare figure. Ignored without money.
curvecurvebooleanfalseDraw each line as a smooth curve through its points instead of straight segments. The curve never overshoots a point, so it shows no rate the series does not hold. Reflected.
emptyMessageempty-messagestringstring | undefinedSentence drawn over the plot when there are no periods, or no column and no line holds a figure above zero. Defaults to "Nothing to plot yet."; the frame, the axes and the legend stay. The chart filters nothing, so this is its only empty string.
everyeverySuiChartEvery"auto" | number'auto'Draw every nth period label, so a dense axis stays legible. auto, the default, measures the widest label against the space one period is given and thins only where the labels would collide; where they fit it draws all of them and the axis is what it always was. Set it to 1 to draw every label at any width, or to a larger number for the stride to start from: where its labels would run into each other it widens to the next multiple that clears them.
fixedSizefixed-sizebooleanfalsePin the plot to width, which is what a fixed export wants, or a row of charts that have to measure the same whatever the space around them. Without it the plot grows to the width the container gives it and the coordinate space tracks the pixel width, so one unit stays one pixel: the periods spread out and the axis text keeps the size it was drawn at. Reflected.
formatEndproperty only((v: number) => string) | undefinedFormats the end axis ticks and every line's tooltip value, so the second measure can be days, a ratio or a count. This chart's end axis is always the rate, so it defaults to a percentage in the page's locale (12.5%, 12,5 % on a German page); sui-chart-line, whose second axis has no fixed meaning, defaults to the plain figure. Property, not attribute.
headingheadingstringstring | undefinedChart heading (the reference title prop, renamed because title is a native attribute). With what the chart shows, it names the plot for a reader; an aria-label or aria-labelledby on the host names the plot instead, and this summary then describes it.
heightheightnumber210ViewBox height of the plot in px.
hoverIndexhover-indexnumber-1Hovered period index; -1 when idle. The pointer and the arrow keys set it. The hovered period keeps its full strength and the other periods dim, and the tooltip reads every series at that period. A write from outside does the same and emits nothing, which is how sui-chart-synced mirrors a sibling panel.
labelsproperty onlystring[][]Period labels. Property, not attribute.
lineproperty onlySuiComboSeriesInputSuiComboSeries | SuiComboSeries[]{ label: '', data: [] }Line series on the end axis: one series or a list. A lone line is the flagged series and takes the accent; with two or more, the accent goes to the one marked accent and the rest walk the brand ramp on from the columns. Under dir="rtl" the start axis, the columns', stands at the right edge beside the title and the end axis at the left. Property, not attribute.
loadingloadingbooleanfalseThe figures are still on their way: a shimmer stands in for the plot, the frame and its heading stay, and the host is aria-busy. Reflected, so a page can style the wait.
moneymoneybooleanfalseMoney formatting in currency on the start axis, compact ($120k), and on the column values in the tooltip, exact ($121,695). Off, the figures print plainly, which is what a count wants; set it for revenue against a rate. The end axis is formatEnd's. Reflected.
subtitlesubtitlestringstring | undefinedOne-line context under the title.
widthwidthnumber460Width of the plot in px. The plot grows to the width its container gives it, and this is the starting figure, used until the container has been measured and wherever it measures zero; with fixedSize it is the plot's exact width. The card adds 34px of padding and border on top of it, and the end axis takes 34px out of it.

Events

EventDetailReactVueDescription
suiHoverSuiChartHoverDetailonSuiHover@sui-hoverFires when the pointer or the keyboard moves the hover to another period; { index: -1 } when the pointer leaves, or Escape or a blur takes the keyboard's point away.

Methods

MethodSignatureDescription
setFocus()setFocus() => Promise<void>Moves focus to the plot, the chart's one tab stop, from which the arrow keys reach the columns and the points. Does nothing while there is nothing to plot.

Slots

SlotDescription
header-endControls at the end of the head row, after the title.

Types

SuiChartEvery typeHow the x labels thin: a fixed stride, or auto to measure the widest label against the room one point is given and draw only as many as fit.
No members.
SuiChartHoverDetail interfaceDetail of suiHover: the hovered index, or -1 when the pointer leaves the plot. What the index counts is each chart's own: an x position here, a point on a scatter, an axis on a radar.
indexnumber
SuiComboSeries interfaceA labeled numeric series for the combo chart's columns or lines. accent claims the single accent highlight; color overrides the ramp step.
labelstring
datanumber[]
accent?boolean
color?string
SuiComboSeriesInput typeOne series or a list of them, so a chart with one column and one line keeps writing one object.
No members.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also