{"version":3,"file":"data-viz-ramps.cjs","names":[],"sources":["../../src/theme/data-viz-ramps.ts"],"sourcesContent":["import { contrastRatio, hexToOklch, oklchToHex } from \"./color\";\n\n/** Number of steps in a sequential ramp. */\nexport const SEQUENTIAL_STEPS = 7;\n\n/** Steps per arm of a diverging scale, excluding the neutral midpoint. */\nexport const DIVERGING_ARM_STEPS = 4;\n\n/** A diverging scale: cool arm (extreme → near-mid), the midpoint, then the warm arm. */\nexport interface DivergingRamp {\n    cool: string[];\n    mid: string;\n    warm: string[];\n}\n\n/**\n * Lightness range a ramp spans, per mode.\n *\n * Dark mode is **selected, not flipped**: its own band, chosen for the dark surface.\n * Mechanically inverting the light ramp yields steps that are either invisible\n * against a near-black surface or so bright they read as highlights.\n */\nconst BAND = {\n    light: { from: 0.93, to: 0.34 },\n    dark: { from: 0.28, to: 0.86 },\n} as const;\n\n/**\n * Chroma shaped as a dome across the ramp.\n *\n * A constant chroma makes the pale end look muddy and the dark end look neon. The\n * dome keeps the extremes believable while the middle carries the hue, which is\n * what makes a heatmap readable at a glance.\n *\n * @param t - Position along the ramp, `0`–`1`.\n * @returns A multiplier for the peak chroma.\n */\nfunction chromaDome(t: number): number {\n    return 0.35 + 0.65 * (1 - Math.abs(2 * t - 1) ** 1.6);\n}\n\nexport interface BuildRampOptions {\n    /** How many steps to emit. Defaults to {@link SEQUENTIAL_STEPS}. */\n    steps?: number;\n    /** Chroma at the middle of the ramp. */\n    peakChroma?: number;\n}\n\n/**\n * Build a single-hue ramp with evenly spaced perceptual lightness.\n *\n * Even spacing in OKLCH lightness — not in RGB — is what makes equal data steps look\n * like equal colour steps.\n *\n * Steps come back in **token order**: index 0 is the end that means \"near zero\", so\n * it is the lightest on a light canvas and the darkest on a dark one. That is the\n * order `--tempest-chart-sequential-1…7` is written in, and it makes index 0 the step\n * nearest the surface in both modes — which is what `ordinalStart` walks from.\n * Returning a fixed light→dark order instead would put token 1 at opposite ends of\n * the scale depending on the mode, and a dark theme would paint every heatmap\n * inverted.\n *\n * @param hue - OKLCH hue in degrees.\n * @param mode - Which lightness band to span.\n * @param options - Shape of the ramp: `steps` (how many to emit) and `peakChroma`\n *   (chroma at the middle).\n * @returns Hex steps, near-zero end first.\n */\nexport function buildRamp(\n    hue: number,\n    mode: \"light\" | \"dark\",\n    { steps = SEQUENTIAL_STEPS, peakChroma = 0.19 }: BuildRampOptions = {},\n): string[] {\n    const band = BAND[mode];\n    const out: string[] = [];\n    for (let i = 0; i < steps; i += 1) {\n        const t = steps === 1 ? 0 : i / (steps - 1);\n        out.push(\n            oklchToHex({\n                l: band.from + (band.to - band.from) * t,\n                c: peakChroma * chromaDome(t),\n                h: hue,\n            }),\n        );\n    }\n    return out;\n}\n\n/**\n * The first index of `ramp` that clears `minContrast` against `surface`.\n *\n * A **sequential** ramp may let its near-zero end recede into the surface — that is\n * what \"almost nothing\" should look like on a heatmap. An **ordinal** ramp may not:\n * every step is a discrete mark a reader has to see. This is how a consumer finds\n * where the ordinal-safe slice of a sequential ramp begins.\n *\n * @param ramp - Hex steps in token order, near-zero end first.\n * @param surface - The chart surface the ramp sits on.\n * @param minContrast - Floor to clear. Default `2` — the ordinal floor.\n * @returns The first safe index, or `0` when every step already clears it.\n */\nexport function ordinalStart(ramp: string[], surface: string, minContrast = 2): number {\n    const index = ramp.findIndex((step) => contrastRatio(step, surface) >= minContrast);\n    return index < 0 ? ramp.length - 1 : index;\n}\n\n/**\n * Build a diverging scale from two opposing hues around a neutral midpoint.\n *\n * The midpoint is **grey, never a hue**: a coloured midpoint reads as a third\n * category instead of as \"no deviation\", which is the one thing a diverging scale\n * exists to show. The arms get equal step counts so neither side looks like it\n * carries more range than the other.\n *\n * @param params.coolHue - OKLCH hue for the negative arm.\n * @param params.warmHue - OKLCH hue for the positive arm.\n * @param params.mid - Neutral midpoint, in hex.\n * @param params.mode - Which lightness band to span.\n * @param params.steps - Steps per arm.\n * @returns The scale, each arm ordered extreme → nearest the midpoint.\n */\nexport function buildDivergingRamp({\n    coolHue,\n    warmHue,\n    mid,\n    mode,\n    steps = DIVERGING_ARM_STEPS,\n}: {\n    coolHue: number;\n    warmHue: number;\n    mid: string;\n    mode: \"light\" | \"dark\";\n    steps?: number;\n}): DivergingRamp {\n    /*\n     * Each arm spans from its extreme to just short of the midpoint, so the two\n     * arms together read as one continuous scale rather than two ramps abutting.\n     */\n    const band = mode === \"light\" ? { from: 0.86, to: 0.4 } : { from: 0.82, to: 0.42 };\n    const arm = (hue: number): string[] => {\n        const out: string[] = [];\n        for (let i = 0; i < steps; i += 1) {\n            const t = steps === 1 ? 0 : i / (steps - 1);\n            out.push(\n                oklchToHex({\n                    l: band.from + (band.to - band.from) * t,\n                    c: 0.17 * chromaDome(1 - t * 0.5),\n                    h: hue,\n                }),\n            );\n        }\n        return out.reverse();\n    };\n    return { cool: arm(coolHue), mid, warm: [...arm(warmHue)].reverse() };\n}\n\n/**\n * The hue of a hex colour, for feeding the ramp builders.\n *\n * @param hex - Any hex colour.\n * @returns Its OKLCH hue in degrees.\n */\nexport function hueOf(hex: string): number {\n    return hexToOklch(hex).h;\n}\n"],"mappings":"+BAsBA,IAAM,EAAO,CACT,MAAO,CAAE,KAAM,IAAM,GAAI,GAAK,EAC9B,KAAM,CAAE,KAAM,IAAM,GAAI,GAAK,CACjC,EAYA,SAAS,EAAW,EAAmB,CACnC,MAAO,KAAO,KAAQ,EAAI,KAAK,IAAI,EAAI,EAAI,CAAC,GAAK,IACrD,CA6BA,SAAgB,EACZ,EACA,EACA,CAAE,QAAA,EAA0B,aAAa,KAA2B,CAAC,EAC7D,CACR,IAAM,EAAO,EAAK,GACZ,EAAgB,CAAC,EACvB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,GAAK,EAAG,CAC/B,IAAM,EAAI,IAAU,EAAI,EAAI,GAAK,EAAQ,GACzC,EAAI,KACA,EAAA,WAAW,CACP,EAAG,EAAK,MAAQ,EAAK,GAAK,EAAK,MAAQ,EACvC,EAAG,EAAa,EAAW,CAAC,EAC5B,EAAG,CACP,CAAC,CACL,CACJ,CACA,OAAO,CACX,CAmCA,SAAgB,EAAmB,CAC/B,UACA,UACA,MACA,OACA,QAAA,GAOc,CAKd,IAAM,EAAO,IAAS,QAAU,CAAE,KAAM,IAAM,GAAI,EAAI,EAAI,CAAE,KAAM,IAAM,GAAI,GAAK,EAC3E,EAAO,GAA0B,CACnC,IAAM,EAAgB,CAAC,EACvB,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,GAAK,EAAG,CAC/B,IAAM,EAAI,IAAU,EAAI,EAAI,GAAK,EAAQ,GACzC,EAAI,KACA,EAAA,WAAW,CACP,EAAG,EAAK,MAAQ,EAAK,GAAK,EAAK,MAAQ,EACvC,EAAG,IAAO,EAAW,EAAI,EAAI,EAAG,EAChC,EAAG,CACP,CAAC,CACL,CACJ,CACA,OAAO,EAAI,QAAQ,CACvB,EACA,MAAO,CAAE,KAAM,EAAI,CAAO,EAAG,MAAK,KAAM,CAAC,GAAG,EAAI,CAAO,CAAC,CAAC,CAAC,QAAQ,CAAE,CACxE,CAQA,SAAgB,EAAM,EAAqB,CACvC,OAAO,EAAA,WAAW,CAAG,CAAC,CAAC,CAC3B"}