TanStack
shadcn/ui Charts

shadcn Bar Chart

bar

646 lines · 2 files · 14.4 kB

cases/144-shadcn-bar-default/example.tsx185 lines · entry
cases/144-shadcn-bar-default/example.tsx
import { useMemo } from 'react'
import {
  barY,
  defineChart,
  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 { shadcnColors, shadcnMonths } from '@charts-poc/demo-data/shadcn'
import './styles.css'
function createDefinition() {
  const rows = shadcnMonths
  const marks = [
    barY(rows, {
      id: 'visitor-bars',
      x: 'month',
      y: 'desktop',
      fill: (row) =>
        false && row.desktop < 0 ? shadcnColors[1] : shadcnColors[0],
      radius: 8,
    }),
  ]
  return defineChart({
    marks,
    x: shadcnXAxis(),
    y: { scale: scaleLinear, grid: true, axis: false },
    margin: { top: 5, right: 5, bottom: 35, left: 5 },
    theme: shadcnTheme(),
  })
}
function shadcnXAxis() {
  return {
    scale: () => scaleBand<string>().paddingInner(0.2).paddingOuter(0.1),
    axis: {
      line: false,
      ticks: {
        size: 0,
        padding: 10,
        format: (value: string) => value.slice(0, 3),
      },
    },
  }
}
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 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,
    })),
  }
}
export function createExampleChart() {
  return defineChart(createDefinition(), {
    svgAnimation: false,
    focus: 'group-x',
    keyboard: true,
    tooltip: {
      use: tooltip,
      className: 'sc-chart-tooltip',
      anchor: 'group-center',
      placement: 'auto',
      offset: undefined,
      sort: 'color-domain',
      content: (points) => shadcnTooltipContent(points),
    },
  })
}
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 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 = (
    <TrendFooter note="Showing total visitors for the last 6 months" />
  )
  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>Bar Chart</h2>
            <p>January - June 2024</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="Bar Chart"
            />
          </div>
          {legend ? <div className="sc-chart-footer">{legend}</div> : null}
        </div>
        {footer ? <footer className="sc-card-footer">{footer}</footer> : null}
      </article>
    </div>
  )
}
function TrendFooter({ note }: { note: string }) {
  return (
    <>
      <div className="sc-trend">
        Trending up by 5.2% this month{' '}
        <svg viewBox="0 0 24 24" aria-hidden="true">
          <path d="m3 17 6-6 4 4 8-8" />
          <path d="M14 7h7v7" />
        </svg>
      </div>
      <div className="sc-footer-note">{note}</div>
    </>
  )
}
cases/144-shadcn-bar-default/styles.css461 lines · dependency
cases/144-shadcn-bar-default/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;
}