450 lines · 2 files · 11.7 kB
cases/164-shadcn-pie-interactive/example.tsx265 lines · entry
cases/164-shadcn-pie-interactive/example.tsx
import { useMemo, useState } from 'react'
import { defineChart, type ChartPoint } from '@tanstack/charts'
import {
focusGroupAngle,
pie,
polar,
radialArc,
radialText,
} from '@tanstack/charts/polar'
import { RendererChart } from '@tanstack/charts/react/tooltip'
import { tooltip } from '@tanstack/charts/tooltip'
import { motion } from '@tanstack/charts/motion'
import { scaleLinear } from 'd3-scale'
import {
shadcnColors,
type ShadcnBrowserDatum,
} from '@tanstack/charts-data/shadcn'
import './styles.css'
const interactivePieRows: readonly ShadcnBrowserDatum[] = [
{ browser: 'january', visitors: 186 },
{ browser: 'february', visitors: 305 },
{ browser: 'march', visitors: 237 },
{ browser: 'april', visitors: 173 },
{ browser: 'may', visitors: 209 },
]
export function createExampleChart(activeMonth = 'january') {
const arcs = pie(interactivePieRows, {
value: 'visitors',
startAngle: Math.PI / 2,
endAngle: (-Math.PI * 3) / 2,
})
const activeIndex = Math.max(
0,
interactivePieRows.findIndex((row) => row.browser === activeMonth),
)
const activeRow = interactivePieRows[activeIndex]!
const active = arcs.filter((row) => row.browser === activeRow.browser)
const activeColor = shadcnColors[activeIndex]!
return defineChart(
{
marks: [
polar({
radiusRatio: 0.78,
scales: {
angle: { scale: scaleLinear().domain([0, Math.PI * 2]) },
radius: { scale: scaleLinear().domain([0, 1]) },
},
marks: [
radialArc(arcs, {
id: 'month-slices',
key: 'browser',
innerRadius: 60,
color: 'browser',
stroke: 'var(--background)',
strokeWidth: 1,
}),
radialArc(active, {
id: 'active-month',
key: 'browser',
innerRadius: 60,
outerRadius: ({ radius }) => radius + 10,
fill: activeColor,
stroke: 'var(--background)',
strokeWidth: 5,
}),
radialArc(active, {
id: 'active-month-ring',
key: 'browser',
innerRadius: ({ radius }) => radius + 12,
outerRadius: ({ radius }) => radius + 25,
fill: activeColor,
stroke: 'var(--background)',
strokeWidth: 3,
}),
radialText(
[
{
id: 'total',
angle: 0,
radius: 0,
text: String(activeRow.visitors),
},
],
{
id: 'active-total',
angle: 'angle',
radius: 'radius',
key: 'id',
text: 'text',
dy: -5,
fill: 'var(--foreground)',
fontSize: 30,
fontWeight: 700,
},
),
radialText(
[{ id: 'label', angle: 0, radius: 0, text: 'Visitors' }],
{
id: 'active-label',
angle: 'angle',
radius: 'radius',
key: 'id',
text: 'text',
dy: 20,
fill: 'var(--muted-foreground)',
fontSize: 12,
},
),
],
}),
],
scales: {
x: null,
y: null,
},
color: {
domain: interactivePieRows.map((row) => row.browser),
range: shadcnColors,
},
margin: 0,
},
{
svgAnimation: false,
focus: focusGroupAngle,
tooltip: {
use: tooltip,
className: 'sc-chart-tooltip',
anchor: 'group-center',
placement: 'auto',
sort: 'color-domain',
content: (points) => shadcnTooltipContent(points),
},
},
)
}
function shadcnTooltipContent<TDatum>(points: readonly ChartPoint<TDatum>[]) {
const point = points.find((candidate) => browserMetric(candidate.datum))
const metric = point && browserMetric(point.datum)
return metric
? {
title: titleCase(metric.browser),
rows: [
{
label: 'Visitors',
value: metric.visitors.toLocaleString('en-US'),
color: point.color,
},
],
}
: { rows: [] }
}
function browserMetric(datum: unknown) {
if (!datum || typeof datum !== 'object') return undefined
const browser = Reflect.get(datum, 'browser')
const visitors = Reflect.get(datum, 'visitors')
return typeof browser === 'string' &&
typeof visitors === 'number' &&
Number.isFinite(visitors)
? { browser, visitors }
: undefined
}
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 [activeMonth, setActiveMonth] = useState('january')
const chartDefinition = useMemo(
() => createExampleChart(activeMonth),
[activeMonth],
)
const contentWidth = Math.max(1, width - 50)
const chartWidth = Math.min(300, contentWidth)
const chartHeight = chartWidth
return (
<div className="sc-example" style={{ width, height }}>
<article className="sc-card sc-interactive-pie" style={{ width }}>
<header className="sc-card-header">
<div className="sc-card-heading">
<h2>Pie Chart - Interactive</h2>
<p>January - June 2024</p>
</div>
<div className="sc-card-action">
<SelectControl
value={activeMonth}
onChange={setActiveMonth}
options={interactivePieRows.map((row, index) => ({
value: row.browser,
label: titleCase(row.browser),
swatch: shadcnColors[index],
}))}
/>
</div>
</header>
<div className="sc-card-content sc-centered">
<div
className="sc-chart"
style={{ width: chartWidth, height: chartHeight }}
>
<RendererChart
definition={chartDefinition}
renderer={renderer}
initialWidth={chartWidth}
height={chartHeight}
ariaLabel="Pie Chart - Interactive"
/>
</div>
</div>
</article>
</div>
)
}
function SelectControl({
value,
options,
onChange,
}: {
value: string
options: readonly {
value: string
label: string
swatch?: string
}[]
onChange: (value: string) => void
}) {
const selected = options.find((option) => option.value === value)
return (
<label className="sc-select-display">
{selected?.swatch ? (
<span
className="sc-select-swatch"
style={{ background: selected.swatch }}
/>
) : null}
<select
aria-label="Select a value"
value={value}
onChange={(event) => onChange(event.currentTarget.value)}
>
{options.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path
d="m6 9 6 6 6-6"
fill="none"
stroke="currentColor"
strokeWidth="2"
/>
</svg>
</label>
)
}cases/164-shadcn-pie-interactive/styles.css185 lines · dependency
cases/164-shadcn-pie-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-centered {
justify-content: center;
align-items: center;
text-align: center;
}
.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-pie .sc-card-header {
display: flex;
align-items: flex-start;
gap: 16px;
padding-bottom: 0;
}
.sc-interactive-pie .sc-card-heading {
flex: 1;
gap: 4px;
}
.sc-interactive-pie .sc-card-content {
padding-bottom: 44px;
}
.sc-interactive-pie .sc-chart {
transform: translateY(55px);
}
.sc-interactive-pie .sc-card-action {
transform: translateY(48px);
}
.sc-card-action {
margin-left: auto;
}
.sc-select-display {
position: relative;
display: flex;
min-width: 130px;
height: 36px;
align-items: center;
gap: 8px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--background);
color: var(--foreground);
font-size: 14px;
box-shadow: 0 1px 2px rgb(0 0 0 / 4%);
}
.sc-select-display:focus-within {
outline: 2px solid var(--foreground);
outline-offset: 2px;
}
.sc-select-display select {
min-width: 0;
flex: 1;
appearance: none;
border: 0;
outline: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
.sc-select-display svg {
width: 14px;
height: 14px;
flex: none;
color: var(--muted-foreground);
pointer-events: none;
}
.sc-select-swatch {
width: 12px;
height: 12px;
flex: none;
border-radius: 3px;
}