Skip to content
ShineUI
Install

Line chart

Lines, areas and stacked areas.

<sui-chart-line>ChartsStable

About

Line, area and stacked-area chart over a categorical x axis, for a balance or a rate over time. Series draw from the brand ramp, one may take the accent, and one may take its own axis at the inline end. Hovering shows every series at the nearest point. Pair it with a table of the same figures.

Usage

<sui-chart-line heading="Cash balance" subtitle="Operating, payroll and money market · hover for detail" caption="Dashed line is the prior year · dashed rule is the policy floor" money></sui-chart-line> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-chart-line"); el.labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; /* string[] */ el.series = [ { label: "2026", data: [96825, 104200, 99400, 112800, 118300, 121900, 145334] }, { label: "2025", data: [82100, 85400, 88900, 84200, 91600, 95800, 99200], dashed: true }, ]; /* SuiChartSeries[] */ el.addEventListener("suiHover", (e) => console.log(e.detail)); </script>
import { SuiChartLine } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; /* string[] */ const series = [ { label: "2026", data: [96825, 104200, 99400, 112800, 118300, 121900, 145334] }, { label: "2025", data: [82100, 85400, 88900, 84200, 91600, 95800, 99200], dashed: true }, ]; /* SuiChartSeries[] */ return <SuiChartLine heading="Cash balance" subtitle="Operating, payroll and money market · hover for detail" caption="Dashed line is the prior year · dashed rule is the policy floor" money labels={labels} series={series} onSuiHover={(e) => console.log(e.detail)} />; }
<script setup> import { SuiChartLine } from "@shineui/vue"; import "@shineui/core/shineui.css"; const labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; // string[] const series = [ { label: "2026", data: [96825, 104200, 99400, 112800, 118300, 121900, 145334] }, { label: "2025", data: [82100, 85400, 88900, 84200, 91600, 95800, 99200], dashed: true }, ]; // SuiChartSeries[] const onChange = (e) => console.log(e.detail); </script> <template> <SuiChartLine heading="Cash balance" subtitle="Operating, payroll and money market · hover for detail" caption="Dashed line is the prior year · dashed rule is the policy floor" money :labels="labels" :series="series" @sui-hover="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiChartLine } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiChartLine], template: `<sui-chart-line heading="Cash balance" subtitle="Operating, payroll and money market · hover for detail" caption="Dashed line is the prior year · dashed rule is the policy floor" money [labels]="labels" [series]="series" (suiHover)="onChange($event)"></sui-chart-line>`, }) export class Example { labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul"]; // string[] series = [ { label: "2026", data: [96825, 104200, 99400, 112800, 118300, 121900, 145334] }, { label: "2025", data: [82100, 85400, 88900, 84200, 91600, 95800, 99200], dashed: true }, ]; // SuiChartSeries[] 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 visible series, and the arrow keys reach every point. Legend keys toggle a series. Series take the ramp in order; dashed marks the prior year, refLine draws the floor, and money prints the axis compact and the tooltip exact.

<sui-chart-line heading="Cash balance" subtitle="Operating, payroll and money market · hover for detail" caption="Dashed line is the prior year · dashed rule is the policy floor" money></sui-chart-line>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-line'); el.labels = M; el.series = [ { label: '2026', data: CASH }, { label: '2025', data: PRIOR, dashed: true }, ]; el.refLine = { value: 90000, label: 'Reserve floor' }; }

In another currency

currency names the ISO code money prints in, USD by default. The page's locale places the symbol and sets the grouping: this English page reads €120k on the axis and €121,900 in the tooltip, where a page with lang="de" reads 120.000 € and 121.900 €. A demo cannot switch the locale for one chart; the page decides.

<sui-chart-line heading="Cash balance, euro account" subtitle="currency=&quot;EUR&quot; · hover for the exact figure" money currency="EUR"></sui-chart-line>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-line'); el.labels = M; el.series = [{ label: '2026', data: CASH }]; }

Curved

curve draws each series as a smooth line through the same points, and areas, stacks and range bands follow it. The curve never overshoots a point, so a balance never reads higher or lower than a figure it holds; hover, dots and the tooltip are unchanged.

