Search

Jump to any page.

Data display

Sparkline

Word-sized trend line that inherits colour and never mirrors.

Volume+24%

pnpm dlx shadcn@latest add @paragon/sparkline
Brings incn
registry/paragon/components/sparkline.tsx
import * as React from "react"
import { cn } from "cn"

/**
 * A word-sized trend line. Stretches to its box, keeps a hairline stroke at any
 * size, and inherits colour from the text. Time runs left to right in every
 * language, so it never mirrors.
 */
function Sparkline({
  values,
  area = false,
  className,
  ...props
}: Omit<React.ComponentProps<"svg">, "values"> & {
  values: number[]
  /** Fill beneath the line with a soft fade of the same colour. */
  area?: boolean
}) {
  const id = React.useId()
  if (values.length < 2) return null

  const min = Math.min(...values)
  const max = Math.max(...values)
  const range = max - min || 1
  const points = values.map((value, index) => [
    (index / (values.length - 1)) * 100,
    30 - ((value - min) / range) * 28,
  ])
  const line = points.map(([x, y], index) => `${index ? "L" : "M"}${x.toFixed(2)} ${y.toFixed(2)}`).join(" ")

  return (
    <svg
      viewBox="0 0 100 32"
      preserveAspectRatio="none"
      aria-hidden="true"
      data-slot="sparkline"
      className={cn("h-8 w-24 overflow-visible text-foreground/70", className)}
      {...props}
    >
      {area ? (
        <>
          <defs>
            <linearGradient id={`${id}-fill`} x1="0" x2="0" y1="0" y2="1">
              <stop offset="0%" stopColor="currentColor" stopOpacity="0.18" />
              <stop offset="100%" stopColor="currentColor" stopOpacity="0" />
            </linearGradient>
          </defs>
          <path d={`${line} L100 32 L0 32 Z`} fill={`url(#${id}-fill)`} />
        </>
      ) : null}
      <path
        d={line}
        fill="none"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  )
}

export { Sparkline }