SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
2.4 KB · 55 lines tsx
Raw Blame History
1'use client';23import { useState } from 'react';4import { compactNumber, fullNumber } from './scale';56export interface BarDatum {7  label: string;8  value: number;9  sublabel?: string;10  color?: string;11  href?: string;12}1314/**15 * Horizontal bar chart (magnitude by category). Bars ≤ 24px thick, 4px rounded data-end, single16 * hue by default, per-mark hover tooltip, direct value label at the tip.17 */18export function BarChart({ data, currency = false, maxBars = 12, ariaLabel, className, percent = false }: { data: BarDatum[]; currency?: boolean; maxBars?: number; ariaLabel: string; className?: string; percent?: boolean }) {19  const [hover, setHover] = useState<number | null>(null);20  const rows = data.slice(0, maxBars);21  if (!rows.length) return <div className={`text-xs text-subtle ${className ?? ''}`}>No data</div>;22  const max = Math.max(...rows.map((r) => Math.abs(r.value)), 1e-9);23  const fmt = (v: number) => (percent ? `${(v * 100).toFixed(1)}%` : compactNumber(v, { currency }));24  const fmtFull = (v: number) => (percent ? `${(v * 100).toFixed(2)}%` : fullNumber(v, { currency }));25  return (26    <div className={`relative ${className ?? ''}`} role="img" aria-label={ariaLabel}>27      <ul className="space-y-1.5">28        {rows.map((r, i) => {29          const w = Math.max(2, (Math.abs(r.value) / max) * 100);30          return (31            <li key={`${r.label}-${i}`} className="grid grid-cols-[minmax(0,9rem)_1fr_auto] items-center gap-2 text-[12px]" onMouseEnter={() => setHover(i)} onMouseLeave={() => setHover(null)}>32              <span className="truncate text-muted" title={r.label}>33                {r.href ? (34                  <a href={r.href} className="hover:text-fg">35                    {r.label}36                  </a>37                ) : (38                  r.label39                )}40              </span>41              <span className="relative h-4">42                <span className="absolute inset-y-0 left-0 rounded-r-[4px]" style={{ width: `${w}%`, background: r.color ?? 'var(--ri-index)', opacity: hover === null || hover === i ? 1 : 0.55 }} />43              </span>44              <span className="num w-16 text-right font-medium text-fg" title={fmtFull(r.value)}>45                {fmt(r.value)}46              </span>47            </li>48          );49        })}50      </ul>51      {hover !== null && rows[hover]?.sublabel ? <div className="mt-1 text-[11px] text-subtle">{rows[hover]!.sublabel}</div> : null}52    </div>53  );54}55