<sui-chart-line heading="Cash balance" subtitle="Smoothed · dashed line is the prior year" curve money></sui-chart-line>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-line'); el.labels = M; el.series = [ { label: '2026', data: CASH }, { label: '2025', data: PRIOR, dashed: true }, ]; }

Second axis

A series with axis "end" is measured against its own scale, drawn on a second axis at the inline end in accent ink so the two are never read as one. Set formatEnd to word its ticks and its tooltip row; the end scale fits its own values instead of anchoring on zero.

<sui-chart-line heading="Cash and days to pay" subtitle="Money on the left, days on the right" caption="Collections improved four days while cash rose" money></sui-chart-line>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-line'); el.labels = M; el.series = [ { label: 'Cash', data: CASH }, { label: 'Days to pay', data: [41, 40, 38, 39, 37, 36, 37], axis: 'end' }, ]; el.formatEnd = (v) => v + 'd'; }

Point markers

mark sets the marker for a whole series and marks sets it per point, so a forecast, a restated figure or a flagged event carries its own shape. Shapes do the work, never a color of their own, and a point with its own marker draws even where the automatic dots are off.

<sui-chart-line heading="Cash balance" subtitle="Diamonds mark a restated month, the last point is a forecast" caption="Squares are estimates, the filled dot is the forecast" money></sui-chart-line>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-line'); el.labels = M; el.series = [ { label: '2026', data: CASH, marks: [null, null, 'diamond', null, 'square', null, 'solid'] }, { label: '2025', data: PRIOR, dashed: true, mark: 'none' }, ]; }

Markers on hover, and nothing to plot

markers decides when the automatic dots draw. auto marks every point of a short series, and only the hovered point of a long one or a lone area, whose fill carries the shape; hover does that for any chart, always and none pin it. A chart with nothing to plot keeps its frame and says so; emptyMessage is the sentence.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-chart-line id="ln-hover" heading="Cash balance" subtitle="Hover a month for the point" markers="hover" height="160" money></sui-chart-line> <sui-chart-line id="ln-empty" heading="Tasks closed" subtitle="This quarter" kind="area" height="160" empty-message="No tasks were closed this quarter."></sui-chart-line> </div>
// setup(root): properties and events setup(root) { const hover = root.querySelector('#ln-hover'); hover.labels = M; hover.series = [{ label: '2026', data: CASH }, { label: '2025', data: PRIOR, dashed: true }]; const empty = root.querySelector('#ln-empty'); empty.labels = ['Wk 1', 'Wk 2', 'Wk 3', 'Wk 4', 'Wk 5', 'Wk 6', 'Wk 7', 'Wk 8']; empty.series = [{ label: 'Closed', data: [0, 0, 0, 0, 0, 0, 0, 0] }]; }

Range band

A series with range draws its low and high envelope behind the line at 14% alpha. The axis takes the envelope into its domain.

<sui-chart-line heading="Receivables range" subtitle="Daily A/R with the intramonth low and high" caption="Band is the intramonth low to high" money></sui-chart-line>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-line'); el.labels = M; el.series = [{ label: 'A/R balance', data: k([118, 126, 121, 134, 141, 137, 141]), range: [k([104, 112, 108, 118, 126, 122, 128]), k([131, 139, 135, 148, 155, 150, 152])], }]; }

Stacked area

kind="stacked-area" composes the series and the axis runs to the stack total. The tooltip lists each layer; toggling a key off drops it from the stack.

<sui-chart-line heading="Cash by account" subtitle="Stacked area composition" kind="stacked-area" money></sui-chart-line>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-line'); el.labels = M; el.series = [ { label: 'Operating', data: k([52, 58, 54, 63, 66, 68, 88]) }, { label: 'Payroll', data: k([12, 12, 13, 12, 13, 12, 12]) }, { label: 'Money market', data: k([33, 34, 32, 38, 39, 42, 45]) }, ]; }

Plain area with a control

kind="area" fills under the line with its own color, fading from 30% at the line to nothing at the baseline. A single series draws no legend. plain drops the card for embedding in a panel that has its own frame, and the header-end slot puts a control beside the heading.

