387 lines · 2 files · 9.9 kB
cases/156-shadcn-line-interactive/example.tsx214 lines · entry
cases/156-shadcn-line-interactive/example.tsx
import { useMemo, useState } from 'react'
import { d3Curve, defineChart, lineY, type ChartPoint } from '@tanstack/charts'
import { RendererChart } from '@tanstack/charts/react/tooltip'
import { tooltip } from '@tanstack/charts/tooltip'
import { motion } from '@tanstack/charts/motion'
import { scaleLinear, scalePoint } from 'd3-scale'
import { curveMonotoneX } from 'd3-shape'
import {
shadcnColors,
type ShadcnMonthDatum,
} from '@tanstack/charts-data/shadcn'
import interactiveAreaData from '@tanstack/charts-data/shadcn-area-interactive-data'
import './styles.css'
type InteractiveSeries = 'desktop' | 'mobile'
const twoSeries = ['desktop', 'mobile'] as const
const interactiveAreaRows = interactiveAreaData as readonly {
date: string
desktop: number
mobile: number
}[]
const interactiveBarRows: readonly ShadcnMonthDatum[] = interactiveAreaRows.map(
(row) => ({
month: row.date,
desktop: row.desktop,
mobile: row.mobile,
tablet: 0,
}),
)
export function createExampleChart(
activeSeries: InteractiveSeries = 'desktop',
) {
return defineChart(
({ width }) => ({
marks: [
lineY(interactiveBarRows, {
id: 'daily-line',
x: 'month',
y: (row) => row[activeSeries],
z: () => activeSeries,
key: 'month',
curve: d3Curve(curveMonotoneX),
stroke:
activeSeries === 'desktop' ? shadcnColors[0] : shadcnColors[1],
strokeWidth: 2,
}),
],
scales: {
x: {
scale: scalePoint,
axis: {
line: false,
ticks: {
values: [
'2024-04-10',
'2024-04-21',
'2024-05-02',
'2024-05-13',
'2024-05-25',
'2024-06-05',
'2024-06-16',
'2024-06-29',
],
size: 0,
padding: 10,
format: formatMonthDay,
},
},
},
y: {
scale: scaleLinear().domain([0, 600]),
grid: true,
axis: {
line: false,
ticks: { values: [0, 150, 300, 450, 600], size: 0 },
tickLabels: false,
},
},
},
color: { domain: twoSeries, range: shadcnColors.slice(0, 2) },
margin: {
top: 5,
right: width < 400 ? 24 : 12,
bottom: 35,
left: 12,
},
theme: shadcnTheme(),
}),
{
svgAnimation: false,
focus: 'group-x',
tooltip: {
use: tooltip,
className: 'sc-chart-tooltip',
anchor: 'group-center',
placement: 'auto',
sort: 'color-domain',
content: (points) => shadcnTooltipContent(points),
},
},
)
}
function formatMonthDay(value: string) {
return new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
timeZone: 'UTC',
}).format(new Date(value))
}
function shadcnTheme() {
return {
foreground: 'var(--muted-foreground, var(--muted))',
grid: 'var(--border)',
background: 'transparent',
}
}
function shadcnTooltipContent<TDatum>(points: readonly ChartPoint<TDatum>[]) {
return {
title: String(points[0]?.xValue ?? ''),
rows: points.map((point) => ({
label: titleCase(
String(
point.group ??
point.markId.replace(
/-?(bars|lines|areas|slices|values|radar)$/u,
'',
),
),
),
value: Number(point.yValue ?? point.xValue ?? 0).toLocaleString('en-US'),
color: point.color,
})),
}
}
function titleCase(value: string) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
export const definition = createExampleChart()
const renderer = motion({
initial: 'always',
transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 },
})
export interface ExampleProps {
width?: number
height?: number
}
export default function Example({ width = 640, height = 600 }: ExampleProps) {
const [activeSeries, setActiveSeries] = useState<InteractiveSeries>('desktop')
const chartDefinition = useMemo(
() => createExampleChart(activeSeries),
[activeSeries],
)
const contentWidth = Math.max(1, width - 50)
const chartWidth = contentWidth
const chartHeight = 250
return (
<div className="sc-example" style={{ width, height }}>
<article className="sc-card sc-interactive-line" style={{ width }}>
<header className="sc-card-header">
<div className="sc-card-heading">
<h2>Line Chart - Interactive</h2>
<p>Showing total visitors for the last 3 months</p>
</div>
<div className="sc-card-action">
<BarMetrics active={activeSeries} onChange={setActiveSeries} />
</div>
</header>
<div className="sc-card-content">
<div
className="sc-chart"
style={{ width: chartWidth, height: chartHeight }}
>
<RendererChart
definition={chartDefinition}
renderer={renderer}
initialWidth={chartWidth}
height={chartHeight}
ariaLabel="Line Chart - Interactive"
/>
</div>
</div>
</article>
</div>
)
}
function BarMetrics({
active,
onChange,
}: {
active: InteractiveSeries
onChange: (series: InteractiveSeries) => void
}) {
const totals = {
desktop: interactiveBarRows.reduce((sum, row) => sum + row.desktop, 0),
mobile: interactiveBarRows.reduce((sum, row) => sum + row.mobile, 0),
}
return (
<>
{twoSeries.map((series) => (
<button
key={series}
type="button"
className="sc-bar-metric"
data-active={active === series}
aria-pressed={active === series}
onClick={() => onChange(series)}
>
<span>{titleCase(series)}</span>
<strong>{totals[series].toLocaleString('en-US')}</strong>
</button>
))}
</>
)
}cases/156-shadcn-line-interactive/styles.css173 lines · dependency
cases/156-shadcn-line-interactive/styles.css
.sc-example {
--background: oklch(1 0 0);
--foreground: oklch(0 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--border: oklch(0.922 0 0);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
display: flex;
justify-content: center;
align-items: flex-start;
overflow: hidden;
color: var(--foreground);
background: var(--background);
font-family:
Inter,
ui-sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
sans-serif;
text-rendering: geometricPrecision;
}
:root[data-theme='dark'] .sc-example {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--border: oklch(1 0 0 / 10%);
--chart-1: oklch(0.809 0.105 251.813);
--chart-2: oklch(0.623 0.214 259.815);
--chart-3: oklch(0.546 0.245 262.881);
--chart-4: oklch(0.488 0.243 264.376);
--chart-5: oklch(0.424 0.199 265.638);
}
.sc-example,
.sc-example * {
box-sizing: border-box;
}
.sc-card {
display: flex;
flex-direction: column;
gap: 24px;
border: 1px solid var(--border);
border-radius: 14px;
background: var(--card);
color: var(--card-foreground);
padding: 24px 0;
}
.sc-card-header {
display: grid;
gap: 8px;
padding: 0 24px;
}
.sc-card-heading {
display: grid;
gap: 8px;
}
.sc-card-header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
line-height: 1;
letter-spacing: -0.01em;
}
.sc-card-header p {
margin: 0;
color: var(--muted-foreground);
font-size: 14px;
line-height: 20px;
}
.sc-card-content {
display: flex;
min-height: 0;
flex-direction: column;
padding: 0 24px;
}
.sc-chart {
position: relative;
flex: none;
}
.sc-chart > * {
display: block;
}
.sc-example .ts-chart {
color: var(--muted-foreground);
}
.sc-example .ts-chart__grid line,
.sc-example .ts-chart__polar-grid path,
.sc-example .ts-chart__polar-grid line {
stroke: var(--border);
}
.sc-example .ts-chart__axes text,
.sc-example .ts-chart__polar-grid text {
fill: var(--muted-foreground);
font-size: 12px;
}
.sc-example .ts-chart-tooltip {
min-width: 128px;
padding: 7px 10px !important;
border: 1px solid var(--border) !important;
border-radius: 8px !important;
background: var(--card) !important;
color: var(--card-foreground) !important;
box-shadow: 0 2px 6px rgb(0 0 0 / 8%);
font-size: 12px;
}
.sc-interactive-line {
gap: 0;
padding: 0;
}
.sc-interactive-line .sc-card-header {
display: flex;
min-height: 99px;
align-items: stretch;
gap: 0;
padding: 0;
border-bottom: 1px solid var(--border);
}
.sc-interactive-line .sc-card-heading {
flex: 1;
align-content: center;
gap: 4px;
padding: 16px 24px;
}
.sc-interactive-line .sc-card-action {
display: flex;
margin: 0;
}
.sc-interactive-line .sc-card-content {
padding: 43px 24px 0;
}
.sc-card-action {
margin-left: auto;
}
.sc-bar-metric {
display: grid;
width: 168px;
align-content: center;
gap: 6px;
padding: 16px 32px;
border: 0;
border-left: 1px solid var(--border);
background: transparent;
color: inherit;
font: inherit;
text-align: left;
cursor: pointer;
}
.sc-bar-metric[data-active='true'] {
background: var(--muted);
}
.sc-bar-metric:focus-visible {
outline: 2px solid var(--foreground);
outline-offset: -3px;
}
.sc-bar-metric span {
color: var(--muted-foreground);
font-size: 12px;
}
.sc-bar-metric strong {
font-size: 30px;
line-height: 1;
letter-spacing: -0.03em;
}