← back to Ken

kalshi-dash/src/components/Sparkline.jsx

92 lines

import React, { useMemo } from 'react';

/**
 * Sparkline — tiny inline SVG line chart with gradient fill
 *
 * Props:
 *   data   — array of numbers
 *   color  — CSS color or var() (default: var(--cyan))
 *   width  — px (default: 60)
 *   height — px (default: 20)
 *   id     — unique string for gradient ID (required when multiple sparklines are on the same page)
 *
 * Usage:
 *   <Sparkline data={[12,18,14,22,19,25,21]} color="var(--green)" id="balance-spark" />
 */
export default function Sparkline({ data = [], color = 'var(--cyan)', width = 60, height = 20, id = 'spark' }) {
  const points = useMemo(() => {
    if (!data || data.length < 2) return null;

    const min = Math.min(...data);
    const max = Math.max(...data);
    const range = max - min || 1;
    const pad = 2; // px padding top/bottom

    const xs = data.map((_, i) => (i / (data.length - 1)) * width);
    const ys = data.map(v => height - pad - ((v - min) / range) * (height - pad * 2));

    // Polyline points string
    const polyline = xs.map((x, i) => `${x},${ys[i]}`).join(' ');

    // Closed path for fill (go to bottom-right corner, bottom-left, back up)
    const fillPath =
      `M ${xs[0]},${ys[0]} ` +
      xs.slice(1).map((x, i) => `L ${x},${ys[i + 1]}`).join(' ') +
      ` L ${xs[xs.length - 1]},${height} L ${xs[0]},${height} Z`;

    return { polyline, fillPath };
  }, [data, width, height]);

  if (!points) {
    return <svg width={width} height={height} />;
  }

  const gradId = `spark-grad-${id}`;

  return (
    <svg
      width={width}
      height={height}
      viewBox={`0 0 ${width} ${height}`}
      style={{ display: 'inline-block', verticalAlign: 'middle', flexShrink: 0 }}
      aria-hidden="true"
    >
      <defs>
        <linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor={color} stopOpacity="0.25" />
          <stop offset="100%" stopColor={color} stopOpacity="0.01" />
        </linearGradient>
      </defs>

      {/* Gradient fill area */}
      <path
        d={points.fillPath}
        fill={`url(#${gradId})`}
      />

      {/* The line */}
      <polyline
        points={points.polyline}
        fill="none"
        stroke={color}
        strokeWidth="1.5"
        strokeLinejoin="round"
        strokeLinecap="round"
      />

      {/* Endpoint dot */}
      {(() => {
        const lastX = (data.length - 1) / (data.length - 1) * width;
        const min = Math.min(...data);
        const max = Math.max(...data);
        const range = max - min || 1;
        const pad = 2;
        const lastY = height - pad - ((data[data.length - 1] - min) / range) * (height - pad * 2);
        return (
          <circle cx={lastX} cy={lastY} r="2" fill={color} />
        );
      })()}
    </svg>
  );
}