<sui-chart-line heading="Cash" kind="area" plain height="160" money> <sui-button slot="header-end" size="sm" variant="ghost" label="Export"></sui-button> </sui-chart-line>
// setup(root): properties and events setup(root) { const el = root.querySelector('sui-chart-line'); el.labels = M; el.series = [{ label: 'Cash', data: CASH }]; }

Dense axis

every thins the x labels so they never collide. auto, the default, measures the widest label against the room one point has and draws as many as fit, so the axis follows the width; a number pins the stride, one label in six here. Set it to 1 to draw every label at any width.

<div style="display:grid;grid-template-columns:1fr 1fr;gap:14px"> <sui-chart-line id="ln-auto" heading="Cash balance" subtitle="24 months · labels thin to what fits" height="160" money></sui-chart-line> <sui-chart-line id="ln-six" heading="Cash balance" subtitle="24 months · one label in six" every="6" height="160" money></sui-chart-line> </div>
// setup(root): properties and events setup(root) { for (const el of root.querySelectorAll('sui-chart-line')) { el.labels = M24; el.series = [{ label: 'Cash', data: CASH24 }]; } }

Events

suiHover fires when the hovered month changes and reports { index }, with index -1 on leave. hoverIndex is mutable: a write from outside moves the guide and the tooltip but emits nothing, which is how sui-chart-synced mirrors a sibling.

Idle.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-chart-line id="ln-events" heading="Cash balance" subtitle="Hover a month" money></sui-chart-line> <span id="ln-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const el = root.querySelector('#ln-events'); const log = root.querySelector('#ln-log'); el.labels = M; el.series = [{ label: '2026', data: CASH }]; el.addEventListener('suiHover', (e) => { const i = e.detail.index; log.textContent = i < 0 ? 'Idle.' : `${M[i]}: $${CASH[i].toLocaleString('en-US')}.`; }); }

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. Every chart draws the same wait.

<sui-chart-line heading="Cash balance" subtitle="Fetching the quarter" height="150" loading></sui-chart-line>

Drawing your own

The palette is public as tokens, for a plot the family does not draw: --viz-1 to --viz-4 are the series ramp in order and --accent is the highlighted series, so your plot follows the theme the way the charts do. The scale arithmetic and the drawing helpers stay inside the library.

.my-plot .series-1 { stroke: var(--viz-1); } .my-plot .series-2 { stroke: var(--viz-2); } .my-plot .highlight { stroke: var(--accent); }

Keyboard

KeyAction
TabMoves to the plot, then through the legend keys, one button per series. A single series draws no legend.
ArrowRight ArrowLeftMoves to the next or the previous point and shows its tooltip; a reader hears the label, the series and the figure. A gap in a line is stepped over.
ArrowDown ArrowUpMoves to the next or the previous series at the same point.
Home EndMoves to the first or the last point 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-line>

Properties

