Skip to content
All Taste components

Animated charts

Drawing line chart

A weekly trend line that draws itself left to right over last week’s, then marks today with a pulsing point.

line draw · area fill · comparison line · end point · tooltip · legend

Active users
24,380+8.1%
This weekLast week
3,920 today
/* Add once to globals.css (draw comes from the Taste preset):
@keyframes area-in { from { opacity: 0; transform: scaleY(.55); } }
@keyframes breathe { from { transform: scale(1); opacity: .6; } to { transform: scale(1.35); opacity: .16; } }
*/

const thisWeek = [38, 44, 41, 52, 49, 58, 55, 66, 61, 72, 69, 82];
const lastWeek = [34, 36, 39, 40, 44, 43, 47, 50, 49, 53, 56, 58];
const W = 320, H = 150;

const toPoints = (vals: number[]) => vals.map((n, i) => [(i * W) / (vals.length - 1), 144 - (n - 30) * 2.1] as const);
const round = (n: number) => Math.round(n * 10) / 10;

/** Smooth path through the points (Catmull-Rom as cubic Bézier). */
function smooth(vals: number[]) {
  const p = toPoints(vals);
  let d = "M" + round(p[0][0]) + "," + round(p[0][1]);
  for (let i = 0; i < p.length - 1; i++) {
    const a = p[Math.max(i - 1, 0)], b = p[i], c = p[i + 1], e = p[Math.min(i + 2, p.length - 1)];
    d += " C" + round(b[0] + (c[0] - a[0]) / 6) + "," + round(b[1] + (c[1] - a[1]) / 6) +
      " " + round(c[0] - (e[0] - b[0]) / 6) + "," + round(c[1] - (e[1] - b[1]) / 6) +
      " " + round(c[0]) + "," + round(c[1]);
  }
  return d;
}

export function DrawingLineChart() {
  const line = smooth(thisWeek);
  const end = toPoints(thisWeek)[thisWeek.length - 1];
  return (
    <section aria-label="Active users chart" className="mx-auto grid max-w-2xl gap-4 rounded-lg border border-border bg-bg p-[18px] font-sans">
      <div className="flex flex-wrap items-start justify-between gap-3">
        <div>
          <p className="text-xs font-medium text-fg-subtle">Active users</p>
          <div className="mt-1 flex items-center gap-2.5">
            <span className="text-[28px] font-medium tabular-nums tracking-tight text-fg">24,380</span>
            <span className="rounded-full bg-success-soft px-2 py-0.5 text-xs font-medium text-success-fg">+8.1%</span>
          </div>
        </div>
        <div className="flex gap-3.5 text-xs text-fg-muted">
          <span className="flex items-center gap-1.5"><i className="h-0.5 w-3.5 rounded bg-primary" />This week</span>
          <span className="flex items-center gap-1.5"><i className="h-0.5 w-3.5 rounded bg-border-strong" />Last week</span>
        </div>
      </div>

      <div className="relative">
        <svg viewBox={"0 0 " + W + " " + H} className="block h-auto w-full overflow-visible" aria-hidden>
          <defs>
            <linearGradient id="line-fill" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0" stopColor="var(--tst-primary)" stopOpacity=".24" />
              <stop offset="1" stopColor="var(--tst-primary)" stopOpacity="0" />
            </linearGradient>
          </defs>
          {[20, 62, 104, 146].map((y) => <line key={y} x1="0" x2={W} y1={y} y2={y} className="stroke-border [stroke-dasharray:3_4]" />)}
          <path d={smooth(lastWeek)} pathLength={1} className="fill-none stroke-border-strong [stroke-dasharray:1] [stroke-linecap:round] [stroke-width:1.5] animate-draw motion-reduce:animate-none" />
          <path d={line + " L" + W + "," + H + " L0," + H + " Z"} fill="url(#line-fill)" className="origin-bottom [transform-box:fill-box] animate-[area-in_1s_cubic-bezier(.2,.7,.1,1)_.45s_both] motion-reduce:animate-none" />
          <path d={line} pathLength={1} className="fill-none stroke-primary [stroke-dasharray:1] [stroke-linecap:round] [stroke-width:2.25] animate-draw [animation-delay:120ms] motion-reduce:animate-none" />
          <circle cx={end[0]} cy={end[1]} r="9" className="origin-center fill-primary [transform-box:fill-box] animate-[breathe_1.6s_ease-in-out_1.4s_infinite_alternate] motion-reduce:animate-none" opacity=".3" />
          <circle cx={end[0]} cy={end[1]} r="4.5" className="origin-center fill-primary stroke-bg [stroke-width:2] [transform-box:fill-box] animate-pop [animation-delay:1050ms] motion-reduce:animate-none" />
        </svg>
        <div className="absolute right-0 -translate-y-[calc(100%+14px)]" style={{ top: (end[1] / H) * 100 + "%" }}>
          <div className="origin-bottom-right whitespace-nowrap rounded bg-fg px-2.5 py-1.5 text-xs text-bg shadow-lg animate-pop [animation-delay:1150ms] motion-reduce:animate-none">
            <b>3,920</b> <span className="opacity-70">today</span>
          </div>
        </div>
      </div>
      <div aria-hidden className="-mt-2 grid grid-cols-7 text-center text-[10.5px] text-fg-subtle">
        {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((d) => <span key={d}>{d}</span>)}
      </div>
    </section>
  );
}

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