Charts Docs
Menu
Chart Guides
- Overview
- shadcn/ui Charts
- Lines and Areas
- Bars and Rankings
- Scatterplots and Relationships
- Distributions
- Heatmaps and Densities
- Intervals and Financial Charts
- Stacked and Composed Charts
- Facets and Multiple Views
- Networks and Hierarchies
- Maps and Spatial Charts
- Polar and Radar Charts
- Annotations and Overlays
- Interactive Charts
- Themes and Motion
Individual Charts
- Apple stock line with seasonal gaps
- Industry unemployment with end labels
- San Francisco temperature range band
- Industry unemployment stacked area
- Sorted vertical bars
- Horizontal ranking with long labels
- Grouped bars
- Stacked bars
- Bubble scatterplot
- Histogram
- Labeled ordinal heatmap
- Anscombe quartet small multiples
- Open-to-close price intervals
- Point estimates with error bars
- Grouped boxplot
- Ranked lollipop chart
- Dumbbell comparison
- Cumulative histogram
- San Francisco temperature moving averages
- Industry share of unemployment
- Streamgraph
- Bollinger band
- Quantitative binned heatmap
- Precipitation calendar heatmap
- Diverging Likert responses
- Parallel coordinates
- Candlestick chart
- Annual gasoline-price waterfall
- Two-period slopegraph
- Scatterplot with linear regression
- Metro inequality change arrows
- Apple close versus moving average
- Pointer-selected tooltip
- Grouped X tooltip
- Tidy hierarchy tree
- Delaunay spatial network
- Filled wind-speed contours
- Point density contours
- Westport House floor plan
- Les Misérables character network
- English letter frequency waffle
- Two-dimensional vector field
- Hexagonally binned density
- Framed guide-free scatterplot
- Empirical cumulative distribution
- Faceted distributions
- Beeswarm distribution
- Log-scale Flare class sizes
- Industry unemployment bump chart
- Indexed industry unemployment
- Directed connected scatterplot
- Scatterplot with marginal histograms
- Annotated AAPL extrema
- Mean penguin mass reducer bars
- AAPL lag-one autocorrelation
- Industry unemployment percentile ribbon
- Ridgeline density comparison
- Violin distribution comparison
- Marimekko survey composition
- Voronoi nearest-point interaction
- Seattle weather with three y axes
- Palmer penguins by species and sex
- Stacked and adjacent Seattle weather bars
- Automobile specifications scatter
- Flare analytics treemap
- Simple radar chart
- English letter frequency pie
- English letter frequency donut
- Survey agreement share gauge
- Snapped axis pointer with grouped tooltip
- Interactive series legend
- Linked chart and data table selection
- Focus and context time window
- Expanding pinned energy tooltip
- Scrollable resource timeline lanes
- Streaming window preservation
- Synchronized cursors across views
- Free cursor over car measurements
- Observed monthly AAPL brush selection
- Wheel zoom and pan over AAPL closes
- AAPL close playback scrubber
- Editable event range
- Letter frequency pie with labels
- Letter frequency donut with total
- Rounded letter frequency donut
- Nested Flare package sizes
- English letter frequency rose
- County unemployment gauge
- Comparative radar chart
- Concentric letter frequency bars
- Flare analytics sunburst
- World learning-poverty choropleth
- World population bubble map
- Orthographic globe with graticule
- HMS Beagle voyage
- Seattle daily high-temperature polar line
- Surface wind polar scatter
- World population-density choropleth
- United States county unemployment choropleth
- Standard world projection gallery
- Basic Sankey
- Sankey
- Staggered entrance motion
- Interrupted spring updates
- Spring line updates
- Definition-owned motion
- Cross-chart geometry morph
- Focus and crosshair motion
- Token use calendar heatmap
- Stacked bars with band and rule cursors
- Themed interactive area card
- Active bar dashboard
- Premium KPI sparklines
- Active donut metric
- Theme palette matrix
- Sales conversion funnel
- Drillable Flare sunburst
- shadcn dashboard
- shadcn Bar Chart - Multiple
- shadcn Pie Chart - Donut with Text
- shadcn Radar Chart - Multiple
- shadcn Radial Chart - Text
- shadcn Tooltip - Advanced
- shadcn Area Chart - Axes
- shadcn Area Chart
- shadcn Area Chart - Gradient
- shadcn Area Chart - Icons
- shadcn Area Chart - Interactive
- shadcn Area Chart - Legend
- shadcn Area Chart - Linear
- shadcn Area Chart - Stacked Expanded
- shadcn Area Chart - Stacked
- shadcn Area Chart - Step
- shadcn Bar Chart - Active
- shadcn Bar Chart
- shadcn Bar Chart - Horizontal
- shadcn Bar Chart - Interactive
- shadcn Bar Chart - Custom Label
- shadcn Bar Chart - Label
- shadcn Bar Chart - Mixed
- shadcn Bar Chart - Negative
- shadcn Bar Chart - Stacked + Legend
- shadcn Line Chart
- shadcn Line Chart - Dots Colors
- shadcn Line Chart - Custom Dots
- shadcn Line Chart - Dots
- shadcn Line Chart - Interactive
- shadcn Line Chart - Custom Label
- shadcn Line Chart - Label
- shadcn Line Chart - Linear
- shadcn Line Chart - Multiple
- shadcn Line Chart - Step
- shadcn Pie Chart - Donut Active
- shadcn Pie Chart - Donut
- shadcn Pie Chart - Interactive
- shadcn Pie Chart - Custom Label
- shadcn Pie Chart - Label List
- shadcn Pie Chart - Label
- shadcn Pie Chart - Legend
- shadcn Pie Chart - Separator None
- shadcn Pie Chart
- shadcn Pie Chart - Stacked
- shadcn Radar Chart
- shadcn Radar Chart - Dots
- shadcn Radar Chart - Grid Circle Filled
- shadcn Radar Chart - Grid Circle - No lines
- shadcn Radar Chart - Grid Circle
- shadcn Radar Chart - Grid Custom
- shadcn Radar Chart - Grid Filled
- shadcn Radar Chart - Grid None
- shadcn Radar Chart - Icons
- shadcn Radar Chart - Custom Label
- shadcn Radar Chart - Legend
- shadcn Radar Chart - Lines Only
- shadcn Radar Chart - Radius Axis
- shadcn Radial Chart - Grid
- shadcn Radial Chart - Label
- shadcn Radial Chart - Shape
- shadcn Radial Chart
- shadcn Radial Chart - Stacked
- shadcn Tooltip - Default
- shadcn Tooltip - Formatter
- shadcn Tooltip - Icons
- shadcn Tooltip - Line Indicator
- shadcn Tooltip - No Indicator
- shadcn Tooltip - Custom label
- shadcn Tooltip - Label Formatter
- shadcn Tooltip - No Label
488 lines · 2 files · 12.4 kB
cases/122-premium-kpi-sparklines/example.tsx389 lines · entry
cases/122-premium-kpi-sparklines/example.tsx
import { useMemo, useRef } from 'react'
import { Chart } from '@tanstack/charts/react/core'
import { premiumKpisForRevision } from './model'
import type { CSSProperties } from 'react'
import type { PremiumKpiMetric } from './model'
import { areaY, d3Curve, defineChart, lineY } from '@tanstack/charts'
import { motion } from '@tanstack/charts/motion'
import { scaleLinear } from '@tanstack/charts/scales/linear'
import { scalePoint } from '@tanstack/charts/scales/point'
import { curveMonotoneX } from 'd3-shape'
import type { PremiumKpiId } from './model'
const fullGap = 12
function KpiCard({
metric,
width,
height,
idPrefix,
preview = false,
}: {
readonly metric: PremiumKpiMetric
readonly width: number
readonly height: number
readonly idPrefix?: string
readonly preview?: boolean
}) {
const definition = useMemo(() => premiumKpiDefinition(metric), [metric])
const renderer = useMemo(() => createPremiumKpiRenderer(!preview), [preview])
const compact = preview || width < 200 || height < 150
const padding = compact ? 9 : 18
const headerHeight = compact ? 44 : 72
const chartWidth = Math.max(24, width - padding * 2)
const chartHeight = Math.max(24, height - padding * 2 - headerHeight)
return (
<article
data-premium-kpi={metric.id}
data-conformance-view={metric.id}
data-compact={compact || undefined}
className="premium-kpi-card"
style={
{
'--premium-kpi-card-width': `${width}px`,
'--premium-kpi-card-height': `${height}px`,
} as CSSProperties
}
>
<header className="premium-kpi-header">
<span className="premium-kpi-label">{metric.label}</span>
<span
className="premium-kpi-trend"
aria-label={`${metric.trendDirection === 'up' ? 'Up' : 'Down'} ${metric.trend}`}
>
<span aria-hidden="true">
{metric.trendDirection === 'up' ? '↑' : '↓'}
</span>{' '}
{metric.trend}
</span>
<strong className="premium-kpi-value">{metric.value}</strong>
</header>
<div className="premium-kpi-chart">
<Chart
idPrefix={idPrefix ? `${idPrefix}-${metric.id}` : undefined}
definition={definition}
renderer={renderer}
width={chartWidth}
height={chartHeight}
ariaLabel={`${metric.label} trend, ending at ${metric.value}`}
/>
</div>
</article>
)
}
function fullLayout(width: number, height: number) {
const padding = 14
const availableWidth = Math.max(1, width - padding * 2)
const availableHeight = Math.max(1, height - padding * 2)
const horizontal = width >= 680 || width / height >= 1.2
return horizontal
? {
columns: 'repeat(3, minmax(0, 1fr))',
rows: 'minmax(0, 1fr)',
cardWidth: Math.max(1, (availableWidth - fullGap * 2) / 3),
cardHeight: availableHeight,
}
: {
columns: 'minmax(0, 1fr)',
rows: 'repeat(3, minmax(0, 1fr))',
cardWidth: availableWidth,
cardHeight: Math.max(1, (availableHeight - fullGap * 2) / 3),
}
}
const premiumKpiStyles = `
.premium-kpi-shell {
--premium-kpi-canvas: light-dark(#f5f6f8, #09090b);
--premium-kpi-card: light-dark(rgba(255, 255, 255, 0.92), rgba(24, 24, 27, 0.9));
--premium-kpi-foreground: light-dark(#18181b, #fafafa);
--premium-kpi-muted: light-dark(#71717a, #a1a1aa);
--premium-kpi-border: light-dark(rgba(24, 24, 27, 0.09), rgba(250, 250, 250, 0.1));
--premium-kpi-shadow: light-dark(rgba(24, 24, 27, 0.07), rgba(0, 0, 0, 0.34));
color-scheme: inherit;
box-sizing: border-box;
display: grid;
place-items: stretch;
padding: 14px;
overflow: hidden;
color: var(--premium-kpi-foreground);
background: var(--premium-kpi-canvas);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
.premium-kpi-grid {
display: grid;
min-width: 0;
min-height: 0;
}
.premium-kpi-card {
--premium-kpi-accent: #6d5dfc;
box-sizing: border-box;
display: flex;
flex-direction: column;
width: var(--premium-kpi-card-width);
height: var(--premium-kpi-card-height);
min-width: 0;
min-height: 0;
overflow: hidden;
padding: 18px;
border: 1px solid var(--premium-kpi-border);
border-radius: 18px;
background: var(--premium-kpi-card);
box-shadow: 0 14px 42px var(--premium-kpi-shadow);
}
.premium-kpi-card[data-premium-kpi="customers"] {
--premium-kpi-accent: #0f91c7;
}
.premium-kpi-card[data-premium-kpi="churn"] {
--premium-kpi-accent: #0c9b6c;
}
.premium-kpi-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: baseline;
column-gap: 8px;
flex: 0 0 72px;
}
.premium-kpi-label {
overflow: hidden;
color: var(--premium-kpi-muted);
font-size: 12px;
font-weight: 620;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.premium-kpi-value {
grid-column: 1 / -1;
margin-top: 7px;
font-size: clamp(24px, 3.1vw, 36px);
font-weight: 730;
letter-spacing: -0.055em;
line-height: 1;
}
.premium-kpi-trend {
color: var(--premium-kpi-accent);
font-size: 11px;
font-weight: 700;
line-height: 1;
white-space: nowrap;
}
.premium-kpi-chart {
display: grid;
flex: 1 1 auto;
min-width: 0;
min-height: 0;
align-items: end;
}
.premium-kpi-preview {
display: flex;
gap: 5px;
padding: 0;
background: transparent;
}
.premium-kpi-card[data-compact] {
padding: 9px;
border-radius: 11px;
box-shadow: 0 6px 18px var(--premium-kpi-shadow);
}
.premium-kpi-card[data-compact] .premium-kpi-header {
flex-basis: 44px;
column-gap: 4px;
}
.premium-kpi-card[data-compact] .premium-kpi-label {
font-size: 7px;
}
.premium-kpi-card[data-compact] .premium-kpi-value {
margin-top: 3px;
font-size: 15px;
}
.premium-kpi-card[data-compact] .premium-kpi-trend {
font-size: 7px;
}
.premium-kpi-preview-stack {
display: flex;
flex-direction: column;
gap: 5px;
min-width: 0;
}
`
export interface ExampleProps {
width?: number
height?: number
revision?: number
}
const monotone = d3Curve(curveMonotoneX)
const accentFallbacks = {
revenue: '#6d5dfc',
customers: '#0f91c7',
churn: '#0c9b6c',
} satisfies Record<PremiumKpiId, string>
export const premiumKpiSpring = {
type: 'spring' as const,
stiffness: 180,
damping: 24,
mass: 0.8,
}
export function premiumKpiDefinition(metric: PremiumKpiMetric) {
const values = metric.rows.map((row) => row.value)
const minimum = Math.min(...values)
const maximum = Math.max(...values)
const padding = Math.max((maximum - minimum) * 0.16, 0.1)
const baseline = minimum - padding
const accent = `var(--premium-kpi-accent, ${accentFallbacks[metric.id]})`
const line = lineY(metric.rows, {
id: `${metric.id}-line`,
x: 'period',
y: 'value',
key: 'id',
stroke: accent,
strokeWidth: 2.4,
curve: monotone,
})
const marks =
metric.surface === 'area'
? [
areaY(metric.rows, {
id: `${metric.id}-area`,
x: 'period',
y: 'value',
y1: baseline,
key: 'id',
fill: `url(#${metric.id}-fill)`,
fillOpacity: 1,
curve: monotone,
}),
line,
]
: [line]
return defineChart(
{
guides: false,
marks,
gradients:
metric.surface === 'area'
? [
{
id: `${metric.id}-fill`,
x1: 0,
y1: 0,
x2: 0,
y2: 1,
stops: [
{
offset: 0,
color: accent,
opacity: 0.26,
},
{
offset: 1,
color: accent,
opacity: 0,
},
],
},
]
: [],
scales: {
x: {
scale: scalePoint<number>()
.domain(metric.rows.map((row) => row.period))
.padding(0.08),
axis: false,
},
y: {
scale: scaleLinear().domain([baseline, maximum + padding]),
axis: false,
},
},
margin: { top: 4, right: 3, bottom: 3, left: 3 },
clip: true,
motion: { transition: premiumKpiSpring },
},
{
focus: false,
pointer: false,
keyboard: false,
tooltip: false,
svgAnimation: false,
},
)
}
export function createPremiumKpiRenderer(initial = true) {
return motion({
initial,
respectReducedMotion: true,
transition: premiumKpiSpring,
})
}
export default function PremiumKpiSparklines({
width = 640,
height = 480,
revision = 0,
}: ExampleProps = {}) {
const input = { width, height, revision, preview: false, interactive: true }
const idPrefix = '122-premium-kpi-sparklines'
const rootRef = useRef<HTMLElement>(null)
const metrics = premiumKpisForRevision(input.revision)
const layout = fullLayout(input.width, input.height)
return (
<section
ref={rootRef}
data-conformance-view="main"
data-premium-kpi-shell=""
aria-label="Business metrics"
className="premium-kpi-shell"
style={{ width: input.width, height: input.height }}
>
<style>{premiumKpiStyles}</style>
<div
className="premium-kpi-grid"
style={{
gridTemplateColumns: layout.columns,
gridTemplateRows: layout.rows,
gap: fullGap,
}}
>
{metrics.map((metric) => (
<KpiCard
key={metric.id}
metric={metric}
width={layout.cardWidth}
height={layout.cardHeight}
idPrefix={idPrefix}
/>
))}
</div>
</section>
)
}cases/122-premium-kpi-sparklines/model.ts99 lines · dependency
cases/122-premium-kpi-sparklines/model.ts
export type PremiumKpiId = 'revenue' | 'customers' | 'churn'
export interface PremiumKpiPoint {
readonly id: string
readonly period: number
readonly value: number
}
export interface PremiumKpiMetric {
readonly id: PremiumKpiId
readonly label: string
readonly value: string
readonly trend: string
readonly trendDirection: 'up' | 'down'
readonly surface: 'area' | 'line'
readonly rows: readonly PremiumKpiPoint[]
}
const stages = [
{
revenue: [
264_400, 281_200, 276_800, 302_600, 319_100, 337_500, 331_800, 356_200,
371_600, 365_900, 394_300, 412_840,
],
customers: [
2_810, 2_950, 3_030, 3_170, 3_120, 3_290, 3_410, 3_500, 3_590, 3_670,
3_750, 3_842,
],
churn: [2.8, 2.6, 2.7, 2.5, 2.4, 2.3, 2.35, 2.15, 2.1, 1.95, 1.86, 1.7],
},
{
revenue: [
279_200, 288_900, 286_400, 315_300, 329_800, 351_500, 345_900, 369_600,
388_100, 381_700, 406_900, 429_180,
],
customers: [
2_940, 3_020, 3_140, 3_240, 3_210, 3_380, 3_500, 3_610, 3_690, 3_790,
3_880, 3_976,
],
churn: [
2.7, 2.55, 2.62, 2.42, 2.33, 2.2, 2.24, 2.05, 1.98, 1.84, 1.75, 1.62,
],
},
] as const
export function premiumKpisForRevision(
revision: number,
): readonly PremiumKpiMetric[] {
const stage = stages[Math.abs(revision) % stages.length] ?? stages[0]
return [
{
id: 'revenue',
label: 'Monthly revenue',
value: formatRevenue(last(stage.revenue)),
trend: revision % 2 === 0 ? '+9.2%' : '+10.6%',
trendDirection: 'up',
surface: 'area',
rows: points('revenue', stage.revenue),
},
{
id: 'customers',
label: 'Active customers',
value: last(stage.customers).toLocaleString('en-US'),
trend: revision % 2 === 0 ? '+6.4%' : '+7.1%',
trendDirection: 'up',
surface: 'line',
rows: points('customers', stage.customers),
},
{
id: 'churn',
label: 'Net churn',
value: `${last(stage.churn).toFixed(1)}%`,
trend: revision % 2 === 0 ? '−0.5 pt' : '−0.6 pt',
trendDirection: 'down',
surface: 'area',
rows: points('churn', stage.churn),
},
]
}
function points(
metric: PremiumKpiId,
values: readonly number[],
): readonly PremiumKpiPoint[] {
return values.map((value, period) => ({
id: `${metric}-${period}`,
period,
value,
}))
}
function formatRevenue(value: number) {
return `$${(value / 1_000).toFixed(1)}K`
}
function last(values: readonly number[]) {
return values[values.length - 1] ?? 0
}