PropertyAttributeTypeDefaultDescription
captioncaptionstringstring | undefinedFootnote under the legend: units, source, what the marks mean.
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 series as a smooth curve through its points instead of straight segments. The curve never overshoots a point, so it shows no figure the series does not hold. Areas, stacks and range bands follow it; hover, dots and the tooltip are unchanged. Reflected.
emptyMessageempty-messagestringstring | undefinedSentence drawn over the plot when there are no series or every point of every series is zero. Defaults to "Nothing to plot yet."; the frame, the axes and the legend stay, so the card keeps its place on the page while the figures are still to come. The chart filters nothing, so this is its only empty string.
everyeverySuiChartEvery"auto" | number'auto'Draw every nth x label, so a dense axis stays legible. auto, the default, measures the widest label against the space one point 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 line spreads out over more x space and the axis text keeps the size it was drawn at. Reflected.
formatEndproperty only((v: number) => string) | undefinedFormats the end axis ticks and the tooltip values of every series on it, so the second measure can be a rate, days or a count. Defaults to the plain figure with every decimal it has, in the page's locale, since a second axis here has no fixed meaning; sui-chart-combo, whose end axis is always the rate, defaults to a percentage instead. 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.
heightheightnumber200ViewBox height of the plot in px.
hoverIndexhover-indexnumber-1Hovered x index; -1 when idle. The pointer and the arrow keys set it and the chart draws a guide line and the tooltip at that index. A write from outside moves both and emits nothing, which is how sui-chart-synced mirrors a sibling panel.
kindkind"area" | "line" | "stacked-area"'line'Geometry: plain lines, filled areas or a stacked composition. An area is the series color fading from 30% under the line to nothing at the baseline, so the fill carries the theme's hue on whatever ground the chart sits on. Reflected.
labelsproperty onlystring[][]X-axis category labels. 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.
markersmarkersSuiChartMarkers"always" | "auto" | "hover" | "none"'auto'When the automatic point markers draw. auto, the default, marks every point of a series with 8 or fewer points and only the hovered point of a longer one, as it does for a single area series, whose fill and line already carry the shape. always marks every point, hover only the point under the pointer, none never. A series with its own mark or marks draws whatever this says. Reflected.
moneymoneybooleanfalseFormat axis and tooltip values as money in currency ($120k axis, $121,695 tooltip). Off, the figures print plainly, which is what a count or a rate wants; set it for a balance. Reflected.
plainplainbooleanfalseNo card: transparent, borderless, unpadded plot for embedding (navigator detail, dashboards). Reflected.
refLineproperty onlySuiChartRefLineSuiChartRefLine | undefinedReference line on the start axis: a dashed rule in the deep accent at value with an optional label, for a policy floor or a budget. Property, not attribute.
seriesproperty onlySuiChartSeries[][]Series to plot. One may be accent; dashed marks a comparison such as the prior year; range draws a low and high envelope behind the line; axis: 'end' moves a series onto the second value axis; mark and marks set its point markers. Two or more series get legend keys, buttons that toggle a series; a single series draws no legend and nothing to toggle, since hiding the only line would leave an empty plot. Property, not attribute.
subtitlesubtitlestringstring | undefinedOne-line context under the title.
widthwidthnumber440Width 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 a second value axis takes 34px out of it rather than widening the card.

Events

EventDetailReactVueDescription
suiHoverSuiChartHoverDetailonSuiHover@sui-hoverFires when the pointer or the keyboard moves the hover to another index; { index: -1 } when the pointer leaves the plot, 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 points. Does nothing while there is nothing to plot.

Slots

SlotDescription
header-endControls at the end of the head row, after the title (a segmented control, a period select).

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
SuiChartMark typePoint marker shape. dot is the default hollow circle, solid fills it, square and diamond mark a point measured differently (an estimate, a restated figure, a flagged event), and none hides it. Shapes carry the meaning so a marker never needs a hue of its own.
No members.
SuiChartMarkers typeWhen the automatic point markers draw: auto decides by the data, always marks every point, hover marks the point under the pointer only, none draws no automatic marker.
No members.
SuiChartRefLine interfaceReference line annotation: a dashed rule in the deep accent at value with an optional right-aligned label.
valuenumber
label?string
color?stringThe rule's own color, which its label takes too. Defaults to var(--accent-deep). Any CSS color or var(--token) works; use a token so the color follows the theme. The label is text, so pick a color that reads at 4.5:1 on the card.
SuiChartSeries interfaceOne plotted series. accent claims the single accent highlight; dashed marks a comparison (prior year); range draws a low/high envelope behind data; color overrides the ramp; accentAt (bar charts) names the one category drawn in accent; axis (line charts) puts the series on the end value axis; mark and marks (line charts) set the point markers.
labelstring
datanumber[]
accent?boolean
dashed?boolean
color?string
range?[number[], number[]]
accentAt?string
axis?'start' | 'end'Value axis this series is measured against, in reading order. start, the default, is the first axis; end gives the series its own scale and ticks on a second axis at the inline end, fitted to its own values rather than anchored on zero. Ignored when kind is stacked-area, where every series shares one scale. Any other value reads as start. Under dir="rtl" the start axis stands at the right edge beside the title and the end axis at the left.
mark?SuiChartMarkMarker drawn at every point of this series. Defaults to dot while markers are showing.
marks?(SuiChartMark | null | undefined)[]Markers per point, parallel to data; a gap falls back to mark. A point with a marker of its own always draws, whatever markers says, so a series can mark only what matters.

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