@stackframe/dashboard-ui-components 2.8.84 → 2.8.85
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/analytics-chart/analytics-chart-pie.d.ts +67 -0
- package/dist/components/analytics-chart/analytics-chart-pie.d.ts.map +1 -0
- package/dist/components/analytics-chart/analytics-chart-pie.js +253 -0
- package/dist/components/analytics-chart/analytics-chart-pie.js.map +1 -0
- package/dist/components/analytics-chart/analytics-chart.d.ts +554 -0
- package/dist/components/analytics-chart/analytics-chart.d.ts.map +1 -0
- package/dist/components/analytics-chart/analytics-chart.js +1021 -0
- package/dist/components/analytics-chart/analytics-chart.js.map +1 -0
- package/dist/components/analytics-chart/default-analytics-chart-tooltip.d.ts +66 -0
- package/dist/components/analytics-chart/default-analytics-chart-tooltip.d.ts.map +1 -0
- package/dist/components/analytics-chart/default-analytics-chart-tooltip.js +179 -0
- package/dist/components/analytics-chart/default-analytics-chart-tooltip.js.map +1 -0
- package/dist/components/analytics-chart/format.d.ts +13 -0
- package/dist/components/analytics-chart/format.d.ts.map +1 -0
- package/dist/components/analytics-chart/format.js +138 -0
- package/dist/components/analytics-chart/format.js.map +1 -0
- package/dist/components/analytics-chart/index.d.ts +8 -0
- package/dist/components/analytics-chart/index.js +184 -0
- package/dist/components/analytics-chart/palette.d.ts +15 -0
- package/dist/components/analytics-chart/palette.d.ts.map +1 -0
- package/dist/components/analytics-chart/palette.js +60 -0
- package/dist/components/analytics-chart/palette.js.map +1 -0
- package/dist/components/analytics-chart/render-data-series.d.ts +28 -0
- package/dist/components/analytics-chart/render-data-series.d.ts.map +1 -0
- package/dist/components/analytics-chart/render-data-series.js +109 -0
- package/dist/components/analytics-chart/render-data-series.js.map +1 -0
- package/dist/components/analytics-chart/state.d.ts +54 -0
- package/dist/components/analytics-chart/state.d.ts.map +1 -0
- package/dist/components/analytics-chart/state.js +142 -0
- package/dist/components/analytics-chart/state.js.map +1 -0
- package/dist/components/analytics-chart/strings.d.ts +33 -0
- package/dist/components/analytics-chart/strings.d.ts.map +1 -0
- package/dist/components/analytics-chart/strings.js +37 -0
- package/dist/components/analytics-chart/strings.js.map +1 -0
- package/dist/components/analytics-chart/types.d.ts +157 -0
- package/dist/components/analytics-chart/types.d.ts.map +1 -0
- package/dist/components/analytics-chart/types.js +21 -0
- package/dist/components/analytics-chart/types.js.map +1 -0
- package/dist/components/badge.d.ts +16 -0
- package/dist/components/badge.d.ts.map +1 -1
- package/dist/components/badge.js +16 -0
- package/dist/components/badge.js.map +1 -1
- package/dist/components/button.d.ts +15 -1
- package/dist/components/button.d.ts.map +1 -1
- package/dist/components/button.js +14 -0
- package/dist/components/button.js.map +1 -1
- package/dist/components/card.d.ts +28 -0
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +28 -0
- package/dist/components/card.js.map +1 -1
- package/dist/components/chart-card.d.ts +29 -0
- package/dist/components/chart-card.d.ts.map +1 -1
- package/dist/components/chart-card.js +29 -0
- package/dist/components/chart-card.js.map +1 -1
- package/dist/components/chart-legend.d.ts +1 -2
- package/dist/components/chart-legend.d.ts.map +1 -1
- package/dist/components/chart-legend.js +0 -4
- package/dist/components/chart-legend.js.map +1 -1
- package/dist/components/data-grid/data-grid-sizing.d.ts +11 -0
- package/dist/components/data-grid/data-grid-sizing.d.ts.map +1 -0
- package/dist/components/data-grid/data-grid-sizing.js +34 -0
- package/dist/components/data-grid/data-grid-sizing.js.map +1 -0
- package/dist/components/data-grid/data-grid-toolbar.d.ts +31 -0
- package/dist/components/data-grid/data-grid-toolbar.d.ts.map +1 -0
- package/dist/components/data-grid/data-grid-toolbar.js +226 -0
- package/dist/components/data-grid/data-grid-toolbar.js.map +1 -0
- package/dist/components/data-grid/data-grid.d.ts +233 -0
- package/dist/components/data-grid/data-grid.d.ts.map +1 -0
- package/dist/components/data-grid/data-grid.js +871 -0
- package/dist/components/data-grid/data-grid.js.map +1 -0
- package/dist/components/data-grid/index.d.ts +7 -0
- package/dist/components/data-grid/index.js +176 -0
- package/dist/components/data-grid/state.d.ts +91 -0
- package/dist/components/data-grid/state.d.ts.map +1 -0
- package/dist/components/data-grid/state.js +329 -0
- package/dist/components/data-grid/state.js.map +1 -0
- package/dist/components/data-grid/strings.d.ts +8 -0
- package/dist/components/data-grid/strings.d.ts.map +1 -0
- package/dist/components/data-grid/strings.js +42 -0
- package/dist/components/data-grid/strings.js.map +1 -0
- package/dist/components/data-grid/types.d.ts +242 -0
- package/dist/components/data-grid/types.d.ts.map +1 -0
- package/dist/components/data-grid/types.js +0 -0
- package/dist/components/data-grid/use-data-source.d.ts +79 -0
- package/dist/components/data-grid/use-data-source.d.ts.map +1 -0
- package/dist/components/data-grid/use-data-source.js +236 -0
- package/dist/components/data-grid/use-data-source.js.map +1 -0
- package/dist/components/empty-state.d.ts +16 -0
- package/dist/components/empty-state.d.ts.map +1 -1
- package/dist/components/empty-state.js +16 -0
- package/dist/components/empty-state.js.map +1 -1
- package/dist/components/metric-card.d.ts +24 -0
- package/dist/components/metric-card.d.ts.map +1 -1
- package/dist/components/metric-card.js +24 -0
- package/dist/components/metric-card.js.map +1 -1
- package/dist/components/progress-bar.d.ts +10 -0
- package/dist/components/progress-bar.d.ts.map +1 -1
- package/dist/components/progress-bar.js +10 -0
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/separator.d.ts +9 -0
- package/dist/components/separator.d.ts.map +1 -1
- package/dist/components/separator.js +9 -0
- package/dist/components/separator.js.map +1 -1
- package/dist/components/skeleton.d.ts +12 -0
- package/dist/components/skeleton.d.ts.map +1 -1
- package/dist/components/skeleton.js +12 -0
- package/dist/components/skeleton.js.map +1 -1
- package/dist/components/table.d.ts +25 -0
- package/dist/components/table.d.ts.map +1 -1
- package/dist/components/table.js +25 -0
- package/dist/components/table.js.map +1 -1
- package/dist/dashboard-ui-components.global.js +8607 -2902
- package/dist/dashboard-ui-components.global.js.map +4 -4
- package/dist/esm/components/analytics-chart/analytics-chart-pie.d.ts +67 -0
- package/dist/esm/components/analytics-chart/analytics-chart-pie.d.ts.map +1 -0
- package/dist/esm/components/analytics-chart/analytics-chart-pie.js +251 -0
- package/dist/esm/components/analytics-chart/analytics-chart-pie.js.map +1 -0
- package/dist/esm/components/analytics-chart/analytics-chart.d.ts +554 -0
- package/dist/esm/components/analytics-chart/analytics-chart.d.ts.map +1 -0
- package/dist/esm/components/analytics-chart/analytics-chart.js +1019 -0
- package/dist/esm/components/analytics-chart/analytics-chart.js.map +1 -0
- package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.d.ts +66 -0
- package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.d.ts.map +1 -0
- package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.js +176 -0
- package/dist/esm/components/analytics-chart/default-analytics-chart-tooltip.js.map +1 -0
- package/dist/esm/components/analytics-chart/format.d.ts +13 -0
- package/dist/esm/components/analytics-chart/format.d.ts.map +1 -0
- package/dist/esm/components/analytics-chart/format.js +133 -0
- package/dist/esm/components/analytics-chart/format.js.map +1 -0
- package/dist/esm/components/analytics-chart/index.d.ts +8 -0
- package/dist/esm/components/analytics-chart/index.js +9 -0
- package/dist/esm/components/analytics-chart/palette.d.ts +15 -0
- package/dist/esm/components/analytics-chart/palette.d.ts.map +1 -0
- package/dist/esm/components/analytics-chart/palette.js +55 -0
- package/dist/esm/components/analytics-chart/palette.js.map +1 -0
- package/dist/esm/components/analytics-chart/render-data-series.d.ts +28 -0
- package/dist/esm/components/analytics-chart/render-data-series.d.ts.map +1 -0
- package/dist/esm/components/analytics-chart/render-data-series.js +107 -0
- package/dist/esm/components/analytics-chart/render-data-series.js.map +1 -0
- package/dist/esm/components/analytics-chart/state.d.ts +54 -0
- package/dist/esm/components/analytics-chart/state.d.ts.map +1 -0
- package/dist/esm/components/analytics-chart/state.js +126 -0
- package/dist/esm/components/analytics-chart/state.js.map +1 -0
- package/dist/esm/components/analytics-chart/strings.d.ts +33 -0
- package/dist/esm/components/analytics-chart/strings.d.ts.map +1 -0
- package/dist/esm/components/analytics-chart/strings.js +34 -0
- package/dist/esm/components/analytics-chart/strings.js.map +1 -0
- package/dist/esm/components/analytics-chart/types.d.ts +157 -0
- package/dist/esm/components/analytics-chart/types.d.ts.map +1 -0
- package/dist/esm/components/analytics-chart/types.js +18 -0
- package/dist/esm/components/analytics-chart/types.js.map +1 -0
- package/dist/esm/components/badge.d.ts +16 -0
- package/dist/esm/components/badge.d.ts.map +1 -1
- package/dist/esm/components/badge.js +16 -0
- package/dist/esm/components/badge.js.map +1 -1
- package/dist/esm/components/button.d.ts +14 -0
- package/dist/esm/components/button.d.ts.map +1 -1
- package/dist/esm/components/button.js +14 -0
- package/dist/esm/components/button.js.map +1 -1
- package/dist/esm/components/card.d.ts +28 -0
- package/dist/esm/components/card.d.ts.map +1 -1
- package/dist/esm/components/card.js +28 -0
- package/dist/esm/components/card.js.map +1 -1
- package/dist/esm/components/chart-card.d.ts +29 -0
- package/dist/esm/components/chart-card.d.ts.map +1 -1
- package/dist/esm/components/chart-card.js +29 -0
- package/dist/esm/components/chart-card.js.map +1 -1
- package/dist/esm/components/chart-legend.d.ts +1 -2
- package/dist/esm/components/chart-legend.d.ts.map +1 -1
- package/dist/esm/components/chart-legend.js +1 -3
- package/dist/esm/components/chart-legend.js.map +1 -1
- package/dist/esm/components/data-grid/data-grid-sizing.d.ts +11 -0
- package/dist/esm/components/data-grid/data-grid-sizing.d.ts.map +1 -0
- package/dist/esm/components/data-grid/data-grid-sizing.js +29 -0
- package/dist/esm/components/data-grid/data-grid-sizing.js.map +1 -0
- package/dist/esm/components/data-grid/data-grid-toolbar.d.ts +31 -0
- package/dist/esm/components/data-grid/data-grid-toolbar.d.ts.map +1 -0
- package/dist/esm/components/data-grid/data-grid-toolbar.js +223 -0
- package/dist/esm/components/data-grid/data-grid-toolbar.js.map +1 -0
- package/dist/esm/components/data-grid/data-grid.d.ts +233 -0
- package/dist/esm/components/data-grid/data-grid.d.ts.map +1 -0
- package/dist/esm/components/data-grid/data-grid.js +868 -0
- package/dist/esm/components/data-grid/data-grid.js.map +1 -0
- package/dist/esm/components/data-grid/index.d.ts +7 -0
- package/dist/esm/components/data-grid/index.js +7 -0
- package/dist/esm/components/data-grid/state.d.ts +91 -0
- package/dist/esm/components/data-grid/state.d.ts.map +1 -0
- package/dist/esm/components/data-grid/state.js +305 -0
- package/dist/esm/components/data-grid/state.js.map +1 -0
- package/dist/esm/components/data-grid/strings.d.ts +8 -0
- package/dist/esm/components/data-grid/strings.d.ts.map +1 -0
- package/dist/esm/components/data-grid/strings.js +39 -0
- package/dist/esm/components/data-grid/strings.js.map +1 -0
- package/dist/esm/components/data-grid/types.d.ts +242 -0
- package/dist/esm/components/data-grid/types.d.ts.map +1 -0
- package/dist/esm/components/data-grid/types.js +1 -0
- package/dist/esm/components/data-grid/use-data-source.d.ts +79 -0
- package/dist/esm/components/data-grid/use-data-source.d.ts.map +1 -0
- package/dist/esm/components/data-grid/use-data-source.js +234 -0
- package/dist/esm/components/data-grid/use-data-source.js.map +1 -0
- package/dist/esm/components/empty-state.d.ts +16 -0
- package/dist/esm/components/empty-state.d.ts.map +1 -1
- package/dist/esm/components/empty-state.js +16 -0
- package/dist/esm/components/empty-state.js.map +1 -1
- package/dist/esm/components/metric-card.d.ts +24 -0
- package/dist/esm/components/metric-card.d.ts.map +1 -1
- package/dist/esm/components/metric-card.js +24 -0
- package/dist/esm/components/metric-card.js.map +1 -1
- package/dist/esm/components/progress-bar.d.ts +10 -0
- package/dist/esm/components/progress-bar.d.ts.map +1 -1
- package/dist/esm/components/progress-bar.js +10 -0
- package/dist/esm/components/progress-bar.js.map +1 -1
- package/dist/esm/components/separator.d.ts +9 -0
- package/dist/esm/components/separator.d.ts.map +1 -1
- package/dist/esm/components/separator.js +9 -0
- package/dist/esm/components/separator.js.map +1 -1
- package/dist/esm/components/skeleton.d.ts +12 -0
- package/dist/esm/components/skeleton.d.ts.map +1 -1
- package/dist/esm/components/skeleton.js +12 -0
- package/dist/esm/components/skeleton.js.map +1 -1
- package/dist/esm/components/table.d.ts +25 -0
- package/dist/esm/components/table.d.ts.map +1 -1
- package/dist/esm/components/table.js +25 -0
- package/dist/esm/components/table.js.map +1 -1
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +6 -2
- package/dist/index.d.ts +15 -2
- package/dist/index.js +16 -7
- package/package.json +4 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analytics-chart.js","names":["formatValue","DefaultAnalyticsChartTooltip","STROKE_DASHARRAY","EMPTY_SERIES","EMPTY_MATRIX","isAnalyticsChartDataLayer","AnalyticsChartPie","DesignChartContainer","ComposedChart","CartesianGrid","XAxis","YAxis","ReferenceLine","ReferenceArea","MagnifyingGlassMinusIcon","DesignButton","MagnifyingGlassPlusIcon","FlagIcon","XIcon"],"sources":["../../../src/components/analytics-chart/analytics-chart.tsx"],"sourcesContent":["\"use client\";\n\nimport { cn } from \"@stackframe/stack-ui\";\nimport { DesignButton } from \"../button\";\nimport {\n type DesignChartConfig,\n DesignChartContainer,\n} from \"../chart-container\";\nimport {\n CartesianGrid,\n ComposedChart,\n ReferenceArea,\n ReferenceLine,\n XAxis,\n YAxis,\n} from \"recharts\";\nimport {\n FlagIcon,\n MagnifyingGlassMinusIcon,\n MagnifyingGlassPlusIcon,\n XIcon,\n} from \"@phosphor-icons/react\";\nimport {\n type CSSProperties,\n type ReactNode,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { AnalyticsChartPie } from \"./analytics-chart-pie\";\nimport {\n DefaultAnalyticsChartTooltip,\n type AnalyticsChartTooltipContext,\n type AnalyticsChartTooltipLayerView,\n type AnalyticsChartTooltipSegmentRow,\n} from \"./default-analytics-chart-tooltip\";\nimport { formatDelta, formatValue } from \"./format\";\nimport {\n buildRampColors,\n resolveAnalyticsChartPalette,\n} from \"./palette\";\nimport { renderDataSeries } from \"./render-data-series\";\nimport {\n computeLocalInProgressIdx,\n EMPTY_MATRIX,\n EMPTY_SERIES,\n findAnnotationsLayer,\n findCompareLayer,\n findPrimaryLayer,\n isAnalyticsChartDataLayer,\n isTimeseriesState,\n resolveDataLayerStyle,\n STROKE_DASHARRAY,\n type ResolvedDataLayerStyle,\n} from \"./state\";\nimport { resolveAnalyticsChartStrings } from \"./strings\";\nimport type {\n AnalyticsChartDelta,\n AnalyticsChartPalette,\n AnalyticsChartPieProps,\n AnalyticsChartSeries,\n AnalyticsChartState,\n AnalyticsChartTimeseriesState,\n Annotation,\n FormatKind,\n Point,\n} from \"./types\";\nimport { cssIdent, pointValue } from \"./types\";\nimport type { AnalyticsChartStrings } from \"./strings\";\n\n/** Mirrors Recharts' internal `Margin` shape (not exported from their typings). */\nexport type Margin = {\n top?: number,\n right?: number,\n bottom?: number,\n left?: number,\n};\n\n/**\n * Props for {@link AnalyticsChart}.\n *\n * ## HOW TO REFERENCE THIS COMPONENT\n *\n * **In the custom dashboard sandbox** (AI-generated dashboard code): every\n * export lives on the global `DashboardUI` object. Use\n * `DashboardUI.AnalyticsChart`, `DashboardUI.ANALYTICS_CHART_DEFAULT_STATE`,\n * `DashboardUI.pointValue`, etc. **Never** use bare identifiers like\n * `<AnalyticsChart />` inside the sandbox — there is no module system and\n * nothing is destructured into scope. Types (`AnalyticsChartState`,\n * `Point`, …) don't exist at runtime anyway, so just drop the type\n * annotations in sandbox code.\n *\n * **In a regular TypeScript app** (anywhere importing `@stackframe/dashboard-ui-components`\n * directly): `import { AnalyticsChart, ANALYTICS_CHART_DEFAULT_STATE } from\n * \"@stackframe/dashboard-ui-components\"` and use the bare name. Drop the\n * `DashboardUI.` prefix from the examples below when doing so.\n *\n * ## Data shape in 30 seconds\n *\n * - `data` is `Point[]`. Each `Point` is `{ ts: number, values: Record<string, number> }`.\n * `ts` is a Unix millisecond timestamp; `values` is keyed by **layer id**.\n * - `state` is fully controlled. Start from\n * `DashboardUI.ANALYTICS_CHART_DEFAULT_STATE` (which ships with a\n * `\"primary\"` + `\"compare\"` + `\"annotations\"` layer set) and override\n * what you need. Do **not** hand-build the layer array from scratch.\n * - For a breakdown (e.g. signups by region), add `segments` (a `number[][]`\n * with one row per `data` point) and `segmentSeries` (the category labels)\n * to the primary layer. Rows of `segments` should sum to the point's layer\n * value. Same for the compare layer if you want a compared breakdown.\n *\n * ## **SCALE YOUR DATA BEFORE PUTTING IT ON A POINT** (critical)\n *\n * The chart renders every visible layer on a **single shared y-axis**. If\n * two layers are on different orders of magnitude (e.g. revenue in cents\n * `1_200_000` and sign-ups `450`), the smaller series collapses to a flat\n * line at the bottom and the chart looks broken. You **must** normalize\n * both metrics into the same range before building `data`. Rules of thumb:\n *\n * - **Cents → dollars / units**: divide money amounts by 100 (or 1000 for\n * large currencies). Use a `valueFormatter` to render the original unit\n * in tooltips so the UX still reads as \"$12,543\".\n * - **Counts vs rates**: if one layer is a count (e.g. requests) and the\n * other is a ratio (e.g. error rate 0.02), multiply the ratio by the\n * count's scale (or by `max(counts)`) so both sit in the same band.\n * - **Very different counts** (e.g. page views `120_000` vs sign-ups `430`):\n * either divide the large metric (`views / 100`) or promote the small\n * one to a rate (`signups / views * 1000`). Note the transformation in\n * the layer `label` (\"Sign-ups per 1k views\") so it's honest.\n * - **Pick the target range from the layer with the most natural scale**\n * — usually the metric the user actually cares about — and normalize\n * everything else into it. Don't normalize by fighting Recharts with\n * `yDomain` hacks; do it in the data.\n *\n * If the two metrics truly can't share an axis (e.g. latency ms vs error\n * count), render them as **two separate `AnalyticsChart` instances** stacked\n * in the layout instead of jamming them into one chart.\n *\n * ## Example 1 — simplest possible: one area layer, no compare\n *\n * ```jsx\n * // Sandbox dashboard code — everything prefixed with DashboardUI.*\n * function Dashboard() {\n * const data = [\n * { ts: Date.UTC(2026, 2, 1), values: { primary: 420 } },\n * { ts: Date.UTC(2026, 2, 2), values: { primary: 512 } },\n * { ts: Date.UTC(2026, 2, 3), values: { primary: 604 } },\n * // ...one row per time bucket\n * ];\n *\n * // Start from defaults, hide the compare layer so it's a single series.\n * const [state, setState] = React.useState({\n * ...DashboardUI.ANALYTICS_CHART_DEFAULT_STATE,\n * layers: DashboardUI.ANALYTICS_CHART_DEFAULT_STATE.layers.map((l) =>\n * l.kind === \"compare\" ? { ...l, visible: false } : l,\n * ),\n * });\n *\n * return (\n * <DashboardUI.DesignChartCard title=\"Sign-ups\" description=\"Last 30 days\">\n * <DashboardUI.AnalyticsChart\n * data={data}\n * state={state}\n * onChange={setState}\n * />\n * </DashboardUI.DesignChartCard>\n * );\n * }\n * ```\n *\n * ## Example 2 — current vs previous period (compare)\n *\n * Each point carries both layer values under their layer ids. The default\n * state's `\"primary\"` and `\"compare\"` layers are already visible, so no\n * state customization is needed.\n *\n * ```jsx\n * function Dashboard() {\n * const data = rows.map((r) => ({\n * ts: r.bucketTs,\n * values: {\n * primary: r.signupsThisPeriod, // keyed by layer id \"primary\"\n * compare: r.signupsLastPeriod, // keyed by layer id \"compare\"\n * },\n * }));\n *\n * const [state, setState] = React.useState(\n * DashboardUI.ANALYTICS_CHART_DEFAULT_STATE,\n * );\n *\n * return (\n * <DashboardUI.AnalyticsChart\n * data={data}\n * state={state}\n * onChange={setState}\n * />\n * );\n * }\n * ```\n *\n * ## Example 3 — stacked bar with region breakdown (segmented)\n *\n * ```jsx\n * function Dashboard() {\n * const regions = [\n * { key: \"us\", label: \"United States\" },\n * { key: \"eu\", label: \"European Union\" },\n * { key: \"asia\", label: \"Asia-Pacific\" },\n * ];\n *\n * // Row index matches `data` index; column index matches `regions`.\n * // Each row MUST sum to data[i].values.primary.\n * const segments = [\n * [210, 140, 70], // day 0 → total 420\n * [250, 170, 92], // day 1 → total 512\n * [300, 200, 104], // day 2 → total 604\n * ];\n *\n * const data = [\n * { ts: Date.UTC(2026, 2, 1), values: { primary: 420 } },\n * { ts: Date.UTC(2026, 2, 2), values: { primary: 512 } },\n * { ts: Date.UTC(2026, 2, 3), values: { primary: 604 } },\n * ];\n *\n * const [state, setState] = React.useState({\n * ...DashboardUI.ANALYTICS_CHART_DEFAULT_STATE,\n * layers: DashboardUI.ANALYTICS_CHART_DEFAULT_STATE.layers.map((l) => {\n * if (l.kind === \"primary\") {\n * return {\n * ...l,\n * type: \"bar\", // switch from area → stacked bars\n * segmented: true,\n * segments,\n * segmentSeries: regions,\n * };\n * }\n * if (l.kind === \"compare\") {\n * return { ...l, visible: false };\n * }\n * return l;\n * }),\n * });\n *\n * return (\n * <DashboardUI.AnalyticsChart\n * data={data}\n * state={state}\n * onChange={setState}\n * />\n * );\n * }\n * ```\n *\n * ## Example 4 — mixing display types (e.g. revenue bars + signups area)\n *\n * Use two layers. The \"primary\" layer holds one metric; reuse the \"compare\"\n * layer slot for the second metric by overriding its `id`, `label`, and\n * `type`. Then key both values in each `Point`.\n *\n * **IMPORTANT**: the two metrics share a single y-axis, so scale them into\n * the same range before putting them on the point. A common trick is to\n * pass a `valueFormatter` that reports each layer's number with its own\n * unit so tooltips still read correctly.\n *\n * ```jsx\n * function Dashboard() {\n * // Sign-ups are already in range; revenue cents would dwarf them, so\n * // we normalize revenue (cents → dollars) onto the same scale.\n * const data = rows.map((r) => ({\n * ts: r.bucketTs,\n * values: {\n * revenue: r.revenueCents / 100,\n * signups: r.signups,\n * },\n * }));\n *\n * const [state, setState] = React.useState({\n * ...DashboardUI.ANALYTICS_CHART_DEFAULT_STATE,\n * layers: DashboardUI.ANALYTICS_CHART_DEFAULT_STATE.layers.map((l) => {\n * if (l.kind === \"primary\") {\n * return { ...l, id: \"revenue\", label: \"Revenue\", type: \"bar\" };\n * }\n * if (l.kind === \"compare\") {\n * return {\n * ...l,\n * id: \"signups\",\n * label: \"Sign-ups\",\n * type: \"area\",\n * visible: true,\n * };\n * }\n * return l;\n * }),\n * });\n *\n * // Per-layer formatter: `kind` lets you branch per-axis vs per-layer\n * // using the layer id passed in via the tooltip context. For most\n * // cases formatting by raw value is enough.\n * const valueFormatter = (value, kind) => {\n * if (kind.type === \"currency\") return `$${value.toFixed(0)}`;\n * return value.toLocaleString();\n * };\n *\n * return (\n * <DashboardUI.AnalyticsChart\n * data={data}\n * state={state}\n * onChange={setState}\n * valueFormatter={valueFormatter}\n * />\n * );\n * }\n * ```\n *\n * ## Example 5 — segmented sign-ups stacked with a revenue line (mix + segment)\n *\n * Combines Example 3 and Example 4: primary layer is revenue as a line\n * (un-segmented), compare layer is sign-ups as a stacked bar (segmented\n * by region). Remember: row sums of the compare segments must equal\n * `point.values.signups`, and both metrics share one y-axis.\n *\n * ```jsx\n * function Dashboard() {\n * const regions = [\n * { key: \"us\", label: \"United States\" },\n * { key: \"eu\", label: \"European Union\" },\n * { key: \"asia\", label: \"Asia-Pacific\" },\n * ];\n *\n * // Normalize revenue to the same order of magnitude as sign-ups.\n * const data = rows.map((r) => ({\n * ts: r.bucketTs,\n * values: {\n * revenue: r.revenueCents / 100,\n * signups: r.signupsTotal,\n * },\n * }));\n *\n * // Row index matches `data` index. Each row sums to signupsTotal.\n * const signupSegments = rows.map((r) => [\n * r.signupsUs,\n * r.signupsEu,\n * r.signupsAsia,\n * ]);\n *\n * const [state, setState] = React.useState({\n * ...DashboardUI.ANALYTICS_CHART_DEFAULT_STATE,\n * layers: DashboardUI.ANALYTICS_CHART_DEFAULT_STATE.layers.map((l) => {\n * if (l.kind === \"primary\") {\n * return { ...l, id: \"revenue\", label: \"Revenue\", type: \"line\" };\n * }\n * if (l.kind === \"compare\") {\n * return {\n * ...l,\n * id: \"signups\",\n * label: \"Sign-ups\",\n * type: \"bar\",\n * visible: true,\n * segmented: true,\n * segments: signupSegments,\n * segmentSeries: regions,\n * };\n * }\n * return l;\n * }),\n * });\n *\n * return (\n * <DashboardUI.AnalyticsChart\n * data={data}\n * state={state}\n * onChange={setState}\n * />\n * );\n * }\n * ```\n */\nexport type AnalyticsChartProps = {\n /** Time-series points — each point carries `values` keyed by layer id.\n * See {@link AnalyticsChartProps} for full data-shape examples. */\n data: Point[],\n /** Annotations. Fully prop-driven; the consumer owns the array. */\n annotations?: Annotation[],\n /** Fully-controlled state + dispatch. The chart reads every config and\n * persistent-interaction slice from `state` and mutates it through\n * `onChange`. Ephemeral interaction state (hover, brush, pin, draft) is\n * managed internally and surfaces only through the state callbacks. */\n state: AnalyticsChartState,\n onChange: React.Dispatch<React.SetStateAction<AnalyticsChartState>>,\n /** Fired when the user submits the in-chart annotation form. The consumer\n * is expected to append to its own annotations array. */\n onAnnotationCreate?: (annotation: Annotation) => void,\n /** Override any user-visible copy. Shallow-merges over the defaults. */\n strings?: Partial<AnalyticsChartStrings>,\n /** Override segment color ramps. Each ramp is either procedural\n * (hue + sat + lightness range) or explicit (concrete color lists). */\n palette?: Partial<AnalyticsChartPalette>,\n /** Render slot for the tooltip body. Receives a prepared context with\n * the active point, primary/compare layer views, pre-bound formatters,\n * and resolved strings. Defaults to `DefaultAnalyticsChartTooltip`. */\n renderTooltip?: (ctx: AnalyticsChartTooltipContext) => ReactNode,\n /** Recharts plot margins. Also drives overlay positioning math so the\n * crosshair, tooltip anchor, brush popup, and flag markers line up with\n * the actual plot area. Defaults to `{ top: 16, right: 24, bottom: 8, left: 12 }`. */\n plotMargin?: Margin,\n /** Y-axis reserved width in pixels. Defaults to 48. */\n yAxisWidth?: number,\n /** Fractional headroom added to the y-axis top. Defaults to 0.1. */\n yDomainPadding?: number,\n /** Grouped pie configuration. Each field has a sensible default. */\n pie?: AnalyticsChartPieProps,\n /** Custom number formatter. Receives the raw value and the kind to format\n * with — the same function is invoked for both x-axis and y-axis values. */\n valueFormatter?: (value: number, kind: FormatKind) => string,\n};\n\ntype RechartsMouseState = {\n activeTooltipIndex?: number,\n isTooltipActive?: boolean,\n};\n\nconst FALLBACK_PRIMARY_STYLE: ResolvedDataLayerStyle = {\n color: \"#2563eb\",\n type: \"area\",\n strokeStyle: \"solid\",\n fillOpacity: 0,\n};\nconst FALLBACK_COMPARE_STYLE: ResolvedDataLayerStyle = {\n color: \"#f59e0b\",\n type: \"line\",\n strokeStyle: \"dashed\",\n fillOpacity: 0,\n};\nconst FALLBACK_ANNOTATION_COLOR = \"#f59e0b\";\n\nfunction buildTooltipLayerView(args: {\n show: boolean,\n layer: { id: string, label: string } | undefined,\n color: string,\n segmented: boolean,\n segmentSeries: readonly AnalyticsChartSeries[],\n segmentRows: readonly (readonly number[])[],\n segmentTotals: readonly number[],\n segmentColorsLight: readonly string[],\n segmentColorsDark: readonly string[],\n activeIndex: number,\n activePoint: Point,\n fallbackLabel?: string,\n}): AnalyticsChartTooltipLayerView | null {\n const {\n show,\n layer,\n color,\n segmented,\n segmentSeries,\n segmentRows,\n segmentTotals,\n segmentColorsLight,\n segmentColorsDark,\n activeIndex,\n activePoint,\n fallbackLabel,\n } = args;\n if (!show || !layer) return null;\n const segments: AnalyticsChartTooltipSegmentRow[] = segmented\n ? segmentSeries.map((s, sIdx) => ({\n key: s.key,\n label: s.label,\n value: segmentRows[activeIndex]?.[sIdx] ?? 0,\n color: segmentColorsLight[sIdx],\n colorDark: segmentColorsDark[sIdx],\n }))\n : [];\n return {\n id: layer.id,\n label: layer.label || fallbackLabel || \"\",\n color,\n colorDark: color,\n total: segmented\n ? (segmentTotals[activeIndex] ?? 0)\n : pointValue(activePoint, layer.id),\n segmented,\n segments,\n };\n}\n\n/**\n * Preferred chart for all time-series: area, line, bar, compare layers,\n * segmented stacks, tooltips, zoom, and annotations. Wrap in\n * `DesignChartCard` for the title/description chrome. Only fall back to\n * raw Recharts for non-time-series visuals (static rankings etc.).\n *\n * ## Data shape\n *\n * `data` is `Point[]`, where `Point = { ts: number, values: Record<string, number> }`.\n * `ts` is a Unix milliseconds timestamp. `values` maps layer id → numeric value\n * at that bucket. Example:\n *\n * ```ts\n * { ts: 1743465600000, values: { primary: 420 } }\n * { ts: 1743465600000, values: { primary: 420, compare: 380 } } // with compare layer\n * ```\n *\n * ## State is fully controlled — start from ANALYTICS_CHART_DEFAULT_STATE\n *\n * The default state ships with three pre-configured layers: `\"primary\"`,\n * `\"compare\"`, and `\"annotations\"`. ALWAYS spread from\n * `ANALYTICS_CHART_DEFAULT_STATE` and map over `layers` to override. Do NOT\n * hand-build the layer array from scratch — you will miss fields and crash.\n *\n * ```ts\n * // Default state shape (for reference — spread from the constant, don't copy):\n * {\n * view: \"timeseries\",\n * layers: [\n * { id: \"primary\", kind: \"primary\", label: \"Current\", visible: true, color: \"#2563eb\",\n * segmented: false, type: \"area\", strokeStyle: \"solid\", fillOpacity: 0.22, inProgressFromIndex: null },\n * { id: \"compare\", kind: \"compare\", label: \"Previous period\", visible: true, color: \"#f59e0b\",\n * segmented: false, type: \"line\", strokeStyle: \"dashed\", inProgressFromIndex: null },\n * { id: \"annotations\", kind: \"annotations\", label: \"Annotations\", visible: true, color: \"#f59e0b\" },\n * ],\n * xFormatKind: { type: \"datetime\", style: \"short\" },\n * yFormatKind: { type: \"short\" },\n * showGrid: true, showXAxis: true, showYAxis: true,\n * zoomRange: null, pinnedIndex: null,\n * }\n * ```\n *\n * ## onChange — CRITICAL, get this right\n *\n * `onChange` fires with an `AnalyticsChartState` object — NOT your custom\n * wrapper. If you store chart data and state together, `onChange` MUST only\n * update the state part. Keep data and state in SEPARATE hooks:\n *\n * ```tsx\n * // WRONG — overwrites your data with a bare state object, crashes on next render:\n * const [combined, setCombined] = React.useState({ data: [], state: ANALYTICS_CHART_DEFAULT_STATE });\n * <AnalyticsChart data={combined.data} state={combined.state} onChange={setCombined} />\n *\n * // RIGHT — two hooks:\n * const [data, setData] = React.useState([]);\n * const [chartState, setChartState] = React.useState({ ...ANALYTICS_CHART_DEFAULT_STATE });\n * <AnalyticsChart data={data} state={chartState} onChange={setChartState} />\n * ```\n *\n * NEVER pass a setter that manages a combined `{ data, state }` object directly to `onChange`.\n *\n * ## Common patterns\n *\n * ### 1. Simplest — one area layer, no compare\n *\n * ```tsx\n * const data = rows.map(r => ({ ts: r.bucketTs, values: { primary: r.count } }));\n * const [state, setState] = React.useState({\n * ...ANALYTICS_CHART_DEFAULT_STATE,\n * layers: ANALYTICS_CHART_DEFAULT_STATE.layers.map(l =>\n * l.kind === \"compare\" ? { ...l, visible: false } : l\n * ),\n * });\n * <DesignChartCard title=\"Signups\" description=\"Last 30 days\">\n * <AnalyticsChart data={data} state={state} onChange={setState} />\n * </DesignChartCard>\n * ```\n *\n * ### 2. Current vs previous period (compare)\n *\n * ```tsx\n * const data = rows.map(r => ({\n * ts: r.bucketTs,\n * values: { primary: r.thisPeriod, compare: r.lastPeriod },\n * }));\n * const [state, setState] = React.useState(ANALYTICS_CHART_DEFAULT_STATE);\n * <AnalyticsChart data={data} state={state} onChange={setState} />\n * ```\n *\n * ### 3. Stacked bar with breakdown (segmented)\n *\n * ```tsx\n * const regions = [{ key: \"us\", label: \"US\" }, { key: \"eu\", label: \"EU\" }];\n * const segments = rows.map(r => [r.signupsUs, r.signupsEu]); // MUST sum to primary value per row\n * const [state, setState] = React.useState({\n * ...ANALYTICS_CHART_DEFAULT_STATE,\n * layers: ANALYTICS_CHART_DEFAULT_STATE.layers.map(l => {\n * if (l.kind === \"primary\") return { ...l, type: \"bar\", segmented: true, segments, segmentSeries: regions };\n * if (l.kind === \"compare\") return { ...l, visible: false };\n * return l;\n * }),\n * });\n * <AnalyticsChart data={data} state={state} onChange={setState} />\n * ```\n *\n * ### 4. Two metrics on one chart (revenue bars + signups area)\n *\n * ```tsx\n * // IMPORTANT: metrics share one y-axis, so normalize into the same range.\n * const data = rows.map(r => ({\n * ts: r.bucketTs,\n * values: { revenue: r.revenueCents / 100, signups: r.signups },\n * }));\n * const [state, setState] = React.useState({\n * ...ANALYTICS_CHART_DEFAULT_STATE,\n * layers: ANALYTICS_CHART_DEFAULT_STATE.layers.map(l => {\n * if (l.kind === \"primary\") return { ...l, id: \"revenue\", label: \"Revenue\", type: \"bar\" };\n * if (l.kind === \"compare\") return { ...l, id: \"signups\", label: \"Sign-ups\", type: \"area\", visible: true };\n * return l;\n * }),\n * });\n * <AnalyticsChart data={data} state={state} onChange={setState} />\n * ```\n *\n * ### 5. Pie view (distribution / breakdown, non-time-series)\n *\n * Pie needs one data point, `segments` with one row, and `segmentSeries` with labels:\n *\n * ```tsx\n * const categories = [{ key: \"verified\", label: \"Verified\" }, { key: \"unverified\", label: \"Unverified\" }, { key: \"anonymous\", label: \"Anonymous\" }];\n * const total = verified + unverified + anonymous;\n * const data = [{ ts: 0, values: { primary: total } }];\n * const segments = [[verified, unverified, anonymous]]; // one row; values sum to total\n * const [state, setState] = React.useState({\n * ...ANALYTICS_CHART_DEFAULT_STATE,\n * view: \"pie\",\n * layers: ANALYTICS_CHART_DEFAULT_STATE.layers.map(l => {\n * if (l.kind === \"primary\") return { ...l, segmented: true, segments, segmentSeries: categories };\n * if (l.kind === \"compare\") return { ...l, visible: false };\n * return l;\n * }),\n * });\n * <AnalyticsChart data={data} state={state} onChange={setState} />\n * ```\n *\n * ## Segment data contract (MUST follow when segmented: true)\n *\n * `segments` is a 2D array: `segments[dayIndex][categoryIndex] = number`.\n *\n * - Outer length MUST equal `data.length` (one row per Point).\n * - Inner length MUST equal `segmentSeries.length` (one value per category).\n * - Each row MUST sum to `data[dayIndex].values[layerId]` (the layer's total for that day).\n * - `segmentSeries` defines the category labels, in the SAME order as segment columns.\n *\n * Example: if `segmentSeries = [{ key: \"us\", label: \"US\" }, { key: \"eu\", label: \"EU\" }]`\n * and `data[0].values.primary = 420`, then `segments[0]` must be `[usValue, euValue]`\n * where `usValue + euValue === 420`. If rows don't sum to the layer total, stacked bars\n * will render incorrectly (gaps or overflow).\n *\n * ## Palette\n *\n * AnalyticsChart auto-generates segment colors (blue shades for primary, amber for\n * compare). You do NOT need to pass a palette prop — it just works. Segment keys\n * can be any string; the component sanitizes them for CSS purposes internally.\n *\n * ## Layer quick reference\n *\n * - Layer `type` options: `\"area\" | \"line\" | \"bar\"`\n * - Layer `kind` values: `\"primary\" | \"compare\" | \"annotations\"`\n * - To hide a layer: `{ ...l, visible: false }`\n * - To switch chart type: `{ ...l, type: \"bar\" }` (or `\"line\"`, `\"area\"`)\n * - To rename a layer: `{ ...l, id: \"myMetric\", label: \"My Metric\" }`\n *\n * ## Formatting (xFormatKind / yFormatKind on state)\n *\n * - `{ type: \"numeric\" }` — plain number\n * - `{ type: \"short\" }` — abbreviated (1.2K, 3.4M) — good default for y-axis\n * - `{ type: \"currency\", currency: \"USD\", divisor: 100 }` — for cents → dollars\n * - `{ type: \"percent\", source: \"fraction\" }` — for 0..1 → \"45.2%\"\n * - `{ type: \"datetime\", style: \"short\" }` — good default for x-axis timestamps\n *\n * Set these on state:\n * `{ ...ANALYTICS_CHART_DEFAULT_STATE, yFormatKind: { type: \"currency\", currency: \"USD\" } }`\n *\n * ## Scale warning\n *\n * All visible layers share ONE y-axis. If magnitudes differ wildly (e.g. revenue\n * cents vs signup count), normalize the data BEFORE building Points. If\n * normalization is impossible, use two separate `AnalyticsChart` instances stacked\n * vertically.\n */\nexport function AnalyticsChart({\n data: fullData,\n annotations: fullAnnotations = [],\n state,\n onChange,\n onAnnotationCreate,\n strings: stringsOverride,\n palette: paletteOverride,\n renderTooltip,\n plotMargin,\n yAxisWidth = 48,\n yDomainPadding = 0.1,\n pie,\n valueFormatter,\n}: AnalyticsChartProps) {\n const resolvedPlotMargin = useMemo<Required<Margin>>(\n () => ({\n top: plotMargin?.top ?? 16,\n right: plotMargin?.right ?? 24,\n bottom: plotMargin?.bottom ?? 8,\n left: plotMargin?.left ?? 12,\n }),\n [plotMargin],\n );\n const fmtValue = valueFormatter ?? formatValue;\n const strings = useMemo(\n () => resolveAnalyticsChartStrings(stringsOverride),\n [stringsOverride],\n );\n const palette = useMemo(\n () => resolveAnalyticsChartPalette(paletteOverride),\n [paletteOverride],\n );\n\n const renderTooltipFn = useMemo(\n () => renderTooltip ?? ((ctx: AnalyticsChartTooltipContext) => <DefaultAnalyticsChartTooltip ctx={ctx} />),\n [renderTooltip],\n );\n\n const { xFormatKind, yFormatKind, layers } = state;\n const timeseries = isTimeseriesState(state) ? state : null;\n const showGrid = timeseries?.showGrid ?? false;\n const showXAxis = timeseries?.showXAxis ?? false;\n const showYAxis = timeseries?.showYAxis ?? false;\n const zoomRange = timeseries?.zoomRange ?? null;\n const pinnedIndex = timeseries?.pinnedIndex ?? null;\n\n const primaryLayer = findPrimaryLayer(layers);\n const compareLayer = findCompareLayer(layers);\n const annotationsLayer = findAnnotationsLayer(layers);\n const showPrimary = primaryLayer?.visible ?? false;\n const showCompare = compareLayer?.visible ?? false;\n const showAnnotationsLayer = annotationsLayer?.visible ?? false;\n\n const primaryStyle = primaryLayer ? resolveDataLayerStyle(primaryLayer) : FALLBACK_PRIMARY_STYLE;\n const compareStyle = compareLayer ? resolveDataLayerStyle(compareLayer) : FALLBACK_COMPARE_STYLE;\n const primaryColor = primaryStyle.color;\n const compareColor = compareStyle.color;\n const annotationColor = annotationsLayer?.color ?? FALLBACK_ANNOTATION_COLOR;\n const primaryStroke = STROKE_DASHARRAY[primaryStyle.strokeStyle];\n const compareStroke = STROKE_DASHARRAY[compareStyle.strokeStyle];\n const primaryFillOpacity = primaryStyle.fillOpacity;\n const compareFillOpacity = compareStyle.fillOpacity;\n\n const setTimeseriesField = useCallback(\n <K extends keyof AnalyticsChartTimeseriesState>(\n key: K,\n value: AnalyticsChartTimeseriesState[K],\n ) => {\n onChange((prev) => {\n if (prev.view !== \"timeseries\") return prev;\n return { ...prev, [key]: value };\n });\n },\n [onChange],\n );\n\n const wrapperRef = useRef<HTMLDivElement>(null);\n const [hoverIndex, setHoverIndex] = useState<number | null>(null);\n const [committedRange, setCommittedRange] = useState<[number, number] | null>(null);\n const [annotationDraft, setAnnotationDraft] = useState<string | null>(null);\n const [dragAnchor, setDragAnchor] = useState<number | null>(null);\n const [brushStart, setBrushStart] = useState<number | null>(null);\n const [brushEnd, setBrushEnd] = useState<number | null>(null);\n const [pieHoverKey, setPieHoverKey] = useState<string | null>(null);\n\n const activeIndex = pinnedIndex ?? hoverIndex;\n\n const primarySegmentSeries = useMemo<readonly AnalyticsChartSeries[]>(\n () => primaryLayer?.segmentSeries ?? EMPTY_SERIES,\n [primaryLayer?.segmentSeries],\n );\n const compareSegmentSeries = useMemo<readonly AnalyticsChartSeries[]>(\n () => compareLayer?.segmentSeries ?? EMPTY_SERIES,\n [compareLayer?.segmentSeries],\n );\n const primaryFullSegments = useMemo<readonly (readonly number[])[]>(\n () => primaryLayer?.segments ?? EMPTY_MATRIX,\n [primaryLayer?.segments],\n );\n const compareFullSegments = useMemo<readonly (readonly number[])[]>(\n () => compareLayer?.segments ?? EMPTY_MATRIX,\n [compareLayer?.segments],\n );\n\n const primarySegmented =\n (primaryLayer?.segmented ?? false)\n && showPrimary\n && primarySegmentSeries.length > 0\n && primaryFullSegments.length > 0;\n const compareSegmented =\n (compareLayer?.segmented ?? false)\n && showCompare\n && compareSegmentSeries.length > 0\n && compareFullSegments.length > 0;\n\n const visibleStart = zoomRange ? zoomRange[0] : 0;\n const visibleEnd = zoomRange ? zoomRange[1] : fullData.length - 1;\n\n const data = useMemo(\n () => fullData.slice(visibleStart, visibleEnd + 1),\n [fullData, visibleStart, visibleEnd],\n );\n const primarySegments = useMemo(\n () => primaryFullSegments.slice(visibleStart, visibleEnd + 1),\n [primaryFullSegments, visibleStart, visibleEnd],\n );\n const compareSegments = useMemo(\n () => compareFullSegments.slice(visibleStart, visibleEnd + 1),\n [compareFullSegments, visibleStart, visibleEnd],\n );\n const primarySegmentTotals = useMemo(\n () => primarySegments.map((row) => row.reduce((a, b) => a + b, 0)),\n [primarySegments],\n );\n const compareSegmentTotals = useMemo(\n () => compareSegments.map((row) => row.reduce((a, b) => a + b, 0)),\n [compareSegments],\n );\n\n const yDomainMax = useMemo(() => {\n const dataLayerIds = layers.filter(isAnalyticsChartDataLayer).map((l) => l.id);\n const layerMaxes = dataLayerIds.map((id) =>\n data.reduce((m, p) => Math.max(m, pointValue(p, id)), 0),\n );\n const primaryStackMax = primarySegmentTotals.reduce((m, v) => Math.max(m, v), 0);\n const compareStackMax = compareSegmentTotals.reduce((m, v) => Math.max(m, v), 0);\n const rawMax = Math.max(0, ...layerMaxes, primaryStackMax, compareStackMax);\n return Math.ceil(rawMax * (1 + yDomainPadding));\n }, [data, layers, primarySegmentTotals, compareSegmentTotals, yDomainPadding]);\n\n const segmentColors = useMemo(() => {\n return {\n primary: {\n light: buildRampColors(palette.primary, primarySegmentSeries.length, \"light\"),\n dark: buildRampColors(palette.primary, primarySegmentSeries.length, \"dark\"),\n },\n compare: {\n light: buildRampColors(palette.compare, compareSegmentSeries.length, \"light\"),\n dark: buildRampColors(palette.compare, compareSegmentSeries.length, \"dark\"),\n },\n };\n }, [primarySegmentSeries.length, compareSegmentSeries.length, palette]);\n\n const aggregatedPrimarySegments = useMemo(\n () =>\n primarySegmentSeries.map((_, sIdx) =>\n primarySegments.reduce((acc, row) => acc + (row[sIdx] ?? 0), 0),\n ),\n [primarySegmentSeries, primarySegments],\n );\n const aggregatedCompareSegments = useMemo(\n () =>\n compareSegmentSeries.map((_, sIdx) =>\n compareSegments.reduce((acc, row) => acc + (row[sIdx] ?? 0), 0),\n ),\n [compareSegmentSeries, compareSegments],\n );\n const aggregatedPrimaryTotal = useMemo(\n () => aggregatedPrimarySegments.reduce((a, b) => a + b, 0),\n [aggregatedPrimarySegments],\n );\n const aggregatedCompareTotal = useMemo(\n () => aggregatedCompareSegments.reduce((a, b) => a + b, 0),\n [aggregatedCompareSegments],\n );\n\n const annotations = useMemo(() => {\n return fullAnnotations\n .filter((a) => a.index >= visibleStart && a.index <= visibleEnd)\n .map((a) => ({ ...a, index: a.index - visibleStart }));\n }, [fullAnnotations, visibleStart, visibleEnd]);\n\n const brushing = brushStart != null;\n const N = data.length;\n\n const primaryKey = primaryLayer?.id ?? \"__analytics_primary\";\n const compareKey = compareLayer?.id ?? \"__analytics_compare\";\n // Segment keys must be valid CSS `<ident>` tokens — colons break `--color-${key}` declarations.\n const primarySolidKey = `${primaryKey}_solid`;\n const primaryDashedKey = `${primaryKey}_dashed`;\n const compareSolidKey = `${compareKey}_solid`;\n const compareDashedKey = `${compareKey}_dashed`;\n const primarySegKey = useCallback(\n (segKey: string) => `${primaryKey}_seg_${cssIdent(segKey)}`,\n [primaryKey],\n );\n const compareSegKey = useCallback(\n (segKey: string) => `${compareKey}_seg_${cssIdent(segKey)}`,\n [compareKey],\n );\n\n const primaryInProgressLocalIdx = computeLocalInProgressIdx(\n primaryLayer?.inProgressFromIndex,\n visibleStart,\n visibleEnd,\n );\n const compareInProgressLocalIdx = computeLocalInProgressIdx(\n compareLayer?.inProgressFromIndex,\n visibleStart,\n visibleEnd,\n );\n const primaryHasInProgress =\n primaryInProgressLocalIdx != null\n && !primarySegmented\n && (primaryStyle.type === \"line\" || primaryStyle.type === \"area\");\n const compareHasInProgress =\n compareInProgressLocalIdx != null\n && !compareSegmented\n && (compareStyle.type === \"line\" || compareStyle.type === \"area\");\n\n const chartData = useMemo(() => {\n return data.map((point, i) => {\n const row: Record<string, number | string | null> = {\n index: i,\n ts: point.ts,\n };\n for (const [k, v] of Object.entries(point.values)) {\n row[k] = v;\n }\n if (primaryLayer && primaryHasInProgress) {\n const k = primaryInProgressLocalIdx as number;\n const v = pointValue(point, primaryLayer.id);\n row[primarySolidKey] = i < k ? v : null;\n row[primaryDashedKey] = i >= k - 1 ? v : null;\n }\n if (compareLayer && compareHasInProgress) {\n const k = compareInProgressLocalIdx as number;\n const v = pointValue(point, compareLayer.id);\n row[compareSolidKey] = i < k ? v : null;\n row[compareDashedKey] = i >= k - 1 ? v : null;\n }\n if (primarySegmented) {\n primarySegmentSeries.forEach((s, sIdx) => {\n row[primarySegKey(s.key)] = primarySegments[i]?.[sIdx] ?? 0;\n });\n }\n if (compareSegmented) {\n compareSegmentSeries.forEach((s, sIdx) => {\n row[compareSegKey(s.key)] = compareSegments[i]?.[sIdx] ?? 0;\n });\n }\n return row;\n });\n }, [\n data,\n primaryLayer,\n compareLayer,\n primarySolidKey,\n primaryDashedKey,\n compareSolidKey,\n compareDashedKey,\n primarySegKey,\n compareSegKey,\n primarySegments,\n compareSegments,\n primarySegmentSeries,\n compareSegmentSeries,\n primarySegmented,\n compareSegmented,\n primaryHasInProgress,\n compareHasInProgress,\n primaryInProgressLocalIdx,\n compareInProgressLocalIdx,\n ]);\n\n const chartConfig = useMemo<DesignChartConfig>(() => {\n const primaryLabel = primaryLayer?.label ?? \"\";\n const compareLabel = compareLayer?.label ?? \"\";\n const config: DesignChartConfig = {};\n if (primaryLayer) {\n config[primaryLayer.id] = { label: primaryLabel, color: primaryColor };\n if (primaryHasInProgress) {\n config[primarySolidKey] = { label: primaryLabel, color: primaryColor };\n config[primaryDashedKey] = { label: primaryLabel, color: primaryColor };\n }\n }\n if (compareLayer) {\n config[compareLayer.id] = { label: compareLabel, color: compareColor };\n if (compareHasInProgress) {\n config[compareSolidKey] = { label: compareLabel, color: compareColor };\n config[compareDashedKey] = { label: compareLabel, color: compareColor };\n }\n }\n if (primarySegmented) {\n primarySegmentSeries.forEach((s, i) => {\n config[primarySegKey(s.key)] = {\n label: s.label,\n theme: {\n light: segmentColors.primary.light[i],\n dark: segmentColors.primary.dark[i],\n },\n };\n });\n }\n if (compareSegmented) {\n compareSegmentSeries.forEach((s, i) => {\n config[compareSegKey(s.key)] = {\n label: s.label,\n theme: {\n light: segmentColors.compare.light[i],\n dark: segmentColors.compare.dark[i],\n },\n };\n });\n }\n return config;\n }, [\n primaryLayer,\n compareLayer,\n primarySolidKey,\n primaryDashedKey,\n compareSolidKey,\n compareDashedKey,\n primarySegKey,\n compareSegKey,\n primaryColor,\n compareColor,\n primaryHasInProgress,\n compareHasInProgress,\n primarySegmented,\n compareSegmented,\n primarySegmentSeries,\n compareSegmentSeries,\n segmentColors,\n ]);\n\n const handleChartMouseMove = useCallback(\n (rechartsState: RechartsMouseState) => {\n const i = rechartsState.activeTooltipIndex;\n if (typeof i !== \"number\") return;\n setHoverIndex(i);\n if (dragAnchor != null && (brushStart != null || i !== dragAnchor)) {\n if (brushStart == null) setBrushStart(dragAnchor);\n setBrushEnd(i);\n }\n },\n [dragAnchor, brushStart],\n );\n const handleChartMouseDown = useCallback(\n (rechartsState: RechartsMouseState, e: React.MouseEvent) => {\n if (e.button !== 0) return;\n const i = rechartsState.activeTooltipIndex;\n if (typeof i !== \"number\") return;\n e.preventDefault();\n setDragAnchor(i);\n setAnnotationDraft(null);\n },\n [],\n );\n const handleChartMouseUp = useCallback(\n (_: RechartsMouseState, e: React.MouseEvent) => {\n e.stopPropagation();\n if (brushStart != null && brushEnd != null) {\n const lo = Math.min(brushStart, brushEnd);\n const hi = Math.max(brushStart, brushEnd);\n setBrushStart(null);\n setBrushEnd(null);\n setDragAnchor(null);\n if (hi - lo >= 1) setCommittedRange([lo, hi]);\n return;\n }\n setDragAnchor(null);\n if (pinnedIndex != null) {\n setTimeseriesField(\"pinnedIndex\", null);\n } else if (hoverIndex != null) {\n setTimeseriesField(\"pinnedIndex\", hoverIndex);\n }\n },\n [\n brushStart,\n brushEnd,\n hoverIndex,\n pinnedIndex,\n setTimeseriesField,\n ],\n );\n const handleChartMouseLeave = useCallback(() => {\n if (!brushing) setHoverIndex(null);\n }, [brushing]);\n\n const handleKeyDown = useCallback(\n (e: React.KeyboardEvent<HTMLDivElement>) => {\n if (e.key === \"ArrowRight\" || e.key === \"ArrowLeft\") {\n e.preventDefault();\n setHoverIndex((cur) => {\n const base = cur ?? pinnedIndex ?? 0;\n return e.key === \"ArrowRight\"\n ? Math.min(N - 1, base + 1)\n : Math.max(0, base - 1);\n });\n return;\n }\n if (e.key === \"Home\") {\n e.preventDefault();\n setHoverIndex(0);\n return;\n }\n if (e.key === \"End\") {\n e.preventDefault();\n setHoverIndex(N - 1);\n return;\n }\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n if (pinnedIndex != null) {\n setTimeseriesField(\"pinnedIndex\", null);\n } else if (hoverIndex != null) {\n setTimeseriesField(\"pinnedIndex\", hoverIndex);\n }\n }\n },\n [N, hoverIndex, pinnedIndex, setTimeseriesField],\n );\n\n useEffect(() => {\n if (pinnedIndex == null) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") setTimeseriesField(\"pinnedIndex\", null);\n };\n const onDown = (e: MouseEvent) => {\n if (!wrapperRef.current) return;\n if (!wrapperRef.current.contains(e.target as Node)) setTimeseriesField(\"pinnedIndex\", null);\n };\n window.addEventListener(\"keydown\", onKey);\n window.addEventListener(\"mousedown\", onDown);\n return () => {\n window.removeEventListener(\"keydown\", onKey);\n window.removeEventListener(\"mousedown\", onDown);\n };\n }, [pinnedIndex, setTimeseriesField]);\n\n const plotInset = resolvedPlotMargin.left + resolvedPlotMargin.right;\n const indexToCss = useCallback(\n (i: number): string => {\n if (N <= 1) return `calc(${resolvedPlotMargin.left}px + (100% - ${plotInset}px) * 0.5)`;\n const t = Math.max(0, Math.min(1, i / (N - 1)));\n return `calc(${resolvedPlotMargin.left}px + (100% - ${plotInset}px) * ${t})`;\n },\n [N, resolvedPlotMargin.left, plotInset],\n );\n const tooltipXPct = activeIndex != null\n ? N <= 1\n ? 50\n : (activeIndex / (N - 1)) * 100\n : 0;\n const shouldFlip = tooltipXPct > 68;\n\n const activePoint = activeIndex != null ? data[activeIndex] : null;\n\n const chartAriaLabel = primaryLayer?.label || \"Chart\";\n\n if (state.view === \"pie\") {\n return (\n <AnalyticsChartPie\n wrapperRef={wrapperRef}\n primarySegmentSeries={primarySegmentSeries}\n compareSegmentSeries={compareSegmentSeries}\n aggregatedPrimarySegments={aggregatedPrimarySegments}\n aggregatedCompareSegments={aggregatedCompareSegments}\n aggregatedPrimaryTotal={aggregatedPrimaryTotal}\n aggregatedCompareTotal={aggregatedCompareTotal}\n segmentColors={segmentColors}\n showPrimary={showPrimary}\n showCompare={showCompare}\n xFormatKind={xFormatKind}\n yFormatKind={yFormatKind}\n hoverKey={pieHoverKey}\n setHoverKey={setPieHoverKey}\n zoomRange={zoomRange}\n onResetZoom={() => {\n onChange((prev) => ({ ...prev, zoomRange: null, pinnedIndex: null }));\n setCommittedRange(null);\n setAnnotationDraft(null);\n setHoverIndex(null);\n }}\n visibleStart={visibleStart}\n visibleEnd={visibleEnd}\n fullData={fullData}\n strings={strings}\n fmtValue={fmtValue}\n pie={pie}\n />\n );\n }\n\n return (\n <div\n ref={wrapperRef}\n className={cn(\n \"relative w-full select-none rounded-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/40 dark:focus-visible:ring-blue-400/40\",\n brushing && \"[&_.recharts-wrapper]:cursor-ew-resize\",\n !brushing && \"[&_.recharts-wrapper]:cursor-crosshair\",\n )}\n onClick={(e) => {\n e.stopPropagation();\n }}\n onKeyDown={handleKeyDown}\n onFocus={() => {\n if (hoverIndex == null) setHoverIndex(pinnedIndex ?? Math.floor(N / 2));\n }}\n tabIndex={0}\n role=\"img\"\n aria-label={`${chartAriaLabel} over the visible ${data.length}-day range. Use arrow keys to move the cursor, Enter to pin, Escape to release. Click and drag to select a range.`}\n >\n <DesignChartContainer\n config={chartConfig}\n className=\"aspect-auto h-[260px] w-full sm:h-[320px]\"\n >\n <ComposedChart\n data={chartData}\n margin={resolvedPlotMargin}\n onMouseMove={handleChartMouseMove}\n onMouseDown={handleChartMouseDown}\n onMouseUp={handleChartMouseUp}\n onMouseLeave={handleChartMouseLeave}\n >\n {showGrid && <CartesianGrid vertical={false} strokeDasharray=\"3 3\" />}\n {showXAxis && (\n <XAxis\n dataKey=\"index\"\n tickLine={false}\n axisLine={false}\n tickMargin={10}\n minTickGap={32}\n tickFormatter={(value: number | string) => {\n const idx = Number(value);\n if (idx < 0 || idx >= data.length) return \"\";\n return fmtValue(data[idx]!.ts, xFormatKind);\n }}\n />\n )}\n {showYAxis && (\n <YAxis\n tickLine={false}\n axisLine={false}\n tickMargin={8}\n width={yAxisWidth}\n domain={[0, yDomainMax]}\n allowDataOverflow={false}\n tickFormatter={(value: number | string) =>\n fmtValue(Math.round(Number(value)), yFormatKind)\n }\n />\n )}\n\n {showAnnotationsLayer\n && annotations.map((a) => (\n <ReferenceLine\n key={a.index}\n x={a.index}\n stroke={annotationColor}\n strokeDasharray=\"3 4\"\n strokeOpacity={0.55}\n ifOverflow=\"visible\"\n />\n ))}\n\n {committedRange && (\n <ReferenceArea\n x1={committedRange[0]}\n x2={committedRange[1]}\n fill={primaryColor}\n fillOpacity={0.07}\n stroke={primaryColor}\n strokeOpacity={0.45}\n strokeDasharray=\"3 3\"\n ifOverflow=\"visible\"\n />\n )}\n\n {brushStart != null && brushEnd != null && (\n <ReferenceArea\n x1={Math.min(brushStart, brushEnd)}\n x2={Math.max(brushStart, brushEnd)}\n fill={primaryColor}\n fillOpacity={0.15}\n stroke={primaryColor}\n strokeOpacity={0.55}\n ifOverflow=\"visible\"\n />\n )}\n\n {showPrimary && primaryLayer && renderDataSeries({\n layer: primaryLayer,\n segmented: primarySegmented,\n segmentSeries: primarySegmentSeries,\n segKey: primarySegKey,\n stackId: \"primary-segments\",\n strokeDasharray: primaryStroke,\n segmentedStrokeDasharray: undefined,\n fillOpacity: primaryFillOpacity,\n segmentedFillOpacity: 0.78,\n strokeWidth: 2,\n segmentedStrokeWidth: 0.75,\n inProgressKeys: primaryHasInProgress\n ? { solid: primarySolidKey, dashed: primaryDashedKey }\n : null,\n })}\n\n {showCompare && compareLayer && renderDataSeries({\n layer: compareLayer,\n segmented: compareSegmented,\n segmentSeries: compareSegmentSeries,\n segKey: compareSegKey,\n stackId: \"compare-segments\",\n strokeDasharray: compareStroke,\n segmentedStrokeDasharray: compareStroke,\n fillOpacity: compareFillOpacity,\n segmentedFillOpacity: 0.6,\n baseOpacity: compareSegmented ? 0.9 : 1,\n strokeWidth: 1.5,\n segmentedStrokeWidth: 0.75,\n inProgressKeys: compareHasInProgress\n ? { solid: compareSolidKey, dashed: compareDashedKey }\n : null,\n })}\n </ComposedChart>\n </DesignChartContainer>\n\n {activeIndex != null && activePoint && !brushing && (\n <div\n className=\"pointer-events-none absolute inset-y-4 z-10 w-px border-l border-dashed border-foreground/30\"\n style={{ left: indexToCss(activeIndex) }}\n />\n )}\n\n {brushStart != null && brushEnd != null && (() => {\n const lo = Math.min(brushStart, brushEnd);\n const hi = Math.max(brushStart, brushEnd);\n const days = hi - lo + 1;\n return (\n <div\n className=\"pointer-events-none absolute -top-1 z-30 -translate-x-1/2 -translate-y-full rounded-lg border border-blue-500/30 bg-background/95 px-3 py-1.5 text-[11px] shadow-lg backdrop-blur-xl dark:border-blue-400/30\"\n style={{ left: indexToCss((lo + hi) / 2) }}\n >\n <div className=\"flex items-center gap-2\">\n <span className=\"font-mono text-[10px] uppercase tracking-wider text-muted-foreground\">\n {strings.rangeLabel}\n </span>\n <span className=\"font-mono tabular-nums text-foreground\">\n {fmtValue(data[lo]!.ts, xFormatKind)} – {fmtValue(data[hi]!.ts, xFormatKind)}\n </span>\n <span className=\"font-mono text-[10px] tabular-nums text-muted-foreground\">\n · {strings.daysShort(days)}\n </span>\n </div>\n </div>\n );\n })()}\n\n {zoomRange && (\n <div className=\"absolute right-2 top-2 z-20\">\n <button\n type=\"button\"\n onClick={() => {\n onChange((prev) => ({ ...prev, zoomRange: null, pinnedIndex: null }));\n setCommittedRange(null);\n setAnnotationDraft(null);\n setHoverIndex(null);\n }}\n className=\"inline-flex items-center gap-1.5 rounded-full bg-blue-500/10 px-2.5 py-1 font-mono text-[10px] font-semibold uppercase tracking-wider text-blue-600 ring-1 ring-blue-500/30 transition-colors duration-150 hover:bg-blue-500/15 hover:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/50 dark:text-blue-300 dark:ring-blue-400/30\"\n >\n <MagnifyingGlassMinusIcon weight=\"bold\" className=\"size-3\" aria-hidden=\"true\" />\n <span>{strings.resetZoom}</span>\n </button>\n </div>\n )}\n\n {committedRange && !brushing && (() => {\n const [lo, hi] = committedRange;\n const center = (lo + hi) / 2;\n const centerPct = N <= 1 ? 50 : (center / (N - 1)) * 100;\n const snapLeft = centerPct < 22;\n const snapRight = centerPct > 78;\n const anchorStyle: CSSProperties = snapLeft\n ? { left: \"8px\" }\n : snapRight\n ? { right: \"8px\" }\n : { left: indexToCss(center), transform: \"translateX(-50%)\" };\n const days = hi - lo + 1;\n const draft = annotationDraft;\n return (\n <div className=\"absolute top-2 z-30\" style={anchorStyle}>\n <div className=\"flex items-center gap-1 rounded-full border border-foreground/10 bg-background/95 py-1 pl-2 pr-1 shadow-[0_10px_28px_rgba(15,23,42,0.18)] backdrop-blur-xl dark:shadow-[0_10px_28px_rgba(0,0,0,0.55)]\">\n {draft == null ? (\n <>\n <span className=\"flex items-center gap-1.5 whitespace-nowrap px-1 font-mono text-[10px] tabular-nums text-muted-foreground\">\n <span className=\"text-foreground\">\n {fmtValue(data[lo]!.ts, xFormatKind)} – {fmtValue(data[hi]!.ts, xFormatKind)}\n </span>\n <span className=\"text-foreground/30\">·</span>\n <span>{strings.daysShort(days)}</span>\n </span>\n <div className=\"h-4 w-px bg-foreground/10\" aria-hidden=\"true\" />\n <DesignButton\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-7 gap-1.5 px-2 text-[11px]\"\n onClick={() => {\n onChange((prev) => ({\n ...prev,\n zoomRange: [visibleStart + lo, visibleStart + hi],\n pinnedIndex: null,\n }));\n setCommittedRange(null);\n setAnnotationDraft(null);\n setHoverIndex(null);\n }}\n >\n <MagnifyingGlassPlusIcon weight=\"bold\" className=\"size-3.5\" aria-hidden=\"true\" />\n {strings.zoomIn}\n </DesignButton>\n <DesignButton\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-7 gap-1.5 px-2 text-[11px]\"\n onClick={() => setAnnotationDraft(\"\")}\n >\n <FlagIcon weight=\"bold\" className=\"size-3.5\" aria-hidden=\"true\" />\n {strings.annotate}\n </DesignButton>\n <DesignButton\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-7 w-7 p-0\"\n aria-label={strings.clearSelection}\n onClick={() => setCommittedRange(null)}\n >\n <XIcon weight=\"bold\" className=\"size-3.5\" aria-hidden=\"true\" />\n </DesignButton>\n </>\n ) : (\n <form\n className=\"flex items-center gap-1 px-1\"\n onSubmit={(e) => {\n e.preventDefault();\n const label = draft.trim();\n if (label.length === 0) return;\n const mid = Math.round((lo + hi) / 2);\n onAnnotationCreate?.({\n index: visibleStart + mid,\n label,\n description: label,\n });\n setCommittedRange(null);\n setAnnotationDraft(null);\n }}\n >\n <FlagIcon\n weight=\"bold\"\n className=\"size-3.5 text-amber-500 dark:text-amber-400\"\n aria-hidden=\"true\"\n />\n <input\n autoFocus\n type=\"text\"\n value={draft}\n onChange={(e) => setAnnotationDraft(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === \"Escape\") {\n e.preventDefault();\n setAnnotationDraft(null);\n }\n }}\n maxLength={40}\n placeholder={strings.annotationPlaceholder}\n aria-label={strings.annotationLabelAria}\n className=\"w-44 bg-transparent px-1 py-0.5 font-mono text-[11px] text-foreground placeholder:text-muted-foreground/60 focus-visible:outline-none\"\n />\n <DesignButton\n type=\"submit\"\n size=\"sm\"\n variant=\"default\"\n className=\"h-7 px-2.5 text-[11px]\"\n disabled={draft.trim().length === 0}\n >\n {strings.save}\n </DesignButton>\n <DesignButton\n type=\"button\"\n size=\"sm\"\n variant=\"ghost\"\n className=\"h-7 px-2 text-[11px]\"\n onClick={() => setAnnotationDraft(null)}\n >\n {strings.cancel}\n </DesignButton>\n </form>\n )}\n </div>\n </div>\n );\n })()}\n\n {showAnnotationsLayer && (\n <div className=\"pointer-events-none absolute inset-x-0 top-1 h-0\">\n {annotations.map((a) => {\n return (\n <div\n key={a.index}\n className=\"absolute -translate-x-1/2\"\n style={{ left: indexToCss(a.index) }}\n >\n <button\n type=\"button\"\n aria-label={a.description}\n className=\"peer pointer-events-auto inline-flex items-center gap-1 rounded-full bg-amber-500/15 px-1.5 py-[1px] font-mono text-[9px] font-semibold uppercase tracking-wider text-amber-700 ring-1 ring-amber-500/30 dark:text-amber-300 dark:bg-amber-500/20 dark:ring-amber-500/40 transition-colors duration-150 hover:bg-amber-500/25 hover:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500/50\"\n >\n <FlagIcon weight=\"fill\" className=\"size-2.5\" aria-hidden=\"true\" />\n {a.label}\n </button>\n <span\n role=\"tooltip\"\n className=\"pointer-events-none absolute left-1/2 top-full z-30 mt-1 hidden -translate-x-1/2 whitespace-nowrap rounded-md bg-foreground px-2 py-1 text-[10px] font-normal text-background shadow-md peer-hover:block peer-focus-visible:block\"\n >\n {a.description}\n </span>\n </div>\n );\n })}\n </div>\n )}\n\n {activeIndex != null && activePoint && (() => {\n const primaryView = buildTooltipLayerView({\n show: showPrimary,\n layer: primaryLayer,\n color: primaryColor,\n segmented: primarySegmented,\n segmentSeries: primarySegmentSeries,\n segmentRows: primarySegments,\n segmentTotals: primarySegmentTotals,\n segmentColorsLight: segmentColors.primary.light,\n segmentColorsDark: segmentColors.primary.dark,\n activeIndex,\n activePoint,\n fallbackLabel: \"Chart\",\n });\n const compareView = buildTooltipLayerView({\n show: showCompare,\n layer: compareLayer,\n color: compareColor,\n segmented: compareSegmented,\n segmentSeries: compareSegmentSeries,\n segmentRows: compareSegments,\n segmentTotals: compareSegmentTotals,\n segmentColorsLight: segmentColors.compare.light,\n segmentColorsDark: segmentColors.compare.dark,\n activeIndex,\n activePoint,\n });\n const delta: AnalyticsChartDelta | null = primaryView && compareView\n ? formatDelta(primaryView.total, compareView.total)\n : null;\n const ctx: AnalyticsChartTooltipContext = {\n activeIndex,\n point: activePoint,\n isPinned: pinnedIndex != null,\n primary: primaryView,\n compare: compareView,\n delta,\n formatValue: (v) => fmtValue(v, yFormatKind),\n formatDate: (ts) => fmtValue(ts, xFormatKind),\n strings,\n };\n return (\n <div\n className={cn(\n \"pointer-events-none absolute top-10 z-20\",\n \"transition-[transform,opacity] duration-150\",\n )}\n style={{\n left: indexToCss(activeIndex),\n transform: shouldFlip\n ? \"translateX(calc(-100% - 16px))\"\n : \"translateX(16px)\",\n }}\n >\n {renderTooltipFn(ctx)}\n </div>\n );\n })()}\n </div>\n );\n}\n\n"],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AnalyticsChartStrings } from "./strings.js";
|
|
2
|
+
import { AnalyticsChartDelta, Point } from "./types.js";
|
|
3
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/components/analytics-chart/default-analytics-chart-tooltip.d.ts
|
|
6
|
+
/** Trend pill — small rounded badge with an up/down/flat arrow, a signed
|
|
7
|
+
* percentage, and an optional trailing label. Shared between the default
|
|
8
|
+
* tooltip, the pie view, and the demo panels (which re-export it). */
|
|
9
|
+
declare function TrendPill({
|
|
10
|
+
delta,
|
|
11
|
+
label,
|
|
12
|
+
size
|
|
13
|
+
}: {
|
|
14
|
+
delta: AnalyticsChartDelta;
|
|
15
|
+
label?: string;
|
|
16
|
+
size?: "sm" | "md";
|
|
17
|
+
}): react_jsx_runtime0.JSX.Element;
|
|
18
|
+
type AnalyticsChartTooltipSegmentRow = {
|
|
19
|
+
key: string;
|
|
20
|
+
label: string;
|
|
21
|
+
value: number; /** Light-theme color for the dot / swatch. */
|
|
22
|
+
color: string; /** Dark-theme color for the dot / swatch. */
|
|
23
|
+
colorDark: string;
|
|
24
|
+
};
|
|
25
|
+
type AnalyticsChartTooltipLayerView = {
|
|
26
|
+
/** Stable layer id (e.g. `"signups"`, `"previous"`). */id: string; /** Consumer-provided layer label. */
|
|
27
|
+
label: string; /** Resolved layer color (light theme). */
|
|
28
|
+
color: string; /** Resolved layer color (dark theme). */
|
|
29
|
+
colorDark: string;
|
|
30
|
+
/** Flat total for this layer at the hovered index. Populated regardless
|
|
31
|
+
* of segmentation so consumers can render the same number in either mode. */
|
|
32
|
+
total: number; /** True iff this layer is rendered as a stacked breakdown. */
|
|
33
|
+
segmented: boolean;
|
|
34
|
+
/** Per-segment rows — empty when `segmented === false`. Order matches
|
|
35
|
+
* `segmentSeries`. */
|
|
36
|
+
segments: AnalyticsChartTooltipSegmentRow[];
|
|
37
|
+
};
|
|
38
|
+
type AnalyticsChartTooltipContext = {
|
|
39
|
+
/** Index into the visible window. */activeIndex: number; /** Raw point at `activeIndex` — convenient for `.ts` access. */
|
|
40
|
+
point: Point; /** True when the tooltip is pinned (via click) and stable under hover. */
|
|
41
|
+
isPinned: boolean; /** Primary layer view or null when the primary layer is hidden. */
|
|
42
|
+
primary: AnalyticsChartTooltipLayerView | null; /** Compare layer view or null when the compare layer is hidden. */
|
|
43
|
+
compare: AnalyticsChartTooltipLayerView | null;
|
|
44
|
+
/** Flat-mode delta between primary and compare totals. Null when either
|
|
45
|
+
* side is hidden. Consumers should feed this into their trend pill. */
|
|
46
|
+
delta: AnalyticsChartDelta | null; /** Pre-bound value formatter for y-axis values. */
|
|
47
|
+
formatValue: (v: number) => string; /** Pre-bound formatter for x-axis values. */
|
|
48
|
+
formatDate: (ts: number) => string; /** Resolved strings — already merged with defaults. */
|
|
49
|
+
strings: AnalyticsChartStrings;
|
|
50
|
+
};
|
|
51
|
+
type DefaultAnalyticsChartTooltipProps = {
|
|
52
|
+
ctx: AnalyticsChartTooltipContext;
|
|
53
|
+
};
|
|
54
|
+
/** The default tooltip body. The tooltip is rendered as an
|
|
55
|
+
* absolutely-positioned sibling of `<ChartContainer>`, which means it sits
|
|
56
|
+
* OUTSIDE the `[data-chart=…]` subtree that scopes shadcn's `--color-${key}`
|
|
57
|
+
* CSS variables. We therefore cannot reference those variables for segment
|
|
58
|
+
* swatches — instead, every swatch uses a single span with `--c-l`/`--c-d`
|
|
59
|
+
* custom properties + Tailwind arbitrary variants so one DOM element covers
|
|
60
|
+
* both themes. */
|
|
61
|
+
declare function DefaultAnalyticsChartTooltip({
|
|
62
|
+
ctx
|
|
63
|
+
}: DefaultAnalyticsChartTooltipProps): react_jsx_runtime0.JSX.Element;
|
|
64
|
+
//#endregion
|
|
65
|
+
export { AnalyticsChartTooltipContext, AnalyticsChartTooltipLayerView, AnalyticsChartTooltipSegmentRow, DefaultAnalyticsChartTooltip, DefaultAnalyticsChartTooltipProps, TrendPill };
|
|
66
|
+
//# sourceMappingURL=default-analytics-chart-tooltip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-analytics-chart-tooltip.d.ts","names":[],"sources":["../../../src/components/analytics-chart/default-analytics-chart-tooltip.tsx"],"mappings":";;;;;;;;iBAegB,SAAA,CAAA;EACd,KAAA;EACA,KAAA;EACA;AAAA;EAEA,KAAA,EAAO,mBAAA;EACP,KAAA;EACA,IAAA;AAAA,IACD,kBAAA,CAAA,GAAA,CAAA,OAAA;AAAA,KAqBW,+BAAA;EACV,GAAA;EACA,KAAA;EACA,KAAA,UAxBD;EA0BC,KAAA,UAjCA;EAmCA,SAAA;AAAA;AAAA,KAGU,8BAAA;EApCV,wDAsCA,EAAA,UApCO;EAsCP,KAAA,UApCA;EAsCA,KAAA,UArCD;EAuCC,SAAA;EAvCD;AAqBD;EAqBE,KAAA;EAEA,SAAA;EAtBA;;EAyBA,QAAA,EAAU,+BAAA;AAAA;AAAA,KAGA,4BAAA;EAtBD,qCAwBT,WAAA,UArBU;EAuBV,KAAA,EAAO,KAAA;EAEP,QAAA,WAvBA;EAyBA,OAAA,EAAS,8BAAA,SArBT;EAuBA,OAAA,EAAS,8BAAA;EAlBT;;EAqBA,KAAA,EAAO,mBAAA,SAhBG;EAkBV,WAAA,GAAc,CAAA,qBAlB2B;EAoBzC,UAAA,GAAa,EAAA,qBAjByB;EAmBtC,OAAA,EAAS,qBAAA;AAAA;AAAA,KAGC,iCAAA;EACV,GAAA,EAAK,4BAAA;AAAA;;;;;;;;iBAUS,4BAAA,CAAA;EAA+B;AAAA,GAAO,iCAAA,GAAiC,kBAAA,CAAA,GAAA,CAAA,OAAA"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_chunk = require('../../chunk-BE-pF4vm.js');
|
|
3
|
+
let _phosphor_icons_react = require("@phosphor-icons/react");
|
|
4
|
+
let _stackframe_stack_ui = require("@stackframe/stack-ui");
|
|
5
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
6
|
+
|
|
7
|
+
//#region src/components/analytics-chart/default-analytics-chart-tooltip.tsx
|
|
8
|
+
/** Trend pill — small rounded badge with an up/down/flat arrow, a signed
|
|
9
|
+
* percentage, and an optional trailing label. Shared between the default
|
|
10
|
+
* tooltip, the pie view, and the demo panels (which re-export it). */
|
|
11
|
+
function TrendPill({ delta, label, size = "sm" }) {
|
|
12
|
+
const { pct, sign } = delta;
|
|
13
|
+
const tone = sign === "up" ? "text-emerald-600 dark:text-emerald-400 bg-emerald-500/10" : sign === "down" ? "text-rose-600 dark:text-rose-400 bg-rose-500/10" : "text-muted-foreground bg-foreground/[0.06]";
|
|
14
|
+
const Icon = sign === "up" ? _phosphor_icons_react.ArrowUpIcon : sign === "down" ? _phosphor_icons_react.ArrowDownIcon : _phosphor_icons_react.MinusIcon;
|
|
15
|
+
const text = pct == null ? "—" : `${pct > 0 ? "+" : ""}${pct}%`;
|
|
16
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
17
|
+
className: (0, _stackframe_stack_ui.cn)("inline-flex items-center gap-1 rounded-full font-mono tabular-nums font-medium", size === "md" ? "px-2 py-0.5 text-[11px]" : "px-1.5 py-[1px] text-[10px]", tone),
|
|
18
|
+
children: [
|
|
19
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, {
|
|
20
|
+
weight: "bold",
|
|
21
|
+
className: size === "md" ? "size-3" : "size-2.5",
|
|
22
|
+
"aria-hidden": "true"
|
|
23
|
+
}),
|
|
24
|
+
text,
|
|
25
|
+
label && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
26
|
+
className: "ml-0.5 text-muted-foreground font-normal",
|
|
27
|
+
children: label
|
|
28
|
+
})
|
|
29
|
+
]
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
/** The default tooltip body. The tooltip is rendered as an
|
|
33
|
+
* absolutely-positioned sibling of `<ChartContainer>`, which means it sits
|
|
34
|
+
* OUTSIDE the `[data-chart=…]` subtree that scopes shadcn's `--color-${key}`
|
|
35
|
+
* CSS variables. We therefore cannot reference those variables for segment
|
|
36
|
+
* swatches — instead, every swatch uses a single span with `--c-l`/`--c-d`
|
|
37
|
+
* custom properties + Tailwind arbitrary variants so one DOM element covers
|
|
38
|
+
* both themes. */
|
|
39
|
+
function DefaultAnalyticsChartTooltip({ ctx }) {
|
|
40
|
+
const { point, isPinned, primary, compare, delta, formatValue: fv, formatDate: fd, strings } = ctx;
|
|
41
|
+
const anySegmented = (primary?.segmented ?? false) || (compare?.segmented ?? false);
|
|
42
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
43
|
+
className: "rounded-xl border border-foreground/10 bg-background/95 px-3 py-2.5 shadow-[0_10px_28px_rgba(15,23,42,0.18)] backdrop-blur-xl dark:shadow-[0_10px_28px_rgba(0,0,0,0.55)] min-w-[180px]",
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
46
|
+
className: "flex items-center justify-between gap-3",
|
|
47
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
48
|
+
className: "font-mono text-[10px] uppercase tracking-wider text-muted-foreground",
|
|
49
|
+
children: fd(point.ts)
|
|
50
|
+
}), isPinned && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
51
|
+
className: "inline-flex items-center gap-1 rounded-full bg-blue-500/10 px-1.5 py-[1px] text-[9px] font-semibold uppercase tracking-wider text-blue-600 dark:text-blue-300",
|
|
52
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.PushPinSimpleIcon, {
|
|
53
|
+
weight: "fill",
|
|
54
|
+
className: "size-2.5",
|
|
55
|
+
"aria-hidden": "true"
|
|
56
|
+
}), strings.pinnedBadge]
|
|
57
|
+
})]
|
|
58
|
+
}),
|
|
59
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
60
|
+
className: "mt-2 flex flex-col gap-1.5",
|
|
61
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipLayerRows, {
|
|
62
|
+
view: primary,
|
|
63
|
+
keyPrefix: "p",
|
|
64
|
+
fv,
|
|
65
|
+
muted: false
|
|
66
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipLayerRows, {
|
|
67
|
+
view: compare,
|
|
68
|
+
keyPrefix: "c",
|
|
69
|
+
fv,
|
|
70
|
+
muted: true
|
|
71
|
+
})]
|
|
72
|
+
}),
|
|
73
|
+
anySegmented && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
74
|
+
className: "mt-2 flex flex-col gap-1 border-t border-foreground/[0.07] pt-2",
|
|
75
|
+
children: [
|
|
76
|
+
primary?.segmented && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayerSummaryRow, {
|
|
77
|
+
label: `${primary.label}${strings.layerTotalSuffix}`,
|
|
78
|
+
value: fv(primary.total),
|
|
79
|
+
muted: false
|
|
80
|
+
}),
|
|
81
|
+
compare?.segmented && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayerSummaryRow, {
|
|
82
|
+
label: `${compare.label}${strings.layerTotalSuffix}`,
|
|
83
|
+
value: fv(compare.total),
|
|
84
|
+
muted: true
|
|
85
|
+
}),
|
|
86
|
+
primary?.segmented && compare?.segmented && delta && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
87
|
+
className: "mt-1 flex items-center justify-between",
|
|
88
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
89
|
+
className: "font-mono text-[10px] uppercase tracking-wider text-muted-foreground",
|
|
90
|
+
children: strings.deltaVsPrev
|
|
91
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrendPill, {
|
|
92
|
+
delta,
|
|
93
|
+
size: "sm"
|
|
94
|
+
})]
|
|
95
|
+
})
|
|
96
|
+
]
|
|
97
|
+
}),
|
|
98
|
+
!anySegmented && delta && primary && compare && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
99
|
+
className: "mt-2 flex items-center justify-between border-t border-foreground/[0.07] pt-2",
|
|
100
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
101
|
+
className: "font-mono text-[10px] uppercase tracking-wider text-muted-foreground",
|
|
102
|
+
children: strings.deltaVsPrev
|
|
103
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TrendPill, {
|
|
104
|
+
delta,
|
|
105
|
+
size: "sm"
|
|
106
|
+
})]
|
|
107
|
+
}),
|
|
108
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
109
|
+
className: "mt-2 flex items-center gap-1 text-[10px] text-muted-foreground",
|
|
110
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_phosphor_icons_react.CursorClickIcon, {
|
|
111
|
+
weight: "bold",
|
|
112
|
+
className: "size-2.5",
|
|
113
|
+
"aria-hidden": "true"
|
|
114
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: isPinned ? strings.hintClickAnywhereUnpin : strings.hintClickToPin })]
|
|
115
|
+
})
|
|
116
|
+
]
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Render either a single flat row or the per-segment breakdown rows for
|
|
120
|
+
* one tooltip layer view. Collapses what were formerly two parallel
|
|
121
|
+
* primary/compare branches into one component. */
|
|
122
|
+
function TooltipLayerRows({ view, keyPrefix, fv, muted }) {
|
|
123
|
+
if (!view) return null;
|
|
124
|
+
if (!view.segmented) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayerTotalRow, {
|
|
125
|
+
light: view.color,
|
|
126
|
+
dark: view.colorDark,
|
|
127
|
+
label: view.label,
|
|
128
|
+
value: fv(view.total),
|
|
129
|
+
muted
|
|
130
|
+
});
|
|
131
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: view.segments.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LayerTotalRow, {
|
|
132
|
+
light: s.color,
|
|
133
|
+
dark: s.colorDark,
|
|
134
|
+
label: s.label,
|
|
135
|
+
value: fv(s.value),
|
|
136
|
+
muted
|
|
137
|
+
}, `${keyPrefix}-${s.key}`)) });
|
|
138
|
+
}
|
|
139
|
+
/** Single row: swatch + label + value. One DOM element for the swatch —
|
|
140
|
+
* dark-mode color is picked up via `--c-d`. */
|
|
141
|
+
function LayerTotalRow({ light, dark, label, value, muted }) {
|
|
142
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
143
|
+
className: "flex items-center gap-2.5",
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
146
|
+
className: "size-2 rounded-full bg-[var(--c-l)] dark:bg-[var(--c-d)]",
|
|
147
|
+
style: {
|
|
148
|
+
"--c-l": light,
|
|
149
|
+
"--c-d": dark
|
|
150
|
+
}
|
|
151
|
+
}),
|
|
152
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
153
|
+
className: "text-[11px] text-muted-foreground",
|
|
154
|
+
children: label
|
|
155
|
+
}),
|
|
156
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
157
|
+
className: (0, _stackframe_stack_ui.cn)("ml-auto font-mono text-xs tabular-nums", muted ? "text-muted-foreground" : "font-semibold text-foreground"),
|
|
158
|
+
children: value
|
|
159
|
+
})
|
|
160
|
+
]
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function LayerSummaryRow({ label, value, muted }) {
|
|
164
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
165
|
+
className: "flex items-center justify-between",
|
|
166
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
167
|
+
className: "font-mono text-[10px] uppercase tracking-wider text-muted-foreground",
|
|
168
|
+
children: label
|
|
169
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
170
|
+
className: (0, _stackframe_stack_ui.cn)("font-mono text-xs tabular-nums", muted ? "text-muted-foreground" : "font-semibold text-foreground"),
|
|
171
|
+
children: value
|
|
172
|
+
})]
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
exports.DefaultAnalyticsChartTooltip = DefaultAnalyticsChartTooltip;
|
|
178
|
+
exports.TrendPill = TrendPill;
|
|
179
|
+
//# sourceMappingURL=default-analytics-chart-tooltip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"default-analytics-chart-tooltip.js","names":["ArrowUpIcon","ArrowDownIcon","MinusIcon","PushPinSimpleIcon","CursorClickIcon"],"sources":["../../../src/components/analytics-chart/default-analytics-chart-tooltip.tsx"],"sourcesContent":["import { cn } from \"@stackframe/stack-ui\";\nimport {\n ArrowDownIcon,\n ArrowUpIcon,\n CursorClickIcon,\n MinusIcon,\n PushPinSimpleIcon,\n} from \"@phosphor-icons/react\";\nimport type { CSSProperties } from \"react\";\nimport type { AnalyticsChartStrings } from \"./strings\";\nimport type { AnalyticsChartDelta, Point } from \"./types\";\n\n/** Trend pill — small rounded badge with an up/down/flat arrow, a signed\n * percentage, and an optional trailing label. Shared between the default\n * tooltip, the pie view, and the demo panels (which re-export it). */\nexport function TrendPill({\n delta,\n label,\n size = \"sm\",\n}: {\n delta: AnalyticsChartDelta,\n label?: string,\n size?: \"sm\" | \"md\",\n}) {\n const { pct, sign } = delta;\n const tone =\n sign === \"up\" ? \"text-emerald-600 dark:text-emerald-400 bg-emerald-500/10\"\n : sign === \"down\" ? \"text-rose-600 dark:text-rose-400 bg-rose-500/10\"\n : \"text-muted-foreground bg-foreground/[0.06]\";\n const Icon = sign === \"up\" ? ArrowUpIcon : sign === \"down\" ? ArrowDownIcon : MinusIcon;\n const text = pct == null ? \"—\" : `${pct > 0 ? \"+\" : \"\"}${pct}%`;\n return (\n <span className={cn(\n \"inline-flex items-center gap-1 rounded-full font-mono tabular-nums font-medium\",\n size === \"md\" ? \"px-2 py-0.5 text-[11px]\" : \"px-1.5 py-[1px] text-[10px]\",\n tone,\n )}>\n <Icon weight=\"bold\" className={size === \"md\" ? \"size-3\" : \"size-2.5\"} aria-hidden=\"true\" />\n {text}\n {label && <span className=\"ml-0.5 text-muted-foreground font-normal\">{label}</span>}\n </span>\n );\n}\n\nexport type AnalyticsChartTooltipSegmentRow = {\n key: string,\n label: string,\n value: number,\n /** Light-theme color for the dot / swatch. */\n color: string,\n /** Dark-theme color for the dot / swatch. */\n colorDark: string,\n};\n\nexport type AnalyticsChartTooltipLayerView = {\n /** Stable layer id (e.g. `\"signups\"`, `\"previous\"`). */\n id: string,\n /** Consumer-provided layer label. */\n label: string,\n /** Resolved layer color (light theme). */\n color: string,\n /** Resolved layer color (dark theme). */\n colorDark: string,\n /** Flat total for this layer at the hovered index. Populated regardless\n * of segmentation so consumers can render the same number in either mode. */\n total: number,\n /** True iff this layer is rendered as a stacked breakdown. */\n segmented: boolean,\n /** Per-segment rows — empty when `segmented === false`. Order matches\n * `segmentSeries`. */\n segments: AnalyticsChartTooltipSegmentRow[],\n};\n\nexport type AnalyticsChartTooltipContext = {\n /** Index into the visible window. */\n activeIndex: number,\n /** Raw point at `activeIndex` — convenient for `.ts` access. */\n point: Point,\n /** True when the tooltip is pinned (via click) and stable under hover. */\n isPinned: boolean,\n /** Primary layer view or null when the primary layer is hidden. */\n primary: AnalyticsChartTooltipLayerView | null,\n /** Compare layer view or null when the compare layer is hidden. */\n compare: AnalyticsChartTooltipLayerView | null,\n /** Flat-mode delta between primary and compare totals. Null when either\n * side is hidden. Consumers should feed this into their trend pill. */\n delta: AnalyticsChartDelta | null,\n /** Pre-bound value formatter for y-axis values. */\n formatValue: (v: number) => string,\n /** Pre-bound formatter for x-axis values. */\n formatDate: (ts: number) => string,\n /** Resolved strings — already merged with defaults. */\n strings: AnalyticsChartStrings,\n};\n\nexport type DefaultAnalyticsChartTooltipProps = {\n ctx: AnalyticsChartTooltipContext,\n};\n\n/** The default tooltip body. The tooltip is rendered as an\n * absolutely-positioned sibling of `<ChartContainer>`, which means it sits\n * OUTSIDE the `[data-chart=…]` subtree that scopes shadcn's `--color-${key}`\n * CSS variables. We therefore cannot reference those variables for segment\n * swatches — instead, every swatch uses a single span with `--c-l`/`--c-d`\n * custom properties + Tailwind arbitrary variants so one DOM element covers\n * both themes. */\nexport function DefaultAnalyticsChartTooltip({ ctx }: DefaultAnalyticsChartTooltipProps) {\n const { point, isPinned, primary, compare, delta, formatValue: fv, formatDate: fd, strings } = ctx;\n const anySegmented = (primary?.segmented ?? false) || (compare?.segmented ?? false);\n return (\n <div className=\"rounded-xl border border-foreground/10 bg-background/95 px-3 py-2.5 shadow-[0_10px_28px_rgba(15,23,42,0.18)] backdrop-blur-xl dark:shadow-[0_10px_28px_rgba(0,0,0,0.55)] min-w-[180px]\">\n <div className=\"flex items-center justify-between gap-3\">\n <span className=\"font-mono text-[10px] uppercase tracking-wider text-muted-foreground\">\n {fd(point.ts)}\n </span>\n {isPinned && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-blue-500/10 px-1.5 py-[1px] text-[9px] font-semibold uppercase tracking-wider text-blue-600 dark:text-blue-300\">\n <PushPinSimpleIcon weight=\"fill\" className=\"size-2.5\" aria-hidden=\"true\" />\n {strings.pinnedBadge}\n </span>\n )}\n </div>\n <div className=\"mt-2 flex flex-col gap-1.5\">\n <TooltipLayerRows view={primary} keyPrefix=\"p\" fv={fv} muted={false} />\n <TooltipLayerRows view={compare} keyPrefix=\"c\" fv={fv} muted />\n </div>\n {anySegmented && (\n <div className=\"mt-2 flex flex-col gap-1 border-t border-foreground/[0.07] pt-2\">\n {primary?.segmented && (\n <LayerSummaryRow\n label={`${primary.label}${strings.layerTotalSuffix}`}\n value={fv(primary.total)}\n muted={false}\n />\n )}\n {compare?.segmented && (\n <LayerSummaryRow\n label={`${compare.label}${strings.layerTotalSuffix}`}\n value={fv(compare.total)}\n muted\n />\n )}\n {primary?.segmented && compare?.segmented && delta && (\n <div className=\"mt-1 flex items-center justify-between\">\n <span className=\"font-mono text-[10px] uppercase tracking-wider text-muted-foreground\">\n {strings.deltaVsPrev}\n </span>\n <TrendPill delta={delta} size=\"sm\" />\n </div>\n )}\n </div>\n )}\n {!anySegmented && delta && primary && compare && (\n <div className=\"mt-2 flex items-center justify-between border-t border-foreground/[0.07] pt-2\">\n <span className=\"font-mono text-[10px] uppercase tracking-wider text-muted-foreground\">\n {strings.deltaVsPrev}\n </span>\n <TrendPill delta={delta} size=\"sm\" />\n </div>\n )}\n <div className=\"mt-2 flex items-center gap-1 text-[10px] text-muted-foreground\">\n <CursorClickIcon weight=\"bold\" className=\"size-2.5\" aria-hidden=\"true\" />\n <span>{isPinned ? strings.hintClickAnywhereUnpin : strings.hintClickToPin}</span>\n </div>\n </div>\n );\n}\n\n/** Render either a single flat row or the per-segment breakdown rows for\n * one tooltip layer view. Collapses what were formerly two parallel\n * primary/compare branches into one component. */\nfunction TooltipLayerRows({\n view,\n keyPrefix,\n fv,\n muted,\n}: {\n view: AnalyticsChartTooltipLayerView | null,\n keyPrefix: string,\n fv: (v: number) => string,\n muted: boolean,\n}) {\n if (!view) return null;\n if (!view.segmented) {\n return (\n <LayerTotalRow\n light={view.color}\n dark={view.colorDark}\n label={view.label}\n value={fv(view.total)}\n muted={muted}\n />\n );\n }\n return (\n <>\n {view.segments.map((s) => (\n <LayerTotalRow\n key={`${keyPrefix}-${s.key}`}\n light={s.color}\n dark={s.colorDark}\n label={s.label}\n value={fv(s.value)}\n muted={muted}\n />\n ))}\n </>\n );\n}\n\n/** Single row: swatch + label + value. One DOM element for the swatch —\n * dark-mode color is picked up via `--c-d`. */\nfunction LayerTotalRow({\n light,\n dark,\n label,\n value,\n muted,\n}: {\n light: string,\n dark: string,\n label: string,\n value: string,\n muted: boolean,\n}) {\n return (\n <div className=\"flex items-center gap-2.5\">\n <span\n className=\"size-2 rounded-full bg-[var(--c-l)] dark:bg-[var(--c-d)]\"\n style={{ \"--c-l\": light, \"--c-d\": dark } as CSSProperties}\n />\n <span className=\"text-[11px] text-muted-foreground\">{label}</span>\n <span className={cn(\n \"ml-auto font-mono text-xs tabular-nums\",\n muted ? \"text-muted-foreground\" : \"font-semibold text-foreground\",\n )}>\n {value}\n </span>\n </div>\n );\n}\n\nfunction LayerSummaryRow({\n label,\n value,\n muted,\n}: {\n label: string,\n value: string,\n muted: boolean,\n}) {\n return (\n <div className=\"flex items-center justify-between\">\n <span className=\"font-mono text-[10px] uppercase tracking-wider text-muted-foreground\">\n {label}\n </span>\n <span className={cn(\n \"font-mono text-xs tabular-nums\",\n muted ? \"text-muted-foreground\" : \"font-semibold text-foreground\",\n )}>\n {value}\n </span>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AnalyticsChartDelta, FormatKind, FormatKindType } from "./types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/components/analytics-chart/format.d.ts
|
|
4
|
+
declare const FORMAT_KIND_TYPES: FormatKindType[];
|
|
5
|
+
declare const DEFAULT_FORMAT_KIND: { [K in FormatKindType]: Extract<FormatKind, {
|
|
6
|
+
type: K;
|
|
7
|
+
}> };
|
|
8
|
+
/** `short` uses compact notation (e.g. `1.2K`), not a custom `k` suffix. */
|
|
9
|
+
declare function formatValue(value: number, kind: FormatKind): string;
|
|
10
|
+
declare function formatDelta(current: number, previous: number): AnalyticsChartDelta;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { DEFAULT_FORMAT_KIND, FORMAT_KIND_TYPES, formatDelta, formatValue };
|
|
13
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","names":[],"sources":["../../../src/components/analytics-chart/format.ts"],"mappings":";;;cAEa,iBAAA,EAAmB,cAAA;AAAA,cASnB,mBAAA,UAA6B,cAAA,GAAiB,OAAA,CAAQ,UAAA;EAAc,IAAA,EAAM,CAAA;AAAA;;iBAoBvE,WAAA,CAAY,KAAA,UAAe,IAAA,EAAM,UAAA;AAAA,iBA+DjC,WAAA,CAAY,OAAA,UAAiB,QAAA,WAAmB,mBAAA"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
//#region src/components/analytics-chart/format.ts
|
|
4
|
+
const FORMAT_KIND_TYPES = [
|
|
5
|
+
"numeric",
|
|
6
|
+
"short",
|
|
7
|
+
"currency",
|
|
8
|
+
"duration",
|
|
9
|
+
"datetime",
|
|
10
|
+
"percent"
|
|
11
|
+
];
|
|
12
|
+
const DEFAULT_FORMAT_KIND = {
|
|
13
|
+
numeric: {
|
|
14
|
+
type: "numeric",
|
|
15
|
+
locale: "en-US",
|
|
16
|
+
decimals: 0
|
|
17
|
+
},
|
|
18
|
+
short: {
|
|
19
|
+
type: "short",
|
|
20
|
+
precision: 1,
|
|
21
|
+
locale: "en-US"
|
|
22
|
+
},
|
|
23
|
+
currency: {
|
|
24
|
+
type: "currency",
|
|
25
|
+
currency: "USD",
|
|
26
|
+
divisor: 100,
|
|
27
|
+
locale: "en-US"
|
|
28
|
+
},
|
|
29
|
+
duration: {
|
|
30
|
+
type: "duration",
|
|
31
|
+
unit: "s",
|
|
32
|
+
showZero: false
|
|
33
|
+
},
|
|
34
|
+
datetime: {
|
|
35
|
+
type: "datetime",
|
|
36
|
+
style: "short",
|
|
37
|
+
locale: "en-US"
|
|
38
|
+
},
|
|
39
|
+
percent: {
|
|
40
|
+
type: "percent",
|
|
41
|
+
source: "fraction",
|
|
42
|
+
decimals: 1
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function formatRelative(value, locale) {
|
|
46
|
+
const diff = value - Date.now();
|
|
47
|
+
const absSec = Math.abs(diff) / 1e3;
|
|
48
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
49
|
+
if (absSec >= 86400) return rtf.format(Math.round(diff / 864e5), "day");
|
|
50
|
+
if (absSec >= 3600) return rtf.format(Math.round(diff / 36e5), "hour");
|
|
51
|
+
if (absSec >= 60) return rtf.format(Math.round(diff / 6e4), "minute");
|
|
52
|
+
return "just now";
|
|
53
|
+
}
|
|
54
|
+
/** `short` uses compact notation (e.g. `1.2K`), not a custom `k` suffix. */
|
|
55
|
+
function formatValue(value, kind) {
|
|
56
|
+
switch (kind.type) {
|
|
57
|
+
case "numeric": {
|
|
58
|
+
const decimals = kind.decimals ?? 0;
|
|
59
|
+
return value.toLocaleString(kind.locale ?? "en-US", {
|
|
60
|
+
minimumFractionDigits: decimals,
|
|
61
|
+
maximumFractionDigits: decimals
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
case "short": {
|
|
65
|
+
const precision = kind.precision ?? 1;
|
|
66
|
+
return new Intl.NumberFormat(kind.locale ?? "en-US", {
|
|
67
|
+
notation: "compact",
|
|
68
|
+
compactDisplay: "short",
|
|
69
|
+
minimumFractionDigits: precision,
|
|
70
|
+
maximumFractionDigits: precision
|
|
71
|
+
}).format(value);
|
|
72
|
+
}
|
|
73
|
+
case "currency": {
|
|
74
|
+
const divisor = kind.divisor ?? 1;
|
|
75
|
+
return new Intl.NumberFormat(kind.locale ?? "en-US", {
|
|
76
|
+
style: "currency",
|
|
77
|
+
currency: kind.currency ?? "USD"
|
|
78
|
+
}).format(value / divisor);
|
|
79
|
+
}
|
|
80
|
+
case "duration": {
|
|
81
|
+
const unit = kind.unit ?? "s";
|
|
82
|
+
const seconds = unit === "ms" ? value / 1e3 : unit === "m" ? value * 60 : unit === "h" ? value * 3600 : value;
|
|
83
|
+
const h = Math.floor(seconds / 3600);
|
|
84
|
+
const m = Math.floor(seconds % 3600 / 60);
|
|
85
|
+
const s = Math.floor(seconds % 60);
|
|
86
|
+
if (h > 0) return `${h}h ${m}m ${s}s`;
|
|
87
|
+
if (m > 0) return `${m}m ${s}s`;
|
|
88
|
+
if (unit === "ms" && seconds < 1) return `${Math.round(value)}ms`;
|
|
89
|
+
if (s > 0 || kind.showZero) return `${s}s`;
|
|
90
|
+
return "0s";
|
|
91
|
+
}
|
|
92
|
+
case "datetime": {
|
|
93
|
+
const d = new Date(value);
|
|
94
|
+
const style = kind.style ?? "short";
|
|
95
|
+
const locale = kind.locale ?? "en-US";
|
|
96
|
+
if (style === "iso") return d.toISOString();
|
|
97
|
+
if (style === "relative") return formatRelative(value, locale);
|
|
98
|
+
if (style === "long") return d.toLocaleString(locale, {
|
|
99
|
+
dateStyle: "medium",
|
|
100
|
+
timeStyle: "short"
|
|
101
|
+
});
|
|
102
|
+
return d.toLocaleDateString(locale, {
|
|
103
|
+
month: "short",
|
|
104
|
+
day: "numeric"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
case "percent": {
|
|
108
|
+
const source = kind.source ?? "fraction";
|
|
109
|
+
const decimals = kind.decimals ?? 1;
|
|
110
|
+
return `${(source === "basis" ? value / 100 : source === "whole" ? value : value * 100).toFixed(decimals)}%`;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function formatDelta(current, previous) {
|
|
115
|
+
if (!Number.isFinite(current) || !Number.isFinite(previous)) return {
|
|
116
|
+
pct: null,
|
|
117
|
+
sign: "na"
|
|
118
|
+
};
|
|
119
|
+
if (previous === 0) return current === 0 ? {
|
|
120
|
+
pct: 0,
|
|
121
|
+
sign: "flat"
|
|
122
|
+
} : {
|
|
123
|
+
pct: null,
|
|
124
|
+
sign: "na"
|
|
125
|
+
};
|
|
126
|
+
const pct = Number(((current - previous) / previous * 100).toFixed(1));
|
|
127
|
+
return {
|
|
128
|
+
pct,
|
|
129
|
+
sign: pct > 0 ? "up" : pct < 0 ? "down" : "flat"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
//#endregion
|
|
134
|
+
exports.DEFAULT_FORMAT_KIND = DEFAULT_FORMAT_KIND;
|
|
135
|
+
exports.FORMAT_KIND_TYPES = FORMAT_KIND_TYPES;
|
|
136
|
+
exports.formatDelta = formatDelta;
|
|
137
|
+
exports.formatValue = formatValue;
|
|
138
|
+
//# sourceMappingURL=format.js.map
|