import React from "react"; /* Pure-SVG chart primitives ported from the design. */ export function Sparkline( { points, color = "#00b884", width = 100, height = 28, filled = true, strokeWidth = 1.5 } ) { if ( ! points || points.length < 2 ) return null; const max = Math.max( ...points ), min = Math.min( ...points ), range = max - min || 1; const stepX = width / ( points.length - 1 ); const yOf = ( v ) => height - 2 - ( ( v - min ) / range ) * ( height - 4 ); const d = points.map( ( v, i ) => `${ i === 0 ? "M" : "L" }${ i * stepX },${ yOf( v ) }` ).join( " " ); const id = React.useId(); return ( { filled && ( <> ) } ); } export function AreaChart( { series, labels, height = 240, colors, formatY = ( v ) => v } ) { const ref = React.useRef( null ); const [ w, setW ] = React.useState( 800 ); const baseId = React.useId().replace( /:/g, "" ); React.useEffect( () => { if ( ! ref.current ) return; const ro = new ResizeObserver( ( entries ) => setW( entries[ 0 ].contentRect.width ) ); ro.observe( ref.current ); return () => ro.disconnect(); }, [] ); const padL = 44, padR = 16, padT = 12, padB = 28; const innerW = Math.max( 100, w - padL - padR ); const innerH = height - padT - padB; const allValues = series.flatMap( ( s ) => s.data ); const max = Math.max( ...allValues, 1 ); const pow = Math.pow( 10, Math.floor( Math.log10( max ) ) ); const niceMax = Math.ceil( max / pow ) * pow; const yMax = Math.max( niceMax, max * 1.1 ); const xOf = ( i ) => padL + ( i / Math.max( 1, labels.length - 1 ) ) * innerW; const yOf = ( v ) => padT + innerH - ( v / yMax ) * innerH; const yTicks = 4; const ticks = Array.from( { length: yTicks + 1 }, ( _, i ) => ( yMax / yTicks ) * i ); const xTickStep = Math.max( 1, Math.ceil( labels.length / 8 ) ); const [ hover, setHover ] = React.useState( null ); return (
setHover( null ) } onMouseMove={ ( e ) => { const rect = e.currentTarget.getBoundingClientRect(); const i = Math.round( ( ( e.clientX - rect.left - padL ) / innerW ) * ( labels.length - 1 ) ); setHover( i >= 0 && i < labels.length ? i : null ); } }> { ticks.map( ( t, i ) => ( { formatY( Math.round( t ) ) } ) ) } { labels.map( ( l, i ) => ( i % xTickStep === 0 || i === labels.length - 1 ) ? ( { l } ) : null ) } { series.map( ( s, si ) => { const color = s.color || colors?.[ si ] || "#00b884"; const d = s.data.map( ( v, i ) => `${ i === 0 ? "M" : "L" }${ xOf( i ) },${ yOf( v ) }` ).join( " " ); const area = `${ d } L${ xOf( s.data.length - 1 ) },${ padT + innerH } L${ xOf( 0 ) },${ padT + innerH } Z`; const gid = `g${ baseId }${ si }`; return ( ); } ) } { hover !== null && ( { series.map( ( s, si ) => ( ) ) } ) } { hover !== null && (
{ labels[ hover ] }
{ series.map( ( s, si ) => (
{ s.name } { formatY( s.data[ hover ] ) }
) ) }
) }
); } export function Donut( { segments, size = 180, thickness = 26, centerLabel, centerValue } ) { const cx = size / 2, cy = size / 2, r = ( size - thickness ) / 2; const total = segments.reduce( ( s, x ) => s + x.value, 0 ) || 1; let acc = 0; const arcs = segments.map( ( seg ) => { const start = ( acc / total ) * Math.PI * 2 - Math.PI / 2; acc += seg.value; const end = ( acc / total ) * Math.PI * 2 - Math.PI / 2; const large = end - start > Math.PI ? 1 : 0; return { d: `M${ cx + Math.cos( start ) * r },${ cy + Math.sin( start ) * r } A${ r },${ r } 0 ${ large } 1 ${ cx + Math.cos( end ) * r },${ cy + Math.sin( end ) * r }`, color: seg.color }; } ); return ( { arcs.map( ( a, i ) => ) } { centerValue != null && ( <> { centerValue } { centerLabel } ) } ); } export function HBar( { items, max, color = "#00b884", format = ( v ) => v } ) { const m = max || Math.max( ...items.map( ( i ) => i.value ), 1 ); return (
{ items.map( ( it, i ) => (
{ it.name }
{ format( it.value ) }
) ) }
); }