@acronis-platform/ui-react 5.4.1 → 5.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ui/area-chart/area-chart.js +107 -105
- package/dist/components/ui/area-chart/area-chart.js.map +1 -1
- package/dist/components/ui/bar-chart/bar-chart.js +135 -132
- package/dist/components/ui/bar-chart/bar-chart.js.map +1 -1
- package/dist/components/ui/chart/chart-format.js +36 -31
- package/dist/components/ui/chart/chart-format.js.map +1 -1
- package/dist/components/ui/chart/chart.js +9 -7
- package/dist/components/ui/chart/chart.js.map +1 -1
- package/dist/components/ui/chart/use-x-axis-layout.js +71 -0
- package/dist/components/ui/chart/use-x-axis-layout.js.map +1 -0
- package/dist/components/ui/composed-chart/composed-chart.js +141 -139
- package/dist/components/ui/composed-chart/composed-chart.js.map +1 -1
- package/dist/components/ui/confidence-cone/confidence-cone.js +70 -67
- package/dist/components/ui/confidence-cone/confidence-cone.js.map +1 -1
- package/dist/components/ui/histogram/histogram.js +54 -51
- package/dist/components/ui/histogram/histogram.js.map +1 -1
- package/dist/components/ui/line-chart/line-chart.js +119 -117
- package/dist/components/ui/line-chart/line-chart.js.map +1 -1
- package/dist/components/ui/scatter-chart/scatter-chart.js +53 -51
- package/dist/components/ui/scatter-chart/scatter-chart.js.map +1 -1
- package/dist/llms/area-chart.md +10 -1
- package/dist/llms/bar-chart.md +10 -1
- package/dist/llms/composed-chart.md +10 -1
- package/dist/llms/confidence-cone.md +10 -1
- package/dist/llms/histogram.md +10 -1
- package/dist/llms/line-chart.md +10 -1
- package/dist/llms/scatter-chart.md +10 -1
- package/dist/llms.txt +1 -1
- package/dist/src/components/ui/chart/chart-format.d.ts +10 -4
- package/dist/src/components/ui/chart/chart.d.ts +1 -1
- package/dist/src/components/ui/chart/use-x-axis-layout.d.ts +28 -0
- package/dist/ui-react.css +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart-format.js","names":[],"sources":["../../../../src/components/ui/chart/chart-format.ts"],"sourcesContent":["// Shared axis helpers for the cartesian chart components (BarChart, LineChart,\n// AreaChart, ComposedChart, ScatterChart, ConfidenceCone, Histogram). Three\n// concerns live here so every chart formats + toggles axes the same way rather\n// than each reimplementing it:\n//\n// 1. `CartesianChartProps` — the common axis knobs each cartesian chart mixes\n// into its own props (show/hide either axis, per-axis tick formatting).\n// 2. A small set of tick formatters (+ a factory) callers pass to\n// `xTickFormatter` / `yTickFormatter`.\n// 3. `resolveAxisDomain` — maps the `yAxisDomain` preset to a recharts\n// `domain`, shared so all 7 charts can't drift apart on it.\n//\n// These format only the axis *tick labels*; series colors and tokens are\n// unaffected. Formatters coerce to a number and pass non-numeric values through\n// untouched (a category axis stays readable if a formatter is applied to it).\n\nimport type { AxisDomainItem } from 'recharts';\n\nimport type { ChartTooltipContentType } from './chart';\n\n/** A recharts-compatible tick formatter: `(value, index) => label`. */\nexport type TickFormatter = (value: number | string, index?: number) => string;\n\n/**\n * Props shared by every cartesian chart (Bar, Line, Area, Composed, Scatter,\n * ConfidenceCone, Histogram) — chrome toggles, axis titles/units, tick\n * formatting/visibility, and grid trim. All optional. `showLegend` and `xUnit`\n * are deliberately *not* here: `showLegend` doesn't apply to `Histogram` and\n * `xUnit` only to charts with a numeric X axis, so they stay per-component.\n */\nexport interface CartesianChartProps {\n /** Render the CartesianGrid. Defaults to `true`. */\n showGrid?: boolean;\n /** Render the hover tooltip. Defaults to `true`. */\n showTooltip?: boolean;\n /**\n * Replace the default tooltip. Pass a configured `ChartTooltipContent` (from\n * this library) — e.g. with a `formatter` / `labelFormatter` — to customize it\n * without composing recharts yourself. Ignored when `showTooltip` is false.\n */\n tooltipContent?: ChartTooltipContentType;\n /** Title rendered beneath the X axis. */\n xAxisLabel?: string;\n /** Title rendered beside the Y axis (rotated). */\n yAxisLabel?: string;\n /**\n * Unit suffix appended to the Y axis's tick values (recharts `unit`) — applies\n * when the Y axis is the numeric one. `ComposedChart` with\n * `orientation=\"horizontal\"` puts the values on X instead; use `xUnit` there.\n */\n yUnit?: string;\n /** Show the X axis (its ticks + title). Defaults to `true`. */\n showXAxis?: boolean;\n /** Show the Y axis (its ticks + title). Defaults to `true`. */\n showYAxis?: boolean;\n /** Format each X-axis tick value — e.g. `formatCompactNumber` or a `createTickFormatter(...)`. */\n xTickFormatter?: TickFormatter;\n /** Format each Y-axis tick value — e.g. `formatCompactNumber` or a `createTickFormatter(...)`. */\n yTickFormatter?: TickFormatter;\n /**\n * Rotate the X-axis tick labels by this many degrees. A negative angle (e.g.\n * `-45`) tilts them up toward the right; the tick anchor + axis height adjust\n * to keep long labels readable.\n */\n xAxisAngle?: number;\n /**\n * X-axis tick density — a recharts placement strategy, or a fixed number of\n * ticks to *skip* between two rendered ones (recharts `interval`: `0` shows\n * every tick, `1` every other one, `2` every third, …).\n */\n xAxisInterval?:\n | number\n | 'preserveStart'\n | 'preserveEnd'\n | 'preserveStartEnd'\n | 'equidistantPreserveStart';\n /**\n * Desired number of ticks on the value axis (recharts `tickCount`; a hint, not\n * exact). Applies to whichever axis holds the values — Y for most charts, X for\n * `BarChart` with `orientation=\"horizontal\"`.\n */\n yAxisTickCount?: number;\n /**\n * Value-axis domain preset. Applies to whichever axis holds the values — Y for\n * most charts, X for `BarChart` with `orientation=\"horizontal\"`.\n *\n * - `auto` — fit the data at both ends; the axis need not include 0.\n * - `zero` — anchor the axis at 0. This is also recharts' behavior when the\n * prop is omitted, so it's the explicit form of the default.\n * - `dataMin-dataMax` — tight to the data, with no padding.\n */\n yAxisDomain?: 'auto' | 'dataMin-dataMax' | 'zero';\n /** Draw grid lines dashed instead of solid. */\n gridDashed?: boolean;\n /** Show horizontal grid lines. Defaults to each chart's own default when unset. */\n gridHorizontal?: boolean;\n /** Show vertical grid lines. Defaults to each chart's own default when unset. */\n gridVertical?: boolean;\n}\n\n/**\n * Map a `yAxisDomain` preset to a recharts `domain`. Shared by all 7 cartesian\n * charts, and applied to whichever axis holds the values (X for horizontal bars).\n *\n * `undefined` (no preset) is left to recharts, whose default is `[0, 'auto']` —\n * i.e. already zero-anchored. That's why `auto` has to be spelled out as\n * `['auto', 'auto']`: passing `undefined` for it would silently anchor at 0 and\n * make the preset indistinguishable from `zero`.\n */\nexport function resolveAxisDomain(\n preset: 'auto' | 'dataMin-dataMax' | 'zero' | undefined\n): Readonly<[AxisDomainItem, AxisDomainItem]> | undefined {\n switch (preset) {\n case 'auto':\n return ['auto', 'auto'];\n case 'zero':\n return [0, 'auto'];\n case 'dataMin-dataMax':\n return ['dataMin', 'dataMax'];\n default:\n return undefined;\n }\n}\n\n/**\n * `text-anchor` for a rotated X tick row, so the tick's text ends (or begins) at\n * the tick it labels instead of straddling it. Shared by all 7 cartesian charts.\n *\n * A tick rotated anti-clockwise trails off to the lower left, so it must be\n * anchored at its end; a clockwise one trails to the lower right and is anchored\n * at its start. Only an *omitted* angle keeps recharts' own default (`middle`):\n * the branch is on `angle < 0`, so `angle={0}` anchors at `start` like any\n * clockwise rotation. A caller that normalises \"upright\" to `0` instead of\n * leaving `xAxisAngle` off therefore gets the rotated-tick treatment — here and\n * in `resolveXAxisHeight`.\n */\nexport function resolveRotatedTickAnchor(\n angle: number | undefined\n): 'start' | 'end' | undefined {\n if (angle == null) return undefined;\n return angle < 0 ? 'end' : 'start';\n}\n\n/**\n * Height to reserve for the X tick row, or `undefined` to keep recharts' own\n * default. Shared by all 7 cartesian charts.\n *\n * recharts' default 30 covers one upright tick row and nothing else, so a\n * rotated row (+20) and an axis title (+18) each need their own allowance. The\n * two are additive rather than exclusive — a chart can rotate its ticks *and*\n * title the axis, which a label-or-angle ternary under-allocates.\n *\n * The +20 keys off the angle being *present*, not non-zero, so `angle={0}`\n * reserves the rotated row's height as well — the same edge case\n * `resolveRotatedTickAnchor` has, kept consistent between the two.\n */\nexport function resolveXAxisHeight(\n label: string | undefined,\n angle: number | undefined\n): number | undefined {\n if (!label && angle == null) return undefined;\n return 30 + (angle != null ? 20 : 0) + (label ? 18 : 0);\n}\n\n/**\n * recharts `label` for an X axis title, placed below the tick row. Shared by all\n * 7 cartesian charts; `insideTop` is for a secondary scale on the top edge.\n */\nexport function resolveXAxisTitle(\n label: string | undefined,\n position: 'insideBottom' | 'insideTop' = 'insideBottom',\n offset = 0\n) {\n if (!label) return undefined;\n return { value: label, position, offset };\n}\n\n/**\n * recharts `label` for a rotated value-axis title, angled so it reads from the\n * outside in — upward on a left axis (the convention), downward on a right one,\n * where -90° would put the text's baseline against the plot. Shared by all 7\n * cartesian charts; only `ComposedChart` currently uses the right-hand side.\n */\nexport function resolveYAxisTitle(\n label: string | undefined,\n orientation: 'left' | 'right' = 'left'\n) {\n if (!label) return undefined;\n return {\n value: label,\n angle: orientation === 'left' ? -90 : 90,\n position:\n orientation === 'left' ? ('insideLeft' as const) : ('insideRight' as const),\n style: { textAnchor: 'middle' as const },\n };\n}\n\n/**\n * A category range on a cartesian chart: `from`/`to` accept either the\n * category's value (an `xKey` cell, e.g. `'Sep'`) or its 0-based row index. Both\n * ends are inclusive, and either can be omitted to run to that end of the data.\n * Shared by the charts that scope something to a slice of the categories — a\n * reference band, a per-series style override.\n */\nexport interface ChartCategoryRange {\n from?: string | number;\n to?: string | number;\n}\n\n/**\n * Resolve a category range to inclusive row indices. A bound is either the\n * category's own value (matched against the `xKey` cell) or a 0-based index;\n * an omitted bound runs to that end of the data. Returns `undefined` when the\n * data is empty or the range resolves to nothing (an unknown value, or a `to`\n * that lands before its `from`).\n *\n * Exported for unit tests; not part of the package's public API.\n */\nexport function resolveCategoryRange(\n range: ChartCategoryRange,\n data: ReadonlyArray<Record<string, unknown>>,\n xKey: string\n): [number, number] | undefined {\n if (data.length === 0) return undefined;\n\n const resolve = (bound: string | number | undefined, fallback: number) => {\n if (bound === undefined) return fallback;\n // A numeric bound is an index unless the categories are numbers themselves,\n // in which case matching a category value is the more useful reading.\n const asValue = data.findIndex((row) => row[xKey] === bound);\n if (asValue !== -1) return asValue;\n if (typeof bound === 'number' && Number.isInteger(bound)) {\n return bound >= 0 && bound < data.length ? bound : -1;\n }\n return -1;\n };\n\n const start = resolve(range.from, 0);\n const end = resolve(range.to, data.length - 1);\n if (start === -1 || end === -1 || start > end) return undefined;\n return [start, end];\n}\n\n/**\n * Which value axis a series is measured against, on a chart that can carry two.\n *\n * `secondary` is the opt-in: the second axis exists only when at least one series\n * asks for it, so a chart whose series all leave this unset keeps the single\n * shared scale — and its recharts output — exactly as before.\n */\nexport type ChartYAxisTarget = 'primary' | 'secondary';\n\n/**\n * Second value-axis props, for a chart whose series can be measured on two\n * independent scales. **Only `ComposedChart` implements these today.** It is the\n * one chart in the suite where two scales are unambiguous: its series already\n * differ in mark type, so a bar read against the left axis and a line against the\n * right can't be mistaken for two marks sharing one. On a single-mark chart (Bar,\n * Area) two scales invite comparing heights that aren't comparable, which is why\n * these props are not mixed into `CartesianChartProps`.\n *\n * The primary axis is configured through `CartesianChartProps` (`yAxisLabel`,\n * `yUnit`, `yTickFormatter`, `yAxisTickCount`, `yAxisDomain`, `showYAxis`); every\n * `secondary*` prop here is its counterpart on the second axis, so the two scales\n * format and bound themselves independently, and each is inert until a series\n * selects that axis.\n *\n * `yAxisOrientation` is the exception on both counts: it places the *primary*\n * axis, and it applies whether or not a second one exists. It lives here because\n * it is only meaningful once a chart can mirror a pair of axes — a single-axis\n * chart has no second side to give up.\n */\nexport interface SecondaryYAxisProps {\n /**\n * Which side the *primary* value axis sits on. Defaults to `left`. The secondary\n * axis always takes the opposite side. Unlike the `secondary*` props below, this\n * one applies to a single-scale chart too.\n *\n * Inert on a chart whose marks grow horizontally: the value axis is X there, so\n * the primary sits along the bottom edge and the secondary along the top, and\n * neither has a left/right side to pick.\n */\n yAxisOrientation?: 'left' | 'right';\n /**\n * Show the secondary axis's ticks + title. Defaults to `true`. Setting it false\n * keeps the second *scale* (series still measure against it) and only drops its\n * chrome — the same meaning `showYAxis` has for the primary axis.\n */\n showSecondaryYAxis?: boolean;\n /** Title rendered beside the secondary axis (rotated). */\n secondaryYAxisLabel?: string;\n /** Unit suffix appended to the secondary axis's tick values (recharts `unit`). */\n secondaryYUnit?: string;\n /** Format each secondary-axis tick value — the counterpart of `yTickFormatter`. */\n secondaryYTickFormatter?: TickFormatter;\n /** Desired number of ticks on the secondary axis (recharts `tickCount`; a hint, not exact). */\n secondaryYAxisTickCount?: number;\n /** Domain preset for the secondary axis — same presets as `yAxisDomain`. */\n secondaryYAxisDomain?: 'auto' | 'dataMin-dataMax' | 'zero';\n}\n\n/**\n * How a series interpolates between its points (recharts `type`), shared by the\n * Line and Area families so both offer the same set:\n *\n * - `linear` — straight segments between points.\n * - `monotone` — smoothed, without overshooting a point's value.\n * - `natural` — a natural cubic spline; smoother than `monotone`, but it may\n * overshoot, so read it as a trend rather than exact values.\n * - `basis` — a B-spline that is *not* required to pass through the points at\n * all; the smoothest option, and the least literal.\n * - `step` — right-angle segments, changing value at the midpoint between two\n * points; `stepBefore` changes at the leading point and `stepAfter` at the\n * trailing one.\n */\nexport type ChartCurveType =\n | 'linear'\n | 'monotone'\n | 'natural'\n | 'basis'\n | 'step'\n | 'stepBefore'\n | 'stepAfter';\n\n/** recharts animation easing curves. */\nexport type ChartAnimationEasing =\n | 'ease'\n | 'ease-in'\n | 'ease-out'\n | 'ease-in-out'\n | 'linear';\n\n/**\n * Entrance-animation props shared by *every* chart type (not just the cartesian\n * ones) — charts hardcode `isAnimationActive={false}` otherwise. All optional and\n * off by default, so an unset chart renders byte-identically to before (and its\n * VR baselines stay stable). Spread `resolveAnimation(props)` onto each series.\n */\nexport interface ChartAnimationProps {\n /** Enable entrance animation. Defaults to `false` (keeps output/baselines stable). */\n animate?: boolean;\n /** Animation duration in ms (recharts `animationDuration`). */\n animationDuration?: number;\n /** Delay before the animation starts, in ms (recharts `animationBegin`). */\n animationBegin?: number;\n /** Easing curve (recharts `animationEasing`). */\n animationEasing?: ChartAnimationEasing;\n}\n\n/** The resolved recharts animation props to spread onto a series/shape. */\nexport interface ResolvedAnimation {\n isAnimationActive: boolean | 'auto';\n animationDuration?: number;\n animationBegin?: number;\n animationEasing?: ChartAnimationEasing;\n}\n\n/**\n * Turn the shared `ChartAnimationProps` into the recharts series props. The\n * timing props only appear when given, so an unset chart spreads exactly\n * `{ isAnimationActive: false }` — the previous hardcoded value.\n *\n * `animate` maps to recharts' `'auto'`, not to a literal `true`: `'auto'` is the\n * only value that honors `prefers-reduced-motion` (and disables the animation in\n * SSR). recharts resolves it as\n * `isActive = isActiveProp === 'auto' ? !isSsr && !prefersReducedMotion : isActiveProp`,\n * so a literal `true` would force motion on users who asked for none — this\n * library has no other reduced-motion escape hatch.\n */\nexport function resolveAnimation(props: ChartAnimationProps): ResolvedAnimation {\n const { animate = false, animationDuration, animationBegin, animationEasing } =\n props;\n return {\n isAnimationActive: animate ? 'auto' : false,\n ...(animationDuration !== undefined ? { animationDuration } : {}),\n ...(animationBegin !== undefined ? { animationBegin } : {}),\n ...(animationEasing !== undefined ? { animationEasing } : {}),\n };\n}\n\n/**\n * Remove synthetic `_proj_*` series entries from a recharts payload array so\n * they don't appear in custom tooltip or legend content. Safe to call with\n * `undefined` — returns `undefined` unchanged.\n */\nexport function dropProjectionPayload<T extends { dataKey?: unknown }>(\n payload: readonly T[] | undefined\n): T[] | undefined {\n return payload?.filter(\n (item) =>\n !(typeof item.dataKey === 'string' && item.dataKey.startsWith('_proj_'))\n );\n}\n\n/** Where a data label sits on a cartesian series (recharts `LabelList` position). */\nexport type CartesianLabelPosition =\n | 'top'\n | 'bottom'\n | 'left'\n | 'right'\n | 'center'\n | 'insideTop'\n | 'insideBottom'\n | 'insideLeft'\n | 'insideRight'\n | 'insideStart'\n | 'insideEnd';\n\n/**\n * Where a data label sits on a *polar* series (Pie, RadialBar). Deliberately not\n * `CartesianLabelPosition`: recharts routes a polar label list through\n * `getAttrsOfPolarLabel` / `renderRadialLabel`, which understand only these\n * seven values — every cartesian-only position (`top`, `insideLeft`, …) silently\n * collapses to the same mid-radius placement, so offering them would advertise\n * distinctions that don't exist.\n */\nexport type PolarLabelPosition =\n | 'outside'\n | 'center'\n | 'centerTop'\n | 'centerBottom'\n | 'insideStart'\n | 'insideEnd'\n | 'end';\n\n/** Any position accepted by the suite's data labels. */\nexport type ChartLabelPosition = CartesianLabelPosition | PolarLabelPosition;\n\n/**\n * Data-label props shared by the charts that can annotate each point/segment with\n * its value. `labelPosition` stays per-chart (the valid position set differs by\n * family), but `showLabels` + `labelFormatter` are common. Label formatting reuses\n * the same `TickFormatter` type as the axes, so a label and its axis read the same.\n */\nexport interface ChartDataLabelProps {\n /** Render a value label on each data point/segment. Defaults to `false`. */\n showLabels?: boolean;\n /** Format each label value — pass the same formatter used on the value axis. */\n labelFormatter?: TickFormatter;\n}\n\n/**\n * Fill for a label that sits on the chart *surface* — i.e. an outside position\n * (`top`, `right`, polar `outside`). Inverts with the theme, so it only works\n * against the surface background.\n */\nexport const CHART_LABEL_FILL_CLASS = 'fill-[var(--ui-text-on-surface-primary)]!';\n\n/**\n * Fill for a label that sits on an *opaque series fill* — any `inside*`\n * position, a polar mid-radius/centroid placement, or a stacked bar/arc segment\n * (an area's fill is translucent, so it uses the on-surface token). The on-surface\n * token must not be used there: it resolves near-white in dark mode and drops to\n * ~1.6:1 against the saturated status/brand fills the charts colour series with,\n * failing the `accessibility/contrast` grammar rule (`must`, WCAG 1.4.3). This is\n * the same \"text on a strong colored surface\" token `Treemap` already uses; it is\n * white in both themes, so it holds up over every series colour.\n */\nexport const CHART_LABEL_FILL_ON_SERIES_CLASS =\n 'fill-[var(--ui-text-on-status-strong-neutral)]!';\n\nexport const CHART_LABEL_FONT_SIZE = 12;\n\n/**\n * Plot-area inset to apply when a Line/Area series carries outside data labels.\n * recharts hands those label lists `parentViewBox: undefined` (its own source\n * calls this out as a bug in `cartesian/Line.js`), so `getCartesianPosition`'s\n * `clamp` has nothing to clamp against and the first/last point's value is cut\n * off at the SVG edge. Widening the margin is the only lever a composition over\n * recharts has. `Bar` does pass a `parentViewBox`, so bar series don't need it.\n */\nexport const CHART_LABEL_MARGIN = {\n top: 16,\n right: 24,\n bottom: 5,\n left: 12,\n} as const;\n\n/** Positions that draw the label on top of the series fill rather than the surface. */\nconst ON_SERIES_LABEL_POSITIONS = new Set<ChartLabelPosition>([\n 'center',\n 'centerTop',\n 'centerBottom',\n 'insideTop',\n 'insideBottom',\n 'insideLeft',\n 'insideRight',\n 'insideStart',\n 'insideEnd',\n 'end',\n]);\n\n/**\n * Resolve where a cartesian value label sits, given the caller's override, the\n * layout, and the position of the series' growing end (`top` for a vertical bar\n * or a line/area point, `right` for a horizontal bar).\n *\n * The stacked branch is the reason this isn't inlined: a stacked segment has no\n * free space at its growing end — the next segment is drawn there — so a `top`\n * label lands *inside* its neighbour rather than over its own segment. Centring\n * it in its own segment is the readable placement; `resolveLabelFillClass` then\n * pairs it with the fill that has contrast over that family's series colour.\n */\nexport function resolveCartesianLabelPosition(options: {\n labelPosition?: CartesianLabelPosition;\n isStacked?: boolean;\n growingEnd?: CartesianLabelPosition;\n}): CartesianLabelPosition {\n const { labelPosition, isStacked = false, growingEnd = 'top' } = options;\n if (labelPosition !== undefined) return labelPosition;\n return isStacked ? 'center' : growingEnd;\n}\n\n/**\n * Pick the label fill that actually has contrast at `position`. Callers must pass\n * the *resolved* position (after their own default), never `undefined` for a\n * polar chart — recharts' polar fallback is a mid-radius placement, which is on\n * the fill even though no `inside*` value was given.\n *\n * Returns a `className`, not a `fill` attribute, and the utility is `!`-flagged:\n * the cartesian charts scope `[&_.recharts-label]:fill-foreground` on their\n * container to theme axis titles, and a `LabelList`'s text carries\n * `.recharts-label` too — so a CSS rule would quietly beat an SVG presentation\n * attribute and undo the contrast fix.\n *\n * `translucentSeries` opts a family out of the on-fill token: an area's fill is\n * a gradient (0.8 → 0.1 alpha) or a flat `fillOpacity`, so what sits behind the\n * label is the surface tinted by the series colour, not the series colour. The\n * white on-fill token disappears into it in light mode — the theme-inverting\n * on-surface token is the readable one there, and stays white in dark mode.\n */\nexport function resolveLabelFillClass(\n position: ChartLabelPosition,\n options: { translucentSeries?: boolean } = {}\n): string {\n if (options.translucentSeries) return CHART_LABEL_FILL_CLASS;\n return ON_SERIES_LABEL_POSITIONS.has(position)\n ? CHART_LABEL_FILL_ON_SERIES_CLASS\n : CHART_LABEL_FILL_CLASS;\n}\n\n/**\n * Adapt a `TickFormatter` to recharts' `LabelList` `formatter` prop, whose value\n * type is wider. Returns `undefined` when no formatter is given, so a label\n * renders its raw value. Lets labels reuse the same formatters as the value axis.\n *\n * Null/undefined is filtered out rather than forwarded: recharts builds a label\n * entry for *every* point including the null ones that `connectNulls` is meant to\n * bridge, and the tick formatters coerce (`formatCompactNumber(null)` is `\"0\"`,\n * `formatPercent(undefined)` is `\"undefined\"`) — which would paint a phantom\n * value over an intentional gap.\n */\nexport function toLabelFormatter(\n formatter: TickFormatter | undefined\n): ((value: unknown) => string) | undefined {\n if (!formatter) return undefined;\n return (value: unknown) =>\n value == null ? '' : formatter(value as number | string);\n}\n\n/** Where a reference line's caption sits relative to the rule. */\nexport type ChartReferenceLabelPosition =\n | 'top'\n | 'insideTopLeft'\n | 'insideTopRight'\n | 'insideBottomLeft'\n | 'insideBottomRight';\n\n/**\n * A dashed rule across the value axis — a target, a threshold, or a series\n * average. Shared by the cartesian charts that draw one (`BarChart`,\n * `LineChart`, `AreaChart`) so the config reads the same on all three.\n */\nexport interface ChartReferenceLine {\n /** Fixed position on the value axis. Takes precedence over `average`. */\n value?: number;\n /**\n * Draw the line at the mean of one series (a `dataKeys` entry) or, when\n * `true`, of every plotted series' values.\n */\n average?: boolean | string;\n /** Optional caption rendered alongside the line. */\n label?: string;\n /**\n * Where the caption sits. Defaults to the chart's own placement — move it\n * when the rule lands where the series already is and the two collide.\n */\n labelPosition?: ChartReferenceLabelPosition;\n}\n\n/**\n * Normalize the `referenceLine` prop — a single config or an array — to a list.\n * Every chart accepts both forms, so this keeps the render path uniform.\n */\nexport function toReferenceLineList(\n referenceLine: ChartReferenceLine | ChartReferenceLine[] | undefined\n): ChartReferenceLine[] {\n if (!referenceLine) return [];\n return Array.isArray(referenceLine) ? referenceLine : [referenceLine];\n}\n\n/**\n * Resolve a `referenceLine` config to a position on the value axis: a fixed\n * `value` wins; otherwise the mean of the requested series (a single `dataKeys`\n * entry, or all of them when `average` is `true`). Returns `undefined` when\n * there is nothing to draw (no config, or no numeric values to average) — the\n * caller skips the line rather than drawing it at 0.\n *\n * Rows are `unknown`-valued because `ComposedChart` types its data that way; the\n * body only ever reads cells that are numbers, so a wider row type costs nothing\n * and saves every caller a cast.\n */\nexport function resolveChartReferenceValue(\n referenceLine: ChartReferenceLine | undefined,\n data: ReadonlyArray<Record<string, unknown>>,\n dataKeys: string[]\n): number | undefined {\n if (!referenceLine) return undefined;\n if (typeof referenceLine.value === 'number') return referenceLine.value;\n if (!referenceLine.average) return undefined;\n\n const keys =\n typeof referenceLine.average === 'string'\n ? [referenceLine.average]\n : dataKeys;\n const nums = data.flatMap((row) =>\n keys\n .map((key) => row[key])\n .filter((value): value is number => typeof value === 'number')\n );\n if (nums.length === 0) return undefined;\n return nums.reduce((sum, n) => sum + n, 0) / nums.length;\n}\n\n/**\n * Stroke (and caption fill) of a reference rule. The muted text token, so the\n * rule reads as annotation rather than as another series — and stays legible in\n * both themes without competing with the series colors.\n */\nexport const CHART_REFERENCE_LINE_STROKE =\n 'var(--ui-text-on-surface-secondary)';\n\n/**\n * The recharts props that paint a reference rule: a dashed line in the muted\n * text token with an optional caption. Shared so the three charts that draw one\n * can't drift on the stroke, dash pattern, or caption styling.\n *\n * `labelPosition` is the chart's default placement; a config's own\n * `labelPosition` overrides it at the call site.\n *\n * `ifOverflow: 'extendDomain'` is part of the contract, not a detail: a target\n * above the data maximum is exactly the case a reference line is for, and\n * recharts' default (`discard`) would silently drop it.\n */\nexport function resolveReferenceLineProps(\n label: string | undefined,\n labelPosition: ChartReferenceLabelPosition = 'insideTopRight'\n) {\n return {\n stroke: CHART_REFERENCE_LINE_STROKE,\n strokeDasharray: '4 4',\n ifOverflow: 'extendDomain' as const,\n label: label\n ? {\n value: label,\n position: labelPosition,\n fill: CHART_REFERENCE_LINE_STROKE,\n fontSize: CHART_LABEL_FONT_SIZE,\n }\n : undefined,\n };\n}\n\n/**\n * Range-brush props for the cartesian charts that support one — `BarChart`,\n * `LineChart`, `AreaChart`, `ComposedChart`. Deliberately not folded into\n * `CartesianChartProps`: `ScatterChart` plots a continuous X (the brush indexes\n * rows, not values) and `Histogram` / `ConfidenceCone` render a derived series,\n * so a brush over their row order would slice the wrong thing.\n */\nexport interface ChartBrushProps {\n /**\n * Render a range brush beneath the chart — drag its handles (or the selected\n * window) to zoom the series to a slice of the data. Defaults to `false`.\n */\n showBrush?: boolean;\n /** Height of the brush strip in px. Defaults to 28. */\n brushHeight?: number;\n /**\n * Accessible name for the brush's two range handles. Both are focusable\n * `role=\"slider\"` elements, so they need one. Defaults to\n * `'Chart range selector'`.\n */\n brushAriaLabel?: string;\n}\n\n/**\n * Height of the brush strip. recharts' own default is 40, which eats an eighth\n * of a 320px-tall dashboard widget; 28 still fits the travellers and the\n * range captions.\n */\nexport const CHART_BRUSH_HEIGHT = 28;\n\n/**\n * Accessible name for the brush handles. recharts' own fallback is\n * `\"Min value: \".concat(start, \", Max value: \").concat(end)`, where both halves\n * come from a `name` property on the data row — a field none of our charts\n * require, so it announces \"Min value: undefined, Max value: undefined\". Always\n * pass `ariaLabel` to suppress it.\n */\nexport const CHART_BRUSH_ARIA_LABEL = 'Chart range selector';\n\n/** The resolved recharts brush props to spread onto a `<Brush>`. */\nexport interface ResolvedBrush {\n height: number;\n fill: string;\n stroke: string;\n travellerWidth: number;\n ariaLabel: string;\n}\n\n/**\n * Theme + size a recharts `<Brush>` from the shared `ChartBrushProps`.\n *\n * recharts derives the *whole* brush from two color props, so both have to be\n * chosen for every element they reach (`cartesian/Brush.js`):\n * - `fill` paints only the strip's background rect.\n * - `stroke` paints its border, the traveller handles' **fill**, the selected\n * window at 20% opacity, and the range caption text.\n *\n * Hence the mid-grey text token rather than the border one: the travellers are\n * solid `stroke`-colored rects with two hardcoded `#fff` grip lines drawn on\n * top, so a pale stroke would render the grips invisible in light mode. That\n * token is theme-stable (same grey in light and dark), and white grips read\n * against it either way.\n */\nexport function resolveBrushProps({\n brushHeight,\n brushAriaLabel = CHART_BRUSH_ARIA_LABEL,\n}: Omit<ChartBrushProps, 'showBrush'>): ResolvedBrush {\n return {\n // recharts drops the brush entirely when height <= 0, which would turn\n // `showBrush` into a silent no-op — treat a non-positive height as unset.\n height:\n brushHeight != null && brushHeight > 0 ? brushHeight : CHART_BRUSH_HEIGHT,\n fill: 'var(--ui-background-surface-secondary)',\n stroke: 'var(--ui-text-on-surface-secondary)',\n travellerWidth: 8,\n ariaLabel: brushAriaLabel,\n };\n}\n\nconst toNumber = (value: number | string): number | null => {\n // `Number('')` and `Number(' ')` are 0, not NaN — so blank strings have to be\n // rejected before the finite check, or an empty tick label would render \"0\".\n if (typeof value === 'string' && value.trim() === '') return null;\n const n = typeof value === 'number' ? value : Number(value);\n return Number.isFinite(n) ? n : null;\n};\n\n/**\n * Compact thousands/millions: `1234 → \"1.2K\"`, `1_500_000 → \"1.5M\"`. Non-numeric\n * values (including blank strings) pass through unchanged. Use for large-count\n * axes (revenue, users, …).\n *\n * Formats in `en`. For another locale use\n * `createTickFormatter({ notation: 'compact', maximumFractionDigits: 1 }, locale)`.\n */\nexport const formatCompactNumber: TickFormatter = (value) => {\n const n = toNumber(value);\n if (n === null) return String(value);\n return new Intl.NumberFormat('en', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(n);\n};\n\n/**\n * Append a percent sign to an already-scaled value: `41.8 → \"41.8%\"`. (The value\n * is treated as a percentage, not a 0–1 fraction.) Non-numeric values (including\n * blank strings) pass through.\n *\n * Appends a bare `%`, which is not how every locale writes it. For locale-correct\n * output on values that really are fractions, use\n * `createTickFormatter({ style: 'percent' }, locale)`.\n */\nexport const formatPercent: TickFormatter = (value) => {\n const n = toNumber(value);\n if (n === null) return String(value);\n return `${n}%`;\n};\n\n/**\n * Build a tick formatter from `Intl.NumberFormat` options — the escape hatch for\n * currency, fixed decimals, or a specific locale. E.g.\n * `createTickFormatter({ style: 'currency', currency: 'USD', notation: 'compact' })`\n * → `146500 → \"$147K\"`. Non-numeric values pass through unchanged.\n */\nexport function createTickFormatter(\n options: Intl.NumberFormatOptions,\n locale = 'en'\n): TickFormatter {\n const nf = new Intl.NumberFormat(locale, options);\n return (value) => {\n const n = toNumber(value);\n return n === null ? String(value) : nf.format(n);\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chart-format.js","names":[],"sources":["../../../../src/components/ui/chart/chart-format.ts"],"sourcesContent":["// Shared axis helpers for the cartesian chart components (BarChart, LineChart,\n// AreaChart, ComposedChart, ScatterChart, ConfidenceCone, Histogram). Three\n// concerns live here so every chart formats + toggles axes the same way rather\n// than each reimplementing it:\n//\n// 1. `CartesianChartProps` — the common axis knobs each cartesian chart mixes\n// into its own props (show/hide either axis, per-axis tick formatting).\n// 2. A small set of tick formatters (+ a factory) callers pass to\n// `xTickFormatter` / `yTickFormatter`.\n// 3. `resolveAxisDomain` — maps the `yAxisDomain` preset to a recharts\n// `domain`, shared so all 7 charts can't drift apart on it.\n//\n// These format only the axis *tick labels*; series colors and tokens are\n// unaffected. Formatters coerce to a number and pass non-numeric values through\n// untouched (a category axis stays readable if a formatter is applied to it).\n\nimport type { AxisDomainItem } from 'recharts';\n\nimport type { ChartTooltipContentType } from './chart';\n\n/** A recharts-compatible tick formatter: `(value, index) => label`. */\nexport type TickFormatter = (value: number | string, index?: number) => string;\n\n/**\n * Format the source values a chart may render on its X axis. The concrete ticks\n * remain recharts' responsibility; the displayed values become a stable layout\n * key so a rotated category row is remeasured when they change.\n */\nexport function resolveXAxisTickLabels(\n values: readonly unknown[],\n formatter?: TickFormatter\n): Array<string | number> {\n return values.map((value, index) => {\n if (typeof value !== 'string' && typeof value !== 'number') return '';\n return formatter ? formatter(value, index) : value;\n });\n}\n\n/**\n * Props shared by every cartesian chart (Bar, Line, Area, Composed, Scatter,\n * ConfidenceCone, Histogram) — chrome toggles, axis titles/units, tick\n * formatting/visibility, and grid trim. All optional. `showLegend` and `xUnit`\n * are deliberately *not* here: `showLegend` doesn't apply to `Histogram` and\n * `xUnit` only to charts with a numeric X axis, so they stay per-component.\n */\nexport interface CartesianChartProps {\n /** Render the CartesianGrid. Defaults to `true`. */\n showGrid?: boolean;\n /** Render the hover tooltip. Defaults to `true`. */\n showTooltip?: boolean;\n /**\n * Replace the default tooltip. Pass a configured `ChartTooltipContent` (from\n * this library) — e.g. with a `formatter` / `labelFormatter` — to customize it\n * without composing recharts yourself. Ignored when `showTooltip` is false.\n */\n tooltipContent?: ChartTooltipContentType;\n /** Title rendered beneath the X axis. */\n xAxisLabel?: string;\n /** Title rendered beside the Y axis (rotated). */\n yAxisLabel?: string;\n /**\n * Unit suffix appended to the Y axis's tick values (recharts `unit`) — applies\n * when the Y axis is the numeric one. `ComposedChart` with\n * `orientation=\"horizontal\"` puts the values on X instead; use `xUnit` there.\n */\n yUnit?: string;\n /** Show the X axis (its ticks + title). Defaults to `true`. */\n showXAxis?: boolean;\n /** Show the Y axis (its ticks + title). Defaults to `true`. */\n showYAxis?: boolean;\n /** Format each X-axis tick value — e.g. `formatCompactNumber` or a `createTickFormatter(...)`. */\n xTickFormatter?: TickFormatter;\n /** Format each Y-axis tick value — e.g. `formatCompactNumber` or a `createTickFormatter(...)`. */\n yTickFormatter?: TickFormatter;\n /**\n * Rotate the X-axis tick labels by this many degrees. A negative angle (e.g.\n * `-45`) tilts them up toward the right; the tick anchor + axis height adjust\n * to keep long labels readable.\n */\n xAxisAngle?: number;\n /**\n * X-axis tick density — a recharts placement strategy, or a fixed number of\n * ticks to *skip* between two rendered ones (recharts `interval`: `0` shows\n * every tick, `1` every other one, `2` every third, …).\n */\n xAxisInterval?:\n | number\n | 'preserveStart'\n | 'preserveEnd'\n | 'preserveStartEnd'\n | 'equidistantPreserveStart';\n /**\n * Desired number of ticks on the value axis (recharts `tickCount`; a hint, not\n * exact). Applies to whichever axis holds the values — Y for most charts, X for\n * `BarChart` with `orientation=\"horizontal\"`.\n */\n yAxisTickCount?: number;\n /**\n * Value-axis domain preset. Applies to whichever axis holds the values — Y for\n * most charts, X for `BarChart` with `orientation=\"horizontal\"`.\n *\n * - `auto` — fit the data at both ends; the axis need not include 0.\n * - `zero` — anchor the axis at 0. This is also recharts' behavior when the\n * prop is omitted, so it's the explicit form of the default.\n * - `dataMin-dataMax` — tight to the data, with no padding.\n */\n yAxisDomain?: 'auto' | 'dataMin-dataMax' | 'zero';\n /** Draw grid lines dashed instead of solid. */\n gridDashed?: boolean;\n /** Show horizontal grid lines. Defaults to each chart's own default when unset. */\n gridHorizontal?: boolean;\n /** Show vertical grid lines. Defaults to each chart's own default when unset. */\n gridVertical?: boolean;\n}\n\n/**\n * Map a `yAxisDomain` preset to a recharts `domain`. Shared by all 7 cartesian\n * charts, and applied to whichever axis holds the values (X for horizontal bars).\n *\n * `undefined` (no preset) is left to recharts, whose default is `[0, 'auto']` —\n * i.e. already zero-anchored. That's why `auto` has to be spelled out as\n * `['auto', 'auto']`: passing `undefined` for it would silently anchor at 0 and\n * make the preset indistinguishable from `zero`.\n */\nexport function resolveAxisDomain(\n preset: 'auto' | 'dataMin-dataMax' | 'zero' | undefined\n): Readonly<[AxisDomainItem, AxisDomainItem]> | undefined {\n switch (preset) {\n case 'auto':\n return ['auto', 'auto'];\n case 'zero':\n return [0, 'auto'];\n case 'dataMin-dataMax':\n return ['dataMin', 'dataMax'];\n default:\n return undefined;\n }\n}\n\n/**\n * `text-anchor` for a rotated X tick row, so the tick's text ends (or begins) at\n * the tick it labels instead of straddling it. Shared by all 7 cartesian charts.\n *\n * A tick rotated anti-clockwise trails off to the lower left, so it must be\n * anchored at its end; a clockwise one trails to the lower right and is anchored\n * at its start. Only an *omitted* angle keeps recharts' own default (`middle`):\n * the branch is on `angle < 0`, so `angle={0}` anchors at `start` like any\n * clockwise rotation. A caller that normalises \"upright\" to `0` instead of\n * leaving `xAxisAngle` off therefore gets the rotated-tick treatment — here and\n * in `resolveXAxisHeight`.\n */\nexport function resolveRotatedTickAnchor(\n angle: number | undefined\n): 'start' | 'end' | undefined {\n if (angle == null) return undefined;\n return angle < 0 ? 'end' : 'start';\n}\n\n/**\n * Height to reserve for the X tick row, or `undefined` to keep recharts' own\n * default. Shared by all 7 cartesian charts.\n *\n * recharts' default 30 covers one upright tick row and nothing else. The\n * browser refines the rotated-label fallback after layout through\n * `useXAxisLayout`, using the labels' actual SVG bounds rather than an\n * estimate based on character count.\n *\n * The +20 keys off the angle being *present*, not non-zero, so `angle={0}`\n * reserves the rotated row's height as well — the same edge case\n * `resolveRotatedTickAnchor` has, kept consistent between the two.\n */\nexport function resolveXAxisHeight(\n label: string | undefined,\n angle: number | undefined\n): number | undefined {\n if (!label && angle == null) return undefined;\n\n const titleHeight = label ? 18 : 0;\n if (angle == null) return 30 + titleHeight;\n\n // This modest first-pass allocation prevents a visible jump for ordinary\n // rotated labels. A longest-label estimate is exactly what creates the empty\n // area the post-layout measurement replaces.\n return 30 + 20 + titleHeight;\n}\n\n/**\n * recharts `label` for an X axis title, placed below the tick row. Shared by all\n * 7 cartesian charts; `insideTop` is for a secondary scale on the top edge.\n */\nexport function resolveXAxisTitle(\n label: string | undefined,\n position: 'insideBottom' | 'insideTop' = 'insideBottom',\n offset = 0\n) {\n if (!label) return undefined;\n return { value: label, position, offset };\n}\n\n/**\n * recharts `label` for a rotated value-axis title, angled so it reads from the\n * outside in — upward on a left axis (the convention), downward on a right one,\n * where -90° would put the text's baseline against the plot. Shared by all 7\n * cartesian charts; only `ComposedChart` currently uses the right-hand side.\n */\nexport function resolveYAxisTitle(\n label: string | undefined,\n orientation: 'left' | 'right' = 'left'\n) {\n if (!label) return undefined;\n return {\n value: label,\n angle: orientation === 'left' ? -90 : 90,\n position:\n orientation === 'left' ? ('insideLeft' as const) : ('insideRight' as const),\n style: { textAnchor: 'middle' as const },\n };\n}\n\n/**\n * A category range on a cartesian chart: `from`/`to` accept either the\n * category's value (an `xKey` cell, e.g. `'Sep'`) or its 0-based row index. Both\n * ends are inclusive, and either can be omitted to run to that end of the data.\n * Shared by the charts that scope something to a slice of the categories — a\n * reference band, a per-series style override.\n */\nexport interface ChartCategoryRange {\n from?: string | number;\n to?: string | number;\n}\n\n/**\n * Resolve a category range to inclusive row indices. A bound is either the\n * category's own value (matched against the `xKey` cell) or a 0-based index;\n * an omitted bound runs to that end of the data. Returns `undefined` when the\n * data is empty or the range resolves to nothing (an unknown value, or a `to`\n * that lands before its `from`).\n *\n * Exported for unit tests; not part of the package's public API.\n */\nexport function resolveCategoryRange(\n range: ChartCategoryRange,\n data: ReadonlyArray<Record<string, unknown>>,\n xKey: string\n): [number, number] | undefined {\n if (data.length === 0) return undefined;\n\n const resolve = (bound: string | number | undefined, fallback: number) => {\n if (bound === undefined) return fallback;\n // A numeric bound is an index unless the categories are numbers themselves,\n // in which case matching a category value is the more useful reading.\n const asValue = data.findIndex((row) => row[xKey] === bound);\n if (asValue !== -1) return asValue;\n if (typeof bound === 'number' && Number.isInteger(bound)) {\n return bound >= 0 && bound < data.length ? bound : -1;\n }\n return -1;\n };\n\n const start = resolve(range.from, 0);\n const end = resolve(range.to, data.length - 1);\n if (start === -1 || end === -1 || start > end) return undefined;\n return [start, end];\n}\n\n/**\n * Which value axis a series is measured against, on a chart that can carry two.\n *\n * `secondary` is the opt-in: the second axis exists only when at least one series\n * asks for it, so a chart whose series all leave this unset keeps the single\n * shared scale — and its recharts output — exactly as before.\n */\nexport type ChartYAxisTarget = 'primary' | 'secondary';\n\n/**\n * Second value-axis props, for a chart whose series can be measured on two\n * independent scales. **Only `ComposedChart` implements these today.** It is the\n * one chart in the suite where two scales are unambiguous: its series already\n * differ in mark type, so a bar read against the left axis and a line against the\n * right can't be mistaken for two marks sharing one. On a single-mark chart (Bar,\n * Area) two scales invite comparing heights that aren't comparable, which is why\n * these props are not mixed into `CartesianChartProps`.\n *\n * The primary axis is configured through `CartesianChartProps` (`yAxisLabel`,\n * `yUnit`, `yTickFormatter`, `yAxisTickCount`, `yAxisDomain`, `showYAxis`); every\n * `secondary*` prop here is its counterpart on the second axis, so the two scales\n * format and bound themselves independently, and each is inert until a series\n * selects that axis.\n *\n * `yAxisOrientation` is the exception on both counts: it places the *primary*\n * axis, and it applies whether or not a second one exists. It lives here because\n * it is only meaningful once a chart can mirror a pair of axes — a single-axis\n * chart has no second side to give up.\n */\nexport interface SecondaryYAxisProps {\n /**\n * Which side the *primary* value axis sits on. Defaults to `left`. The secondary\n * axis always takes the opposite side. Unlike the `secondary*` props below, this\n * one applies to a single-scale chart too.\n *\n * Inert on a chart whose marks grow horizontally: the value axis is X there, so\n * the primary sits along the bottom edge and the secondary along the top, and\n * neither has a left/right side to pick.\n */\n yAxisOrientation?: 'left' | 'right';\n /**\n * Show the secondary axis's ticks + title. Defaults to `true`. Setting it false\n * keeps the second *scale* (series still measure against it) and only drops its\n * chrome — the same meaning `showYAxis` has for the primary axis.\n */\n showSecondaryYAxis?: boolean;\n /** Title rendered beside the secondary axis (rotated). */\n secondaryYAxisLabel?: string;\n /** Unit suffix appended to the secondary axis's tick values (recharts `unit`). */\n secondaryYUnit?: string;\n /** Format each secondary-axis tick value — the counterpart of `yTickFormatter`. */\n secondaryYTickFormatter?: TickFormatter;\n /** Desired number of ticks on the secondary axis (recharts `tickCount`; a hint, not exact). */\n secondaryYAxisTickCount?: number;\n /** Domain preset for the secondary axis — same presets as `yAxisDomain`. */\n secondaryYAxisDomain?: 'auto' | 'dataMin-dataMax' | 'zero';\n}\n\n/**\n * How a series interpolates between its points (recharts `type`), shared by the\n * Line and Area families so both offer the same set:\n *\n * - `linear` — straight segments between points.\n * - `monotone` — smoothed, without overshooting a point's value.\n * - `natural` — a natural cubic spline; smoother than `monotone`, but it may\n * overshoot, so read it as a trend rather than exact values.\n * - `basis` — a B-spline that is *not* required to pass through the points at\n * all; the smoothest option, and the least literal.\n * - `step` — right-angle segments, changing value at the midpoint between two\n * points; `stepBefore` changes at the leading point and `stepAfter` at the\n * trailing one.\n */\nexport type ChartCurveType =\n | 'linear'\n | 'monotone'\n | 'natural'\n | 'basis'\n | 'step'\n | 'stepBefore'\n | 'stepAfter';\n\n/** recharts animation easing curves. */\nexport type ChartAnimationEasing =\n | 'ease'\n | 'ease-in'\n | 'ease-out'\n | 'ease-in-out'\n | 'linear';\n\n/**\n * Entrance-animation props shared by *every* chart type (not just the cartesian\n * ones) — charts hardcode `isAnimationActive={false}` otherwise. All optional and\n * off by default, so an unset chart renders byte-identically to before (and its\n * VR baselines stay stable). Spread `resolveAnimation(props)` onto each series.\n */\nexport interface ChartAnimationProps {\n /** Enable entrance animation. Defaults to `false` (keeps output/baselines stable). */\n animate?: boolean;\n /** Animation duration in ms (recharts `animationDuration`). */\n animationDuration?: number;\n /** Delay before the animation starts, in ms (recharts `animationBegin`). */\n animationBegin?: number;\n /** Easing curve (recharts `animationEasing`). */\n animationEasing?: ChartAnimationEasing;\n}\n\n/** The resolved recharts animation props to spread onto a series/shape. */\nexport interface ResolvedAnimation {\n isAnimationActive: boolean | 'auto';\n animationDuration?: number;\n animationBegin?: number;\n animationEasing?: ChartAnimationEasing;\n}\n\n/**\n * Turn the shared `ChartAnimationProps` into the recharts series props. The\n * timing props only appear when given, so an unset chart spreads exactly\n * `{ isAnimationActive: false }` — the previous hardcoded value.\n *\n * `animate` maps to recharts' `'auto'`, not to a literal `true`: `'auto'` is the\n * only value that honors `prefers-reduced-motion` (and disables the animation in\n * SSR). recharts resolves it as\n * `isActive = isActiveProp === 'auto' ? !isSsr && !prefersReducedMotion : isActiveProp`,\n * so a literal `true` would force motion on users who asked for none — this\n * library has no other reduced-motion escape hatch.\n */\nexport function resolveAnimation(props: ChartAnimationProps): ResolvedAnimation {\n const { animate = false, animationDuration, animationBegin, animationEasing } =\n props;\n return {\n isAnimationActive: animate ? 'auto' : false,\n ...(animationDuration !== undefined ? { animationDuration } : {}),\n ...(animationBegin !== undefined ? { animationBegin } : {}),\n ...(animationEasing !== undefined ? { animationEasing } : {}),\n };\n}\n\n/**\n * Remove synthetic `_proj_*` series entries from a recharts payload array so\n * they don't appear in custom tooltip or legend content. Safe to call with\n * `undefined` — returns `undefined` unchanged.\n */\nexport function dropProjectionPayload<T extends { dataKey?: unknown }>(\n payload: readonly T[] | undefined\n): T[] | undefined {\n return payload?.filter(\n (item) =>\n !(typeof item.dataKey === 'string' && item.dataKey.startsWith('_proj_'))\n );\n}\n\n/** Where a data label sits on a cartesian series (recharts `LabelList` position). */\nexport type CartesianLabelPosition =\n | 'top'\n | 'bottom'\n | 'left'\n | 'right'\n | 'center'\n | 'insideTop'\n | 'insideBottom'\n | 'insideLeft'\n | 'insideRight'\n | 'insideStart'\n | 'insideEnd';\n\n/**\n * Where a data label sits on a *polar* series (Pie, RadialBar). Deliberately not\n * `CartesianLabelPosition`: recharts routes a polar label list through\n * `getAttrsOfPolarLabel` / `renderRadialLabel`, which understand only these\n * seven values — every cartesian-only position (`top`, `insideLeft`, …) silently\n * collapses to the same mid-radius placement, so offering them would advertise\n * distinctions that don't exist.\n */\nexport type PolarLabelPosition =\n | 'outside'\n | 'center'\n | 'centerTop'\n | 'centerBottom'\n | 'insideStart'\n | 'insideEnd'\n | 'end';\n\n/** Any position accepted by the suite's data labels. */\nexport type ChartLabelPosition = CartesianLabelPosition | PolarLabelPosition;\n\n/**\n * Data-label props shared by the charts that can annotate each point/segment with\n * its value. `labelPosition` stays per-chart (the valid position set differs by\n * family), but `showLabels` + `labelFormatter` are common. Label formatting reuses\n * the same `TickFormatter` type as the axes, so a label and its axis read the same.\n */\nexport interface ChartDataLabelProps {\n /** Render a value label on each data point/segment. Defaults to `false`. */\n showLabels?: boolean;\n /** Format each label value — pass the same formatter used on the value axis. */\n labelFormatter?: TickFormatter;\n}\n\n/**\n * Fill for a label that sits on the chart *surface* — i.e. an outside position\n * (`top`, `right`, polar `outside`). Inverts with the theme, so it only works\n * against the surface background.\n */\nexport const CHART_LABEL_FILL_CLASS = 'fill-[var(--ui-text-on-surface-primary)]!';\n\n/**\n * Fill for a label that sits on an *opaque series fill* — any `inside*`\n * position, a polar mid-radius/centroid placement, or a stacked bar/arc segment\n * (an area's fill is translucent, so it uses the on-surface token). The on-surface\n * token must not be used there: it resolves near-white in dark mode and drops to\n * ~1.6:1 against the saturated status/brand fills the charts colour series with,\n * failing the `accessibility/contrast` grammar rule (`must`, WCAG 1.4.3). This is\n * the same \"text on a strong colored surface\" token `Treemap` already uses; it is\n * white in both themes, so it holds up over every series colour.\n */\nexport const CHART_LABEL_FILL_ON_SERIES_CLASS =\n 'fill-[var(--ui-text-on-status-strong-neutral)]!';\n\nexport const CHART_LABEL_FONT_SIZE = 12;\n\n/**\n * Plot-area inset to apply when a Line/Area series carries outside data labels.\n * recharts hands those label lists `parentViewBox: undefined` (its own source\n * calls this out as a bug in `cartesian/Line.js`), so `getCartesianPosition`'s\n * `clamp` has nothing to clamp against and the first/last point's value is cut\n * off at the SVG edge. Widening the margin is the only lever a composition over\n * recharts has. `Bar` does pass a `parentViewBox`, so bar series don't need it.\n */\nexport const CHART_LABEL_MARGIN = {\n top: 16,\n right: 24,\n bottom: 5,\n left: 12,\n} as const;\n\n/** Positions that draw the label on top of the series fill rather than the surface. */\nconst ON_SERIES_LABEL_POSITIONS = new Set<ChartLabelPosition>([\n 'center',\n 'centerTop',\n 'centerBottom',\n 'insideTop',\n 'insideBottom',\n 'insideLeft',\n 'insideRight',\n 'insideStart',\n 'insideEnd',\n 'end',\n]);\n\n/**\n * Resolve where a cartesian value label sits, given the caller's override, the\n * layout, and the position of the series' growing end (`top` for a vertical bar\n * or a line/area point, `right` for a horizontal bar).\n *\n * The stacked branch is the reason this isn't inlined: a stacked segment has no\n * free space at its growing end — the next segment is drawn there — so a `top`\n * label lands *inside* its neighbour rather than over its own segment. Centring\n * it in its own segment is the readable placement; `resolveLabelFillClass` then\n * pairs it with the fill that has contrast over that family's series colour.\n */\nexport function resolveCartesianLabelPosition(options: {\n labelPosition?: CartesianLabelPosition;\n isStacked?: boolean;\n growingEnd?: CartesianLabelPosition;\n}): CartesianLabelPosition {\n const { labelPosition, isStacked = false, growingEnd = 'top' } = options;\n if (labelPosition !== undefined) return labelPosition;\n return isStacked ? 'center' : growingEnd;\n}\n\n/**\n * Pick the label fill that actually has contrast at `position`. Callers must pass\n * the *resolved* position (after their own default), never `undefined` for a\n * polar chart — recharts' polar fallback is a mid-radius placement, which is on\n * the fill even though no `inside*` value was given.\n *\n * Returns a `className`, not a `fill` attribute, and the utility is `!`-flagged:\n * the cartesian charts scope `[&_.recharts-label]:fill-foreground` on their\n * container to theme axis titles, and a `LabelList`'s text carries\n * `.recharts-label` too — so a CSS rule would quietly beat an SVG presentation\n * attribute and undo the contrast fix.\n *\n * `translucentSeries` opts a family out of the on-fill token: an area's fill is\n * a gradient (0.8 → 0.1 alpha) or a flat `fillOpacity`, so what sits behind the\n * label is the surface tinted by the series colour, not the series colour. The\n * white on-fill token disappears into it in light mode — the theme-inverting\n * on-surface token is the readable one there, and stays white in dark mode.\n */\nexport function resolveLabelFillClass(\n position: ChartLabelPosition,\n options: { translucentSeries?: boolean } = {}\n): string {\n if (options.translucentSeries) return CHART_LABEL_FILL_CLASS;\n return ON_SERIES_LABEL_POSITIONS.has(position)\n ? CHART_LABEL_FILL_ON_SERIES_CLASS\n : CHART_LABEL_FILL_CLASS;\n}\n\n/**\n * Adapt a `TickFormatter` to recharts' `LabelList` `formatter` prop, whose value\n * type is wider. Returns `undefined` when no formatter is given, so a label\n * renders its raw value. Lets labels reuse the same formatters as the value axis.\n *\n * Null/undefined is filtered out rather than forwarded: recharts builds a label\n * entry for *every* point including the null ones that `connectNulls` is meant to\n * bridge, and the tick formatters coerce (`formatCompactNumber(null)` is `\"0\"`,\n * `formatPercent(undefined)` is `\"undefined\"`) — which would paint a phantom\n * value over an intentional gap.\n */\nexport function toLabelFormatter(\n formatter: TickFormatter | undefined\n): ((value: unknown) => string) | undefined {\n if (!formatter) return undefined;\n return (value: unknown) =>\n value == null ? '' : formatter(value as number | string);\n}\n\n/** Where a reference line's caption sits relative to the rule. */\nexport type ChartReferenceLabelPosition =\n | 'top'\n | 'insideTopLeft'\n | 'insideTopRight'\n | 'insideBottomLeft'\n | 'insideBottomRight';\n\n/**\n * A dashed rule across the value axis — a target, a threshold, or a series\n * average. Shared by the cartesian charts that draw one (`BarChart`,\n * `LineChart`, `AreaChart`) so the config reads the same on all three.\n */\nexport interface ChartReferenceLine {\n /** Fixed position on the value axis. Takes precedence over `average`. */\n value?: number;\n /**\n * Draw the line at the mean of one series (a `dataKeys` entry) or, when\n * `true`, of every plotted series' values.\n */\n average?: boolean | string;\n /** Optional caption rendered alongside the line. */\n label?: string;\n /**\n * Where the caption sits. Defaults to the chart's own placement — move it\n * when the rule lands where the series already is and the two collide.\n */\n labelPosition?: ChartReferenceLabelPosition;\n}\n\n/**\n * Normalize the `referenceLine` prop — a single config or an array — to a list.\n * Every chart accepts both forms, so this keeps the render path uniform.\n */\nexport function toReferenceLineList(\n referenceLine: ChartReferenceLine | ChartReferenceLine[] | undefined\n): ChartReferenceLine[] {\n if (!referenceLine) return [];\n return Array.isArray(referenceLine) ? referenceLine : [referenceLine];\n}\n\n/**\n * Resolve a `referenceLine` config to a position on the value axis: a fixed\n * `value` wins; otherwise the mean of the requested series (a single `dataKeys`\n * entry, or all of them when `average` is `true`). Returns `undefined` when\n * there is nothing to draw (no config, or no numeric values to average) — the\n * caller skips the line rather than drawing it at 0.\n *\n * Rows are `unknown`-valued because `ComposedChart` types its data that way; the\n * body only ever reads cells that are numbers, so a wider row type costs nothing\n * and saves every caller a cast.\n */\nexport function resolveChartReferenceValue(\n referenceLine: ChartReferenceLine | undefined,\n data: ReadonlyArray<Record<string, unknown>>,\n dataKeys: string[]\n): number | undefined {\n if (!referenceLine) return undefined;\n if (typeof referenceLine.value === 'number') return referenceLine.value;\n if (!referenceLine.average) return undefined;\n\n const keys =\n typeof referenceLine.average === 'string'\n ? [referenceLine.average]\n : dataKeys;\n const nums = data.flatMap((row) =>\n keys\n .map((key) => row[key])\n .filter((value): value is number => typeof value === 'number')\n );\n if (nums.length === 0) return undefined;\n return nums.reduce((sum, n) => sum + n, 0) / nums.length;\n}\n\n/**\n * Stroke (and caption fill) of a reference rule. The muted text token, so the\n * rule reads as annotation rather than as another series — and stays legible in\n * both themes without competing with the series colors.\n */\nexport const CHART_REFERENCE_LINE_STROKE =\n 'var(--ui-text-on-surface-secondary)';\n\n/**\n * The recharts props that paint a reference rule: a dashed line in the muted\n * text token with an optional caption. Shared so the three charts that draw one\n * can't drift on the stroke, dash pattern, or caption styling.\n *\n * `labelPosition` is the chart's default placement; a config's own\n * `labelPosition` overrides it at the call site.\n *\n * `ifOverflow: 'extendDomain'` is part of the contract, not a detail: a target\n * above the data maximum is exactly the case a reference line is for, and\n * recharts' default (`discard`) would silently drop it.\n */\nexport function resolveReferenceLineProps(\n label: string | undefined,\n labelPosition: ChartReferenceLabelPosition = 'insideTopRight'\n) {\n return {\n stroke: CHART_REFERENCE_LINE_STROKE,\n strokeDasharray: '4 4',\n ifOverflow: 'extendDomain' as const,\n label: label\n ? {\n value: label,\n position: labelPosition,\n fill: CHART_REFERENCE_LINE_STROKE,\n fontSize: CHART_LABEL_FONT_SIZE,\n }\n : undefined,\n };\n}\n\n/**\n * Range-brush props for the cartesian charts that support one — `BarChart`,\n * `LineChart`, `AreaChart`, `ComposedChart`. Deliberately not folded into\n * `CartesianChartProps`: `ScatterChart` plots a continuous X (the brush indexes\n * rows, not values) and `Histogram` / `ConfidenceCone` render a derived series,\n * so a brush over their row order would slice the wrong thing.\n */\nexport interface ChartBrushProps {\n /**\n * Render a range brush beneath the chart — drag its handles (or the selected\n * window) to zoom the series to a slice of the data. Defaults to `false`.\n */\n showBrush?: boolean;\n /** Height of the brush strip in px. Defaults to 28. */\n brushHeight?: number;\n /**\n * Accessible name for the brush's two range handles. Both are focusable\n * `role=\"slider\"` elements, so they need one. Defaults to\n * `'Chart range selector'`.\n */\n brushAriaLabel?: string;\n}\n\n/**\n * Height of the brush strip. recharts' own default is 40, which eats an eighth\n * of a 320px-tall dashboard widget; 28 still fits the travellers and the\n * range captions.\n */\nexport const CHART_BRUSH_HEIGHT = 28;\n\n/**\n * Accessible name for the brush handles. recharts' own fallback is\n * `\"Min value: \".concat(start, \", Max value: \").concat(end)`, where both halves\n * come from a `name` property on the data row — a field none of our charts\n * require, so it announces \"Min value: undefined, Max value: undefined\". Always\n * pass `ariaLabel` to suppress it.\n */\nexport const CHART_BRUSH_ARIA_LABEL = 'Chart range selector';\n\n/** The resolved recharts brush props to spread onto a `<Brush>`. */\nexport interface ResolvedBrush {\n height: number;\n fill: string;\n stroke: string;\n travellerWidth: number;\n ariaLabel: string;\n}\n\n/**\n * Theme + size a recharts `<Brush>` from the shared `ChartBrushProps`.\n *\n * recharts derives the *whole* brush from two color props, so both have to be\n * chosen for every element they reach (`cartesian/Brush.js`):\n * - `fill` paints only the strip's background rect.\n * - `stroke` paints its border, the traveller handles' **fill**, the selected\n * window at 20% opacity, and the range caption text.\n *\n * Hence the mid-grey text token rather than the border one: the travellers are\n * solid `stroke`-colored rects with two hardcoded `#fff` grip lines drawn on\n * top, so a pale stroke would render the grips invisible in light mode. That\n * token is theme-stable (same grey in light and dark), and white grips read\n * against it either way.\n */\nexport function resolveBrushProps({\n brushHeight,\n brushAriaLabel = CHART_BRUSH_ARIA_LABEL,\n}: Omit<ChartBrushProps, 'showBrush'>): ResolvedBrush {\n return {\n // recharts drops the brush entirely when height <= 0, which would turn\n // `showBrush` into a silent no-op — treat a non-positive height as unset.\n height:\n brushHeight != null && brushHeight > 0 ? brushHeight : CHART_BRUSH_HEIGHT,\n fill: 'var(--ui-background-surface-secondary)',\n stroke: 'var(--ui-text-on-surface-secondary)',\n travellerWidth: 8,\n ariaLabel: brushAriaLabel,\n };\n}\n\nconst toNumber = (value: number | string): number | null => {\n // `Number('')` and `Number(' ')` are 0, not NaN — so blank strings have to be\n // rejected before the finite check, or an empty tick label would render \"0\".\n if (typeof value === 'string' && value.trim() === '') return null;\n const n = typeof value === 'number' ? value : Number(value);\n return Number.isFinite(n) ? n : null;\n};\n\n/**\n * Compact thousands/millions: `1234 → \"1.2K\"`, `1_500_000 → \"1.5M\"`. Non-numeric\n * values (including blank strings) pass through unchanged. Use for large-count\n * axes (revenue, users, …).\n *\n * Formats in `en`. For another locale use\n * `createTickFormatter({ notation: 'compact', maximumFractionDigits: 1 }, locale)`.\n */\nexport const formatCompactNumber: TickFormatter = (value) => {\n const n = toNumber(value);\n if (n === null) return String(value);\n return new Intl.NumberFormat('en', {\n notation: 'compact',\n maximumFractionDigits: 1,\n }).format(n);\n};\n\n/**\n * Append a percent sign to an already-scaled value: `41.8 → \"41.8%\"`. (The value\n * is treated as a percentage, not a 0–1 fraction.) Non-numeric values (including\n * blank strings) pass through.\n *\n * Appends a bare `%`, which is not how every locale writes it. For locale-correct\n * output on values that really are fractions, use\n * `createTickFormatter({ style: 'percent' }, locale)`.\n */\nexport const formatPercent: TickFormatter = (value) => {\n const n = toNumber(value);\n if (n === null) return String(value);\n return `${n}%`;\n};\n\n/**\n * Build a tick formatter from `Intl.NumberFormat` options — the escape hatch for\n * currency, fixed decimals, or a specific locale. E.g.\n * `createTickFormatter({ style: 'currency', currency: 'USD', notation: 'compact' })`\n * → `146500 → \"$147K\"`. Non-numeric values pass through unchanged.\n */\nexport function createTickFormatter(\n options: Intl.NumberFormatOptions,\n locale = 'en'\n): TickFormatter {\n const nf = new Intl.NumberFormat(locale, options);\n return (value) => {\n const n = toNumber(value);\n return n === null ? String(value) : nf.format(n);\n };\n}\n"],"mappings":";AA4BA,SAAgB,EACd,GACA,GACwB;CACxB,OAAO,EAAO,KAAK,GAAO,MACpB,OAAO,KAAU,YAAY,OAAO,KAAU,WAAiB,KAC5D,IAAY,EAAU,GAAO,CAAK,IAAI,CAC9C;AACH;AAwFA,SAAgB,EACd,GACwD;CACxD,QAAQ,GAAR;EACE,KAAK,QACH,OAAO,CAAC,QAAQ,MAAM;EACxB,KAAK,QACH,OAAO,CAAC,GAAG,MAAM;EACnB,KAAK,mBACH,OAAO,CAAC,WAAW,SAAS;EAC9B,SACE;CACJ;AACF;AAcA,SAAgB,EACd,GAC6B;CACzB,SAAS,MACb,OAAO,IAAQ,IAAI,QAAQ;AAC7B;AAeA,SAAgB,EACd,GACA,GACoB;CACpB,IAAI,CAAC,KAAS,KAAS,MAAM;CAE7B,IAAM,IAAc,IAAQ,KAAK;CAMjC,OALI,KAAS,OAAa,KAAK,IAKxB,KAAU;AACnB;AAMA,SAAgB,EACd,GACA,IAAyC,gBACzC,IAAS,GACT;CACK,OACL,OAAO;EAAE,OAAO;EAAO;EAAU;CAAO;AAC1C;AAQA,SAAgB,EACd,GACA,IAAgC,QAChC;CACK,OACL,OAAO;EACL,OAAO;EACP,OAAO,MAAgB,SAAS,MAAM;EACtC,UACE,MAAgB,SAAU,eAA0B;EACtD,OAAO,EAAE,YAAY,SAAkB;CACzC;AACF;AAuBA,SAAgB,EACd,GACA,GACA,GAC8B;CAC9B,IAAI,EAAK,WAAW,GAAG;CAEvB,IAAM,KAAW,GAAoC,MAAqB;EACxE,IAAI,MAAU,KAAA,GAAW,OAAO;EAGhC,IAAM,IAAU,EAAK,WAAW,MAAQ,EAAI,OAAU,CAAK;EAK3D,OAJI,MAAY,KACZ,OAAO,KAAU,YAAY,OAAO,UAAU,CAAK,KAC9C,KAAS,KAAK,IAAQ,EAAK,SAAS,IAAQ,KAF1B;CAK7B,GAEM,IAAQ,EAAQ,EAAM,MAAM,CAAC,GAC7B,IAAM,EAAQ,EAAM,IAAI,EAAK,SAAS,CAAC;CACzC,YAAU,MAAM,MAAQ,MAAM,IAAQ,IAC1C,OAAO,CAAC,GAAO,CAAG;AACpB;AAgIA,SAAgB,EAAiB,GAA+C;CAC9E,IAAM,EAAE,aAAU,IAAO,sBAAmB,mBAAgB,uBAC1D;CACF,OAAO;EACL,mBAAmB,IAAU,SAAS;EACtC,GAAI,MAAsB,KAAA,IAAoC,CAAC,IAAzB,EAAE,qBAAkB;EAC1D,GAAI,MAAmB,KAAA,IAAiC,CAAC,IAAtB,EAAE,kBAAe;EACpD,GAAI,MAAoB,KAAA,IAAkC,CAAC,IAAvB,EAAE,mBAAgB;CACxD;AACF;AAOA,SAAgB,EACd,GACiB;CACjB,OAAO,GAAS,QACb,MACC,EAAE,OAAO,EAAK,WAAY,YAAY,EAAK,QAAQ,WAAW,QAAQ,EAC1E;AACF;AAsDA,IAAa,IAAyB,6CAYzB,IACX,mDAEW,IAAwB,IAUxB,IAAqB;CAChC,KAAK;CACL,OAAO;CACP,QAAQ;CACR,MAAM;AACR,GAGM,oBAA4B,IAAI,IAAwB;CAC5D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAaD,SAAgB,EAA8B,GAInB;CACzB,IAAM,EAAE,kBAAe,eAAY,IAAO,gBAAa,UAAU;CAEjE,OADI,MAAkB,KAAA,IACf,IAAY,WAAW,IADU;AAE1C;AAoBA,SAAgB,EACd,GACA,IAA2C,CAAC,GACpC;CAER,OADI,EAAQ,oBAA0B,IAC/B,EAA0B,IAAI,CAAQ,IACzC,IACA;AACN;AAaA,SAAgB,EACd,GAC0C;CACrC,OACL,QAAQ,MACN,KAAS,OAAO,KAAK,EAAU,CAAwB;AAC3D;AAoCA,SAAgB,EACd,GACsB;CAEtB,OADK,IACE,MAAM,QAAQ,CAAa,IAAI,IAAgB,CAAC,CAAa,IADzC,CAAC;AAE9B;AAaA,SAAgB,EACd,GACA,GACA,GACoB;CACpB,IAAI,CAAC,GAAe;CACpB,IAAI,OAAO,EAAc,SAAU,UAAU,OAAO,EAAc;CAClE,IAAI,CAAC,EAAc,SAAS;CAE5B,IAAM,IACJ,OAAO,EAAc,WAAY,WAC7B,CAAC,EAAc,OAAO,IACtB,GACA,IAAO,EAAK,SAAS,MACzB,EACG,KAAK,MAAQ,EAAI,EAAI,CAAC,CACtB,QAAQ,MAA2B,OAAO,KAAU,QAAQ,CACjE;CACI,MAAK,WAAW,GACpB,OAAO,EAAK,QAAQ,GAAK,MAAM,IAAM,GAAG,CAAC,IAAI,EAAK;AACpD;AAOA,IAAa,IACX;AAcF,SAAgB,EACd,GACA,IAA6C,kBAC7C;CACA,OAAO;EACL,QAAQ;EACR,iBAAiB;EACjB,YAAY;EACZ,OAAO,IACH;GACE,OAAO;GACP,UAAU;GACV,MAAM;GACN,UAAA;EACF,IACA,KAAA;CACN;AACF;AA8BA,IAAa,IAAqB,IASrB,IAAyB;AA0BtC,SAAgB,EAAkB,EAChC,gBACA,oBAAiB,KACmC;CACpD,OAAO;EAGL,QACE,KAAe,QAAQ,IAAc,IAAI,IAAA;EAC3C,MAAM;EACN,QAAQ;EACR,gBAAgB;EAChB,WAAW;CACb;AACF;AAEA,IAAM,KAAY,MAA0C;CAG1D,IAAI,OAAO,KAAU,YAAY,EAAM,KAAK,MAAM,IAAI,OAAO;CAC7D,IAAM,IAAI,OAAO,KAAU,WAAW,IAAQ,OAAO,CAAK;CAC1D,OAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC,GAUa,KAAsC,MAAU;CAC3D,IAAM,IAAI,EAAS,CAAK;CAExB,OADI,MAAM,OAAa,OAAO,CAAK,IAC5B,IAAI,KAAK,aAAa,MAAM;EACjC,UAAU;EACV,uBAAuB;CACzB,CAAC,CAAC,CAAC,OAAO,CAAC;AACb,GAWa,KAAgC,MAAU;CACrD,IAAM,IAAI,EAAS,CAAK;CAExB,OADI,MAAM,OAAa,OAAO,CAAK,IAC5B,GAAG,EAAE;AACd;AAQA,SAAgB,EACd,GACA,IAAS,MACM;CACf,IAAM,IAAK,IAAI,KAAK,aAAa,GAAQ,CAAO;CAChD,QAAQ,MAAU;EAChB,IAAM,IAAI,EAAS,CAAK;EACxB,OAAO,MAAM,OAAO,OAAO,CAAK,IAAI,EAAG,OAAO,CAAC;CACjD;AACF"}
|
|
@@ -79,23 +79,25 @@ function m() {
|
|
|
79
79
|
if (!e) throw Error("useChart must be used within a <ChartContainer />");
|
|
80
80
|
return e;
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
let
|
|
82
|
+
var h = a.forwardRef(function({ id: n, className: r, children: i, config: o, palette: u = t, ...d }, m) {
|
|
83
|
+
let h = a.useId(), _ = `chart-${n || h.replace(/:/g, "")}`, v = a.useMemo(() => f(o, u), [o, u]);
|
|
84
84
|
return /* @__PURE__ */ s(p.Provider, {
|
|
85
|
-
value: { config:
|
|
85
|
+
value: { config: v },
|
|
86
86
|
children: /* @__PURE__ */ c("div", {
|
|
87
|
+
ref: m,
|
|
87
88
|
id: n,
|
|
88
89
|
"data-slot": "chart",
|
|
89
|
-
"data-chart":
|
|
90
|
+
"data-chart": _,
|
|
90
91
|
className: e("[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden [&_.recharts-trapezoid[stroke='#fff']]:stroke-transparent", "[&_.recharts-brush-traveller:focus-visible]:[outline:3px_solid_var(--ui-focus-primary)] [&_.recharts-brush-traveller:focus-visible]:[outline-offset:1px]", "[&_.recharts-surface]:[direction:ltr] rtl:[&_.recharts-surface_foreignObject]:[direction:rtl]", r),
|
|
91
92
|
...d,
|
|
92
93
|
children: [/* @__PURE__ */ s(g, {
|
|
93
|
-
id:
|
|
94
|
-
config:
|
|
94
|
+
id: _,
|
|
95
|
+
config: v
|
|
95
96
|
}), /* @__PURE__ */ s(l.ResponsiveContainer, { children: i })]
|
|
96
97
|
})
|
|
97
98
|
});
|
|
98
|
-
}
|
|
99
|
+
});
|
|
100
|
+
h.displayName = "ChartContainer";
|
|
99
101
|
var g = ({ id: e, config: t }) => {
|
|
100
102
|
let n = Object.entries(t);
|
|
101
103
|
if (!n.length) return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chart.js","names":[],"sources":["../../../../src/components/ui/chart/chart.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport * as RechartsPrimitive from 'recharts';\nimport type { LegendPayload } from 'recharts/types/component/DefaultLegendContent';\nimport {\n NameType,\n Payload,\n ValueType,\n} from 'recharts/types/component/DefaultTooltipContent';\nimport type { Props as LegendProps } from 'recharts/types/component/Legend';\nimport { TooltipContentProps } from 'recharts/types/component/Tooltip';\n\nimport { cn } from '@/lib/utils';\nimport {\n CHART_DEFAULT_PALETTE,\n CHART_DIVERGING_TOKENS,\n CHART_STATUS_TOKENS,\n resolveSeriesColor,\n type ChartDivergingSide,\n type ChartPalette,\n type ChartSeriesTone,\n} from './chart-palette';\n\n/**\n * Normalize a config key / data name to a CSS-safe custom-property fragment.\n *\n * A `--color-<key>` custom property is invalid when `key` contains a space or\n * other ASCII special character — the browser discards it, and the element\n * falls back to SVG's default black. This function replaces every invalid ASCII\n * character with a hyphen, collapses runs, and trims leading/trailing hyphens.\n *\n * Non-ASCII code points (U+0080+) are **preserved**: CSS Syntax Level 3\n * §4.3.7 classifies them as valid ident-code-points, so `--color-日本` and\n * `--color-Москва` are legal custom properties. Replacing them would collapse\n * all-non-ASCII keys to the empty string and silently make distinct series\n * collide on `--color-`.\n *\n * For an already-CSS-safe key (ASCII letters, digits, `-`, `_`) the output is\n * identical (identity), so existing consumers are unaffected. If the entire\n * input collapses to an empty string (e.g. all ASCII special characters),\n * `--color-` is produced — an edge case with no worse outcome than before.\n *\n * The dev-mode warning fires **once per unique unsanitized key** per page load,\n * so render loops (hover, re-render) do not flood the console.\n */\n// Tracks keys that have already triggered a dev warning — one per page load.\nconst _toCssKeyWarned = new Set<string>();\n\nexport function toCssKey(key: string): string {\n // Only strip ASCII characters that are not valid CSS ident chars.\n // U+0080+ are preserved — they are valid per CSS Syntax L3 §4.3.7.\n const safe = key\n .replace(/[^-a-zA-Z0-9_-]/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n\n if (process.env.NODE_ENV !== 'production' && safe !== key && !_toCssKeyWarned.has(key)) {\n _toCssKeyWarned.add(key);\n console.warn(\n `[ui-react] Chart config key \"${key}\" is not CSS-safe and was sanitized to \"${safe}\". ` +\n `Use CSS-safe keys (ASCII letters, digits, hyphens, underscores, or non-ASCII characters) to avoid this.`\n );\n }\n\n return safe;\n}\n\nexport type ChartConfig = {\n [k in string]: {\n label?: React.ReactNode;\n icon?: React.ComponentType;\n /**\n * Which stop of the container's `palette` this series paints with. Left\n * unset, it is assigned from the series' position. See `ChartSeriesTone`.\n *\n * There is deliberately no free-form `color` here: every series colour\n * comes from the chart's palette, so a chart cannot paint a hue the design\n * system does not define. Pick a different palette on `ChartContainer`, or\n * a different stop of it with this.\n */\n tone?: ChartSeriesTone;\n };\n};\n\n/**\n * A `ChartConfig` with every series' palette colour filled in. This is what the\n * container puts on the context, so the plot, the tooltip rows and the legend\n * markers all read the same resolved colour.\n */\nexport type ResolvedChartConfig = {\n [k in string]: ChartConfig[string] & { color: string };\n};\n\n/**\n * Resolve each series' colour out of `palette` — in the palette's defined\n * order, honouring any `tone` a series named.\n *\n * Exported for tests and for the widget editor, which needs the same resolved\n * map to detect duplicate colours (`findDuplicateTones`) without re-deriving it.\n */\nexport function resolveChartColors(\n config: ChartConfig,\n palette: ChartPalette\n): ResolvedChartConfig {\n const entries = Object.entries(config);\n\n // An aliased series (`tone: { sameAs }`) doesn't take a stop of its own — it\n // is the same metric drawn a second way, so it must not consume a colour or\n // shift the series after it along the palette.\n const isAlias = (item: ChartConfig[string]) => Boolean(item.tone?.sameAs);\n const resolved: Record<string, ChartConfig[string] & { color: string }> = {};\n\n // Warn about overrides that are meaningless under a diverging palette.\n // resolveSeriesColor issues the same warnings for every other palette type,\n // but the diverging branch below bypasses it, so we replicate the check here\n // to preserve the documented dev-warning contract (see ChartSeriesTone).\n const warnDivergingMismatch = (tone: ChartSeriesTone | undefined) => {\n if (process.env.NODE_ENV !== 'production' && tone) {\n if (tone.status) {\n console.warn(\n `[ui-react] A chart series names a status (\"${tone.status}\") under the \"diverging\" palette, which has no status tones. Ignoring.`\n );\n } else if (typeof tone.slot === 'number') {\n console.warn(\n `[ui-react] A chart series pins slot ${tone.slot} under the \"diverging\" palette, whose stops are a ramp and are not individually selectable. Ignoring.`\n );\n }\n }\n };\n\n if (palette.type === 'diverging') {\n const interleavedStops = CHART_DIVERGING_TOKENS[palette.pair];\n // Derive each side's three stops from the interleaved ramp (a at even\n // indices, b at odd indices): a3, a2, a1 and b3, b2, b1.\n const sideStops: Record<ChartDivergingSide, readonly string[]> = {\n a: [interleavedStops[0], interleavedStops[2], interleavedStops[4]],\n b: [interleavedStops[1], interleavedStops[3], interleavedStops[5]],\n };\n\n // First pass: assign sided series — group by side, each group walks its\n // side's three stops strongest-first, wrapping if there are more than three.\n const sideCounters: Record<ChartDivergingSide, number> = { a: 0, b: 0 };\n const takenStops = new Set<string>();\n for (const [key, item] of entries) {\n if (isAlias(item) || !item.tone?.side) continue;\n warnDivergingMismatch(item.tone);\n const side = item.tone.side;\n const stops = sideStops[side];\n const stop = stops[sideCounters[side]++ % stops.length];\n takenStops.add(stop);\n resolved[key] = { ...item, color: stop };\n }\n\n // Second pass: assign un-sided series from the remaining interleaved stops,\n // skipping any stop already claimed by a sided series. Fall back to the\n // full interleaved ramp if all stops were consumed.\n const available = interleavedStops.filter((s) => !takenStops.has(s));\n const pool = available.length > 0 ? available : interleavedStops;\n let unsidedIdx = 0;\n for (const [key, item] of entries) {\n if (isAlias(item) || item.tone?.side) continue;\n warnDivergingMismatch(item.tone);\n resolved[key] = { ...item, color: pool[unsidedIdx++ % pool.length] };\n }\n } else {\n let index = 0;\n for (const [key, item] of entries) {\n if (isAlias(item)) continue;\n resolved[key] = {\n ...item,\n color: resolveSeriesColor(palette, { index: index++, tone: item.tone }),\n };\n }\n }\n\n // An alias may point at another alias, in any order, so a single pass over\n // the aliases can miss a chain that is fully resolvable. Keep sweeping the\n // pending set until nothing more resolves; what is left is a cycle or a\n // target that isn't in this config at all.\n const pending = new Map<string, string>();\n for (const [key, item] of entries) {\n if (isAlias(item)) {\n pending.set(key, item.tone!.sameAs as string);\n }\n }\n\n let changed = true;\n while (changed && pending.size > 0) {\n changed = false;\n for (const [key, target] of pending) {\n const targetColor = resolved[target]?.color;\n if (targetColor !== undefined) {\n resolved[key] = { ...config[key], color: targetColor };\n pending.delete(key);\n changed = true;\n }\n }\n }\n\n for (const [key, target] of pending) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[ui-react] Chart series \"${key}\" aliases \"${target}\", which is not a palette-assigned series in this config. Falling back to neutral.`\n );\n }\n resolved[key] = { ...config[key], color: CHART_STATUS_TOKENS.neutral };\n }\n\n // Rebuilt in the config's own order: the passes above visit non-aliases\n // first, and callers (legend order, `Object.entries` consumers) rely on the\n // order they wrote.\n return Object.fromEntries(entries.map(([key]) => [key, resolved[key]]));\n}\n\ntype ChartContextProps = {\n config: ResolvedChartConfig;\n};\n\nexport type ChartTooltipContentProps = Partial<\n TooltipContentProps<ValueType, NameType>\n> & {\n className?: string;\n hideLabel?: boolean;\n hideIndicator?: boolean;\n nameKey?: string;\n labelKey?: string;\n labelFormatter?: (\n label: TooltipContentProps<number, string>['label'],\n payload: TooltipContentProps<number, string>['payload']\n ) => React.ReactNode;\n formatter?: (\n value: number | string,\n name: string,\n item: Payload<number | string, string>,\n index: number,\n payload: ReadonlyArray<Payload<number | string, string>>\n ) => React.ReactNode;\n labelClassName?: string;\n color?: string;\n};\n\nexport type ChartLegendContentProps = {\n className?: string;\n hideIcon?: boolean;\n verticalAlign?: LegendProps['verticalAlign'];\n payload?: LegendPayload[];\n nameKey?: string;\n /**\n * The series config the entries take their labels and icons from. Defaults to\n * the enclosing `ChartContainer`'s — pass it only to render the legend *outside*\n * the container, which a chart type has to do when the renderer can't lay a\n * legend out inside its plot (see `Treemap`).\n */\n config?: ChartConfig;\n /**\n * `default` — the current horizontal wrap layout (one row of chips).\n * `list` — a vertical list where each entry is `[dot] [label flex-1] [value]`,\n * matching the Figma donut/radial widget legend. Use alongside a manually-built\n * `payload` passed from outside the chart's recharts composition.\n */\n variant?: 'default' | 'list';\n /**\n * When `variant=\"list\"`, the field on `payload[i].payload` to display as the\n * right-hand value. Typically the chart's `dataKey` (the numeric series field).\n */\n valueKey?: string;\n /**\n * Format the value shown in each list entry. When omitted the raw value is\n * stringified with `String()`.\n */\n valueFormatter?: (value: string | number) => string;\n /**\n * Extra Tailwind class(es) applied to the right-hand value in a list entry,\n * merged on top of the default colour (`text-[var(--ui-text-on-surface-link-idle)]`).\n * Pass a replacement colour class to override the default; other utilities\n * (e.g. `tabular-nums`) are additive and leave the default colour in place.\n */\n valueClassName?: string;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nfunction useChart() {\n const context = React.useContext(ChartContext);\n\n if (!context) {\n throw new Error('useChart must be used within a <ChartContainer />');\n }\n\n return context;\n}\n\nexport interface ChartContainerProps extends React.ComponentProps<'div'> {\n /**\n * The series this chart draws, keyed by data field.\n *\n * Declare exactly what the chart plots. A stop is assigned by an entry's\n * position here, not by which series end up rendered, so an entry for a\n * series the chart doesn't draw still consumes a colour and shifts the ones\n * that follow it. (That is deliberate: a series then keeps its colour when a\n * sibling is toggled off, instead of the whole chart recolouring.)\n */\n config: ChartConfig;\n /**\n * The dataviz palette this chart's series are painted from. Series take a\n * stop of it in the palette's defined order, or the one their `tone` names.\n * Defaults to `categorical` — there is no \"no palette\" state.\n */\n palette?: ChartPalette;\n children: React.ComponentProps<\n typeof RechartsPrimitive.ResponsiveContainer\n >['children'];\n}\n\nfunction ChartContainer({\n id,\n className,\n children,\n config,\n palette = CHART_DEFAULT_PALETTE,\n ...props\n}: ChartContainerProps) {\n const uniqueId = React.useId();\n const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;\n // Resolved once and shared with the context, so the tooltip rows and legend\n // markers read the same colors the plot paints with.\n const resolvedConfig = React.useMemo(\n () => resolveChartColors(config, palette),\n [config, palette]\n );\n\n return (\n <ChartContext.Provider value={{ config: resolvedConfig }}>\n <div\n id={id}\n data-slot=\"chart\"\n data-chart={chartId}\n // The `[stroke='#fff']` rules undo recharts' hardcoded white outlines —\n // on pie/radial sectors, line dots and funnel trapezoids. They don't\n // follow the theme, so in dark mode they read as a light hairline around\n // every segment.\n className={cn(\n \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden [&_.recharts-trapezoid[stroke='#fff']]:stroke-transparent\",\n // A `<Brush>` traveller is the one `.recharts-layer` that is a real\n // control (`role=\"slider\"`, tabbable, arrow-key driven), so the blanket\n // `outline-hidden` above would leave it with no focus indicator. Restore\n // one — the `:focus-visible` pseudo-class outranks that rule, so this\n // wins regardless of the order Tailwind emits them in. An `outline`\n // rather than the usual `ring`: box-shadow doesn't paint on SVG children.\n '[&_.recharts-brush-traveller:focus-visible]:[outline:3px_solid_var(--ui-focus-primary)] [&_.recharts-brush-traveller:focus-visible]:[outline-offset:1px]',\n // recharts anchors its axis tick text with the *direction-relative*\n // SVG keywords (`text-anchor: start|end`, see `getTickTextAnchor` in\n // `CartesianAxis`), while placing every mark at a computed physical\n // coordinate. Inheriting `direction: rtl` therefore flips only the\n // text: an `end`-anchored Y tick mirrors about its anchor and lands\n // inside the plot (measured: +24px to +39px), rotated ticks shift\n // ~+15px, and a `ReferenceLine` label at the right edge overflows the\n // surface. Pinning the surface keeps the plot's own coordinate system\n // consistent with the geometry recharts computed for it. The chrome\n // that *should* mirror — tooltip and legend — is HTML outside\n // `.recharts-surface`, so it is unaffected.\n //\n // `Treemap` is the exception: its cell labels are HTML in a\n // `<foreignObject>` *inside* the surface, put there precisely so they\n // mirror with the page (see `treemap.tsx`). That is ordinary flow\n // layout, not a computed SVG coordinate, so the pin has to stop at the\n // `<foreignObject>` boundary and hand the page's direction back.\n '[&_.recharts-surface]:[direction:ltr] rtl:[&_.recharts-surface_foreignObject]:[direction:rtl]',\n className\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={resolvedConfig} />\n <RechartsPrimitive.ResponsiveContainer>\n {children}\n </RechartsPrimitive.ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n}\n\nconst ChartStyle = ({\n id,\n config,\n}: {\n id: string;\n config: ResolvedChartConfig;\n}) => {\n const entries = Object.entries(config);\n\n if (!entries.length) {\n return null;\n }\n\n // One block, no light/dark split: every colour is now a `--ui-dataviz-*`\n // token, and those are `light-dark()` pairs that follow `color-scheme`\n // themselves. The old per-theme `[data-theme='dark']` duplication existed\n // only for hand-written `theme: { light, dark }` config entries, which the\n // palette replaced.\n //\n // Rendered as a text child (not dangerouslySetInnerHTML): React sets it via\n // textContent, which the browser does not HTML-parse, so a `</style>` in a\n // colour can't break out of the tag.\n if (process.env.NODE_ENV !== 'production') {\n const seen = new Map<string, string>();\n for (const [key] of entries) {\n const safe = toCssKey(key);\n const prior = seen.get(safe);\n if (prior !== undefined) {\n console.warn(\n `[ui-react] Chart config keys \"${prior}\" and \"${key}\" both sanitize to \"--color-${safe}\". ` +\n `One will overwrite the other — use unique CSS-safe keys.`\n );\n } else {\n seen.set(safe, key);\n }\n }\n }\n\n return (\n <style>\n {`[data-chart=${id}] {\\n${entries\n .map(([key, itemConfig]) => ` --color-${toCssKey(key)}: ${itemConfig.color};`)\n .join('\\n')}\\n}`}\n </style>\n );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\n/** The `content` a caller passes to override the default tooltip (recharts' `Tooltip['content']`). */\nexport type ChartTooltipContentType = React.ComponentProps<\n typeof ChartTooltip\n>['content'];\n\n/**\n * What `getPayloadConfigFromPayload` resolves a payload item to — the lookup can\n * miss, so every consumer has to cope with `undefined`. Derived from the helper\n * rather than restated, so the two can't drift.\n */\ntype ChartItemConfig = ReturnType<typeof getPayloadConfigFromPayload>;\n\n/** One row of the tooltip's payload, after the `active`/empty guard. */\ntype ChartTooltipItem = NonNullable<\n ChartTooltipContentProps['payload']\n>[number];\n\n/**\n * One tooltip row. Its own three-way branching (caller formatter, config icon,\n * default dot) is what drove the nesting in `ChartTooltipContent`; as a\n * component the map body reads as a single element.\n */\nfunction ChartTooltipRow({\n item,\n index,\n payload,\n itemConfig,\n indicatorColor,\n hideIndicator,\n formatter,\n}: {\n item: ChartTooltipItem;\n index: number;\n payload: NonNullable<ChartTooltipContentProps['payload']>;\n itemConfig: ChartItemConfig;\n indicatorColor?: string;\n hideIndicator: boolean;\n formatter: ChartTooltipContentProps['formatter'];\n}) {\n return (\n <div className=\"flex w-full flex-wrap items-center gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\">\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, payload)\n ) : (\n <>\n {itemConfig?.icon ? (\n <itemConfig.icon />\n ) : (\n !hideIndicator && (\n // A row is always dotted, whatever marker the legend\n // gives that series.\n <div\n className=\"h-2.5 w-2.5 shrink-0 rounded-full\"\n style={{ backgroundColor: indicatorColor }}\n />\n )\n )}\n {/*\n * `gap-4` puts a floor under the name/value separation.\n * `justify-between` alone only separates them while the\n * container has free space to distribute — which the tooltip's\n * `min-w-[8rem]` guarantees for a short value and not for a\n * long one (a currency `tickFormatter`, a `labelFormatter`\n * with units, a value plus its share), where the two would\n * otherwise butt up against each other. A minimum, so a row\n * with slack renders as before.\n */}\n <div className=\"flex flex-1 items-center justify-between gap-4 leading-none\">\n <span className=\"text-muted-foreground\">\n {itemConfig?.label || item.name}\n </span>\n {item.value != null && (\n <span className=\"font-medium tabular-nums text-foreground\">\n {item.value.toLocaleString()}\n </span>\n )}\n </div>\n </>\n )}\n </div>\n );\n}\n\nfunction ChartTooltipContent({\n active,\n payload,\n label,\n className,\n hideLabel = false,\n hideIndicator = false,\n labelFormatter,\n formatter,\n labelClassName,\n color,\n nameKey,\n labelKey,\n}: ChartTooltipContentProps) {\n const { config } = useChart();\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null;\n }\n\n const [item] = payload;\n const key = `${labelKey || item?.dataKey || item?.name || 'value'}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n const value = (() => {\n const v =\n !labelKey && typeof label === 'string'\n ? (config[label as keyof typeof config]?.label ?? label)\n : itemConfig?.label;\n\n return typeof v === 'string' || typeof v === 'number' ? v : undefined;\n })();\n\n if (labelFormatter) {\n return (\n <div className={cn('font-medium', labelClassName)}>\n {labelFormatter(value, payload)}\n </div>\n );\n }\n\n if (!value) {\n return null;\n }\n\n return <div className={cn('font-medium', labelClassName)}>{value}</div>;\n }, [\n label,\n labelFormatter,\n payload,\n hideLabel,\n labelClassName,\n config,\n labelKey,\n ]);\n\n if (!active || !payload?.length) {\n return null;\n }\n\n return (\n <div\n className={cn(\n 'grid min-w-[8rem] items-start gap-1.5 rounded-[var(--ui-tooltip-container-border-radius)] border border-border bg-background px-[var(--ui-tooltip-container-padding-x)] py-[var(--ui-tooltip-container-padding-y)] text-xs text-foreground shadow-md',\n className\n )}\n >\n {tooltipLabel}\n <div className=\"grid gap-1.5\">\n {payload.map((item, index) => {\n const key = `${nameKey || item.name || item.dataKey || 'value'}`;\n\n return (\n <ChartTooltipRow\n key={key}\n item={item}\n index={index}\n payload={payload}\n itemConfig={getPayloadConfigFromPayload(config, item, key)}\n indicatorColor={color || item.payload.fill || item.color}\n hideIndicator={hideIndicator}\n formatter={formatter}\n />\n );\n })}\n </div>\n </div>\n );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\n/**\n * One entry in the `variant=\"list\"` legend: `[dot] [label flex-1] [value]`.\n *\n * The dot is always a circle, regardless of the series' recharts type — the\n * list layout is only used for polar charts (donut, radial) where the fill\n * marker matches the slice/arc color.\n */\nfunction ChartLegendListEntry({\n item,\n itemConfig,\n valueKey,\n valueFormatter,\n valueClassName,\n}: {\n item: LegendPayload;\n itemConfig: ChartItemConfig;\n valueKey?: string;\n valueFormatter?: (value: string | number) => string;\n valueClassName?: string;\n}) {\n const raw = valueKey\n ? (item.payload as Record<string, unknown> | undefined)?.[valueKey]\n : undefined;\n const displayValue =\n raw != null\n ? valueFormatter\n ? valueFormatter(raw as string | number)\n : String(raw)\n : undefined;\n\n return (\n <div className=\"flex w-full items-start gap-2 overflow-hidden\">\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n {itemConfig?.icon ? (\n <itemConfig.icon />\n ) : (\n <div\n className=\"h-2.5 w-2.5 shrink-0 rounded-full\"\n style={{ backgroundColor: item.color }}\n />\n )}\n <span className=\"min-w-0 flex-1 truncate text-xs text-foreground\">\n {itemConfig?.label ?? item.value}\n </span>\n </div>\n {displayValue != null && (\n <span\n className={cn(\n 'shrink-0 text-xs font-semibold text-[var(--ui-text-on-surface-link-idle)]',\n valueClassName\n )}\n >\n {displayValue}\n </span>\n )}\n </div>\n );\n}\n\n/**\n * One legend entry. Split out so the marker it has to derive from the payload\n * sits next to the markup that uses it, instead of above a nested map body.\n */\nfunction ChartLegendEntry({\n item,\n itemConfig,\n hideIcon,\n}: {\n item: LegendPayload;\n itemConfig: ChartItemConfig;\n hideIcon: boolean;\n}) {\n return (\n <div\n className={cn(\n 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground'\n )}\n >\n {itemConfig?.icon && !hideIcon ? (\n <itemConfig.icon />\n ) : (\n <div\n className=\"h-2.5 w-2.5 shrink-0 rounded-full\"\n style={{ backgroundColor: item.color }}\n />\n )}\n {/*\n * Falls back to the series key, the way the tooltip row and the\n * treemap's on-cell label both do. `label` is optional on a\n * `ChartConfig` entry, and `getPayloadConfigFromPayload` can miss\n * entirely, so without this an entry renders as a marker with no\n * text — worst on the charts whose legend is the only place a series\n * is named.\n */}\n {itemConfig?.label ?? item.value}\n </div>\n );\n}\n\nfunction ChartLegendContent({\n className,\n hideIcon = false,\n payload,\n verticalAlign = 'bottom',\n nameKey,\n config: configFromProps,\n variant = 'default',\n valueKey,\n valueFormatter,\n valueClassName,\n}: ChartLegendContentProps) {\n // The context read directly rather than through `useChart()`: a caller that\n // passes its own `config` is rendering the legend outside the container, where\n // the context is absent by design rather than by mistake. Every other caller\n // still gets `useChart()`'s error — the legend has no way to label itself\n // without a config, and failing loudly beats rendering an empty row.\n const contextConfig = React.useContext(ChartContext)?.config;\n const config = configFromProps ?? contextConfig;\n\n if (!config) {\n throw new Error(\n 'ChartLegendContent must be used within a <ChartContainer /> or given a `config` prop'\n );\n }\n\n if (!payload?.length) {\n return null;\n }\n\n if (variant === 'list') {\n return (\n <div data-slot=\"chart-legend\" className={cn('flex flex-col', className)}>\n {payload.map((item) => {\n const key = `${nameKey || item.dataKey || 'value'}`;\n return (\n <ChartLegendListEntry\n key={item.value}\n item={item}\n itemConfig={getPayloadConfigFromPayload(config, item, key)}\n valueKey={valueKey}\n valueFormatter={valueFormatter}\n valueClassName={valueClassName}\n />\n );\n })}\n </div>\n );\n }\n\n return (\n <div\n data-slot=\"chart-legend\"\n className={cn(\n // Wraps rather than overflowing: a legend with many entries (a treemap's\n // one-per-tile, a pie's one-per-slice) is wider than the chart on a narrow\n // surface, and a row that can't wrap paints past the chart's edge. The\n // column gap is unchanged, so a legend that already fits on one row keeps\n // its exact layout.\n 'flex flex-wrap items-center justify-center gap-x-6 gap-y-2',\n verticalAlign === 'top' ? 'pb-3' : 'pt-3',\n className\n )}\n >\n {payload.map((item) => {\n const key = `${nameKey || item.dataKey || 'value'}`;\n\n return (\n <ChartLegendEntry\n key={item.value}\n item={item}\n itemConfig={getPayloadConfigFromPayload(config, item, key)}\n hideIcon={hideIcon}\n />\n );\n })}\n </div>\n );\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n config: ChartConfig,\n payload: unknown,\n key: string\n) {\n if (typeof payload !== 'object' || payload === null) {\n return undefined;\n }\n\n const payloadPayload =\n 'payload' in payload &&\n typeof payload.payload === 'object' &&\n payload.payload !== null\n ? payload.payload\n : undefined;\n\n let configLabelKey: string = key;\n\n if (\n key in payload &&\n typeof payload[key as keyof typeof payload] === 'string'\n ) {\n configLabelKey = payload[key as keyof typeof payload] as string;\n } else if (\n payloadPayload &&\n key in payloadPayload &&\n typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'\n ) {\n configLabelKey = payloadPayload[\n key as keyof typeof payloadPayload\n ] as string;\n }\n\n return configLabelKey in config\n ? config[configLabelKey]\n : config[key as keyof typeof config];\n}\n\nexport {\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n ChartLegend,\n ChartLegendContent,\n ChartStyle,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chart.js","names":[],"sources":["../../../../src/components/ui/chart/chart.tsx"],"sourcesContent":["'use client';\n\nimport * as React from 'react';\nimport * as RechartsPrimitive from 'recharts';\nimport type { LegendPayload } from 'recharts/types/component/DefaultLegendContent';\nimport {\n NameType,\n Payload,\n ValueType,\n} from 'recharts/types/component/DefaultTooltipContent';\nimport type { Props as LegendProps } from 'recharts/types/component/Legend';\nimport { TooltipContentProps } from 'recharts/types/component/Tooltip';\n\nimport { cn } from '@/lib/utils';\nimport {\n CHART_DEFAULT_PALETTE,\n CHART_DIVERGING_TOKENS,\n CHART_STATUS_TOKENS,\n resolveSeriesColor,\n type ChartDivergingSide,\n type ChartPalette,\n type ChartSeriesTone,\n} from './chart-palette';\n\n/**\n * Normalize a config key / data name to a CSS-safe custom-property fragment.\n *\n * A `--color-<key>` custom property is invalid when `key` contains a space or\n * other ASCII special character — the browser discards it, and the element\n * falls back to SVG's default black. This function replaces every invalid ASCII\n * character with a hyphen, collapses runs, and trims leading/trailing hyphens.\n *\n * Non-ASCII code points (U+0080+) are **preserved**: CSS Syntax Level 3\n * §4.3.7 classifies them as valid ident-code-points, so `--color-日本` and\n * `--color-Москва` are legal custom properties. Replacing them would collapse\n * all-non-ASCII keys to the empty string and silently make distinct series\n * collide on `--color-`.\n *\n * For an already-CSS-safe key (ASCII letters, digits, `-`, `_`) the output is\n * identical (identity), so existing consumers are unaffected. If the entire\n * input collapses to an empty string (e.g. all ASCII special characters),\n * `--color-` is produced — an edge case with no worse outcome than before.\n *\n * The dev-mode warning fires **once per unique unsanitized key** per page load,\n * so render loops (hover, re-render) do not flood the console.\n */\n// Tracks keys that have already triggered a dev warning — one per page load.\nconst _toCssKeyWarned = new Set<string>();\n\nexport function toCssKey(key: string): string {\n // Only strip ASCII characters that are not valid CSS ident chars.\n // U+0080+ are preserved — they are valid per CSS Syntax L3 §4.3.7.\n const safe = key\n .replace(/[^-a-zA-Z0-9_-]/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-|-$/g, '');\n\n if (process.env.NODE_ENV !== 'production' && safe !== key && !_toCssKeyWarned.has(key)) {\n _toCssKeyWarned.add(key);\n console.warn(\n `[ui-react] Chart config key \"${key}\" is not CSS-safe and was sanitized to \"${safe}\". ` +\n `Use CSS-safe keys (ASCII letters, digits, hyphens, underscores, or non-ASCII characters) to avoid this.`\n );\n }\n\n return safe;\n}\n\nexport type ChartConfig = {\n [k in string]: {\n label?: React.ReactNode;\n icon?: React.ComponentType;\n /**\n * Which stop of the container's `palette` this series paints with. Left\n * unset, it is assigned from the series' position. See `ChartSeriesTone`.\n *\n * There is deliberately no free-form `color` here: every series colour\n * comes from the chart's palette, so a chart cannot paint a hue the design\n * system does not define. Pick a different palette on `ChartContainer`, or\n * a different stop of it with this.\n */\n tone?: ChartSeriesTone;\n };\n};\n\n/**\n * A `ChartConfig` with every series' palette colour filled in. This is what the\n * container puts on the context, so the plot, the tooltip rows and the legend\n * markers all read the same resolved colour.\n */\nexport type ResolvedChartConfig = {\n [k in string]: ChartConfig[string] & { color: string };\n};\n\n/**\n * Resolve each series' colour out of `palette` — in the palette's defined\n * order, honouring any `tone` a series named.\n *\n * Exported for tests and for the widget editor, which needs the same resolved\n * map to detect duplicate colours (`findDuplicateTones`) without re-deriving it.\n */\nexport function resolveChartColors(\n config: ChartConfig,\n palette: ChartPalette\n): ResolvedChartConfig {\n const entries = Object.entries(config);\n\n // An aliased series (`tone: { sameAs }`) doesn't take a stop of its own — it\n // is the same metric drawn a second way, so it must not consume a colour or\n // shift the series after it along the palette.\n const isAlias = (item: ChartConfig[string]) => Boolean(item.tone?.sameAs);\n const resolved: Record<string, ChartConfig[string] & { color: string }> = {};\n\n // Warn about overrides that are meaningless under a diverging palette.\n // resolveSeriesColor issues the same warnings for every other palette type,\n // but the diverging branch below bypasses it, so we replicate the check here\n // to preserve the documented dev-warning contract (see ChartSeriesTone).\n const warnDivergingMismatch = (tone: ChartSeriesTone | undefined) => {\n if (process.env.NODE_ENV !== 'production' && tone) {\n if (tone.status) {\n console.warn(\n `[ui-react] A chart series names a status (\"${tone.status}\") under the \"diverging\" palette, which has no status tones. Ignoring.`\n );\n } else if (typeof tone.slot === 'number') {\n console.warn(\n `[ui-react] A chart series pins slot ${tone.slot} under the \"diverging\" palette, whose stops are a ramp and are not individually selectable. Ignoring.`\n );\n }\n }\n };\n\n if (palette.type === 'diverging') {\n const interleavedStops = CHART_DIVERGING_TOKENS[palette.pair];\n // Derive each side's three stops from the interleaved ramp (a at even\n // indices, b at odd indices): a3, a2, a1 and b3, b2, b1.\n const sideStops: Record<ChartDivergingSide, readonly string[]> = {\n a: [interleavedStops[0], interleavedStops[2], interleavedStops[4]],\n b: [interleavedStops[1], interleavedStops[3], interleavedStops[5]],\n };\n\n // First pass: assign sided series — group by side, each group walks its\n // side's three stops strongest-first, wrapping if there are more than three.\n const sideCounters: Record<ChartDivergingSide, number> = { a: 0, b: 0 };\n const takenStops = new Set<string>();\n for (const [key, item] of entries) {\n if (isAlias(item) || !item.tone?.side) continue;\n warnDivergingMismatch(item.tone);\n const side = item.tone.side;\n const stops = sideStops[side];\n const stop = stops[sideCounters[side]++ % stops.length];\n takenStops.add(stop);\n resolved[key] = { ...item, color: stop };\n }\n\n // Second pass: assign un-sided series from the remaining interleaved stops,\n // skipping any stop already claimed by a sided series. Fall back to the\n // full interleaved ramp if all stops were consumed.\n const available = interleavedStops.filter((s) => !takenStops.has(s));\n const pool = available.length > 0 ? available : interleavedStops;\n let unsidedIdx = 0;\n for (const [key, item] of entries) {\n if (isAlias(item) || item.tone?.side) continue;\n warnDivergingMismatch(item.tone);\n resolved[key] = { ...item, color: pool[unsidedIdx++ % pool.length] };\n }\n } else {\n let index = 0;\n for (const [key, item] of entries) {\n if (isAlias(item)) continue;\n resolved[key] = {\n ...item,\n color: resolveSeriesColor(palette, { index: index++, tone: item.tone }),\n };\n }\n }\n\n // An alias may point at another alias, in any order, so a single pass over\n // the aliases can miss a chain that is fully resolvable. Keep sweeping the\n // pending set until nothing more resolves; what is left is a cycle or a\n // target that isn't in this config at all.\n const pending = new Map<string, string>();\n for (const [key, item] of entries) {\n if (isAlias(item)) {\n pending.set(key, item.tone!.sameAs as string);\n }\n }\n\n let changed = true;\n while (changed && pending.size > 0) {\n changed = false;\n for (const [key, target] of pending) {\n const targetColor = resolved[target]?.color;\n if (targetColor !== undefined) {\n resolved[key] = { ...config[key], color: targetColor };\n pending.delete(key);\n changed = true;\n }\n }\n }\n\n for (const [key, target] of pending) {\n if (process.env.NODE_ENV !== 'production') {\n console.warn(\n `[ui-react] Chart series \"${key}\" aliases \"${target}\", which is not a palette-assigned series in this config. Falling back to neutral.`\n );\n }\n resolved[key] = { ...config[key], color: CHART_STATUS_TOKENS.neutral };\n }\n\n // Rebuilt in the config's own order: the passes above visit non-aliases\n // first, and callers (legend order, `Object.entries` consumers) rely on the\n // order they wrote.\n return Object.fromEntries(entries.map(([key]) => [key, resolved[key]]));\n}\n\ntype ChartContextProps = {\n config: ResolvedChartConfig;\n};\n\nexport type ChartTooltipContentProps = Partial<\n TooltipContentProps<ValueType, NameType>\n> & {\n className?: string;\n hideLabel?: boolean;\n hideIndicator?: boolean;\n nameKey?: string;\n labelKey?: string;\n labelFormatter?: (\n label: TooltipContentProps<number, string>['label'],\n payload: TooltipContentProps<number, string>['payload']\n ) => React.ReactNode;\n formatter?: (\n value: number | string,\n name: string,\n item: Payload<number | string, string>,\n index: number,\n payload: ReadonlyArray<Payload<number | string, string>>\n ) => React.ReactNode;\n labelClassName?: string;\n color?: string;\n};\n\nexport type ChartLegendContentProps = {\n className?: string;\n hideIcon?: boolean;\n verticalAlign?: LegendProps['verticalAlign'];\n payload?: LegendPayload[];\n nameKey?: string;\n /**\n * The series config the entries take their labels and icons from. Defaults to\n * the enclosing `ChartContainer`'s — pass it only to render the legend *outside*\n * the container, which a chart type has to do when the renderer can't lay a\n * legend out inside its plot (see `Treemap`).\n */\n config?: ChartConfig;\n /**\n * `default` — the current horizontal wrap layout (one row of chips).\n * `list` — a vertical list where each entry is `[dot] [label flex-1] [value]`,\n * matching the Figma donut/radial widget legend. Use alongside a manually-built\n * `payload` passed from outside the chart's recharts composition.\n */\n variant?: 'default' | 'list';\n /**\n * When `variant=\"list\"`, the field on `payload[i].payload` to display as the\n * right-hand value. Typically the chart's `dataKey` (the numeric series field).\n */\n valueKey?: string;\n /**\n * Format the value shown in each list entry. When omitted the raw value is\n * stringified with `String()`.\n */\n valueFormatter?: (value: string | number) => string;\n /**\n * Extra Tailwind class(es) applied to the right-hand value in a list entry,\n * merged on top of the default colour (`text-[var(--ui-text-on-surface-link-idle)]`).\n * Pass a replacement colour class to override the default; other utilities\n * (e.g. `tabular-nums`) are additive and leave the default colour in place.\n */\n valueClassName?: string;\n};\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nfunction useChart() {\n const context = React.useContext(ChartContext);\n\n if (!context) {\n throw new Error('useChart must be used within a <ChartContainer />');\n }\n\n return context;\n}\n\nexport interface ChartContainerProps extends React.ComponentProps<'div'> {\n /**\n * The series this chart draws, keyed by data field.\n *\n * Declare exactly what the chart plots. A stop is assigned by an entry's\n * position here, not by which series end up rendered, so an entry for a\n * series the chart doesn't draw still consumes a colour and shifts the ones\n * that follow it. (That is deliberate: a series then keeps its colour when a\n * sibling is toggled off, instead of the whole chart recolouring.)\n */\n config: ChartConfig;\n /**\n * The dataviz palette this chart's series are painted from. Series take a\n * stop of it in the palette's defined order, or the one their `tone` names.\n * Defaults to `categorical` — there is no \"no palette\" state.\n */\n palette?: ChartPalette;\n children: React.ComponentProps<\n typeof RechartsPrimitive.ResponsiveContainer\n >['children'];\n}\n\nconst ChartContainer = React.forwardRef<HTMLDivElement, ChartContainerProps>(\n function ChartContainer(\n { id, className, children, config, palette = CHART_DEFAULT_PALETTE, ...props },\n ref\n ) {\n const uniqueId = React.useId();\n const chartId = `chart-${id || uniqueId.replace(/:/g, '')}`;\n // Resolved once and shared with the context, so the tooltip rows and legend\n // markers read the same colors the plot paints with.\n const resolvedConfig = React.useMemo(\n () => resolveChartColors(config, palette),\n [config, palette]\n );\n\n return (\n <ChartContext.Provider value={{ config: resolvedConfig }}>\n <div\n ref={ref}\n id={id}\n data-slot=\"chart\"\n data-chart={chartId}\n // The `[stroke='#fff']` rules undo recharts' hardcoded white outlines —\n // on pie/radial sectors, line dots and funnel trapezoids. They don't\n // follow the theme, so in dark mode they read as a light hairline around\n // every segment.\n className={cn(\n \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden [&_.recharts-trapezoid[stroke='#fff']]:stroke-transparent\",\n // A `<Brush>` traveller is the one `.recharts-layer` that is a real\n // control (`role=\"slider\"`, tabbable, arrow-key driven), so the blanket\n // `outline-hidden` above would leave it with no focus indicator. Restore\n // one — the `:focus-visible` pseudo-class outranks that rule, so this\n // wins regardless of the order Tailwind emits them in. An `outline`\n // rather than the usual `ring`: box-shadow doesn't paint on SVG children.\n '[&_.recharts-brush-traveller:focus-visible]:[outline:3px_solid_var(--ui-focus-primary)] [&_.recharts-brush-traveller:focus-visible]:[outline-offset:1px]',\n // recharts anchors its axis tick text with the *direction-relative*\n // SVG keywords (`text-anchor: start|end`, see `getTickTextAnchor` in\n // `CartesianAxis`), while placing every mark at a computed physical\n // coordinate. Inheriting `direction: rtl` therefore flips only the\n // text: an `end`-anchored Y tick mirrors about its anchor and lands\n // inside the plot (measured: +24px to +39px), rotated ticks shift\n // ~+15px, and a `ReferenceLine` label at the right edge overflows the\n // surface. Pinning the surface keeps the plot's own coordinate system\n // consistent with the geometry recharts computed for it. The chrome\n // that *should* mirror — tooltip and legend — is HTML outside\n // `.recharts-surface`, so it is unaffected.\n //\n // `Treemap` is the exception: its cell labels are HTML in a\n // `<foreignObject>` *inside* the surface, put there precisely so they\n // mirror with the page (see `treemap.tsx`). That is ordinary flow\n // layout, not a computed SVG coordinate, so the pin has to stop at the\n // `<foreignObject>` boundary and hand the page's direction back.\n '[&_.recharts-surface]:[direction:ltr] rtl:[&_.recharts-surface_foreignObject]:[direction:rtl]',\n className\n )}\n {...props}\n >\n <ChartStyle id={chartId} config={resolvedConfig} />\n <RechartsPrimitive.ResponsiveContainer>\n {children}\n </RechartsPrimitive.ResponsiveContainer>\n </div>\n </ChartContext.Provider>\n );\n }\n);\n\nChartContainer.displayName = 'ChartContainer';\n\nconst ChartStyle = ({\n id,\n config,\n}: {\n id: string;\n config: ResolvedChartConfig;\n}) => {\n const entries = Object.entries(config);\n\n if (!entries.length) {\n return null;\n }\n\n // One block, no light/dark split: every colour is now a `--ui-dataviz-*`\n // token, and those are `light-dark()` pairs that follow `color-scheme`\n // themselves. The old per-theme `[data-theme='dark']` duplication existed\n // only for hand-written `theme: { light, dark }` config entries, which the\n // palette replaced.\n //\n // Rendered as a text child (not dangerouslySetInnerHTML): React sets it via\n // textContent, which the browser does not HTML-parse, so a `</style>` in a\n // colour can't break out of the tag.\n if (process.env.NODE_ENV !== 'production') {\n const seen = new Map<string, string>();\n for (const [key] of entries) {\n const safe = toCssKey(key);\n const prior = seen.get(safe);\n if (prior !== undefined) {\n console.warn(\n `[ui-react] Chart config keys \"${prior}\" and \"${key}\" both sanitize to \"--color-${safe}\". ` +\n `One will overwrite the other — use unique CSS-safe keys.`\n );\n } else {\n seen.set(safe, key);\n }\n }\n }\n\n return (\n <style>\n {`[data-chart=${id}] {\\n${entries\n .map(([key, itemConfig]) => ` --color-${toCssKey(key)}: ${itemConfig.color};`)\n .join('\\n')}\\n}`}\n </style>\n );\n};\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\n/** The `content` a caller passes to override the default tooltip (recharts' `Tooltip['content']`). */\nexport type ChartTooltipContentType = React.ComponentProps<\n typeof ChartTooltip\n>['content'];\n\n/**\n * What `getPayloadConfigFromPayload` resolves a payload item to — the lookup can\n * miss, so every consumer has to cope with `undefined`. Derived from the helper\n * rather than restated, so the two can't drift.\n */\ntype ChartItemConfig = ReturnType<typeof getPayloadConfigFromPayload>;\n\n/** One row of the tooltip's payload, after the `active`/empty guard. */\ntype ChartTooltipItem = NonNullable<\n ChartTooltipContentProps['payload']\n>[number];\n\n/**\n * One tooltip row. Its own three-way branching (caller formatter, config icon,\n * default dot) is what drove the nesting in `ChartTooltipContent`; as a\n * component the map body reads as a single element.\n */\nfunction ChartTooltipRow({\n item,\n index,\n payload,\n itemConfig,\n indicatorColor,\n hideIndicator,\n formatter,\n}: {\n item: ChartTooltipItem;\n index: number;\n payload: NonNullable<ChartTooltipContentProps['payload']>;\n itemConfig: ChartItemConfig;\n indicatorColor?: string;\n hideIndicator: boolean;\n formatter: ChartTooltipContentProps['formatter'];\n}) {\n return (\n <div className=\"flex w-full flex-wrap items-center gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\">\n {formatter && item?.value !== undefined && item.name ? (\n formatter(item.value, item.name, item, index, payload)\n ) : (\n <>\n {itemConfig?.icon ? (\n <itemConfig.icon />\n ) : (\n !hideIndicator && (\n // A row is always dotted, whatever marker the legend\n // gives that series.\n <div\n className=\"h-2.5 w-2.5 shrink-0 rounded-full\"\n style={{ backgroundColor: indicatorColor }}\n />\n )\n )}\n {/*\n * `gap-4` puts a floor under the name/value separation.\n * `justify-between` alone only separates them while the\n * container has free space to distribute — which the tooltip's\n * `min-w-[8rem]` guarantees for a short value and not for a\n * long one (a currency `tickFormatter`, a `labelFormatter`\n * with units, a value plus its share), where the two would\n * otherwise butt up against each other. A minimum, so a row\n * with slack renders as before.\n */}\n <div className=\"flex flex-1 items-center justify-between gap-4 leading-none\">\n <span className=\"text-muted-foreground\">\n {itemConfig?.label || item.name}\n </span>\n {item.value != null && (\n <span className=\"font-medium tabular-nums text-foreground\">\n {item.value.toLocaleString()}\n </span>\n )}\n </div>\n </>\n )}\n </div>\n );\n}\n\nfunction ChartTooltipContent({\n active,\n payload,\n label,\n className,\n hideLabel = false,\n hideIndicator = false,\n labelFormatter,\n formatter,\n labelClassName,\n color,\n nameKey,\n labelKey,\n}: ChartTooltipContentProps) {\n const { config } = useChart();\n\n const tooltipLabel = React.useMemo(() => {\n if (hideLabel || !payload?.length) {\n return null;\n }\n\n const [item] = payload;\n const key = `${labelKey || item?.dataKey || item?.name || 'value'}`;\n const itemConfig = getPayloadConfigFromPayload(config, item, key);\n const value = (() => {\n const v =\n !labelKey && typeof label === 'string'\n ? (config[label as keyof typeof config]?.label ?? label)\n : itemConfig?.label;\n\n return typeof v === 'string' || typeof v === 'number' ? v : undefined;\n })();\n\n if (labelFormatter) {\n return (\n <div className={cn('font-medium', labelClassName)}>\n {labelFormatter(value, payload)}\n </div>\n );\n }\n\n if (!value) {\n return null;\n }\n\n return <div className={cn('font-medium', labelClassName)}>{value}</div>;\n }, [\n label,\n labelFormatter,\n payload,\n hideLabel,\n labelClassName,\n config,\n labelKey,\n ]);\n\n if (!active || !payload?.length) {\n return null;\n }\n\n return (\n <div\n className={cn(\n 'grid min-w-[8rem] items-start gap-1.5 rounded-[var(--ui-tooltip-container-border-radius)] border border-border bg-background px-[var(--ui-tooltip-container-padding-x)] py-[var(--ui-tooltip-container-padding-y)] text-xs text-foreground shadow-md',\n className\n )}\n >\n {tooltipLabel}\n <div className=\"grid gap-1.5\">\n {payload.map((item, index) => {\n const key = `${nameKey || item.name || item.dataKey || 'value'}`;\n\n return (\n <ChartTooltipRow\n key={key}\n item={item}\n index={index}\n payload={payload}\n itemConfig={getPayloadConfigFromPayload(config, item, key)}\n indicatorColor={color || item.payload.fill || item.color}\n hideIndicator={hideIndicator}\n formatter={formatter}\n />\n );\n })}\n </div>\n </div>\n );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\n/**\n * One entry in the `variant=\"list\"` legend: `[dot] [label flex-1] [value]`.\n *\n * The dot is always a circle, regardless of the series' recharts type — the\n * list layout is only used for polar charts (donut, radial) where the fill\n * marker matches the slice/arc color.\n */\nfunction ChartLegendListEntry({\n item,\n itemConfig,\n valueKey,\n valueFormatter,\n valueClassName,\n}: {\n item: LegendPayload;\n itemConfig: ChartItemConfig;\n valueKey?: string;\n valueFormatter?: (value: string | number) => string;\n valueClassName?: string;\n}) {\n const raw = valueKey\n ? (item.payload as Record<string, unknown> | undefined)?.[valueKey]\n : undefined;\n const displayValue =\n raw != null\n ? valueFormatter\n ? valueFormatter(raw as string | number)\n : String(raw)\n : undefined;\n\n return (\n <div className=\"flex w-full items-start gap-2 overflow-hidden\">\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n {itemConfig?.icon ? (\n <itemConfig.icon />\n ) : (\n <div\n className=\"h-2.5 w-2.5 shrink-0 rounded-full\"\n style={{ backgroundColor: item.color }}\n />\n )}\n <span className=\"min-w-0 flex-1 truncate text-xs text-foreground\">\n {itemConfig?.label ?? item.value}\n </span>\n </div>\n {displayValue != null && (\n <span\n className={cn(\n 'shrink-0 text-xs font-semibold text-[var(--ui-text-on-surface-link-idle)]',\n valueClassName\n )}\n >\n {displayValue}\n </span>\n )}\n </div>\n );\n}\n\n/**\n * One legend entry. Split out so the marker it has to derive from the payload\n * sits next to the markup that uses it, instead of above a nested map body.\n */\nfunction ChartLegendEntry({\n item,\n itemConfig,\n hideIcon,\n}: {\n item: LegendPayload;\n itemConfig: ChartItemConfig;\n hideIcon: boolean;\n}) {\n return (\n <div\n className={cn(\n 'flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground'\n )}\n >\n {itemConfig?.icon && !hideIcon ? (\n <itemConfig.icon />\n ) : (\n <div\n className=\"h-2.5 w-2.5 shrink-0 rounded-full\"\n style={{ backgroundColor: item.color }}\n />\n )}\n {/*\n * Falls back to the series key, the way the tooltip row and the\n * treemap's on-cell label both do. `label` is optional on a\n * `ChartConfig` entry, and `getPayloadConfigFromPayload` can miss\n * entirely, so without this an entry renders as a marker with no\n * text — worst on the charts whose legend is the only place a series\n * is named.\n */}\n {itemConfig?.label ?? item.value}\n </div>\n );\n}\n\nfunction ChartLegendContent({\n className,\n hideIcon = false,\n payload,\n verticalAlign = 'bottom',\n nameKey,\n config: configFromProps,\n variant = 'default',\n valueKey,\n valueFormatter,\n valueClassName,\n}: ChartLegendContentProps) {\n // The context read directly rather than through `useChart()`: a caller that\n // passes its own `config` is rendering the legend outside the container, where\n // the context is absent by design rather than by mistake. Every other caller\n // still gets `useChart()`'s error — the legend has no way to label itself\n // without a config, and failing loudly beats rendering an empty row.\n const contextConfig = React.useContext(ChartContext)?.config;\n const config = configFromProps ?? contextConfig;\n\n if (!config) {\n throw new Error(\n 'ChartLegendContent must be used within a <ChartContainer /> or given a `config` prop'\n );\n }\n\n if (!payload?.length) {\n return null;\n }\n\n if (variant === 'list') {\n return (\n <div data-slot=\"chart-legend\" className={cn('flex flex-col', className)}>\n {payload.map((item) => {\n const key = `${nameKey || item.dataKey || 'value'}`;\n return (\n <ChartLegendListEntry\n key={item.value}\n item={item}\n itemConfig={getPayloadConfigFromPayload(config, item, key)}\n valueKey={valueKey}\n valueFormatter={valueFormatter}\n valueClassName={valueClassName}\n />\n );\n })}\n </div>\n );\n }\n\n return (\n <div\n data-slot=\"chart-legend\"\n className={cn(\n // Wraps rather than overflowing: a legend with many entries (a treemap's\n // one-per-tile, a pie's one-per-slice) is wider than the chart on a narrow\n // surface, and a row that can't wrap paints past the chart's edge. The\n // column gap is unchanged, so a legend that already fits on one row keeps\n // its exact layout.\n 'flex flex-wrap items-center justify-center gap-x-6 gap-y-2',\n verticalAlign === 'top' ? 'pb-3' : 'pt-3',\n className\n )}\n >\n {payload.map((item) => {\n const key = `${nameKey || item.dataKey || 'value'}`;\n\n return (\n <ChartLegendEntry\n key={item.value}\n item={item}\n itemConfig={getPayloadConfigFromPayload(config, item, key)}\n hideIcon={hideIcon}\n />\n );\n })}\n </div>\n );\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n config: ChartConfig,\n payload: unknown,\n key: string\n) {\n if (typeof payload !== 'object' || payload === null) {\n return undefined;\n }\n\n const payloadPayload =\n 'payload' in payload &&\n typeof payload.payload === 'object' &&\n payload.payload !== null\n ? payload.payload\n : undefined;\n\n let configLabelKey: string = key;\n\n if (\n key in payload &&\n typeof payload[key as keyof typeof payload] === 'string'\n ) {\n configLabelKey = payload[key as keyof typeof payload] as string;\n } else if (\n payloadPayload &&\n key in payloadPayload &&\n typeof payloadPayload[key as keyof typeof payloadPayload] === 'string'\n ) {\n configLabelKey = payloadPayload[\n key as keyof typeof payloadPayload\n ] as string;\n }\n\n return configLabelKey in config\n ? config[configLabelKey]\n : config[key as keyof typeof config];\n}\n\nexport {\n ChartContainer,\n ChartTooltip,\n ChartTooltipContent,\n ChartLegend,\n ChartLegendContent,\n ChartStyle,\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { resolveXAxisHeight as e } from "./chart-format.js";
|
|
2
|
+
import * as t from "react";
|
|
3
|
+
//#region src/components/ui/chart/use-x-axis-layout.ts
|
|
4
|
+
var n = {
|
|
5
|
+
top: 5,
|
|
6
|
+
right: 5,
|
|
7
|
+
bottom: 5,
|
|
8
|
+
left: 5
|
|
9
|
+
};
|
|
10
|
+
function r(e, t) {
|
|
11
|
+
if (!t) return e;
|
|
12
|
+
let r = {
|
|
13
|
+
top: e?.top ?? n.top,
|
|
14
|
+
right: e?.right ?? n.right,
|
|
15
|
+
bottom: e?.bottom ?? n.bottom,
|
|
16
|
+
left: e?.left ?? n.left
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
...r,
|
|
20
|
+
left: r.left + (t?.left ?? 0),
|
|
21
|
+
right: r.right + (t?.right ?? 0)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function i(n, r, i, a) {
|
|
25
|
+
let o = e(i, r), [s, c] = t.useState({
|
|
26
|
+
height: o ?? 0,
|
|
27
|
+
margin: {}
|
|
28
|
+
});
|
|
29
|
+
return t.useLayoutEffect(() => {
|
|
30
|
+
c({
|
|
31
|
+
height: o ?? 0,
|
|
32
|
+
margin: {}
|
|
33
|
+
});
|
|
34
|
+
}, [o, a]), t.useLayoutEffect(() => {
|
|
35
|
+
if (r == null) return;
|
|
36
|
+
let e = n.current;
|
|
37
|
+
if (!e) return;
|
|
38
|
+
let t = () => {
|
|
39
|
+
let t = e.querySelector(".recharts-surface"), n = Array.from(e.querySelectorAll(".recharts-xAxis-tick-labels text"));
|
|
40
|
+
if (!t || !n.length) return;
|
|
41
|
+
let r = t.getBoundingClientRect();
|
|
42
|
+
if (!r.width || !r.height) return;
|
|
43
|
+
let i = n.map((e) => e.getBoundingClientRect()), a = Math.min(...i.map((e) => e.left)), s = Math.max(...i.map((e) => e.right)), l = Math.max(...i.map((e) => e.bottom)), u = e.querySelector(".recharts-legend-wrapper")?.getBoundingClientRect(), d = Math.min(r.bottom, u ? u.top - 8 : r.bottom);
|
|
44
|
+
c((e) => {
|
|
45
|
+
let t = Math.ceil(Math.max(0, r.left - a)), n = Math.ceil(Math.max(0, s - r.right)), i = Math.max(0, (e.margin.left ?? 0) + (t ? t + 16 : 0)), c = Math.max(0, (e.margin.right ?? 0) + (n ? n + 16 : 0)), u = Math.max(o ?? 0, Math.ceil(e.height - (d - l))), f = i || c ? {
|
|
46
|
+
left: i || void 0,
|
|
47
|
+
right: c || void 0
|
|
48
|
+
} : {};
|
|
49
|
+
return e.height === u && e.margin.left === f.left && e.margin.right === f.right ? e : {
|
|
50
|
+
height: u,
|
|
51
|
+
margin: f
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
t();
|
|
56
|
+
let i = new ResizeObserver(t);
|
|
57
|
+
return i.observe(e), () => i.disconnect();
|
|
58
|
+
}, [
|
|
59
|
+
r,
|
|
60
|
+
n,
|
|
61
|
+
o,
|
|
62
|
+
a
|
|
63
|
+
]), {
|
|
64
|
+
height: s.height || void 0,
|
|
65
|
+
margin: s.margin.left || s.margin.right ? s.margin : void 0
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
69
|
+
export { r as mergeXAxisLayoutMargin, i as useXAxisLayout };
|
|
70
|
+
|
|
71
|
+
//# sourceMappingURL=use-x-axis-layout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-x-axis-layout.js","names":[],"sources":["../../../../src/components/ui/chart/use-x-axis-layout.ts"],"sourcesContent":["import * as React from 'react';\n\nimport { resolveXAxisHeight } from './chart-format';\n\nexport type XAxisLayout = {\n height: number | undefined;\n margin: { left?: number; right?: number } | undefined;\n};\n\ntype ChartMargin = {\n top?: number;\n right?: number;\n bottom?: number;\n left?: number;\n};\n\ntype MeasuredXAxisLayout = {\n height: number;\n margin: { left?: number; right?: number };\n};\n\nconst RECHARTS_DEFAULT_MARGIN = { top: 5, right: 5, bottom: 5, left: 5 } as const;\n\n/**\n * Add the measured endpoint clearance without turning recharts' per-side\n * defaults into zeroes. Recharts applies its 5px defaults only when the whole\n * margin prop is absent, so a partial left/right margin must be completed here.\n */\nexport function mergeXAxisLayoutMargin(\n baseMargin: ChartMargin | undefined,\n xAxisMargin: XAxisLayout['margin']\n): ChartMargin | undefined {\n if (!xAxisMargin) return baseMargin;\n\n const margin = {\n top: baseMargin?.top ?? RECHARTS_DEFAULT_MARGIN.top,\n right: baseMargin?.right ?? RECHARTS_DEFAULT_MARGIN.right,\n bottom: baseMargin?.bottom ?? RECHARTS_DEFAULT_MARGIN.bottom,\n left: baseMargin?.left ?? RECHARTS_DEFAULT_MARGIN.left,\n };\n\n return {\n ...margin,\n left: margin.left + (xAxisMargin?.left ?? 0),\n right: margin.right + (xAxisMargin?.right ?? 0),\n };\n}\n\n/**\n * Fits a rotated X-axis tick row to the geometry that recharts actually\n * rendered. Estimating from character count leaves conspicuous empty space for\n * narrow glyphs and short endpoint labels; measuring the SVG lets the axis use\n * precisely the room its labels need.\n */\nexport function useXAxisLayout(\n containerRef: React.RefObject<HTMLElement | null>,\n angle: number | undefined,\n label: string | undefined,\n layoutKey: string\n): XAxisLayout {\n const fallbackHeight = resolveXAxisHeight(label, angle);\n const [layout, setLayout] = React.useState<MeasuredXAxisLayout>({\n height: fallbackHeight ?? 0,\n margin: {},\n });\n\n React.useLayoutEffect(() => {\n setLayout({ height: fallbackHeight ?? 0, margin: {} });\n }, [fallbackHeight, layoutKey]);\n\n React.useLayoutEffect(() => {\n if (angle == null) return;\n\n const container = containerRef.current;\n if (!container) return;\n\n const measure = () => {\n const surface = container.querySelector<SVGSVGElement>('.recharts-surface');\n const ticks = Array.from(\n container.querySelectorAll<SVGTextElement>(\n '.recharts-xAxis-tick-labels text'\n )\n );\n if (!surface || !ticks.length) return;\n\n const surfaceBounds = surface.getBoundingClientRect();\n if (!surfaceBounds.width || !surfaceBounds.height) return;\n\n const tickBounds = ticks.map((tick) => tick.getBoundingClientRect());\n const left = Math.min(...tickBounds.map((bounds) => bounds.left));\n const right = Math.max(...tickBounds.map((bounds) => bounds.right));\n const bottom = Math.max(...tickBounds.map((bounds) => bounds.bottom));\n const legendBounds = container\n .querySelector<HTMLElement>('.recharts-legend-wrapper')\n ?.getBoundingClientRect();\n // Recharts' legend is absolutely positioned inside the same responsive\n // area. It can therefore overlap labels well before the SVG's own lower\n // edge. Keep a small, intentional gap to it rather than making the tick\n // row consume every pixel down to that edge.\n const bottomBoundary = Math.min(\n surfaceBounds.bottom,\n legendBounds ? legendBounds.top - 8 : surfaceBounds.bottom\n );\n\n setLayout((current) => {\n const leftOverflow = Math.ceil(Math.max(0, surfaceBounds.left - left));\n const rightOverflow = Math.ceil(Math.max(0, right - surfaceBounds.right));\n const nextLeft = Math.max(\n 0,\n (current.margin.left ?? 0) +\n (leftOverflow ? leftOverflow + 16 : 0)\n );\n const nextRight = Math.max(\n 0,\n (current.margin.right ?? 0) +\n (rightOverflow ? rightOverflow + 16 : 0)\n );\n // The bottom of the surface is the X-axis allocation boundary. The\n // measured distance tells us how much of the provisional height is\n // unused (or how much is still missing), rather than guessing from a\n // string's character count.\n const minimumHeight = fallbackHeight ?? 0;\n const nextHeight = Math.max(\n minimumHeight,\n Math.ceil(current.height - (bottomBoundary - bottom))\n );\n const nextMargin =\n nextLeft || nextRight\n ? { left: nextLeft || undefined, right: nextRight || undefined }\n : {};\n if (\n current.height === nextHeight &&\n current.margin.left === nextMargin.left &&\n current.margin.right === nextMargin.right\n ) {\n return current;\n }\n return { height: nextHeight, margin: nextMargin };\n });\n };\n\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(container);\n return () => observer.disconnect();\n }, [angle, containerRef, fallbackHeight, layoutKey]);\n\n return {\n height: layout.height || undefined,\n margin:\n layout.margin.left || layout.margin.right ? layout.margin : undefined,\n };\n}\n"],"mappings":";;;AAqBA,IAAM,IAA0B;CAAE,KAAK;CAAG,OAAO;CAAG,QAAQ;CAAG,MAAM;AAAE;AAOvE,SAAgB,EACd,GACA,GACyB;CACzB,IAAI,CAAC,GAAa,OAAO;CAEzB,IAAM,IAAS;EACb,KAAK,GAAY,OAAO,EAAwB;EAChD,OAAO,GAAY,SAAS,EAAwB;EACpD,QAAQ,GAAY,UAAU,EAAwB;EACtD,MAAM,GAAY,QAAQ,EAAwB;CACpD;CAEA,OAAO;EACL,GAAG;EACH,MAAM,EAAO,QAAQ,GAAa,QAAQ;EAC1C,OAAO,EAAO,SAAS,GAAa,SAAS;CAC/C;AACF;AAQA,SAAgB,EACd,GACA,GACA,GACA,GACa;CACb,IAAM,IAAiB,EAAmB,GAAO,CAAK,GAChD,CAAC,GAAQ,KAAa,EAAM,SAA8B;EAC9D,QAAQ,KAAkB;EAC1B,QAAQ,CAAC;CACX,CAAC;CAmFD,OAjFA,EAAM,sBAAsB;EAC1B,EAAU;GAAE,QAAQ,KAAkB;GAAG,QAAQ,CAAC;EAAE,CAAC;CACvD,GAAG,CAAC,GAAgB,CAAS,CAAC,GAE9B,EAAM,sBAAsB;EAC1B,IAAI,KAAS,MAAM;EAEnB,IAAM,IAAY,EAAa;EAC/B,IAAI,CAAC,GAAW;EAEhB,IAAM,UAAgB;GACpB,IAAM,IAAU,EAAU,cAA6B,mBAAmB,GACpE,IAAQ,MAAM,KAClB,EAAU,iBACR,kCACF,CACF;GACA,IAAI,CAAC,KAAW,CAAC,EAAM,QAAQ;GAE/B,IAAM,IAAgB,EAAQ,sBAAsB;GACpD,IAAI,CAAC,EAAc,SAAS,CAAC,EAAc,QAAQ;GAEnD,IAAM,IAAa,EAAM,KAAK,MAAS,EAAK,sBAAsB,CAAC,GAC7D,IAAO,KAAK,IAAI,GAAG,EAAW,KAAK,MAAW,EAAO,IAAI,CAAC,GAC1D,IAAQ,KAAK,IAAI,GAAG,EAAW,KAAK,MAAW,EAAO,KAAK,CAAC,GAC5D,IAAS,KAAK,IAAI,GAAG,EAAW,KAAK,MAAW,EAAO,MAAM,CAAC,GAC9D,IAAe,EAClB,cAA2B,0BAA0B,CAAC,EACrD,sBAAsB,GAKpB,IAAiB,KAAK,IAC1B,EAAc,QACd,IAAe,EAAa,MAAM,IAAI,EAAc,MACtD;GAEA,GAAW,MAAY;IACrB,IAAM,IAAe,KAAK,KAAK,KAAK,IAAI,GAAG,EAAc,OAAO,CAAI,CAAC,GAC/D,IAAgB,KAAK,KAAK,KAAK,IAAI,GAAG,IAAQ,EAAc,KAAK,CAAC,GAClE,IAAW,KAAK,IACpB,IACC,EAAQ,OAAO,QAAQ,MACrB,IAAe,IAAe,KAAK,EACxC,GACM,IAAY,KAAK,IACrB,IACC,EAAQ,OAAO,SAAS,MACtB,IAAgB,IAAgB,KAAK,EAC1C,GAMM,IAAa,KAAK,IADF,KAAkB,GAGtC,KAAK,KAAK,EAAQ,UAAU,IAAiB,EAAO,CACtD,GACM,IACJ,KAAY,IACR;KAAE,MAAM,KAAY,KAAA;KAAW,OAAO,KAAa,KAAA;IAAU,IAC7D,CAAC;IAQP,OANE,EAAQ,WAAW,KACnB,EAAQ,OAAO,SAAS,EAAW,QACnC,EAAQ,OAAO,UAAU,EAAW,QAE7B,IAEF;KAAE,QAAQ;KAAY,QAAQ;IAAW;GAClD,CAAC;EACH;EAEA,EAAQ;EACR,IAAM,IAAW,IAAI,eAAe,CAAO;EAE3C,OADA,EAAS,QAAQ,CAAS,SACb,EAAS,WAAW;CACnC,GAAG;EAAC;EAAO;EAAc;EAAgB;CAAS,CAAC,GAE5C;EACL,QAAQ,EAAO,UAAU,KAAA;EACzB,QACE,EAAO,OAAO,QAAQ,EAAO,OAAO,QAAQ,EAAO,SAAS,KAAA;CAChE;AACF"}
|