SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%
2.5 KB · 57 lines tsx
Raw Blame History
1import Link from 'next/link';2import type { ReactNode } from 'react';3import { cn } from '@/lib/cn';4import { fmtInt } from '@/lib/format';56/**7 * Compact heatmap table (rows × columns) with sequential intensity from the accent hue. Server-renderable.8 * Cells with null are drawn hollow, never as 0.9 */10export function Heatmap({ rows, cols, get, rowHref, format = fmtInt, className, colLabel = (c) => c }: { rows: { key: string; label: ReactNode }[]; cols: string[]; get: (row: string, col: string) => number | null; rowHref?: (row: string) => string | undefined; format?: (v: number) => string; className?: string; colLabel?: (c: string) => ReactNode }) {11  let max = 0;12  for (const r of rows) for (const c of cols) max = Math.max(max, get(r.key, c) ?? 0);13  return (14    <div className={cn('table-scroll', className)}>15      <table className="w-full border-separate border-spacing-[2px] text-xs">16        <thead>17          <tr>18            <th className="sticky left-0 z-[2] bg-canvas text-left font-medium text-ink-2" />19            {cols.map((c) => (20              <th key={c} className="whitespace-nowrap px-1 pb-1 text-center font-medium text-ink-3">21                {colLabel(c)}22              </th>23            ))}24          </tr>25        </thead>26        <tbody>27          {rows.map((r) => {28            const href = rowHref?.(r.key);29            return (30              <tr key={r.key}>31                <th className="sticky left-0 z-[2] max-w-[10rem] truncate bg-canvas pr-2 text-left font-medium text-ink-2">32                  {href ? (33                    <Link href={href} className="hover:text-accent">34                      {r.label}35                    </Link>36                  ) : (37                    r.label38                  )}39                </th>40                {cols.map((c) => {41                  const v = get(r.key, c);42                  const a = v === null || max === 0 ? 0 : 0.08 + 0.82 * Math.sqrt(v / max);43                  return (44                    <td key={c} className={cn('tnum h-7 min-w-[2.4rem] rounded-[2px] text-center', v === null && 'border border-dashed border-rule text-ink-3')} style={v === null ? undefined : { background: `color-mix(in srgb, var(--accent) ${Math.round(a * 100)}%, var(--surface-2))`, color: a > 0.55 ? 'var(--accent-ink)' : 'var(--ink)' }} title={v === null ? 'no data' : `${format(v)}`}>45                      {v === null ? '·' : v === 0 ? '' : format(v)}46                    </td>47                  );48                })}49              </tr>50            );51          })}52        </tbody>53      </table>54    </div>55  );56}57