Skip to content
All Taste components

Dashboard widgets

Bar chart card

Bookings per service this month next to last month, as grouped bars.

grouped bars · 6 categories · axis · grid · legend

Bookings by service

October vs September

399
15.3%
OctoberSeptember
export interface BarDatum { label: string; current: number; previous: number }

const sample: BarDatum[] = [
  { label: "Haircut", current: 142, previous: 118 },
  { label: "Colour", current: 86, previous: 92 },
  { label: "Spa", current: 64, previous: 51 },
  { label: "Bridal", current: 22, previous: 15 },
  { label: "Nails", current: 48, previous: 40 },
  { label: "Grooming", current: 37, previous: 30 },
];

export function BarChartCard({
  data = sample,
  title = "Bookings by service",
  periods = ["October", "September"],
  max = 160,
}: { data?: BarDatum[]; title?: string; periods?: [string, string]; max?: number }) {
  const total = data.reduce((s, d) => s + d.current, 0);
  const before = data.reduce((s, d) => s + d.previous, 0);
  const change = ((total / before - 1) * 100).toFixed(1);
  const w = data.length * 100;
  const h = (v: number) => (v / max) * 100;

  return (
    <section className="w-full rounded-lg border border-border bg-bg p-4">
      <div className="flex flex-wrap items-start justify-between gap-3">
        <div>
          <h3 className="text-sm font-medium text-fg">{title}</h3>
          <p className="text-[12.5px] text-fg-subtle">{periods[0]} vs {periods[1]}</p>
        </div>
        <div className="text-right">
          <p className="text-xl font-medium tabular-nums">{total}</p>
          <span className="rounded-full bg-success-soft px-1.5 py-0.5 text-xs font-medium tabular-nums text-success-fg">+{change}%</span>
        </div>
      </div>

      <div className="mt-4 grid grid-cols-[auto_minmax(0,1fr)] gap-x-2.5">
        <div className="-my-1.5 flex h-[180px] flex-col justify-between text-right text-[11px] leading-3 tabular-nums text-fg-subtle" aria-hidden>
          {[1, 0.75, 0.5, 0.25, 0].map((f) => <span key={f}>{max * f}</span>)}
        </div>
        <svg viewBox={"0 0 " + w + " 100"} preserveAspectRatio="none" className="h-[180px] w-full overflow-visible" role="img"
          aria-label={data.map((d) => d.label + ": " + d.current).join(", ")}>
          {[0, 25, 50, 75, 100].map((g) => (
            <line key={g} x1="0" x2={w} y1={g} y2={g} className="text-border" stroke="currentColor" vectorEffect="non-scaling-stroke" />
          ))}
          {data.map((d, n) => (
            <g key={d.label}>
              <rect x={n * 100 + 28} width="20" y={100 - h(d.previous)} height={h(d.previous)} className="fill-border-strong" />
              <rect x={n * 100 + 52} width="20" y={100 - h(d.current)} height={h(d.current)} className="fill-primary" />
            </g>
          ))}
        </svg>
        <div className="col-start-2 mt-2 flex text-[11.5px] text-fg-subtle" aria-hidden>
          {data.map((d) => <span key={d.label} className="min-w-0 flex-1 truncate text-center">{d.label}</span>)}
        </div>
      </div>

      <div className="mt-3.5 flex gap-4 text-xs text-fg-muted">
        <span className="inline-flex items-center gap-1.5"><span className="size-2 rounded-sm bg-primary" />{periods[0]}</span>
        <span className="inline-flex items-center gap-1.5"><span className="size-2 rounded-sm bg-border-strong" />{periods[1]}</span>
      </div>
    </section>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.