Skip to content
All Taste components

Dashboard widgets

Revenue chart

Monthly revenue as an area chart against last year, with a range switch and a hover tooltip.

area + comparison line · axis labels · grid · tooltip · legend · range tabs

Revenue · October

₹34.0L54%
This yearLast year
"use client";
import { useState } from "react";
import { ArrowUpRight } from "lucide-react";
import { cn } from "@/lib/utils";

export interface RevenuePoint { month: string; current: number; previous: number }

const sample: RevenuePoint[] = [
  ["Nov", 14, 10], ["Dec", 16, 11], ["Jan", 15, 13], ["Feb", 19, 12], ["Mar", 21, 14], ["Apr", 20, 15],
  ["May", 24, 15], ["Jun", 26, 17], ["Jul", 25, 18], ["Aug", 29, 18], ["Sep", 32, 20], ["Oct", 34, 22],
].map(([month, current, previous]) => ({ month: month as string, current: current as number, previous: previous as number }));

const ranges = ["3M", "6M", "12M"] as const;
const lakh = (v: number) => "₹" + v.toFixed(1) + "L";

export function RevenueChart({ data = sample, max = 40 }: { data?: RevenuePoint[]; max?: number }) {
  const [range, setRange] = useState<(typeof ranges)[number]>("12M");
  const rows = data.slice(-({ "3M": 3, "6M": 6, "12M": 12 } as const)[range]);
  const [active, setActive] = useState(rows.length - 3);
  const i = Math.min(active, rows.length - 1);
  const x = (n: number) => (rows.length === 1 ? 50 : (n / (rows.length - 1)) * 100);
  const y = (v: number) => 100 - (v / max) * 100;
  const line = (key: "current" | "previous") => rows.map((r, n) => x(n) + "," + y(r[key])).join(" ");
  const last = rows[rows.length - 1];
  const growth = Math.round((last.current / last.previous - 1) * 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>
          <p className="text-[12.5px] text-fg-subtle">Revenue · {last.month}</p>
          <div className="mt-0.5 flex items-center gap-2">
            <span className="text-2xl font-medium tracking-tight tabular-nums">{lakh(last.current)}</span>
            <span className="inline-flex h-5 items-center gap-0.5 rounded-full bg-success-soft px-1.5 text-xs font-medium text-success-fg">
              <ArrowUpRight className="size-3" aria-hidden />{growth}%
            </span>
          </div>
        </div>
        <div role="tablist" aria-label="Range" className="inline-flex gap-0.5 rounded-lg bg-surface-2 p-[3px]">
          {ranges.map((r) => (
            <button key={r} role="tab" aria-selected={range === r} onClick={() => { setRange(r); setActive(0); }}
              className="h-[30px] rounded px-3 text-[13px] font-medium text-fg-muted aria-selected:bg-bg aria-selected:text-fg aria-selected:shadow-sm">
              {r}
            </button>
          ))}
        </div>
      </div>

      <div className="mt-3 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" />This year</span>
        <span className="inline-flex items-center gap-1.5"><span className="w-3 border-t-2 border-dashed border-fg-subtle" />Last year</span>
      </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}>{f ? "₹" + max * f + "L" : "0"}</span>)}
        </div>
        <div className="relative h-[180px]" onMouseLeave={() => setActive(rows.length - 1)}>
          <svg viewBox="0 0 100 100" preserveAspectRatio="none" className="absolute inset-0 size-full overflow-visible" aria-hidden>
            {[0, 25, 50, 75, 100].map((g) => (
              <line key={g} x1="0" x2="100" y1={g} y2={g} className="text-border" stroke="currentColor" vectorEffect="non-scaling-stroke" />
            ))}
            <polygon points={"0,100 " + line("current") + " 100,100"} className="text-primary" fill="currentColor" fillOpacity={0.1} />
            <polyline points={line("previous")} fill="none" className="text-fg-subtle" stroke="currentColor" strokeWidth={1.5} strokeDasharray="4 4" vectorEffect="non-scaling-stroke" />
            <polyline points={line("current")} fill="none" className="text-primary" stroke="currentColor" strokeWidth={2} strokeLinejoin="round" vectorEffect="non-scaling-stroke" />
          </svg>
          {/* Hover columns */}
          <div className="absolute inset-0 flex">
            {rows.map((r, n) => (
              <div key={r.month} className="h-full flex-1" onMouseEnter={() => setActive(n)} aria-hidden />
            ))}
          </div>
          <span className="pointer-events-none absolute inset-y-0 border-l border-dashed border-border-strong" style={{ left: x(i) + "%" }} />
          <span className="pointer-events-none absolute size-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-primary bg-bg" style={{ left: x(i) + "%", top: y(rows[i].current) + "%" }} />
          <div role="status" className={cn(
              "pointer-events-none absolute top-[38%] grid min-w-[132px] gap-1 rounded border border-border bg-bg px-2.5 py-2 text-xs shadow-lg",
              x(i) > 50 ? "-translate-x-[calc(100%+12px)]" : "translate-x-3",
            )} style={{ left: x(i) + "%" }}>
            <span className="font-medium">{rows[i].month}</span>
            <span className="flex justify-between gap-3 text-fg-muted">This year <b className="font-medium tabular-nums text-fg">{lakh(rows[i].current)}</b></span>
            <span className="flex justify-between gap-3 text-fg-muted">Last year <b className="font-medium tabular-nums text-fg">{lakh(rows[i].previous)}</b></span>
          </div>
        </div>
        <div className="col-start-2 mt-2 flex justify-between text-[11px] text-fg-subtle" aria-hidden>
          {rows.map((r, n) => <span key={r.month} className={cn(rows.length > 6 && n % 2 && "invisible")}>{r.month}</span>)}
        </div>
      </div>
    </section>
  );
}

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