~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
82 lines · 3.0 KB Raw History
import { useId } from 'react';
import { cn } from '#/lib/utils.js';

/**
 * A smooth open path through `points` as a Catmull-Rom spline expressed in cubic
 * Béziers, so the sparkline flows in rounded hills rather than jagged peaks.
 * Each control point is pulled a sixth of the way along the segment between its
 * neighbours; the y of every control point is clamped to the drawing box so a
 * spline overshoot can't dip the fill below the baseline or spill past the top.
 * @param {[number, number][]} points
 * @param {number} height
 * @returns {string}
 */
function splinePath(points, height) {
  const clampY = (y) => Math.max(0, Math.min(height, y));
  let d = `M${points[0][0].toFixed(2)} ${points[0][1].toFixed(2)}`;
  for (let i = 0; i < points.length - 1; i++) {
    const [x0, y0] = points[i - 1] || points[i];
    const [x1, y1] = points[i];
    const [x2, y2] = points[i + 1];
    const [x3, y3] = points[i + 2] || points[i + 1];
    const c1x = x1 + (x2 - x0) / 6;
    const c1y = clampY(y1 + (y2 - y0) / 6);
    const c2x = x2 - (x3 - x1) / 6;
    const c2y = clampY(y2 - (y3 - y1) / 6);
    d += ` C${c1x.toFixed(2)} ${c1y.toFixed(2)} ${c2x.toFixed(2)} ${c2y.toFixed(2)} ${x2.toFixed(2)} ${y2.toFixed(2)}`;
  }
  return d;
}

/**
 * A tiny area chart drawn edge-to-edge, meant to sit behind a row as an ambient
 * backdrop rather than a chart you read precisely. `values` are bucket counts,
 * oldest to newest; the shape is normalized to its own peak, smoothed into a
 * spline, and stretched to fill via a non-uniform viewBox. The fill fades out
 * toward the top so text above it stays legible, and the whole thing renders in
 * the ring indigo at low opacity. Nothing is drawn for an empty or all-zero
 * series.
 * @param {{values: number[]|null|undefined, className?: string}} props
 */
export function Sparkline({ values, className }) {
  const gradientId = useId();
  if (!values || values.length < 2) return null;
  const max = Math.max(...values);
  if (max <= 0) return null;

  const width = 100;
  const height = 32;
  const step = width / (values.length - 1);
  const points = /** @type {[number, number][]} */ (
    values.map((value, i) => [i * step, height - (value / max) * height])
  );
  const line = splinePath(points, height);
  const area = `${line} L${width} ${height} L0 ${height} Z`;

  return (
    <svg
      viewBox={`0 0 ${width} ${height}`}
      preserveAspectRatio="none"
      className={cn('h-full w-full text-ring', className)}
      aria-hidden="true"
    >
      <defs>
        <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="currentColor" stopOpacity="0" />
          <stop offset="100%" stopColor="currentColor" stopOpacity="0.14" />
        </linearGradient>
      </defs>
      <path d={area} fill={`url(#${gradientId})`} />
      <path
        d={line}
        fill="none"
        stroke="currentColor"
        strokeOpacity="0.25"
        strokeWidth="1"
        strokeLinejoin="round"
        strokeLinecap="round"
        vectorEffect="non-scaling-stroke"
      />
    </svg>
  );
}