/** * Ergonomic data normalization for the chart family. * * Accepts the simplest thing that works and progressively enhances: * :data="[5, 8, 6, 12]" → one series, x = index * :data="[{ x: 'Jan', y: 5 }, …]" → one series, labelled * :series="[{ name, color, data: number[] }]" → many series * :labels="['Jan','Feb',…]" → shared x labels * * A server can return `{ labels, series }` and drop it straight in. */ import type { ThemeType } from '../../../types' /** A single value the way callers pass it. */ export type RawPoint = number | { x?: string | number; y: number; label?: string } /** A caller-supplied series. */ export interface RawSeries { name?: string /** Tone name (`primary`, `blue`, …) or any CSS color. */ color?: ThemeType | string data: RawPoint[] } /** Normalized point used internally by every chart. */ export interface Point { /** Numeric x for scaling (index if labels are categorical). */ x: number y: number /** Display label for the x value. */ label: string } /** Normalized series used internally by every chart. */ export interface Series { name: string /** Resolved CSS color (already `var(--bgl-…)` or a raw color). */ color?: ThemeType | string points: Point[] } export interface NormalizeOptions { /** Shared x labels; overrides per-point labels when provided. */ labels?: (string | number)[] /** Fallback color (tone name or CSS) for series that don't set their own. Lets a single-series chart honor a top-level `color` prop. */ defaultColor?: ThemeType | string } function toPoints(data: RawPoint[], labels?: (string | number)[]): Point[] { return data.map((d, i) => { if (typeof d === 'number') { return { x: i, y: d, label: String(labels?.[i] ?? i) } } const label = d.label ?? labels?.[i] ?? d.x ?? i return { x: typeof d.x === 'number' ? d.x : i, y: d.y, label: String(label) } }) } /** * Resolve `data` / `series` props into a uniform `Series[]`. * Exactly one of `data` or `series` is expected; `series` wins if both given. */ export function normalizeSeries( data: RawPoint[] | undefined, series: RawSeries[] | undefined, opts: NormalizeOptions = {}, ): Series[] { if (series?.length) { // Only apply the chart-level default to a lone series; multi-series keep // their automatic per-index tone sequence unless each sets its own color. const useDefault = series.length === 1 return series.map((s, i) => ({ name: s.name ?? `Series ${i + 1}`, color: s.color ?? (useDefault ? opts.defaultColor : undefined), points: toPoints(s.data ?? [], opts.labels), })) } if (data?.length) { return [{ name: 'Series 1', color: opts.defaultColor, points: toPoints(data, opts.labels) }] } return [] } /** Flatten all y-values across series (for domain calc). */ export function allValues(series: Series[]): number[] { return series.flatMap(s => s.points.map(p => p.y)) } /** The x labels of the first/longest series (charts share an x axis). */ export function sharedLabels(series: Series[]): string[] { let longest: Point[] = [] for (const s of series) if (s.points.length > longest.length) longest = s.points return longest.map(p => p.label) }