721 lines · 2 files · 17.2 kB
cases/196-shadcn-tooltip-label-formatter/example.tsx260 lines · entry
cases/196-shadcn-tooltip-label-formatter/example.tsx
import { useMemo, useRef } from 'react'
import {
barY,
defineChart,
stack,
type ChartPoint,
type ChartValue,
type DomChartDefinition,
} from '@tanstack/charts'
import { RendererChart } from '@tanstack/charts/react/tooltip'
import { tooltip } from '@tanstack/charts/tooltip'
import { motion } from '@tanstack/charts/motion'
import { scaleBand, scaleLinear } from 'd3-scale'
import {
shadcnActivities,
shadcnColors,
type ShadcnActivityDatum,
} from '@charts-poc/demo-data/shadcn'
import './styles.css'
const activityNames = ['running', 'swimming'] as const
function createDefinition() {
return defineChart({
marks: [
barY(shadcnActivities, {
id: 'activity-bars',
x: 'date',
y: 'value',
z: 'activity',
color: 'activity',
key: (row) => `${row.date}:${row.activity}`,
layout: stack({ order: activityNames }),
radius: 4,
}),
],
x: {
scale: () => scaleBand<string>().paddingInner(0.2).paddingOuter(0.1),
axis: {
line: false,
ticks: { size: 0, padding: 10, format: formatWeekday },
},
},
y: { scale: scaleLinear().domain([0, 1000]), axis: false },
color: { domain: activityNames, range: shadcnColors.slice(0, 2) },
margin: { top: 0, right: 7, bottom: 32, left: 7 },
theme: shadcnTheme(),
})
}
function formatWeekday(value: string) {
return new Intl.DateTimeFormat('en-US', { weekday: 'short' }).format(
new Date(value),
)
}
function shadcnTheme() {
return {
foreground: 'var(--muted-foreground, var(--muted))',
grid: 'var(--border)',
background: 'transparent',
}
}
function titleCase(value: string) {
return value.charAt(0).toUpperCase() + value.slice(1)
}
function ShadcnTooltipBody({
variant,
points,
}: {
variant: string
points: readonly ChartPoint<ShadcnActivityDatum>[]
}) {
const ordered = [...points].sort(
(left, right) =>
activityNames.indexOf(left.datum.activity) -
activityNames.indexOf(right.datum.activity),
)
const noLabel =
variant === 'label-none' ||
variant === 'formatter' ||
variant === 'icons' ||
variant === 'advanced'
const noIndicator =
variant === 'indicator-none' ||
variant === 'label-none' ||
variant === 'formatter' ||
variant === 'icons'
const lineIndicator = variant === 'indicator-line'
const formatted = variant === 'formatter' || variant === 'advanced'
const label =
variant === 'label-formatter'
? 'July 15, 2024'
: variant === 'label-custom'
? 'Activities'
: '2024-07-16'
return (
<div
className={`sc-shadcn-tooltip${variant === 'advanced' ? ' sc-advanced-tooltip' : ''}${noIndicator ? ' sc-tooltip-no-indicator' : ''}`}
>
{noLabel ? null : <strong className="sc-tooltip-label">{label}</strong>}
{ordered.map((point, index) => (
<div className="sc-shadcn-tooltip-row" key={point.datum.activity}>
{variant === 'icons' ? (
<ShadcnActivityIcon activity={point.datum.activity} />
) : noIndicator ? null : (
<span
className={lineIndicator ? 'sc-tooltip-line' : 'sc-tooltip-dot'}
style={{ background: shadcnColors[index] }}
/>
)}
<span>{titleCase(point.datum.activity)}</span>
<b className="sc-tooltip-value">
{point.datum.value}
{formatted ? <span>kcal</span> : null}
</b>
</div>
))}
{variant === 'advanced' ? (
<div className="sc-tooltip-total">
<span>Total</span>
<b className="sc-tooltip-value">
{ordered.reduce((total, point) => total + point.datum.value, 0)}
<span>kcal</span>
</b>
</div>
) : null}
</div>
)
}
function ShadcnActivityIcon({ activity }: { activity: string }) {
return (
<svg className="sc-tooltip-icon" viewBox="0 0 24 24" aria-hidden>
{activity === 'running' ? (
<>
<path d="M4 17c3-1 4-4 4-7l3 2 2-4 3 1" />
<path d="m9 13 4 5M14 5h.01" />
</>
) : (
<>
<path d="M2 16c2-2 4 2 6 0s4 2 6 0 4 2 8 0" />
<path d="M2 20c2-2 4 2 6 0s4 2 6 0 4 2 8 0M5 12l3-3 4 3 3-4 4 4" />
</>
)}
</svg>
)
}
export function createExampleChart() {
return defineChart(createDefinition(), {
svgAnimation: false,
focus: 'group-x',
keyboard: true,
tooltip: {
use: tooltip,
className: 'sc-chart-tooltip',
anchor: (_points, context) => ({
x: context.surface.width * 0.271,
y: context.surface.height * 0.554,
}),
placement: 'bottom-right',
offset: 0,
sort: 'color-domain',
content: () => ({ rows: [] }),
},
})
}
export const definition = createExampleChart()
type ExampleDefinition = ReturnType<typeof createExampleChart>
type ExampleDatum =
ExampleDefinition extends DomChartDefinition<
infer TDatum,
infer _TXValue,
infer _TYValue
>
? TDatum
: never
type ExampleXValue =
ExampleDefinition extends DomChartDefinition<
infer _TDatum,
infer TXValue extends ChartValue,
infer _TYValue
>
? TXValue
: never
type ExampleYValue =
ExampleDefinition extends DomChartDefinition<
infer _TDatum,
infer _TXValue,
infer TYValue extends ChartValue
>
? TYValue
: never
export interface ExampleProps {
width?: number
height?: number
}
export default function Example({ width = 640, height = 600 }: ExampleProps) {
const seededTooltip = useRef(false)
const chartDefinition = definition
const renderer = useMemo(
() =>
motion<ExampleDatum, ExampleXValue, ExampleYValue>({
initial: 'always',
transition: { type: 'spring', stiffness: 170, damping: 18, mass: 1 },
}),
[],
)
const contentWidth = Math.max(1, width - 50)
const chartWidth = contentWidth
const chartHeight = (contentWidth * 9) / 16
const headerAction = null
const legend = null
const footer = null
return (
<div className="sc-example" style={{ width, height }}>
<article className="sc-card sc-default" style={{ width }}>
<header className="sc-card-header">
<div className="sc-card-heading">
<h2>Tooltip - Label Formatter</h2>
<p>Tooltip with label formatter.</p>
</div>
{headerAction ? (
<div className="sc-card-action">{headerAction}</div>
) : null}
</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="Tooltip - Label Formatter"
onRender={({ scene, interaction }) => {
if (seededTooltip.current) return
const point = scene.points.find(
(candidate) =>
(candidate.datum as ShadcnActivityDatum).date ===
'2024-07-16',
)
if (!point) return
seededTooltip.current = true
interaction.setControlledFocus(point, {
source: 'programmatic',
})
}}
renderTooltipBody={({ points }) => (
<ShadcnTooltipBody
points={points as readonly ChartPoint<ShadcnActivityDatum>[]}
variant="label-formatter"
/>
)}
/>
</div>
{legend ? <div className="sc-chart-footer">{legend}</div> : null}
</div>
{footer ? <footer className="sc-card-footer">{footer}</footer> : null}
</article>
</div>
)
}cases/196-shadcn-tooltip-label-formatter/styles.css461 lines · dependency
cases/196-shadcn-tooltip-label-formatter/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-chart-clip .ts-chart {
overflow: hidden !important;
}
.sc-card-footer {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
padding: 0 24px;
font-size: 14px;
line-height: 1;
}
.sc-chart-footer {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
margin-top: 12px;
color: var(--muted-foreground);
font-size: 12px;
}
.sc-legend-item {
display: inline-flex;
align-items: center;
gap: 6px;
}
.sc-legend-dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
.sc-legend-icon {
width: 12px;
height: 12px;
fill: none;
stroke: var(--muted-foreground);
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.sc-pie-legend {
display: flex;
width: 260px;
flex-wrap: wrap;
justify-content: center;
gap: 10px 16px;
}
.sc-pie-legend .sc-legend-item {
flex: 0 0 70px;
justify-content: center;
}
.sc-centered {
justify-content: center;
align-items: center;
text-align: center;
}
.sc-trend {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.sc-trend svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.sc-footer-note {
color: var(--muted-foreground);
}
.sc-example .ts-chart {
color: var(--muted-foreground);
}
.sc-example .recharts-surface {
overflow: visible;
}
.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-advanced-tooltip {
display: grid;
width: 158px;
gap: 6px;
}
.sc-advanced-row {
display: grid;
grid-template-columns: 10px 1fr auto;
align-items: center;
gap: 6px;
}
.sc-advanced-swatch {
width: 10px;
height: 10px;
border-radius: 2px;
}
.sc-advanced-value {
display: flex;
align-items: baseline;
gap: 2px;
color: var(--foreground);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.sc-advanced-unit {
color: var(--muted-foreground);
font-family: inherit;
font-weight: 400;
}
.sc-advanced-total {
display: flex;
align-items: center;
margin-top: 2px;
padding-top: 7px;
border-top: 1px solid var(--border);
color: var(--foreground);
font-weight: 500;
}
.sc-advanced-total .sc-advanced-value {
margin-left: auto;
}
.sc-shadcn-tooltip {
display: grid;
gap: 5px;
}
.sc-tooltip-label {
color: var(--foreground);
font-weight: 500;
}
.sc-shadcn-tooltip-row {
display: grid;
grid-template-columns: 9px minmax(0, 1fr) auto;
align-items: center;
gap: 7px;
color: var(--muted-foreground);
}
.sc-tooltip-no-indicator .sc-shadcn-tooltip-row {
grid-template-columns: minmax(0, 1fr) auto;
}
.sc-tooltip-dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
.sc-tooltip-line {
width: 3px;
height: 16px;
border-radius: 2px;
}
.sc-tooltip-icon {
width: 11px;
height: 11px;
fill: none;
stroke: var(--muted-foreground);
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}
.sc-tooltip-value {
display: flex;
align-items: baseline;
gap: 2px;
color: var(--foreground);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-weight: 500;
font-variant-numeric: tabular-nums;
}
.sc-tooltip-value span {
color: var(--muted-foreground);
font-family: inherit;
font-weight: 400;
}
.sc-tooltip-total {
display: flex;
justify-content: space-between;
margin-top: 1px;
padding-top: 7px;
border-top: 1px solid var(--border);
color: var(--foreground);
font-weight: 500;
}
.sc-interactive-area {
gap: 0;
padding-top: 0;
}
.sc-interactive-area .sc-card-header {
display: flex;
align-items: center;
gap: 16px;
padding-top: 20px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border);
}
.sc-interactive-area .sc-card-heading {
flex: 1;
gap: 4px;
}
.sc-interactive-area .sc-card-content {
padding-top: 24px;
}
.sc-interactive-area .sc-chart-footer {
margin-top: 8px;
}
.sc-interactive-bar {
gap: 0;
padding: 0 0 30px;
}
.sc-interactive-bar .sc-card-header {
display: flex;
min-height: 99px;
align-items: stretch;
gap: 0;
padding: 0;
border-bottom: 1px solid var(--border);
}
.sc-interactive-bar .sc-card-heading {
flex: 1;
align-content: center;
gap: 4px;
padding: 16px 24px;
}
.sc-interactive-bar .sc-card-action {
display: flex;
margin: 0;
}
.sc-interactive-bar .sc-card-content {
padding: 43px 24px 0;
}
.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-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;
}
.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;
}