Skip to content
ShineUI
Install

Synced charts

One hover across many charts.

<sui-chart-synced>ChartsStable

About

Shared hover across stacked panels; it draws nothing itself. Put two or more charts whose indexes mean the same thing in its slot: when one reports suiHover, the container sets hoverIndex on all of them so every panel shows the same row, and emits one suiHover of its own. Set hoverIndex from outside to drive every panel.

Usage

<sui-chart-synced> <sui-chart-line id="sy-line" heading="Closing cash" subtitle="Panel 1 of 2 · hover to sync" height="160" money></sui-chart-line> <sui-chart-bar id="sy-bar" heading="Revenue vs. spend" subtitle="Panel 2 of 2" height="160" money></sui-chart-bar> </sui-chart-synced> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-chart-synced"); el.addEventListener("suiHover", (e) => console.log(e.detail)); </script>
import { SuiChartSynced } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return ( <SuiChartSynced onSuiHover={(e) => console.log(e.detail)}> <sui-chart-line id="sy-line" heading="Closing cash" subtitle="Panel 1 of 2 · hover to sync" height="160" money></sui-chart-line> <sui-chart-bar id="sy-bar" heading="Revenue vs. spend" subtitle="Panel 2 of 2" height="160" money></sui-chart-bar> </SuiChartSynced> ); }
<script setup> import { SuiChartSynced } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiChartSynced @sui-hover="onChange"><sui-chart-line id="sy-line" heading="Closing cash" subtitle="Panel 1 of 2 · hover to sync" height="160" money></sui-chart-line> <sui-chart-bar id="sy-bar" heading="Revenue vs. spend" subtitle="Panel 2 of 2" height="160" money></sui-chart-bar></SuiChartSynced> </template>
import { Component } from "@angular/core"; import { SuiChartSynced } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiChartSynced], template: `<sui-chart-synced (suiHover)="onChange($event)"><sui-chart-line id="sy-line" heading="Closing cash" subtitle="Panel 1 of 2 · hover to sync" height="160" money></sui-chart-line> <sui-chart-bar id="sy-bar" heading="Revenue vs. spend" subtitle="Panel 2 of 2" height="160" money></sui-chart-bar></sui-chart-synced>`, }) export class Example { onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

Hover either panel and the other follows: the container stops a panel's own suiHover at the panel and sets hoverIndex on every sibling, so both cards read the same month. It lays its panels out in a column, 14px apart, and each panel stays its own tab stop, so its arrow keys move the hover in both.

<sui-chart-synced> <sui-chart-line id="sy-line" heading="Closing cash" subtitle="Panel 1 of 2 · hover to sync" height="160" money></sui-chart-line> <sui-chart-bar id="sy-bar" heading="Revenue vs. spend" subtitle="Panel 2 of 2" height="160" money></sui-chart-bar> </sui-chart-synced>
// setup(root): properties and events setup(root) { cashLine(root.querySelector('#sy-line')); revenueBar(root.querySelector('#sy-bar')); }

Events

The container emits one suiHover per change of the shared index, reporting { index } with index -1 on leave, not once per panel. Writing hoverIndex pushes a period to every panel and emits nothing, so the buttons move the readout while the log keeps what the pointer last reported.

Idle.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-chart-synced id="sy-events"> <sui-chart-line id="sy-events-line" heading="Closing cash" height="150" money></sui-chart-line> <sui-chart-bar id="sy-events-bar" heading="Revenue vs. spend" height="150" money></sui-chart-bar> </sui-chart-synced> <div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center"> <sui-button id="sy-mar" size="sm" variant="outline" label="Show March"></sui-button> <sui-button id="sy-jul" size="sm" variant="outline" label="Show July"></sui-button> <sui-button id="sy-off" size="sm" variant="ghost" label="Clear"></sui-button> <span id="sy-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 sync = root.querySelector('#sy-events'); const log = root.querySelector('#sy-log'); cashLine(root.querySelector('#sy-events-line')); revenueBar(root.querySelector('#sy-events-bar')); sync.addEventListener('suiHover', (e) => { const i = e.detail.index; log.textContent = i < 0 ? 'Idle.' : `Shared index ${i}: ${M[i]}.`; }); root.querySelector('#sy-mar').addEventListener('click', () => { sync.hoverIndex = 2; }); root.querySelector('#sy-jul').addEventListener('click', () => { sync.hoverIndex = 6; }); root.querySelector('#sy-off').addEventListener('click', () => { sync.hoverIndex = -1; }); }

