TanStack
shadcn/ui Charts

shadcn Tooltip - Label Formatter

tooltip

394 lines · 2 files · 10.8 kB

cases/196-shadcn-tooltip-label-formatter/example.tsx214 lines · entry
cases/196-shadcn-tooltip-label-formatter/example.tsx
import { useRef } from 'react'
import { barY, defineChart, stack, 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 { scaleBand, scaleLinear } from 'd3-scale'
import {
  shadcnActivities,
  shadcnColors,
  type ShadcnActivityDatum,
} from '@tanstack/charts-data/shadcn'
import './styles.css'
const activityNames = ['running', 'swimming'] as const
export function createExampleChart() {
  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(),
    },
    {
      svgAnimation: false,
      focus: 'group-x',
      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: [] }),
      },
    },
  )
}
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 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 seededTooltip = useRef(false)
  const contentWidth = Math.max(1, width - 50)
  const chartWidth = contentWidth
  const chartHeight = (contentWidth * 9) / 16
  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>
        </header>
        <div className="sc-card-content">
          <div
            className="sc-chart"
            style={{ width: chartWidth, height: chartHeight }}
          >
            <RendererChart
              definition={definition}
              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>
        </div>
      </article>
    </div>
  )
}
cases/196-shadcn-tooltip-label-formatter/styles.css180 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-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-advanced-tooltip {
  display: grid;
  width: 158px;
  gap: 6px;
}
.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;
}