dqnamoexperiments / animated-sparkline
Animated Sparkline
A compact SVG sparkline that draws itself, morphs between trend states, and shifts color for positive or negative movement.
Revenue
$12,478
Curve
Trend
+42%"use client";import NumberFlow, { type Format } from "@number-flow/react";import { ArrowDownLeftIcon, ArrowUpRightIcon } from "@phosphor-icons/react";import type { CSSProperties } from "react";import { Sparkline, type SparklineCurve } from "@/components/Sparkline";import { cn } from "@/helpers/classname-helper";type AnimatedSparklinePoint = { label: string; value: number;};type AnimatedSparklineProps = { className?: string; curve?: SparkCurve; data: readonly AnimatedSparklinePoint[]; height?: number; label?: string; replayKey?: number; showValue?: boolean; trendPercent?: number; valueFormat?: Format; valuePrefix?: string; valueSuffix?: string; width?: number;};export type SparkCurve = SparklineCurve;const defaultValueFormat = { maximumFractionDigits: 1,} satisfies Format;const sparklineAnimationTiming = { duration: 980, easing: "cubic-bezier(0.25, 1, 0.5, 1)",} as const;function formatSparklineValue({ format, prefix = "", suffix = "", value,}: { format: Format; prefix?: string; suffix?: string; value: number;}) { return `${prefix}${new Intl.NumberFormat("en-US", format).format(value)}${suffix}`;}export function AnimatedSparkline({ className, curve = "smooth", data, height = 72, label = "Trend", replayKey = 0, showValue = true, trendPercent, valueFormat = defaultValueFormat, valuePrefix, valueSuffix, width = 220,}: AnimatedSparklineProps) { const firstValue = data[0]?.value ?? 0; const lastValue = data.at(-1)?.value ?? firstValue; const delta = firstValue === 0 ? 0 : ((lastValue - firstValue) / firstValue) * 100; const displayDelta = trendPercent ?? delta; const absoluteDelta = Math.abs(displayDelta); const isPositive = displayDelta >= 0; const color = isPositive ? "green" : "red"; return ( <div className={cn( "overflow-hidden rounded-[13px] max-w-xs h-max border border-grayscale-3 bg-white p-3 small-shadow dark:border-grayscale-4 dark:bg-grayscale-3 dark:shadow-none", className, )} style={ { "--spark-3": `var(--${color}-3)`, "--spark-4": `var(--${color}-4)`, "--spark-9": `var(--${color}-9)`, "--spark-11": `var(--${color}-11)`, } as CSSProperties } > <div className="flex items-start gap-4 justify-between"> <div className="min-w-0"> <p className="font-mono font-semibold text-[10px] text-grayscale-10 uppercase leading-none"> {label} </p> {showValue ? ( <p className="mt-1 font-bold font-number font-semibold text-grayscale-12 text-2xl leading-none"> {formatSparklineValue({ format: valueFormat, prefix: valuePrefix, suffix: valueSuffix, value: lastValue, })} </p> ) : null} </div> <div className="grid grid-cols-[14px_auto] items-center gap-0.5 rounded-full font-number font-semibold text-grayscale-11 text-sm uppercase leading-none tabular-nums"> {isPositive ? ( <ArrowUpRightIcon aria-hidden className="text-[var(--spark-9)]" size={14} weight="bold" /> ) : ( <ArrowDownLeftIcon aria-hidden className="text-[var(--spark-9)]" size={14} weight="bold" /> )} <NumberFlow format={defaultValueFormat} spinTiming={sparklineAnimationTiming} suffix="%" transformTiming={sparklineAnimationTiming} value={absoluteDelta} /> </div> </div> <Sparkline aria-label={`${label} ${isPositive ? "up" : "down"} ${absoluteDelta.toFixed(1)} percent`} className="mt-2 block h-auto w-full overflow-visible" color="var(--spark-9)" curve={curve} data={data} duration={sparklineAnimationTiming.duration} glow height={height} key={replayKey} replayKey={replayKey} showEndpoint strokeWidth={2} width={width} /> </div> );}Want to work with me?
I run a lil design engineering studio in London where we do fractional design engineering for startups.
THEINTERFACECOMPANYOF LONDON