Mixed panels

Every chart with hoverIndex joins, and the index means what each chart says: here a bar row and a scatter point are the same customer, so hovering either lights the other. Group panels whose rows line up; the container cannot check that for you.

Idle.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-chart-synced id="sy-rows"> <sui-chart-bar id="sy-rows-bar" heading="Open balance" subtitle="One row per customer" orientation="horizontal" money></sui-chart-bar> <sui-chart-scatter id="sy-rows-scatter" heading="Days to pay" subtitle="One point per customer, in the same order" x-label="Days to pay" y-label="Open balance" height="170"></sui-chart-scatter> </sui-chart-synced> <span id="sy-rows-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 log = root.querySelector('#sy-rows-log'); const bar = root.querySelector('#sy-rows-bar'); bar.labels = CUSTOMERS.map((c) => c.name); bar.series = [{ label: 'Open', data: CUSTOMERS.map((c) => c.open) }]; const sc = root.querySelector('#sy-rows-scatter'); sc.formatX = (v) => v + 'd'; sc.formatY = (v) => '$' + Math.round(v / 1000) + 'k'; sc.series = [{ label: 'Customers', points: CUSTOMERS.map((c) => ({ x: c.days, y: c.open, r: 7, name: c.name })) }]; root.querySelector('#sy-rows').addEventListener('suiHover', (e) => { const i = e.detail.index; log.textContent = i < 0 ? 'Idle.' : `Row ${i}: ${CUSTOMERS[i].name}, $${CUSTOMERS[i].open.toLocaleString('en-US')} open, ${CUSTOMERS[i].days} days to pay.`; }); }

A pie in the group

A pie counts its items, so it pairs with a bar of the same categories: hover a slice and the row lights, hover the row and the slice lifts. Toggle a slice off in the legend and its index draws nothing, because that slice is no longer on the plot.

Idle.
<div style="display:flex;flex-direction:column;gap:12px"> <sui-chart-synced id="sy-mixed"> <sui-chart-bar id="sy-mixed-bar" heading="Operating expense mix" subtitle="One row per category" orientation="horizontal" height="150" money></sui-chart-bar> <sui-chart-pie id="sy-mixed-pie" heading="The same mix as shares" subtitle="One slice per category, in the same order" diameter="150" money></sui-chart-pie> </sui-chart-synced> <span id="sy-mixed-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 log = root.querySelector('#sy-mixed-log'); const bar = root.querySelector('#sy-mixed-bar'); bar.labels = MIX.map((m) => m.label); bar.series = [{ label: 'Spend', data: MIX.map((m) => m.value) }]; root.querySelector('#sy-mixed-pie').items = MIX; root.querySelector('#sy-mixed').addEventListener('suiHover', (e) => { const i = e.detail.index; log.textContent = i < 0 ? 'Idle.' : `Item ${i}: ${MIX[i].label}, $${MIX[i].value.toLocaleString('en-US')}.`; }); }

API

<sui-chart-synced>

Properties

PropertyAttributeTypeDefaultDescription
hoverIndexhover-indexnumber-1The shared hovered index; -1 when idle. Setting it pushes the value to every panel and emits nothing, so a parent can drive the group without hearing its own write back. With the panels' own suiHover events as the setter, this replaces the reference (hoverIdx, setHoverIdx) render prop.

Events

EventDetailReactVueDescription
suiHoverSuiChartHoverDetailonSuiHover@sui-hoverFires once per change of the shared index; detail is { index }. A panel's own suiHover stops at the panel, so a listener on the container, or above it, hears this event alone. A panel inside a nested sui-chart-synced belongs to that group, not to this one.

Slots

SlotDescription
(default)The chart panels to keep in sync. Every hoverable chart joins: line, bar, candlestick, combo and waterfall share an x axis; a scatter counts points, a radar axes, a heatmap cells, and a treemap and a pie their items, so group them with panels whose rows line up with that.

Types

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

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.

Typography1

Ag--font-sans

Every name above is listed with its value and its role in Foundations.

See also