import type { ReactNode } from 'react' import { Box, Typography } from '@mui/material' import { styles } from './styles' import { DEFAULT_LEGEND_LABELS, type LegendLabels } from '../../provider/labels' import type { LegendProportionVariable } from '../../stores' export interface LegendProportionUIProps { data: LegendProportionVariable /** Override the renderer's user-facing strings. */ labels?: Pick } const MAX_DIAMETER = 72 const RATIO_FLOOR = 0.3 const MIN_STEPS = 2 const MAX_STEPS = 4 /** * Value breaks, largest→smallest. Explicit `stops` (≥2 entries) win, capped at * the 4 largest; otherwise breaks are derived evenly from `min`/`max` over * `steps` clamped to 2–4. Circles map 1:1 to the breaks. */ function resolveBreaks(data: LegendProportionVariable): number[] { const explicit = (data.stops ?? []).filter((n) => Number.isFinite(n)) if (explicit.length >= MIN_STEPS) { return explicit.sort((a, b) => b - a).slice(0, MAX_STEPS) } const { min, max } = data if (min === max) return [max] const steps = Math.min( MAX_STEPS, Math.max(MIN_STEPS, data.steps ?? MAX_STEPS), ) return Array.from( { length: steps }, (_, i) => max - ((max - min) * i) / (steps - 1), ) } /** Decorative nested-circle diameter for circle `i` of `n` (i=0 outermost). */ function diameterFor(i: number, n: number): number { if (n <= 1) return MAX_DIAMETER const ratio = 1 - (1 - RATIO_FLOOR) * (i / (n - 1)) return MAX_DIAMETER * ratio } /** * Proportional-symbol legend — a stack of decorative nested circles with the * value breaks labelled beside them (largest = MAX, smallest = MIN). Circle * sizes are fixed/decorative, not value-derived, so the legend stays readable * across very wide ranges. * * @experimental This API is new and may change in a future release. */ export function LegendProportionUI({ data, labels = DEFAULT_LEGEND_LABELS, }: LegendProportionUIProps) { const { attribute, formatValue = String } = data const breaks = resolveBreaks(data) const n = breaks.length const labelFor = (value: number, i: number): ReactNode => { const formatted = formatValue(value) if (n <= 1) return formatted if (i === 0) return ( <> {labels.maxPrefix} {formatted} ) if (i === n - 1) return ( <> {labels.minPrefix} {formatted} ) return formatted } return ( {attribute && ( {labels.radiusRangeBy} {attribute} )} {breaks.map((_, i) => { const diameter = diameterFor(i, n) return ( ) })} {breaks.map((value, i) => ( {labelFor(value, i)} ))} ) }