@misoto22/design 0.7.0 → 0.8.0
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/AGENTS.md +87 -0
- package/CLAUDE.md +1 -0
- package/README.md +50 -5
- package/bin/misoto22-design.mjs +59 -13
- package/dist/agent/Accordion.md +24 -1
- package/dist/agent/Alert.md +23 -1
- package/dist/agent/AppShell.md +30 -2
- package/dist/agent/ArchitectureFigure.md +25 -2
- package/dist/agent/AreaChart.md +27 -2
- package/dist/agent/Article.md +26 -3
- package/dist/agent/AspectRatio.md +57 -0
- package/dist/agent/Avatar.md +25 -5
- package/dist/agent/Badge.md +22 -1
- package/dist/agent/BarChart.md +29 -2
- package/dist/agent/BarList.md +27 -1
- package/dist/agent/BigNumber.md +26 -2
- package/dist/agent/BoxPlot.md +24 -1
- package/dist/agent/Breadcrumb.md +24 -2
- package/dist/agent/BulletChart.md +23 -1
- package/dist/agent/Button.md +25 -1
- package/dist/agent/Calendar.md +37 -3
- package/dist/agent/Card.md +24 -2
- package/dist/agent/Checkbox.md +23 -2
- package/dist/agent/Code.md +50 -0
- package/dist/agent/CodeBlock.md +74 -0
- package/dist/agent/Collapsible.md +22 -1
- package/dist/agent/Combobox.md +29 -3
- package/dist/agent/Command.md +25 -2
- package/dist/agent/ComposedChart.md +26 -2
- package/dist/agent/ContextMenu.md +37 -4
- package/dist/agent/DataflowFigure.md +22 -1
- package/dist/agent/DatePicker.md +34 -6
- package/dist/agent/DescriptionList.md +68 -0
- package/dist/agent/Diagram.md +27 -3
- package/dist/agent/DiagramCanvas.md +24 -1
- package/dist/agent/DiagramExportMenu.md +29 -3
- package/dist/agent/DiagramInspector.md +23 -1
- package/dist/agent/DiagramLegend.md +22 -1
- package/dist/agent/DiagramMinimap.md +24 -4
- package/dist/agent/DiagramToolbar.md +23 -1
- package/dist/agent/Dialog.md +26 -3
- package/dist/agent/DropdownMenu.md +45 -6
- package/dist/agent/EmptyState.md +26 -1
- package/dist/agent/ErrorState.md +27 -2
- package/dist/agent/Facet.md +25 -1
- package/dist/agent/Field.md +45 -4
- package/dist/agent/FigureBand.md +24 -1
- package/dist/agent/FloatingIconButton.md +23 -1
- package/dist/agent/FunnelChart.md +23 -1
- package/dist/agent/Heading.md +64 -0
- package/dist/agent/Heatmap.md +25 -1
- package/dist/agent/Histogram.md +26 -2
- package/dist/agent/Input.md +22 -1
- package/dist/agent/Kbd.md +19 -1
- package/dist/agent/LifecycleFigure.md +22 -1
- package/dist/agent/LineChart.md +26 -2
- package/dist/agent/LinkArrow.md +20 -1
- package/dist/agent/Markdown.md +68 -0
- package/dist/agent/NativeSelect.md +23 -2
- package/dist/agent/NavItem.md +21 -1
- package/dist/agent/Pagination.md +28 -2
- package/dist/agent/PieChart.md +26 -2
- package/dist/agent/Popover.md +23 -1
- package/dist/agent/Progress.md +27 -3
- package/dist/agent/RadarChart.md +26 -2
- package/dist/agent/RadialChart.md +26 -3
- package/dist/agent/RadioGroup.md +23 -2
- package/dist/agent/SankeyChart.md +25 -2
- package/dist/agent/ScatterChart.md +27 -2
- package/dist/agent/ScrollArea.md +24 -3
- package/dist/agent/SearchableMenu.md +23 -1
- package/dist/agent/Select.md +29 -3
- package/dist/agent/Separator.md +28 -4
- package/dist/agent/SequenceFigure.md +23 -1
- package/dist/agent/Sheet.md +25 -2
- package/dist/agent/Skeleton.md +24 -2
- package/dist/agent/Slider.md +27 -4
- package/dist/agent/Sparkline.md +23 -1
- package/dist/agent/Spinner.md +24 -2
- package/dist/agent/StatusDot.md +20 -1
- package/dist/agent/StatusPill.md +28 -2
- package/dist/agent/Steps.md +24 -1
- package/dist/agent/Switch.md +21 -1
- package/dist/agent/Table.md +26 -3
- package/dist/agent/Tabs.md +24 -2
- package/dist/agent/Tag.md +40 -9
- package/dist/agent/Text.md +65 -0
- package/dist/agent/Textarea.md +20 -1
- package/dist/agent/Timestamp.md +64 -0
- package/dist/agent/Toast.md +26 -2
- package/dist/agent/ToggleGroup.md +24 -2
- package/dist/agent/Toolbar.md +71 -0
- package/dist/agent/Tooltip.md +23 -1
- package/dist/agent/TreemapChart.md +24 -1
- package/dist/agent/WaterfallChart.md +24 -1
- package/dist/agent/WorkflowFigure.md +24 -2
- package/dist/agent/catalog.json +5602 -745
- package/dist/agent/index.md +13 -4
- package/dist/article.css +16 -4
- package/dist/charts/AreaChart/AreaChart.d.ts +9 -2
- package/dist/charts/AreaChart/AreaChart.js +12 -13
- package/dist/charts/AreaChart/AreaChart.js.map +1 -1
- package/dist/charts/BarChart/BarChart.d.ts +14 -3
- package/dist/charts/BarChart/BarChart.js +20 -12
- package/dist/charts/BarChart/BarChart.js.map +1 -1
- package/dist/charts/BarList/BarList.d.ts +9 -1
- package/dist/charts/BarList/BarList.js +9 -1
- package/dist/charts/BarList/BarList.js.map +1 -1
- package/dist/charts/BigNumber/BigNumber.d.ts +19 -3
- package/dist/charts/BigNumber/BigNumber.js +25 -5
- package/dist/charts/BigNumber/BigNumber.js.map +1 -1
- package/dist/charts/BoxPlot/BoxPlot.js +4 -2
- package/dist/charts/BoxPlot/BoxPlot.js.map +1 -1
- package/dist/charts/BulletChart/BulletChart.js +28 -7
- package/dist/charts/BulletChart/BulletChart.js.map +1 -1
- package/dist/charts/ComposedChart/ComposedChart.d.ts +9 -2
- package/dist/charts/ComposedChart/ComposedChart.js +10 -10
- package/dist/charts/ComposedChart/ComposedChart.js.map +1 -1
- package/dist/charts/FunnelChart/FunnelChart.d.ts +7 -2
- package/dist/charts/FunnelChart/FunnelChart.js +6 -16
- package/dist/charts/FunnelChart/FunnelChart.js.map +1 -1
- package/dist/charts/Heatmap/Heatmap.d.ts +17 -3
- package/dist/charts/Heatmap/Heatmap.js +20 -11
- package/dist/charts/Heatmap/Heatmap.js.map +1 -1
- package/dist/charts/Histogram/Histogram.d.ts +24 -1
- package/dist/charts/Histogram/Histogram.js +43 -11
- package/dist/charts/Histogram/Histogram.js.map +1 -1
- package/dist/charts/LineChart/LineChart.d.ts +9 -2
- package/dist/charts/LineChart/LineChart.js +11 -12
- package/dist/charts/LineChart/LineChart.js.map +1 -1
- package/dist/charts/PieChart/PieChart.d.ts +15 -3
- package/dist/charts/PieChart/PieChart.js +14 -6
- package/dist/charts/PieChart/PieChart.js.map +1 -1
- package/dist/charts/RadarChart/RadarChart.d.ts +15 -3
- package/dist/charts/RadarChart/RadarChart.js +11 -9
- package/dist/charts/RadarChart/RadarChart.js.map +1 -1
- package/dist/charts/RadialChart/RadialChart.d.ts +22 -4
- package/dist/charts/RadialChart/RadialChart.js +28 -6
- package/dist/charts/RadialChart/RadialChart.js.map +1 -1
- package/dist/charts/SankeyChart/SankeyChart.d.ts +16 -4
- package/dist/charts/SankeyChart/SankeyChart.js +14 -6
- package/dist/charts/SankeyChart/SankeyChart.js.map +1 -1
- package/dist/charts/ScatterChart/ScatterChart.d.ts +24 -3
- package/dist/charts/ScatterChart/ScatterChart.js +21 -15
- package/dist/charts/ScatterChart/ScatterChart.js.map +1 -1
- package/dist/charts/Sparkline/Sparkline.js +5 -3
- package/dist/charts/Sparkline/Sparkline.js.map +1 -1
- package/dist/charts/TreemapChart/TreemapChart.d.ts +7 -2
- package/dist/charts/TreemapChart/TreemapChart.js +20 -11
- package/dist/charts/TreemapChart/TreemapChart.js.map +1 -1
- package/dist/charts/WaterfallChart/WaterfallChart.js +4 -2
- package/dist/charts/WaterfallChart/WaterfallChart.js.map +1 -1
- package/dist/charts/lib/export.d.ts +1 -0
- package/dist/charts/lib/figure.d.ts +21 -1
- package/dist/charts/lib/figure.js +5 -2
- package/dist/charts/lib/figure.js.map +1 -1
- package/dist/charts/lib/scale.d.ts +23 -0
- package/dist/charts/lib/scale.js +13 -0
- package/dist/charts/lib/scale.js.map +1 -0
- package/dist/charts/lib/selection.d.ts +20 -0
- package/dist/charts/lib/selection.js +18 -0
- package/dist/charts/lib/selection.js.map +1 -0
- package/dist/charts/lib/toolbar.d.ts +1 -0
- package/dist/components/Accordion/Accordion.js +8 -1
- package/dist/components/Accordion/Accordion.js.map +1 -1
- package/dist/components/AppShell/AppShell.d.ts +6 -0
- package/dist/components/AppShell/AppShell.js +34 -4
- package/dist/components/AppShell/AppShell.js.map +1 -1
- package/dist/components/Article/Article.d.ts +11 -4
- package/dist/components/Article/Article.js +13 -0
- package/dist/components/Article/Article.js.map +1 -1
- package/dist/components/AspectRatio/AspectRatio.d.ts +46 -0
- package/dist/components/AspectRatio/AspectRatio.js +33 -0
- package/dist/components/AspectRatio/AspectRatio.js.map +1 -0
- package/dist/components/Avatar/Avatar.d.ts +12 -3
- package/dist/components/Avatar/Avatar.js +6 -1
- package/dist/components/Avatar/Avatar.js.map +1 -1
- package/dist/components/Breadcrumb/Breadcrumb.d.ts +6 -0
- package/dist/components/Breadcrumb/Breadcrumb.js +15 -0
- package/dist/components/Breadcrumb/Breadcrumb.js.map +1 -1
- package/dist/components/Button/Button.js +14 -0
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.d.ts +29 -3
- package/dist/components/Calendar/Calendar.js +60 -12
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Card/Card.d.ts +6 -0
- package/dist/components/Card/Card.js +1 -1
- package/dist/components/Card/Card.js.map +1 -1
- package/dist/components/Checkbox/Checkbox.d.ts +5 -2
- package/dist/components/Checkbox/Checkbox.js +12 -2
- package/dist/components/Checkbox/Checkbox.js.map +1 -1
- package/dist/components/Code/Code.d.ts +30 -0
- package/dist/components/Code/Code.js +21 -0
- package/dist/components/Code/Code.js.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.d.ts +141 -0
- package/dist/components/CodeBlock/CodeBlock.js +123 -0
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -0
- package/dist/components/Collapsible/Collapsible.js +10 -2
- package/dist/components/Collapsible/Collapsible.js.map +1 -1
- package/dist/components/Combobox/Combobox.d.ts +15 -1
- package/dist/components/Combobox/Combobox.js +25 -5
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.d.ts +4 -2
- package/dist/components/Command/Command.js +13 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.d.ts +30 -5
- package/dist/components/ContextMenu/ContextMenu.js +32 -2
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.d.ts +36 -4
- package/dist/components/DatePicker/DatePicker.js +55 -12
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/components/DescriptionList/DescriptionList.d.ts +73 -0
- package/dist/components/DescriptionList/DescriptionList.js +35 -0
- package/dist/components/DescriptionList/DescriptionList.js.map +1 -0
- package/dist/components/Diagram/Diagram.d.ts +8 -3
- package/dist/components/Diagram/Diagram.js +82 -10
- package/dist/components/Diagram/Diagram.js.map +1 -1
- package/dist/components/Dialog/Dialog.d.ts +18 -3
- package/dist/components/Dialog/Dialog.js +20 -2
- package/dist/components/Dialog/Dialog.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.d.ts +49 -11
- package/dist/components/DropdownMenu/DropdownMenu.js +32 -2
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/EmptyState/EmptyState.d.ts +25 -1
- package/dist/components/EmptyState/EmptyState.js +3 -1
- package/dist/components/EmptyState/EmptyState.js.map +1 -1
- package/dist/components/ErrorState/ErrorState.d.ts +22 -1
- package/dist/components/ErrorState/ErrorState.js +11 -2
- package/dist/components/ErrorState/ErrorState.js.map +1 -1
- package/dist/components/Field/Field.d.ts +70 -3
- package/dist/components/Field/Field.js +53 -12
- package/dist/components/Field/Field.js.map +1 -1
- package/dist/components/Field/field-control.d.ts +31 -0
- package/dist/components/Field/field-control.js +12 -0
- package/dist/components/Field/field-control.js.map +1 -0
- package/dist/components/FloatingIconButton/FloatingIconButton.js +2 -0
- package/dist/components/FloatingIconButton/FloatingIconButton.js.map +1 -1
- package/dist/components/Heading/Heading.d.ts +62 -0
- package/dist/components/Heading/Heading.js +44 -0
- package/dist/components/Heading/Heading.js.map +1 -0
- package/dist/components/Markdown/Markdown.d.ts +139 -0
- package/dist/components/Markdown/Markdown.js +135 -0
- package/dist/components/Markdown/Markdown.js.map +1 -0
- package/dist/components/Markdown/inline.d.ts +29 -0
- package/dist/components/Markdown/inline.js +74 -0
- package/dist/components/Markdown/inline.js.map +1 -0
- package/dist/components/Markdown/nodes.d.ts +65 -0
- package/dist/components/Markdown/nodes.js +1 -0
- package/dist/components/Markdown/nodes.js.map +1 -0
- package/dist/components/Markdown/parse.d.ts +24 -0
- package/dist/components/Markdown/parse.js +110 -0
- package/dist/components/Markdown/parse.js.map +1 -0
- package/dist/components/Markdown/slug.d.ts +15 -0
- package/dist/components/Markdown/slug.js +7 -0
- package/dist/components/Markdown/slug.js.map +1 -0
- package/dist/components/NativeSelect/NativeSelect.d.ts +6 -0
- package/dist/components/NativeSelect/NativeSelect.js +2 -3
- package/dist/components/NativeSelect/NativeSelect.js.map +1 -1
- package/dist/components/Pagination/Pagination.d.ts +24 -1
- package/dist/components/Pagination/Pagination.js +6 -3
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/Progress/Progress.d.ts +17 -5
- package/dist/components/Progress/Progress.js +18 -6
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +6 -0
- package/dist/components/RadioGroup/RadioGroup.js +11 -1
- package/dist/components/RadioGroup/RadioGroup.js.map +1 -1
- package/dist/components/ScrollArea/ScrollArea.d.ts +15 -0
- package/dist/components/ScrollArea/ScrollArea.js +2 -2
- package/dist/components/ScrollArea/ScrollArea.js.map +1 -1
- package/dist/components/SearchableMenu/SearchableMenu.d.ts +11 -2
- package/dist/components/SearchableMenu/SearchableMenu.js +24 -2
- package/dist/components/SearchableMenu/SearchableMenu.js.map +1 -1
- package/dist/components/Select/Select.d.ts +18 -2
- package/dist/components/Select/Select.js +20 -3
- package/dist/components/Select/Select.js.map +1 -1
- package/dist/components/Separator/Separator.d.ts +21 -3
- package/dist/components/Separator/Separator.js +9 -1
- package/dist/components/Separator/Separator.js.map +1 -1
- package/dist/components/Sheet/Sheet.d.ts +3 -0
- package/dist/components/Sheet/Sheet.js +11 -2
- package/dist/components/Sheet/Sheet.js.map +1 -1
- package/dist/components/Skeleton/Skeleton.d.ts +8 -1
- package/dist/components/Skeleton/Skeleton.js +1 -1
- package/dist/components/Skeleton/Skeleton.js.map +1 -1
- package/dist/components/Slider/Slider.d.ts +21 -3
- package/dist/components/Slider/Slider.js +54 -27
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Spinner/Spinner.d.ts +8 -0
- package/dist/components/Spinner/Spinner.js +3 -2
- package/dist/components/Spinner/Spinner.js.map +1 -1
- package/dist/components/StatusPill/StatusPill.d.ts +5 -0
- package/dist/components/StatusPill/StatusPill.js +6 -0
- package/dist/components/StatusPill/StatusPill.js.map +1 -1
- package/dist/components/Table/Table.d.ts +6 -0
- package/dist/components/Table/Table.js +3 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/Tag/Tag.d.ts +68 -9
- package/dist/components/Tag/Tag.js +50 -10
- package/dist/components/Tag/Tag.js.map +1 -1
- package/dist/components/Text/Text.d.ts +62 -0
- package/dist/components/Text/Text.js +30 -0
- package/dist/components/Text/Text.js.map +1 -0
- package/dist/components/Timestamp/Timestamp.d.ts +59 -0
- package/dist/components/Timestamp/Timestamp.js +56 -0
- package/dist/components/Timestamp/Timestamp.js.map +1 -0
- package/dist/components/Toast/Toast.d.ts +9 -1
- package/dist/components/Toast/Toast.js +27 -4
- package/dist/components/Toast/Toast.js.map +1 -1
- package/dist/components/ToggleGroup/ToggleGroup.d.ts +6 -1
- package/dist/components/ToggleGroup/ToggleGroup.js +11 -1
- package/dist/components/ToggleGroup/ToggleGroup.js.map +1 -1
- package/dist/components/Toolbar/Toolbar.d.ts +55 -0
- package/dist/components/Toolbar/Toolbar.js +43 -0
- package/dist/components/Toolbar/Toolbar.js.map +1 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.d.ts +6 -0
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js +48 -8
- package/dist/diagrams/ArchitectureFigure/ArchitectureFigure.js.map +1 -1
- package/dist/diagrams/DataflowFigure/DataflowFigure.js +21 -2
- package/dist/diagrams/DataflowFigure/DataflowFigure.js.map +1 -1
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.d.ts +14 -0
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js +14 -7
- package/dist/diagrams/DiagramCanvas/DiagramCanvas.js.map +1 -1
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.d.ts +38 -4
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js +14 -16
- package/dist/diagrams/DiagramExportMenu/DiagramExportMenu.js.map +1 -1
- package/dist/diagrams/DiagramInspector/DiagramInspector.js +2 -2
- package/dist/diagrams/DiagramInspector/DiagramInspector.js.map +1 -1
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.d.ts +23 -7
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js +45 -18
- package/dist/diagrams/DiagramMinimap/DiagramMinimap.js.map +1 -1
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js +44 -12
- package/dist/diagrams/LifecycleFigure/LifecycleFigure.js.map +1 -1
- package/dist/diagrams/SequenceFigure/SequenceFigure.js +35 -5
- package/dist/diagrams/SequenceFigure/SequenceFigure.js.map +1 -1
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.d.ts +3 -1
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js +76 -28
- package/dist/diagrams/WorkflowFigure/WorkflowFigure.js.map +1 -1
- package/dist/diagrams/index.d.ts +1 -0
- package/dist/diagrams/index.js +10 -0
- package/dist/diagrams/index.js.map +1 -1
- package/dist/diagrams/lib/dev.d.ts +61 -0
- package/dist/diagrams/lib/dev.js +96 -0
- package/dist/diagrams/lib/dev.js.map +1 -0
- package/dist/diagrams/lib/frame.d.ts +48 -7
- package/dist/diagrams/lib/frame.js +17 -2
- package/dist/diagrams/lib/frame.js.map +1 -1
- package/dist/diagrams/lib/wires.js.map +1 -1
- package/dist/diagrams/spec.d.ts +64 -12
- package/dist/index.d.ts +16 -4
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/keyframes.css +57 -3
- package/dist/lib/overlay-container.d.ts +14 -4
- package/dist/lib/overlay-container.js.map +1 -1
- package/dist/lib/warn.d.ts +73 -0
- package/dist/lib/warn.js +38 -0
- package/dist/lib/warn.js.map +1 -0
- package/dist/semantic.css +17 -6
- package/dist/styles.css +1 -1
- package/dist/tokens.css +20 -2
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.js +39 -36
- package/dist/tokens.json +39 -36
- package/llms.txt +35 -0
- package/package.json +5 -2
- package/skills/misoto22-design/evals/evals.json +84 -0
- package/skills/misoto22-design/rules/a11y.md +55 -3
- package/skills/misoto22-design/rules/forms.md +5 -0
|
@@ -7,8 +7,7 @@ import {
|
|
|
7
7
|
useCallback,
|
|
8
8
|
useContext,
|
|
9
9
|
useId,
|
|
10
|
-
useMemo
|
|
11
|
-
useState
|
|
10
|
+
useMemo
|
|
12
11
|
} from "react";
|
|
13
12
|
import {
|
|
14
13
|
LabelList as RechartsLabelList,
|
|
@@ -19,6 +18,8 @@ import {
|
|
|
19
18
|
import { motion, useReducedMotion } from "motion/react";
|
|
20
19
|
import { ChartContainer, colorStops, cssName } from "../lib/chart.js";
|
|
21
20
|
import { ChartFigure } from "../lib/figure.js";
|
|
21
|
+
import {} from "../lib/empty.js";
|
|
22
|
+
import { useChartSelection } from "../lib/selection.js";
|
|
22
23
|
import { ChartBackground } from "../lib/background.js";
|
|
23
24
|
import { ChartLegend, ChartLegendContent } from "../lib/legend.js";
|
|
24
25
|
import { ChartTooltip, ChartTooltipContent } from "../lib/tooltip.js";
|
|
@@ -44,15 +45,15 @@ function PieChart({
|
|
|
44
45
|
className,
|
|
45
46
|
chartProps,
|
|
46
47
|
defaultSelectedSector = null,
|
|
48
|
+
selectedSector: controlledSector,
|
|
47
49
|
onSelectionChange,
|
|
48
50
|
isLoading = false,
|
|
49
|
-
hideDataTable = false
|
|
51
|
+
hideDataTable = false,
|
|
52
|
+
empty
|
|
50
53
|
}) {
|
|
51
54
|
const chartId = useId().replace(/:/g, "");
|
|
52
|
-
const
|
|
53
|
-
const selectSector = useCallback(
|
|
55
|
+
const report = useCallback(
|
|
54
56
|
(name) => {
|
|
55
|
-
setSelectedSector(name);
|
|
56
57
|
if (name === null) {
|
|
57
58
|
onSelectionChange?.(null);
|
|
58
59
|
return;
|
|
@@ -62,6 +63,11 @@ function PieChart({
|
|
|
62
63
|
},
|
|
63
64
|
[data, dataKey, nameKey, onSelectionChange]
|
|
64
65
|
);
|
|
66
|
+
const [selectedSector, selectSector] = useChartSelection(
|
|
67
|
+
controlledSector,
|
|
68
|
+
defaultSelectedSector,
|
|
69
|
+
report
|
|
70
|
+
);
|
|
65
71
|
const context = useMemo(
|
|
66
72
|
() => ({ config, data, dataKey, nameKey, isLoading, selectedSector, selectSector }),
|
|
67
73
|
[config, data, dataKey, isLoading, nameKey, selectSector, selectedSector]
|
|
@@ -78,6 +84,8 @@ function PieChart({
|
|
|
78
84
|
rowKey: nameKey,
|
|
79
85
|
columns: [{ key: dataKey, label: "Value" }]
|
|
80
86
|
},
|
|
87
|
+
isEmpty: !isLoading && data.length === 0,
|
|
88
|
+
empty,
|
|
81
89
|
children: [
|
|
82
90
|
/* @__PURE__ */ jsx(ChartContainer, { config, children: /* @__PURE__ */ jsx(RechartsPieChart, { id: chartId, accessibilityLayer: true, ...chartProps, children }) }),
|
|
83
91
|
/* @__PURE__ */ jsx(LoadingIndicator, { isLoading })
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/charts/PieChart/PieChart.tsx"],"sourcesContent":["'use client'\n\nimport {\n Children,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useId,\n useMemo,\n useState,\n type ComponentProps,\n type FC,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport {\n LabelList as RechartsLabelList,\n Pie as RechartsPie,\n PieChart as RechartsPieChart,\n Sector,\n type PieSectorShapeProps,\n} from 'recharts'\nimport { motion, useReducedMotion } from 'motion/react'\nimport { ChartContainer, colorStops, cssName, type ChartConfig } from '../lib/chart'\nimport { ChartFigure } from '../lib/figure'\nimport { ChartBackground, type ChartBackgroundVariant } from '../lib/background'\nimport { ChartLegend, ChartLegendContent } from '../lib/legend'\nimport { ChartTooltip, ChartTooltipContent } from '../lib/tooltip'\nimport { LoadingIndicator } from '../lib/loading'\nimport { ColorStops } from '../lib/paint'\nimport type { ChartLegendSlotProps, ChartTooltipSlotProps } from '../AreaChart/AreaChart'\n\n/** Equal wedges the skeleton is built from. */\nconst LOADING_SECTORS = 5\n\n/** One full pulse of the loading wave, in seconds. */\nconst LOADING_PULSE = 2\n\n/**\n * How a sector is painted.\n *\n * One variant, and the honesty is deliberate: a wedge is a small, awkwardly\n * shaped area, and a texture inside one reads as noise rather than as identity.\n * Where a bar chart differentiates by fill, a pie differentiates by the gap\n * between sectors — which is what `paddingAngle` is for — and by its legend.\n */\nexport type PieVariant = 'gradient'\n\ninterface PieChartContextValue {\n config: ChartConfig\n data: Record<string, unknown>[]\n dataKey: string\n nameKey: string\n isLoading: boolean\n selectedSector: string | null\n selectSector: (sector: string | null) => void\n}\n\nconst PieChartContext = createContext<PieChartContextValue | null>(null)\n\nfunction usePieChart(): PieChartContextValue {\n const context = useContext(PieChartContext)\n if (!context) throw new Error('A pie chart part must be rendered inside <PieChart>')\n return context\n}\n\nexport interface PieChartProps<TData extends Record<string, unknown>> {\n /** Sector names → their label and paint. Keys must match the `nameKey` values. */\n config: ChartConfig\n /** The rows the chart draws. One entry per point, bar or category. */\n data: TData[]\n /** The row field holding each sector's number. */\n dataKey: keyof TData & string\n /** The row field holding each sector's name. */\n nameKey: keyof TData & string\n /**\n * What the chart shows, in a sentence a reader could act on. Required, and\n * announced to a screen reader even when it is not printed.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n /**\n * The composed parts — axes, grid, tooltip, legend, and the marks\n * themselves.\n */\n children: ReactNode\n /** Merged onto the figure, last, so a call site can size or space it. */\n className?: string\n /** Escape hatch onto the raw Recharts chart element. */\n chartProps?: ComponentProps<typeof RechartsPieChart>\n /** The sector lit on first render. */\n defaultSelectedSector?: string | null\n /** Fires when the selection changes, and with null when it is cleared. */\n onSelectionChange?: (selection: { name: string; value: number } | null) => void\n /**\n * Swaps the marks for an animated skeleton, keeping the measured height so\n * the page does not jump when the data lands.\n */\n isLoading?: boolean\n /**\n * Drops the hidden table view. Only correct when the page prints the data\n * itself.\n */\n hideDataTable?: boolean\n}\n\n/**\n * Parts of one whole.\n *\n * Worth saying plainly: a pie answers \"roughly what share\" and nothing more\n * precise. Comparing two adjacent wedges by eye is unreliable past about five\n * of them, and comparing a wedge across two pies is worse. When the reader\n * needs to rank or compare, a `<BarChart>` answers the same question better.\n *\n * @example\n * <PieChart title=\"Visitors by browser\" config={config} data={data} dataKey=\"visitors\" nameKey=\"browser\">\n * <PieChart.Pie innerRadius=\"55%\" />\n * <PieChart.Tooltip />\n * <PieChart.Legend />\n * </PieChart>\n */\nexport function PieChart<TData extends Record<string, unknown>>({\n config,\n data,\n dataKey,\n nameKey,\n title,\n showTitle,\n description,\n children,\n className,\n chartProps,\n defaultSelectedSector = null,\n onSelectionChange,\n isLoading = false,\n hideDataTable = false,\n}: PieChartProps<TData>) {\n // Not a literal. Two pies on one page shared a hard-coded id in the shape\n // this was ported from, so the second one inherited the first one's paint.\n const chartId = useId().replace(/:/g, '')\n const [selectedSector, setSelectedSector] = useState<string | null>(defaultSelectedSector)\n\n const selectSector = useCallback(\n (name: string | null) => {\n setSelectedSector(name)\n\n if (name === null) {\n onSelectionChange?.(null)\n return\n }\n const row = data.find((item) => item[nameKey] === name)\n if (row) onSelectionChange?.({ name, value: row[dataKey] as number })\n },\n [data, dataKey, nameKey, onSelectionChange],\n )\n\n const context = useMemo<PieChartContextValue>(\n () => ({ config, data, dataKey, nameKey, isLoading, selectedSector, selectSector }),\n [config, data, dataKey, isLoading, nameKey, selectSector, selectedSector],\n )\n\n return (\n <PieChartContext.Provider value={context}>\n <ChartFigure\n title={title}\n showTitle={showTitle}\n description={description}\n className={className}\n table={\n hideDataTable\n ? false\n : {\n rows: data,\n rowKey: nameKey,\n columns: [{ key: dataKey, label: 'Value' }],\n }\n }\n >\n <ChartContainer config={config}>\n <RechartsPieChart id={chartId} accessibilityLayer {...chartProps}>\n {children}\n </RechartsPieChart>\n </ChartContainer>\n <LoadingIndicator isLoading={isLoading} />\n </ChartFigure>\n </PieChartContext.Provider>\n )\n}\n\nexport interface PieProps {\n variant?: PieVariant\n /** Above zero makes it a donut, which is easier to read than a full pie. */\n innerRadius?: number | string\n outerRadius?: number | string\n /** Rounds each wedge's corners. */\n cornerRadius?: number\n /**\n * Degrees between wedges. Negative overlaps them, which the sector's own\n * surface-coloured stroke then separates — the \"stacked cards\" look.\n */\n paddingAngle?: number\n startAngle?: number\n endAngle?: number\n isClickable?: boolean\n /** Sector names drawn with a halo — for the one wedge that is the point. */\n glowingSectors?: string[]\n /** `<PieChart.Label>`. */\n children?: ReactNode\n pieProps?: Omit<ComponentProps<typeof RechartsPie>, 'data' | 'dataKey' | 'nameKey'>\n}\n\n/** Stable identity, so the default never re-triggers the memo below. */\nconst NO_GLOW: string[] = []\n\n/** The wedges themselves, with their gradients scoped to this pie's id. */\nfunction Pie({\n variant = 'gradient',\n innerRadius = 0,\n outerRadius = '80%',\n cornerRadius = 0,\n paddingAngle = 0,\n startAngle = 0,\n endAngle = 360,\n isClickable = false,\n glowingSectors = NO_GLOW,\n children,\n pieProps,\n}: PieProps) {\n const { config, data, dataKey, nameKey, isLoading, selectedSector, selectSector } = usePieChart()\n const id = useId().replace(/:/g, '')\n\n if (isLoading) {\n return (\n <RechartsPie\n data={LOADING_DATA}\n dataKey=\"value\"\n nameKey=\"name\"\n innerRadius={innerRadius}\n outerRadius={outerRadius}\n cornerRadius={cornerRadius}\n paddingAngle={paddingAngle}\n startAngle={startAngle}\n endAngle={endAngle}\n strokeWidth={0}\n isAnimationActive={false}\n shape={(props) => <LoadingSector {...props} />}\n />\n )\n }\n\n const label = resolveLabel(children, dataKey)\n const painted = data.map((row) => ({\n ...row,\n fill: `url(#${id}-colors-${cssName(String(row[nameKey]))})`,\n }))\n\n return (\n <>\n <RechartsPie\n data={painted}\n dataKey={dataKey}\n nameKey={nameKey}\n innerRadius={innerRadius}\n outerRadius={outerRadius}\n cornerRadius={cornerRadius}\n paddingAngle={paddingAngle}\n startAngle={startAngle}\n endAngle={endAngle}\n strokeWidth={0}\n style={isClickable ? { cursor: 'pointer' } : undefined}\n onClick={(_, index) => {\n if (!isClickable) return\n const name = data[index]?.[nameKey] as string\n selectSector(selectedSector === name ? null : name)\n }}\n shape={(props: PieSectorShapeProps) => {\n const name = String(data[props.index ?? 0]?.[nameKey] ?? '')\n const isGlowing = glowingSectors.includes(name)\n const dimmed = isClickable && selectedSector !== null && selectedSector !== name\n\n return (\n <Sector\n {...props}\n fill={`url(#${id}-colors-${cssName(name)})`}\n filter={isGlowing ? `url(#${id}-glow-${cssName(name)})` : undefined}\n // A negative padding angle overlaps the wedges; the surface-\n // coloured stroke is what re-separates them into stacked cards.\n stroke={paddingAngle < 0 ? 'var(--chart-surface)' : 'none'}\n strokeWidth={paddingAngle < 0 ? 5 : 0}\n opacity={dimmed ? 0.15 : 1}\n className=\"transition-opacity duration-(--duration-base)\"\n />\n )\n }}\n {...pieProps}\n >\n {label}\n </RechartsPie>\n <defs>\n <SectorGradients id={id} config={config} variant={variant} />\n {glowingSectors.map((name) => (\n <filter\n key={name}\n id={`${id}-glow-${cssName(name)}`}\n x=\"-100%\"\n y=\"-100%\"\n width=\"300%\"\n height=\"300%\"\n >\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"8\" result=\"blur\" />\n <feColorMatrix\n in=\"blur\"\n type=\"matrix\"\n values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.5 0\"\n result=\"glow\"\n />\n <feMerge>\n <feMergeNode in=\"glow\" />\n <feMergeNode in=\"SourceGraphic\" />\n </feMerge>\n </filter>\n ))}\n </defs>\n </>\n )\n}\n\nexport interface PieLabelProps {\n /** The field printed on each wedge. Defaults to the pie's value key. */\n dataKey?: string\n labelListProps?: Omit<ComponentProps<typeof RechartsLabelList>, 'dataKey'>\n}\n\n/**\n * Numbers printed on the wedges. A slot — the parent reads it.\n *\n * Worth reaching for: a pie's whole weakness is that a wedge's angle is hard to\n * read, and a printed number removes the guess entirely.\n */\nconst Label: FC<PieLabelProps> = () => null\n\n/** Lifts a `<Label>` out of the pie's children into a Recharts label list. */\nfunction resolveLabel(children: ReactNode, valueKey: string): ReactNode {\n let label: ReactNode = null\n\n Children.forEach(children, (child) => {\n if (!isValidElement(child) || child.type !== Label) return\n const { dataKey, labelListProps } = (child as ReactElement<PieLabelProps>).props\n\n label = (\n <RechartsLabelList\n dataKey={dataKey ?? valueKey}\n stroke=\"none\"\n fontSize={12}\n fontWeight={500}\n // Reversed out of the wedge, which is the only ground it ever sits on.\n className=\"fill-(--chart-surface)\"\n {...labelListProps}\n />\n )\n })\n\n return label\n}\n\n/**\n * One diagonal gradient per sector.\n *\n * Diagonal rather than horizontal: a wedge is a radial shape, and a gradient\n * running corner to corner reads as light across the whole pie rather than as\n * a seam through each slice.\n */\nfunction SectorGradients({\n id,\n config,\n}: {\n id: string\n config: ChartConfig\n /** Reserved: the pie has one fill variant. See `PieVariant`. */\n variant: PieVariant\n}) {\n return (\n <>\n {Object.entries(config).map(([key, series]) => (\n <linearGradient\n key={key}\n id={`${id}-colors-${cssName(key)}`}\n x1=\"0\"\n y1=\"0\"\n x2=\"1\"\n y2=\"1\"\n >\n <ColorStops dataKey={cssName(key)} stops={colorStops(series)} />\n </linearGradient>\n ))}\n </>\n )\n}\n\n/** The hover panel. No heading: the wedge's own name is the row label. */\nfunction Tooltip({ variant, roundness, defaultIndex }: ChartTooltipSlotProps) {\n const { isLoading, nameKey } = usePieChart()\n if (isLoading) return null\n\n return (\n <ChartTooltip\n defaultIndex={defaultIndex}\n content={\n <ChartTooltipContent nameKey={nameKey} hideLabel roundness={roundness} variant={variant} />\n }\n />\n )\n}\n\n/**\n * The key. Under the pie and centred by default, because a pie's sectors are\n * named nowhere else — unlike a bar chart, which has a category axis.\n */\nfunction Legend({\n variant,\n align = 'center',\n verticalAlign = 'bottom',\n isClickable = false,\n}: ChartLegendSlotProps) {\n const { nameKey, selectedSector, selectSector } = usePieChart()\n\n return (\n <ChartLegend\n verticalAlign={verticalAlign}\n align={align}\n content={\n <ChartLegendContent\n selected={selectedSector}\n onSelectChange={selectSector}\n isClickable={isClickable}\n nameKey={nameKey}\n variant={variant}\n />\n }\n />\n )\n}\n\n/** The decorative plate behind the pie. Compose it before `<Pie>`. */\nfunction Background({ variant = 'dots' }: { variant?: ChartBackgroundVariant }) {\n return <ChartBackground variant={variant} />\n}\n\n/** Equal wedges for the skeleton. */\nconst LOADING_DATA = Array.from({ length: LOADING_SECTORS }, (_, index) => ({\n name: `loading-${index}`,\n value: 100 / LOADING_SECTORS,\n}))\n\n/**\n * One skeleton wedge, pulsing on a stagger so the wave travels round the pie.\n * Under `prefers-reduced-motion` it holds at a flat mid-opacity instead.\n */\nfunction LoadingSector(props: ComponentProps<typeof Sector> & { index?: number }) {\n const { index = 0, ...sector } = props\n const reduceMotion = useReducedMotion()\n const delay = (index / LOADING_SECTORS) * LOADING_PULSE\n\n if (reduceMotion) {\n return (\n <g opacity={0.3} className=\"text-(--ink)\">\n <Sector {...sector} fill=\"currentColor\" />\n </g>\n )\n }\n\n return (\n <motion.g\n className=\"text-(--ink)\"\n initial={{ opacity: 0.15 }}\n animate={{ opacity: [0.15, 0.5, 0.15] }}\n transition={{ duration: LOADING_PULSE, delay, repeat: Infinity, ease: 'easeInOut' }}\n >\n <Sector {...sector} fill=\"currentColor\" />\n </motion.g>\n )\n}\n\nPieChart.Pie = Pie\nPieChart.Label = Label\nPieChart.Tooltip = Tooltip\nPieChart.Legend = Legend\nPieChart.Background = Background\n\nexport default PieChart\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/PieChart/PieChart.tsx"],"sourcesContent":["'use client'\n\nimport {\n Children,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useId,\n useMemo,\n type ComponentProps,\n type FC,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport {\n LabelList as RechartsLabelList,\n Pie as RechartsPie,\n PieChart as RechartsPieChart,\n Sector,\n type PieSectorShapeProps,\n} from 'recharts'\nimport { motion, useReducedMotion } from 'motion/react'\nimport { ChartContainer, colorStops, cssName, type ChartConfig } from '../lib/chart'\nimport { ChartFigure } from '../lib/figure'\nimport { type ChartEmptyProps } from '../lib/empty'\nimport { useChartSelection } from '../lib/selection'\nimport { ChartBackground, type ChartBackgroundVariant } from '../lib/background'\nimport { ChartLegend, ChartLegendContent } from '../lib/legend'\nimport { ChartTooltip, ChartTooltipContent } from '../lib/tooltip'\nimport { LoadingIndicator } from '../lib/loading'\nimport { ColorStops } from '../lib/paint'\nimport type { ChartLegendSlotProps, ChartTooltipSlotProps } from '../AreaChart/AreaChart'\n\n/** Equal wedges the skeleton is built from. */\nconst LOADING_SECTORS = 5\n\n/** One full pulse of the loading wave, in seconds. */\nconst LOADING_PULSE = 2\n\n/**\n * How a sector is painted.\n *\n * One variant, and the honesty is deliberate: a wedge is a small, awkwardly\n * shaped area, and a texture inside one reads as noise rather than as identity.\n * Where a bar chart differentiates by fill, a pie differentiates by the gap\n * between sectors — which is what `paddingAngle` is for — and by its legend.\n */\nexport type PieVariant = 'gradient'\n\ninterface PieChartContextValue {\n config: ChartConfig\n data: Record<string, unknown>[]\n dataKey: string\n nameKey: string\n isLoading: boolean\n selectedSector: string | null\n selectSector: (sector: string | null) => void\n}\n\nconst PieChartContext = createContext<PieChartContextValue | null>(null)\n\nfunction usePieChart(): PieChartContextValue {\n const context = useContext(PieChartContext)\n if (!context) throw new Error('A pie chart part must be rendered inside <PieChart>')\n return context\n}\n\nexport interface PieChartProps<TData extends Record<string, unknown>> {\n /** Sector names → their label and paint. Keys must match the `nameKey` values. */\n config: ChartConfig\n /** The rows the chart draws. One entry per point, bar or category. */\n data: TData[]\n /** The row field holding each sector's number. */\n dataKey: keyof TData & string\n /** The row field holding each sector's name. */\n nameKey: keyof TData & string\n /**\n * What the chart shows, in a sentence a reader could act on. Required, and\n * announced to a screen reader even when it is not printed.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n /**\n * The composed parts — axes, grid, tooltip, legend, and the marks\n * themselves.\n */\n children: ReactNode\n /** Merged onto the figure, last, so a call site can size or space it. */\n className?: string\n /** Escape hatch onto the raw Recharts chart element. */\n chartProps?: ComponentProps<typeof RechartsPieChart>\n /** The sector lit on first render, when the chart keeps its own selection. */\n defaultSelectedSector?: string | null\n /**\n * The selected sector, driven from outside.\n *\n * Give this and the chart follows it; leave it undefined and the chart keeps\n * its own, starting from `defaultSelectedSector`.\n */\n selectedSector?: string | null\n /** Fires when the selection changes, and with null when it is cleared. */\n onSelectionChange?: (selection: { name: string; value: number } | null) => void\n /**\n * Swaps the marks for an animated skeleton, keeping the measured height so\n * the page does not jump when the data lands.\n */\n isLoading?: boolean\n /**\n * Drops the hidden table view. Only correct when the page prints the data\n * itself.\n */\n hideDataTable?: boolean\n /**\n * What the chart shows when it has nothing to draw. `false` keeps the empty\n * plot, for a chart whose emptiness is itself the reading.\n */\n empty?: ChartEmptyProps | false\n}\n\n/**\n * Parts of one whole.\n *\n * Worth saying plainly: a pie answers \"roughly what share\" and nothing more\n * precise. Comparing two adjacent wedges by eye is unreliable past about five\n * of them, and comparing a wedge across two pies is worse. When the reader\n * needs to rank or compare, a `<BarChart>` answers the same question better.\n *\n * @example\n * <PieChart title=\"Visitors by browser\" config={config} data={data} dataKey=\"visitors\" nameKey=\"browser\">\n * <PieChart.Pie innerRadius=\"55%\" />\n * <PieChart.Tooltip />\n * <PieChart.Legend />\n * </PieChart>\n */\nexport function PieChart<TData extends Record<string, unknown>>({\n config,\n data,\n dataKey,\n nameKey,\n title,\n showTitle,\n description,\n children,\n className,\n chartProps,\n defaultSelectedSector = null,\n selectedSector: controlledSector,\n onSelectionChange,\n isLoading = false,\n hideDataTable = false,\n empty,\n}: PieChartProps<TData>) {\n // Not a literal. Two pies on one page shared a hard-coded id in the shape\n // this was ported from, so the second one inherited the first one's paint.\n const chartId = useId().replace(/:/g, '')\n\n const report = useCallback(\n (name: string | null) => {\n if (name === null) {\n onSelectionChange?.(null)\n return\n }\n const row = data.find((item) => item[nameKey] === name)\n if (row) onSelectionChange?.({ name, value: row[dataKey] as number })\n },\n [data, dataKey, nameKey, onSelectionChange],\n )\n\n const [selectedSector, selectSector] = useChartSelection(\n controlledSector,\n defaultSelectedSector,\n report,\n )\n\n const context = useMemo<PieChartContextValue>(\n () => ({ config, data, dataKey, nameKey, isLoading, selectedSector, selectSector }),\n [config, data, dataKey, isLoading, nameKey, selectSector, selectedSector],\n )\n\n return (\n <PieChartContext.Provider value={context}>\n <ChartFigure\n title={title}\n showTitle={showTitle}\n description={description}\n className={className}\n table={\n hideDataTable\n ? false\n : {\n rows: data,\n rowKey: nameKey,\n columns: [{ key: dataKey, label: 'Value' }],\n }\n }\n isEmpty={!isLoading && data.length === 0}\n empty={empty}\n >\n <ChartContainer config={config}>\n <RechartsPieChart id={chartId} accessibilityLayer {...chartProps}>\n {children}\n </RechartsPieChart>\n </ChartContainer>\n <LoadingIndicator isLoading={isLoading} />\n </ChartFigure>\n </PieChartContext.Provider>\n )\n}\n\nexport interface PieProps {\n variant?: PieVariant\n /** Above zero makes it a donut, which is easier to read than a full pie. */\n innerRadius?: number | string\n outerRadius?: number | string\n /** Rounds each wedge's corners. */\n cornerRadius?: number\n /**\n * Degrees between wedges. Negative overlaps them, which the sector's own\n * surface-coloured stroke then separates — the \"stacked cards\" look.\n */\n paddingAngle?: number\n startAngle?: number\n endAngle?: number\n isClickable?: boolean\n /** Sector names drawn with a halo — for the one wedge that is the point. */\n glowingSectors?: string[]\n /** `<PieChart.Label>`. */\n children?: ReactNode\n pieProps?: Omit<ComponentProps<typeof RechartsPie>, 'data' | 'dataKey' | 'nameKey'>\n}\n\n/** Stable identity, so the default never re-triggers the memo below. */\nconst NO_GLOW: string[] = []\n\n/** The wedges themselves, with their gradients scoped to this pie's id. */\nfunction Pie({\n variant = 'gradient',\n innerRadius = 0,\n outerRadius = '80%',\n cornerRadius = 0,\n paddingAngle = 0,\n startAngle = 0,\n endAngle = 360,\n isClickable = false,\n glowingSectors = NO_GLOW,\n children,\n pieProps,\n}: PieProps) {\n const { config, data, dataKey, nameKey, isLoading, selectedSector, selectSector } = usePieChart()\n const id = useId().replace(/:/g, '')\n\n if (isLoading) {\n return (\n <RechartsPie\n data={LOADING_DATA}\n dataKey=\"value\"\n nameKey=\"name\"\n innerRadius={innerRadius}\n outerRadius={outerRadius}\n cornerRadius={cornerRadius}\n paddingAngle={paddingAngle}\n startAngle={startAngle}\n endAngle={endAngle}\n strokeWidth={0}\n isAnimationActive={false}\n shape={(props) => <LoadingSector {...props} />}\n />\n )\n }\n\n const label = resolveLabel(children, dataKey)\n const painted = data.map((row) => ({\n ...row,\n fill: `url(#${id}-colors-${cssName(String(row[nameKey]))})`,\n }))\n\n return (\n <>\n <RechartsPie\n data={painted}\n dataKey={dataKey}\n nameKey={nameKey}\n innerRadius={innerRadius}\n outerRadius={outerRadius}\n cornerRadius={cornerRadius}\n paddingAngle={paddingAngle}\n startAngle={startAngle}\n endAngle={endAngle}\n strokeWidth={0}\n style={isClickable ? { cursor: 'pointer' } : undefined}\n onClick={(_, index) => {\n if (!isClickable) return\n const name = data[index]?.[nameKey] as string\n selectSector(selectedSector === name ? null : name)\n }}\n shape={(props: PieSectorShapeProps) => {\n const name = String(data[props.index ?? 0]?.[nameKey] ?? '')\n const isGlowing = glowingSectors.includes(name)\n const dimmed = isClickable && selectedSector !== null && selectedSector !== name\n\n return (\n <Sector\n {...props}\n fill={`url(#${id}-colors-${cssName(name)})`}\n filter={isGlowing ? `url(#${id}-glow-${cssName(name)})` : undefined}\n // A negative padding angle overlaps the wedges; the surface-\n // coloured stroke is what re-separates them into stacked cards.\n stroke={paddingAngle < 0 ? 'var(--chart-surface)' : 'none'}\n strokeWidth={paddingAngle < 0 ? 5 : 0}\n opacity={dimmed ? 0.15 : 1}\n className=\"transition-opacity duration-(--duration-base)\"\n />\n )\n }}\n {...pieProps}\n >\n {label}\n </RechartsPie>\n <defs>\n <SectorGradients id={id} config={config} variant={variant} />\n {glowingSectors.map((name) => (\n <filter\n key={name}\n id={`${id}-glow-${cssName(name)}`}\n x=\"-100%\"\n y=\"-100%\"\n width=\"300%\"\n height=\"300%\"\n >\n <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"8\" result=\"blur\" />\n <feColorMatrix\n in=\"blur\"\n type=\"matrix\"\n values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.5 0\"\n result=\"glow\"\n />\n <feMerge>\n <feMergeNode in=\"glow\" />\n <feMergeNode in=\"SourceGraphic\" />\n </feMerge>\n </filter>\n ))}\n </defs>\n </>\n )\n}\n\nexport interface PieLabelProps {\n /** The field printed on each wedge. Defaults to the pie's value key. */\n dataKey?: string\n labelListProps?: Omit<ComponentProps<typeof RechartsLabelList>, 'dataKey'>\n}\n\n/**\n * Numbers printed on the wedges. A slot — the parent reads it.\n *\n * Worth reaching for: a pie's whole weakness is that a wedge's angle is hard to\n * read, and a printed number removes the guess entirely.\n */\nconst Label: FC<PieLabelProps> = () => null\n\n/** Lifts a `<Label>` out of the pie's children into a Recharts label list. */\nfunction resolveLabel(children: ReactNode, valueKey: string): ReactNode {\n let label: ReactNode = null\n\n Children.forEach(children, (child) => {\n if (!isValidElement(child) || child.type !== Label) return\n const { dataKey, labelListProps } = (child as ReactElement<PieLabelProps>).props\n\n label = (\n <RechartsLabelList\n dataKey={dataKey ?? valueKey}\n stroke=\"none\"\n fontSize={12}\n fontWeight={500}\n // Reversed out of the wedge, which is the only ground it ever sits on.\n className=\"fill-(--chart-surface)\"\n {...labelListProps}\n />\n )\n })\n\n return label\n}\n\n/**\n * One diagonal gradient per sector.\n *\n * Diagonal rather than horizontal: a wedge is a radial shape, and a gradient\n * running corner to corner reads as light across the whole pie rather than as\n * a seam through each slice.\n */\nfunction SectorGradients({\n id,\n config,\n}: {\n id: string\n config: ChartConfig\n /** Reserved: the pie has one fill variant. See `PieVariant`. */\n variant: PieVariant\n}) {\n return (\n <>\n {Object.entries(config).map(([key, series]) => (\n <linearGradient\n key={key}\n id={`${id}-colors-${cssName(key)}`}\n x1=\"0\"\n y1=\"0\"\n x2=\"1\"\n y2=\"1\"\n >\n <ColorStops dataKey={cssName(key)} stops={colorStops(series)} />\n </linearGradient>\n ))}\n </>\n )\n}\n\n/** The hover panel. No heading: the wedge's own name is the row label. */\nfunction Tooltip({ variant, roundness, defaultIndex }: ChartTooltipSlotProps) {\n const { isLoading, nameKey } = usePieChart()\n if (isLoading) return null\n\n return (\n <ChartTooltip\n defaultIndex={defaultIndex}\n content={\n <ChartTooltipContent nameKey={nameKey} hideLabel roundness={roundness} variant={variant} />\n }\n />\n )\n}\n\n/**\n * The key. Under the pie and centred by default, because a pie's sectors are\n * named nowhere else — unlike a bar chart, which has a category axis.\n */\nfunction Legend({\n variant,\n align = 'center',\n verticalAlign = 'bottom',\n isClickable = false,\n}: ChartLegendSlotProps) {\n const { nameKey, selectedSector, selectSector } = usePieChart()\n\n return (\n <ChartLegend\n verticalAlign={verticalAlign}\n align={align}\n content={\n <ChartLegendContent\n selected={selectedSector}\n onSelectChange={selectSector}\n isClickable={isClickable}\n nameKey={nameKey}\n variant={variant}\n />\n }\n />\n )\n}\n\n/** The decorative plate behind the pie. Compose it before `<Pie>`. */\nfunction Background({ variant = 'dots' }: { variant?: ChartBackgroundVariant }) {\n return <ChartBackground variant={variant} />\n}\n\n/** Equal wedges for the skeleton. */\nconst LOADING_DATA = Array.from({ length: LOADING_SECTORS }, (_, index) => ({\n name: `loading-${index}`,\n value: 100 / LOADING_SECTORS,\n}))\n\n/**\n * One skeleton wedge, pulsing on a stagger so the wave travels round the pie.\n * Under `prefers-reduced-motion` it holds at a flat mid-opacity instead.\n */\nfunction LoadingSector(props: ComponentProps<typeof Sector> & { index?: number }) {\n const { index = 0, ...sector } = props\n const reduceMotion = useReducedMotion()\n const delay = (index / LOADING_SECTORS) * LOADING_PULSE\n\n if (reduceMotion) {\n return (\n <g opacity={0.3} className=\"text-(--ink)\">\n <Sector {...sector} fill=\"currentColor\" />\n </g>\n )\n }\n\n return (\n <motion.g\n className=\"text-(--ink)\"\n initial={{ opacity: 0.15 }}\n animate={{ opacity: [0.15, 0.5, 0.15] }}\n transition={{ duration: LOADING_PULSE, delay, repeat: Infinity, ease: 'easeInOut' }}\n >\n <Sector {...sector} fill=\"currentColor\" />\n </motion.g>\n )\n}\n\nPieChart.Pie = Pie\nPieChart.Label = Label\nPieChart.Tooltip = Tooltip\nPieChart.Legend = Legend\nPieChart.Background = Background\n\nexport default PieChart\n"],"mappings":"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","names":[]}
|
|
@@ -2,10 +2,10 @@ import * as react from 'react';
|
|
|
2
2
|
import { ReactNode, ComponentProps, FC } from 'react';
|
|
3
3
|
import { RadarChart as RadarChart$1, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis } from 'recharts';
|
|
4
4
|
import { ChartConfig } from '../lib/chart.js';
|
|
5
|
+
import { ChartEmptyProps } from '../lib/empty.js';
|
|
5
6
|
import { ChartBackgroundVariant } from '../lib/background.js';
|
|
6
7
|
import { ChartDotVariant } from '../lib/dot.js';
|
|
7
8
|
import { ChartTooltipSlotProps, ChartLegendSlotProps } from '../AreaChart/AreaChart.js';
|
|
8
|
-
import '../lib/empty.js';
|
|
9
9
|
import '../lib/values.js';
|
|
10
10
|
import '../lib/sonify-control.js';
|
|
11
11
|
import '../lib/sonify.js';
|
|
@@ -52,8 +52,15 @@ interface RadarChartProps<TData extends Record<string, unknown>, TConfig extends
|
|
|
52
52
|
className?: string;
|
|
53
53
|
/** Escape hatch onto the raw Recharts chart element. */
|
|
54
54
|
chartProps?: ComponentProps<typeof RadarChart$1>;
|
|
55
|
-
/** The series lit on first render
|
|
55
|
+
/** The series lit on first render, when the chart keeps its own selection. */
|
|
56
56
|
defaultSelectedDataKey?: string | null;
|
|
57
|
+
/**
|
|
58
|
+
* The selected series, driven from outside.
|
|
59
|
+
*
|
|
60
|
+
* Give this and the chart follows it; leave it undefined and the chart keeps
|
|
61
|
+
* its own, starting from `defaultSelectedDataKey`.
|
|
62
|
+
*/
|
|
63
|
+
selectedDataKey?: string | null;
|
|
57
64
|
/** Fires when the selection changes, and with null when it is cleared. */
|
|
58
65
|
onSelectionChange?: (selectedDataKey: string | null) => void;
|
|
59
66
|
/**
|
|
@@ -70,6 +77,11 @@ interface RadarChartProps<TData extends Record<string, unknown>, TConfig extends
|
|
|
70
77
|
* itself.
|
|
71
78
|
*/
|
|
72
79
|
hideDataTable?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* What the chart shows when it has nothing to draw. `false` keeps the empty
|
|
82
|
+
* plot, for a chart whose emptiness is itself the reading.
|
|
83
|
+
*/
|
|
84
|
+
empty?: ChartEmptyProps | false;
|
|
73
85
|
}
|
|
74
86
|
/**
|
|
75
87
|
* A profile across several named dimensions — the shape for "what is this thing
|
|
@@ -88,7 +100,7 @@ interface RadarChartProps<TData extends Record<string, unknown>, TConfig extends
|
|
|
88
100
|
* <RadarChart.Radar dataKey="current" />
|
|
89
101
|
* </RadarChart>
|
|
90
102
|
*/
|
|
91
|
-
declare function RadarChart<TData extends Record<string, unknown>, TConfig extends Record<string, ChartConfig[string]>>({ config, data, title, showTitle, description, children, className, chartProps, defaultSelectedDataKey, onSelectionChange, isLoading, loadingPoints, angleDataKey, hideDataTable, }: RadarChartProps<TData, TConfig>): react.JSX.Element;
|
|
103
|
+
declare function RadarChart<TData extends Record<string, unknown>, TConfig extends Record<string, ChartConfig[string]>>({ config, data, title, showTitle, description, children, className, chartProps, defaultSelectedDataKey, selectedDataKey: controlledDataKey, onSelectionChange, isLoading, loadingPoints, angleDataKey, hideDataTable, empty, }: RadarChartProps<TData, TConfig>): react.JSX.Element;
|
|
92
104
|
declare namespace RadarChart {
|
|
93
105
|
var Radar: ({ dataKey, variant, fillOpacity, glowing, isClickable, children, radarProps, }: RadarProps) => react.JSX.Element | null;
|
|
94
106
|
var Dot: FC<DotProps>;
|
|
@@ -4,7 +4,6 @@ import {
|
|
|
4
4
|
Children,
|
|
5
5
|
createContext,
|
|
6
6
|
isValidElement,
|
|
7
|
-
useCallback,
|
|
8
7
|
useContext,
|
|
9
8
|
useEffect,
|
|
10
9
|
useId,
|
|
@@ -21,6 +20,8 @@ import {
|
|
|
21
20
|
import { useReducedMotion } from "motion/react";
|
|
22
21
|
import { ChartContainer, colorStops } from "../lib/chart.js";
|
|
23
22
|
import { ChartFigure } from "../lib/figure.js";
|
|
23
|
+
import {} from "../lib/empty.js";
|
|
24
|
+
import { useChartSelection } from "../lib/selection.js";
|
|
24
25
|
import { ChartBackground } from "../lib/background.js";
|
|
25
26
|
import { ChartDot } from "../lib/dot.js";
|
|
26
27
|
import { ChartLegend, ChartLegendContent } from "../lib/legend.js";
|
|
@@ -47,21 +48,20 @@ function RadarChart({
|
|
|
47
48
|
className,
|
|
48
49
|
chartProps,
|
|
49
50
|
defaultSelectedDataKey = null,
|
|
51
|
+
selectedDataKey: controlledDataKey,
|
|
50
52
|
onSelectionChange,
|
|
51
53
|
isLoading = false,
|
|
52
54
|
loadingPoints,
|
|
53
55
|
angleDataKey,
|
|
54
|
-
hideDataTable = false
|
|
56
|
+
hideDataTable = false,
|
|
57
|
+
empty
|
|
55
58
|
}) {
|
|
56
59
|
const chartId = useId().replace(/:/g, "");
|
|
57
|
-
const [selectedDataKey, setSelectedDataKey] = useState(defaultSelectedDataKey);
|
|
58
60
|
const loadingData = useLoadingShape(isLoading, loadingPoints);
|
|
59
|
-
const selectDataKey =
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
[onSelectionChange]
|
|
61
|
+
const [selectedDataKey, selectDataKey] = useChartSelection(
|
|
62
|
+
controlledDataKey,
|
|
63
|
+
defaultSelectedDataKey,
|
|
64
|
+
onSelectionChange
|
|
65
65
|
);
|
|
66
66
|
const context = useMemo(
|
|
67
67
|
() => ({ config, isLoading, selectedDataKey, selectDataKey }),
|
|
@@ -82,6 +82,8 @@ function RadarChart({
|
|
|
82
82
|
label: series.label
|
|
83
83
|
}))
|
|
84
84
|
},
|
|
85
|
+
isEmpty: !isLoading && data.length === 0,
|
|
86
|
+
empty,
|
|
85
87
|
children: [
|
|
86
88
|
/* @__PURE__ */ jsx(ChartContainer, { config, children: /* @__PURE__ */ jsxs(RechartsRadarChart, { id: chartId, data: isLoading ? loadingData : data, ...chartProps, children: [
|
|
87
89
|
children,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/charts/RadarChart/RadarChart.tsx"],"sourcesContent":["'use client'\n\nimport {\n Children,\n createContext,\n isValidElement,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n type ComponentProps,\n type FC,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport {\n PolarAngleAxis as RechartsPolarAngleAxis,\n PolarGrid as RechartsPolarGrid,\n PolarRadiusAxis as RechartsPolarRadiusAxis,\n Radar as RechartsRadar,\n RadarChart as RechartsRadarChart,\n} from 'recharts'\nimport { useReducedMotion } from 'motion/react'\nimport { ChartContainer, colorStops, type ChartConfig } from '../lib/chart'\nimport { ChartFigure } from '../lib/figure'\nimport { ChartBackground, type ChartBackgroundVariant } from '../lib/background'\nimport { ChartDot, type ChartDotVariant } from '../lib/dot'\nimport { ChartLegend, ChartLegendContent } from '../lib/legend'\nimport { ChartTooltip, ChartTooltipContent } from '../lib/tooltip'\nimport { LoadingIndicator } from '../lib/loading'\nimport { ColorStops, GlowFilter, SeriesGradient } from '../lib/paint'\nimport type { ChartLegendSlotProps, ChartTooltipSlotProps } from '../AreaChart/AreaChart'\n\nconst STROKE_WIDTH = 1\n/**\n * How solid a filled radar is, as a multiplier on `--chart-fill`.\n *\n * Above 1 because a radar's fill is the mark itself rather than a wash under a\n * line — it has to hold its shape where two of them overlap.\n */\nconst FILL_STRENGTH = 2.2\nconst LOADING_POINTS = 6\n\n/** One skeleton morph, in milliseconds. */\nconst LOADING_CYCLE = 1500\n\n/**\n * Whether a radar is filled or left as an outline.\n *\n * `lines` is the honest default above two series: filled polygons overlap, and\n * the reader is then judging areas through two layers of translucency, which is\n * the one thing a radar is worst at.\n */\nexport type RadarVariant = 'filled' | 'lines'\n\ninterface RadarChartContextValue {\n config: ChartConfig\n isLoading: boolean\n selectedDataKey: string | null\n selectDataKey: (dataKey: string | null) => void\n}\n\nconst RadarChartContext = createContext<RadarChartContextValue | null>(null)\n\nfunction useRadarChart(): RadarChartContextValue {\n const context = useContext(RadarChartContext)\n if (!context) throw new Error('A radar chart part must be rendered inside <RadarChart>')\n return context\n}\n\ntype ValidateKeys<TData, TConfig> = {\n [K in keyof TConfig]: K extends keyof TData ? ChartConfig[string] : never\n}\n\nexport interface RadarChartProps<\n TData extends Record<string, unknown>,\n TConfig extends Record<string, ChartConfig[string]>,\n> {\n config: TConfig & ValidateKeys<TData, TConfig>\n /** The rows the chart draws. One entry per point, bar or category. */\n data: TData[]\n /**\n * What the chart shows, in a sentence a reader could act on. Required, and\n * announced to a screen reader even when it is not printed.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n /**\n * The composed parts — axes, grid, tooltip, legend, and the marks\n * themselves.\n */\n children: ReactNode\n /** Merged onto the figure, last, so a call site can size or space it. */\n className?: string\n /** Escape hatch onto the raw Recharts chart element. */\n chartProps?: ComponentProps<typeof RechartsRadarChart>\n /** The series lit on first render. Selection dims every other series. */\n defaultSelectedDataKey?: string | null\n /** Fires when the selection changes, and with null when it is cleared. */\n onSelectionChange?: (selectedDataKey: string | null) => void\n /**\n * Swaps the marks for an animated skeleton, keeping the measured height so\n * the page does not jump when the data lands.\n */\n isLoading?: boolean\n /** How many points the skeleton draws. */\n loadingPoints?: number\n /** The row field naming each spoke. Used by the table view. */\n angleDataKey?: keyof TData & string\n /**\n * Drops the hidden table view. Only correct when the page prints the data\n * itself.\n */\n hideDataTable?: boolean\n}\n\n/**\n * A profile across several named dimensions — the shape for \"what is this thing\n * strong and weak at\".\n *\n * It reads a SHAPE, not a set of values: the area a radar encloses depends on\n * the order the spokes happen to be in, so it is the wrong chart for comparing\n * magnitudes and the right one for recognising a silhouette. Two or three\n * series at most.\n *\n * @example\n * <RadarChart title=\"Team skills\" config={config} data={data} angleDataKey=\"skill\">\n * <RadarChart.PolarGrid />\n * <RadarChart.PolarAngleAxis dataKey=\"skill\" />\n * <RadarChart.Tooltip />\n * <RadarChart.Radar dataKey=\"current\" />\n * </RadarChart>\n */\nexport function RadarChart<\n TData extends Record<string, unknown>,\n TConfig extends Record<string, ChartConfig[string]>,\n>({\n config,\n data,\n title,\n showTitle,\n description,\n children,\n className,\n chartProps,\n defaultSelectedDataKey = null,\n onSelectionChange,\n isLoading = false,\n loadingPoints,\n angleDataKey,\n hideDataTable = false,\n}: RadarChartProps<TData, TConfig>) {\n const chartId = useId().replace(/:/g, '')\n const [selectedDataKey, setSelectedDataKey] = useState<string | null>(defaultSelectedDataKey)\n const loadingData = useLoadingShape(isLoading, loadingPoints)\n\n const selectDataKey = useCallback(\n (next: string | null) => {\n setSelectedDataKey(next)\n onSelectionChange?.(next)\n },\n [onSelectionChange],\n )\n\n const context = useMemo<RadarChartContextValue>(\n () => ({ config, isLoading, selectedDataKey, selectDataKey }),\n [config, isLoading, selectDataKey, selectedDataKey],\n )\n\n return (\n <RadarChartContext.Provider value={context}>\n <ChartFigure\n title={title}\n showTitle={showTitle}\n description={description}\n className={className}\n table={\n hideDataTable\n ? false\n : {\n rows: data,\n rowKey: angleDataKey,\n columns: Object.entries(config).map(([key, series]) => ({\n key,\n label: series.label,\n })),\n }\n }\n >\n <ChartContainer config={config}>\n <RechartsRadarChart id={chartId} data={isLoading ? loadingData : data} {...chartProps}>\n {children}\n {isLoading && <LoadingRadar />}\n </RechartsRadarChart>\n </ChartContainer>\n <LoadingIndicator isLoading={isLoading} />\n </ChartFigure>\n </RadarChartContext.Provider>\n )\n}\n\nexport interface RadarProps {\n dataKey: string\n variant?: RadarVariant\n /**\n * How solid the filled area is, 0–1. Left unset it follows `--chart-fill`,\n * which is what keeps it readable on both grounds.\n */\n fillOpacity?: number\n /** A halo around the outline — for the one series that is the point. */\n glowing?: boolean\n isClickable?: boolean\n children?: ReactNode\n radarProps?: Omit<ComponentProps<typeof RechartsRadar>, 'dataKey'>\n}\n\n/** One radar series, with its stroke, fill and glow scoped to its own id. */\nfunction Radar({\n dataKey,\n variant = 'filled',\n fillOpacity,\n glowing = false,\n isClickable = false,\n children,\n radarProps,\n}: RadarProps) {\n const { config, isLoading, selectedDataKey, selectDataKey } = useRadarChart()\n const id = useId().replace(/:/g, '')\n\n if (isLoading) return null\n\n const isSelected = selectedDataKey === null || selectedDataKey === dataKey\n const dimmed = isClickable && !isSelected\n // The fill recedes twice as far as the stroke. A dimmed outline still traces\n // its shape, which is what a radar is read by; a dimmed fill just muddies the\n // one that was picked.\n const opacity = { stroke: dimmed ? 0.2 : 1, fill: dimmed ? 0.1 : 1, dot: dimmed ? 0.2 : 1 }\n const isFilled = variant === 'filled'\n\n const { dot, activeDot } = resolveDots(children, id, dataKey, opacity.dot)\n // `--chart-fill` is a number, not a length, so it can be read into a\n // calc() the SVG attribute accepts — which is how the default follows the\n // ground without the component knowing which ground it is on.\n const filledStrength = `calc(var(--chart-fill) * ${FILL_STRENGTH})` as unknown as number\n const stops = colorStops(config[dataKey])\n\n return (\n <>\n <RechartsRadar\n dataKey={dataKey}\n stroke={`url(#${id}-stroke-${dataKey})`}\n strokeOpacity={opacity.stroke}\n strokeWidth={STROKE_WIDTH}\n fill={isFilled ? `url(#${id}-fill-${dataKey})` : 'none'}\n fillOpacity={isFilled ? (fillOpacity ?? filledStrength) * opacity.fill : 0}\n dot={dot}\n activeDot={activeDot}\n filter={glowing ? `url(#${id}-glow-${dataKey})` : undefined}\n className=\"transition-opacity duration-(--duration-base)\"\n style={isClickable ? { cursor: 'pointer' } : undefined}\n onClick={() => {\n if (!isClickable) return\n selectDataKey(selectedDataKey === dataKey ? null : dataKey)\n }}\n {...radarProps}\n />\n <defs>\n {/* The dots read this one; the stroke and fill get their own below. */}\n <SeriesGradient id={id} dataKey={dataKey} config={config} />\n <linearGradient id={`${id}-stroke-${dataKey}`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <ColorStops dataKey={dataKey} stops={stops} />\n </linearGradient>\n {isFilled && (\n // Radial, and fading outward: a flat fill makes the outer ring — the\n // part carrying the shape — the least readable part of the mark.\n <radialGradient id={`${id}-fill-${dataKey}`} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <ColorStops\n dataKey={dataKey}\n stops={stops}\n opacities={\n stops === 1 ? [0.8, 0.3] : Array.from({ length: stops }, (_, i) => (i === 0 ? 0.8 : 0.3))\n }\n />\n </radialGradient>\n )}\n {glowing && <GlowFilter id={id} dataKey={dataKey} spread={4} intensity={0.6} />}\n </defs>\n </>\n )\n}\n\nexport interface DotProps {\n variant?: ChartDotVariant\n}\n\n/** The resting point marker for the radar it is composed inside. A slot. */\nconst Dot: FC<DotProps> = () => null\n\n/** The marker under the pointer. */\nconst ActiveDot: FC<DotProps> = () => null\n\nfunction resolveDots(children: ReactNode, id: string, dataKey: string, dotOpacity: number) {\n let dot: ComponentProps<typeof RechartsRadar>['dot'] = false\n let activeDot: ComponentProps<typeof RechartsRadar>['activeDot'] = false\n\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) return\n const { variant } = (child as ReactElement<DotProps>).props\n\n if (child.type === Dot) {\n dot = <ChartDot variant={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />\n }\n if (child.type === ActiveDot) {\n activeDot = (\n <ChartDot variant={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />\n )\n }\n })\n\n return { dot, activeDot }\n}\n\n/**\n * The web behind the marks. `gridType=\"circle\"` swaps the polygon for rings,\n * which reads as a scale rather than as a shape and suits a single series.\n */\nfunction PolarGrid({\n gridType = 'polygon',\n stroke = 'var(--chart-grid)',\n strokeDasharray = '3 4',\n ...rest\n}: ComponentProps<typeof RechartsPolarGrid>) {\n return (\n <RechartsPolarGrid\n gridType={gridType}\n stroke={stroke}\n strokeDasharray={strokeDasharray}\n {...rest}\n />\n )\n}\n\n/** The spoke labels around the perimeter. */\nfunction PolarAngleAxis({\n tick = { fill: 'var(--chart-axis)', fontSize: 12 },\n tickLine = false,\n ...rest\n}: ComponentProps<typeof RechartsPolarAngleAxis>) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return <RechartsPolarAngleAxis tick={tick} tickLine={tickLine} {...rest} />\n}\n\n/** The scale running out from the centre. */\nfunction PolarRadiusAxis({\n tick = { fill: 'var(--chart-axis)', fontSize: 10 },\n tickLine = false,\n axisLine = false,\n ...rest\n}: ComponentProps<typeof RechartsPolarRadiusAxis>) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return (\n <RechartsPolarRadiusAxis tick={tick} tickLine={tickLine} axisLine={axisLine} {...rest} />\n )\n}\n\n/** The hover panel. No crosshair: there is no straight axis to run one along. */\nfunction Tooltip({ variant, roundness, defaultIndex }: ChartTooltipSlotProps) {\n const { isLoading, selectedDataKey } = useRadarChart()\n if (isLoading) return null\n\n return (\n <ChartTooltip\n defaultIndex={defaultIndex}\n cursor={false}\n content={\n <ChartTooltipContent selected={selectedDataKey} roundness={roundness} variant={variant} />\n }\n />\n )\n}\n\n/** The key. Under the web and centred, since the spokes name dimensions, not series. */\nfunction Legend({\n variant,\n align = 'center',\n verticalAlign = 'bottom',\n isClickable = false,\n}: ChartLegendSlotProps) {\n const { isLoading, selectedDataKey, selectDataKey } = useRadarChart()\n if (isLoading) return null\n\n return (\n <ChartLegend\n verticalAlign={verticalAlign}\n align={align}\n content={\n <ChartLegendContent\n selected={selectedDataKey}\n onSelectChange={selectDataKey}\n isClickable={isClickable}\n variant={variant}\n />\n }\n />\n )\n}\n\n/** The decorative plate behind the web. */\nfunction Background({ variant = 'dots' }: { variant?: ChartBackgroundVariant }) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return <ChartBackground variant={variant} />\n}\n\n/** A deterministic spoke value, so the server and the client agree. */\nfunction shape(index: number, cycle: number): number {\n const x = Math.sin(index * 91.7 + cycle * 47.3) * 21_942.7\n return 30 + (x - Math.floor(x)) * 70\n}\n\n/**\n * The skeleton's spokes, re-rolled once per cycle.\n *\n * An interval rather than the shimmer's exit callback: a radar's skeleton\n * animates by MORPHING between shapes, which is Recharts' own transition, so\n * there is no travelling highlight to synchronise against.\n */\nfunction useLoadingShape(isLoading: boolean, points = LOADING_POINTS) {\n const [cycle, setCycle] = useState(0)\n\n useEffect(() => {\n if (!isLoading) return\n const timer = setInterval(() => setCycle((previous) => previous + 1), LOADING_CYCLE)\n return () => clearInterval(timer)\n }, [isLoading])\n\n return useMemo(\n () =>\n Array.from({ length: points }, (_, index) => ({\n spoke: String.fromCharCode(65 + index),\n value: shape(index, cycle),\n })),\n [cycle, points],\n )\n}\n\n/** The skeleton radar, morphing between shapes while the data is in flight. */\nfunction LoadingRadar() {\n const reduceMotion = useReducedMotion()\n\n return (\n <RechartsRadar\n dataKey=\"value\"\n stroke=\"currentColor\"\n strokeOpacity={0.3}\n strokeWidth={2}\n fill=\"currentColor\"\n fillOpacity={0.1}\n dot={false}\n className=\"text-(--ink)\"\n isAnimationActive={!reduceMotion}\n animationDuration={LOADING_CYCLE}\n animationEasing=\"ease-in-out\"\n />\n )\n}\n\nRadarChart.Radar = Radar\nRadarChart.Dot = Dot\nRadarChart.ActiveDot = ActiveDot\nRadarChart.PolarGrid = PolarGrid\nRadarChart.PolarAngleAxis = PolarAngleAxis\nRadarChart.PolarRadiusAxis = PolarRadiusAxis\nRadarChart.Tooltip = Tooltip\nRadarChart.Legend = Legend\nRadarChart.Background = Background\n\nexport default RadarChart\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/RadarChart/RadarChart.tsx"],"sourcesContent":["'use client'\n\nimport {\n Children,\n createContext,\n isValidElement,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n type ComponentProps,\n type FC,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport {\n PolarAngleAxis as RechartsPolarAngleAxis,\n PolarGrid as RechartsPolarGrid,\n PolarRadiusAxis as RechartsPolarRadiusAxis,\n Radar as RechartsRadar,\n RadarChart as RechartsRadarChart,\n} from 'recharts'\nimport { useReducedMotion } from 'motion/react'\nimport { ChartContainer, colorStops, type ChartConfig } from '../lib/chart'\nimport { ChartFigure } from '../lib/figure'\nimport { type ChartEmptyProps } from '../lib/empty'\nimport { useChartSelection } from '../lib/selection'\nimport { ChartBackground, type ChartBackgroundVariant } from '../lib/background'\nimport { ChartDot, type ChartDotVariant } from '../lib/dot'\nimport { ChartLegend, ChartLegendContent } from '../lib/legend'\nimport { ChartTooltip, ChartTooltipContent } from '../lib/tooltip'\nimport { LoadingIndicator } from '../lib/loading'\nimport { ColorStops, GlowFilter, SeriesGradient } from '../lib/paint'\nimport type { ChartLegendSlotProps, ChartTooltipSlotProps } from '../AreaChart/AreaChart'\n\nconst STROKE_WIDTH = 1\n/**\n * How solid a filled radar is, as a multiplier on `--chart-fill`.\n *\n * Above 1 because a radar's fill is the mark itself rather than a wash under a\n * line — it has to hold its shape where two of them overlap.\n */\nconst FILL_STRENGTH = 2.2\nconst LOADING_POINTS = 6\n\n/** One skeleton morph, in milliseconds. */\nconst LOADING_CYCLE = 1500\n\n/**\n * Whether a radar is filled or left as an outline.\n *\n * `lines` is the honest default above two series: filled polygons overlap, and\n * the reader is then judging areas through two layers of translucency, which is\n * the one thing a radar is worst at.\n */\nexport type RadarVariant = 'filled' | 'lines'\n\ninterface RadarChartContextValue {\n config: ChartConfig\n isLoading: boolean\n selectedDataKey: string | null\n selectDataKey: (dataKey: string | null) => void\n}\n\nconst RadarChartContext = createContext<RadarChartContextValue | null>(null)\n\nfunction useRadarChart(): RadarChartContextValue {\n const context = useContext(RadarChartContext)\n if (!context) throw new Error('A radar chart part must be rendered inside <RadarChart>')\n return context\n}\n\ntype ValidateKeys<TData, TConfig> = {\n [K in keyof TConfig]: K extends keyof TData ? ChartConfig[string] : never\n}\n\nexport interface RadarChartProps<\n TData extends Record<string, unknown>,\n TConfig extends Record<string, ChartConfig[string]>,\n> {\n config: TConfig & ValidateKeys<TData, TConfig>\n /** The rows the chart draws. One entry per point, bar or category. */\n data: TData[]\n /**\n * What the chart shows, in a sentence a reader could act on. Required, and\n * announced to a screen reader even when it is not printed.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n /**\n * The composed parts — axes, grid, tooltip, legend, and the marks\n * themselves.\n */\n children: ReactNode\n /** Merged onto the figure, last, so a call site can size or space it. */\n className?: string\n /** Escape hatch onto the raw Recharts chart element. */\n chartProps?: ComponentProps<typeof RechartsRadarChart>\n /** The series lit on first render, when the chart keeps its own selection. */\n defaultSelectedDataKey?: string | null\n /**\n * The selected series, driven from outside.\n *\n * Give this and the chart follows it; leave it undefined and the chart keeps\n * its own, starting from `defaultSelectedDataKey`.\n */\n selectedDataKey?: string | null\n /** Fires when the selection changes, and with null when it is cleared. */\n onSelectionChange?: (selectedDataKey: string | null) => void\n /**\n * Swaps the marks for an animated skeleton, keeping the measured height so\n * the page does not jump when the data lands.\n */\n isLoading?: boolean\n /** How many points the skeleton draws. */\n loadingPoints?: number\n /** The row field naming each spoke. Used by the table view. */\n angleDataKey?: keyof TData & string\n /**\n * Drops the hidden table view. Only correct when the page prints the data\n * itself.\n */\n hideDataTable?: boolean\n /**\n * What the chart shows when it has nothing to draw. `false` keeps the empty\n * plot, for a chart whose emptiness is itself the reading.\n */\n empty?: ChartEmptyProps | false\n}\n\n/**\n * A profile across several named dimensions — the shape for \"what is this thing\n * strong and weak at\".\n *\n * It reads a SHAPE, not a set of values: the area a radar encloses depends on\n * the order the spokes happen to be in, so it is the wrong chart for comparing\n * magnitudes and the right one for recognising a silhouette. Two or three\n * series at most.\n *\n * @example\n * <RadarChart title=\"Team skills\" config={config} data={data} angleDataKey=\"skill\">\n * <RadarChart.PolarGrid />\n * <RadarChart.PolarAngleAxis dataKey=\"skill\" />\n * <RadarChart.Tooltip />\n * <RadarChart.Radar dataKey=\"current\" />\n * </RadarChart>\n */\nexport function RadarChart<\n TData extends Record<string, unknown>,\n TConfig extends Record<string, ChartConfig[string]>,\n>({\n config,\n data,\n title,\n showTitle,\n description,\n children,\n className,\n chartProps,\n defaultSelectedDataKey = null,\n selectedDataKey: controlledDataKey,\n onSelectionChange,\n isLoading = false,\n loadingPoints,\n angleDataKey,\n hideDataTable = false,\n empty,\n}: RadarChartProps<TData, TConfig>) {\n const chartId = useId().replace(/:/g, '')\n const loadingData = useLoadingShape(isLoading, loadingPoints)\n const [selectedDataKey, selectDataKey] = useChartSelection(\n controlledDataKey,\n defaultSelectedDataKey,\n onSelectionChange,\n )\n\n const context = useMemo<RadarChartContextValue>(\n () => ({ config, isLoading, selectedDataKey, selectDataKey }),\n [config, isLoading, selectDataKey, selectedDataKey],\n )\n\n return (\n <RadarChartContext.Provider value={context}>\n <ChartFigure\n title={title}\n showTitle={showTitle}\n description={description}\n className={className}\n table={\n hideDataTable\n ? false\n : {\n rows: data,\n rowKey: angleDataKey,\n columns: Object.entries(config).map(([key, series]) => ({\n key,\n label: series.label,\n })),\n }\n }\n isEmpty={!isLoading && data.length === 0}\n empty={empty}\n >\n <ChartContainer config={config}>\n <RechartsRadarChart id={chartId} data={isLoading ? loadingData : data} {...chartProps}>\n {children}\n {isLoading && <LoadingRadar />}\n </RechartsRadarChart>\n </ChartContainer>\n <LoadingIndicator isLoading={isLoading} />\n </ChartFigure>\n </RadarChartContext.Provider>\n )\n}\n\nexport interface RadarProps {\n dataKey: string\n variant?: RadarVariant\n /**\n * How solid the filled area is, 0–1. Left unset it follows `--chart-fill`,\n * which is what keeps it readable on both grounds.\n */\n fillOpacity?: number\n /** A halo around the outline — for the one series that is the point. */\n glowing?: boolean\n isClickable?: boolean\n children?: ReactNode\n radarProps?: Omit<ComponentProps<typeof RechartsRadar>, 'dataKey'>\n}\n\n/** One radar series, with its stroke, fill and glow scoped to its own id. */\nfunction Radar({\n dataKey,\n variant = 'filled',\n fillOpacity,\n glowing = false,\n isClickable = false,\n children,\n radarProps,\n}: RadarProps) {\n const { config, isLoading, selectedDataKey, selectDataKey } = useRadarChart()\n const id = useId().replace(/:/g, '')\n\n if (isLoading) return null\n\n const isSelected = selectedDataKey === null || selectedDataKey === dataKey\n const dimmed = isClickable && !isSelected\n // The fill recedes twice as far as the stroke. A dimmed outline still traces\n // its shape, which is what a radar is read by; a dimmed fill just muddies the\n // one that was picked.\n const opacity = { stroke: dimmed ? 0.2 : 1, fill: dimmed ? 0.1 : 1, dot: dimmed ? 0.2 : 1 }\n const isFilled = variant === 'filled'\n\n const { dot, activeDot } = resolveDots(children, id, dataKey, opacity.dot)\n // `--chart-fill` is a number, not a length, so it can be read into a\n // calc() the SVG attribute accepts — which is how the default follows the\n // ground without the component knowing which ground it is on.\n const filledStrength = `calc(var(--chart-fill) * ${FILL_STRENGTH})` as unknown as number\n const stops = colorStops(config[dataKey])\n\n return (\n <>\n <RechartsRadar\n dataKey={dataKey}\n stroke={`url(#${id}-stroke-${dataKey})`}\n strokeOpacity={opacity.stroke}\n strokeWidth={STROKE_WIDTH}\n fill={isFilled ? `url(#${id}-fill-${dataKey})` : 'none'}\n fillOpacity={isFilled ? (fillOpacity ?? filledStrength) * opacity.fill : 0}\n dot={dot}\n activeDot={activeDot}\n filter={glowing ? `url(#${id}-glow-${dataKey})` : undefined}\n className=\"transition-opacity duration-(--duration-base)\"\n style={isClickable ? { cursor: 'pointer' } : undefined}\n onClick={() => {\n if (!isClickable) return\n selectDataKey(selectedDataKey === dataKey ? null : dataKey)\n }}\n {...radarProps}\n />\n <defs>\n {/* The dots read this one; the stroke and fill get their own below. */}\n <SeriesGradient id={id} dataKey={dataKey} config={config} />\n <linearGradient id={`${id}-stroke-${dataKey}`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n <ColorStops dataKey={dataKey} stops={stops} />\n </linearGradient>\n {isFilled && (\n // Radial, and fading outward: a flat fill makes the outer ring — the\n // part carrying the shape — the least readable part of the mark.\n <radialGradient id={`${id}-fill-${dataKey}`} cx=\"50%\" cy=\"50%\" r=\"50%\">\n <ColorStops\n dataKey={dataKey}\n stops={stops}\n opacities={\n stops === 1 ? [0.8, 0.3] : Array.from({ length: stops }, (_, i) => (i === 0 ? 0.8 : 0.3))\n }\n />\n </radialGradient>\n )}\n {glowing && <GlowFilter id={id} dataKey={dataKey} spread={4} intensity={0.6} />}\n </defs>\n </>\n )\n}\n\nexport interface DotProps {\n variant?: ChartDotVariant\n}\n\n/** The resting point marker for the radar it is composed inside. A slot. */\nconst Dot: FC<DotProps> = () => null\n\n/** The marker under the pointer. */\nconst ActiveDot: FC<DotProps> = () => null\n\nfunction resolveDots(children: ReactNode, id: string, dataKey: string, dotOpacity: number) {\n let dot: ComponentProps<typeof RechartsRadar>['dot'] = false\n let activeDot: ComponentProps<typeof RechartsRadar>['activeDot'] = false\n\n Children.forEach(children, (child) => {\n if (!isValidElement(child)) return\n const { variant } = (child as ReactElement<DotProps>).props\n\n if (child.type === Dot) {\n dot = <ChartDot variant={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />\n }\n if (child.type === ActiveDot) {\n activeDot = (\n <ChartDot variant={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />\n )\n }\n })\n\n return { dot, activeDot }\n}\n\n/**\n * The web behind the marks. `gridType=\"circle\"` swaps the polygon for rings,\n * which reads as a scale rather than as a shape and suits a single series.\n */\nfunction PolarGrid({\n gridType = 'polygon',\n stroke = 'var(--chart-grid)',\n strokeDasharray = '3 4',\n ...rest\n}: ComponentProps<typeof RechartsPolarGrid>) {\n return (\n <RechartsPolarGrid\n gridType={gridType}\n stroke={stroke}\n strokeDasharray={strokeDasharray}\n {...rest}\n />\n )\n}\n\n/** The spoke labels around the perimeter. */\nfunction PolarAngleAxis({\n tick = { fill: 'var(--chart-axis)', fontSize: 12 },\n tickLine = false,\n ...rest\n}: ComponentProps<typeof RechartsPolarAngleAxis>) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return <RechartsPolarAngleAxis tick={tick} tickLine={tickLine} {...rest} />\n}\n\n/** The scale running out from the centre. */\nfunction PolarRadiusAxis({\n tick = { fill: 'var(--chart-axis)', fontSize: 10 },\n tickLine = false,\n axisLine = false,\n ...rest\n}: ComponentProps<typeof RechartsPolarRadiusAxis>) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return (\n <RechartsPolarRadiusAxis tick={tick} tickLine={tickLine} axisLine={axisLine} {...rest} />\n )\n}\n\n/** The hover panel. No crosshair: there is no straight axis to run one along. */\nfunction Tooltip({ variant, roundness, defaultIndex }: ChartTooltipSlotProps) {\n const { isLoading, selectedDataKey } = useRadarChart()\n if (isLoading) return null\n\n return (\n <ChartTooltip\n defaultIndex={defaultIndex}\n cursor={false}\n content={\n <ChartTooltipContent selected={selectedDataKey} roundness={roundness} variant={variant} />\n }\n />\n )\n}\n\n/** The key. Under the web and centred, since the spokes name dimensions, not series. */\nfunction Legend({\n variant,\n align = 'center',\n verticalAlign = 'bottom',\n isClickable = false,\n}: ChartLegendSlotProps) {\n const { isLoading, selectedDataKey, selectDataKey } = useRadarChart()\n if (isLoading) return null\n\n return (\n <ChartLegend\n verticalAlign={verticalAlign}\n align={align}\n content={\n <ChartLegendContent\n selected={selectedDataKey}\n onSelectChange={selectDataKey}\n isClickable={isClickable}\n variant={variant}\n />\n }\n />\n )\n}\n\n/** The decorative plate behind the web. */\nfunction Background({ variant = 'dots' }: { variant?: ChartBackgroundVariant }) {\n const { isLoading } = useRadarChart()\n if (isLoading) return null\n return <ChartBackground variant={variant} />\n}\n\n/** A deterministic spoke value, so the server and the client agree. */\nfunction shape(index: number, cycle: number): number {\n const x = Math.sin(index * 91.7 + cycle * 47.3) * 21_942.7\n return 30 + (x - Math.floor(x)) * 70\n}\n\n/**\n * The skeleton's spokes, re-rolled once per cycle.\n *\n * An interval rather than the shimmer's exit callback: a radar's skeleton\n * animates by MORPHING between shapes, which is Recharts' own transition, so\n * there is no travelling highlight to synchronise against.\n */\nfunction useLoadingShape(isLoading: boolean, points = LOADING_POINTS) {\n const [cycle, setCycle] = useState(0)\n\n useEffect(() => {\n if (!isLoading) return\n const timer = setInterval(() => setCycle((previous) => previous + 1), LOADING_CYCLE)\n return () => clearInterval(timer)\n }, [isLoading])\n\n return useMemo(\n () =>\n Array.from({ length: points }, (_, index) => ({\n spoke: String.fromCharCode(65 + index),\n value: shape(index, cycle),\n })),\n [cycle, points],\n )\n}\n\n/** The skeleton radar, morphing between shapes while the data is in flight. */\nfunction LoadingRadar() {\n const reduceMotion = useReducedMotion()\n\n return (\n <RechartsRadar\n dataKey=\"value\"\n stroke=\"currentColor\"\n strokeOpacity={0.3}\n strokeWidth={2}\n fill=\"currentColor\"\n fillOpacity={0.1}\n dot={false}\n className=\"text-(--ink)\"\n isAnimationActive={!reduceMotion}\n animationDuration={LOADING_CYCLE}\n animationEasing=\"ease-in-out\"\n />\n )\n}\n\nRadarChart.Radar = Radar\nRadarChart.Dot = Dot\nRadarChart.ActiveDot = ActiveDot\nRadarChart.PolarGrid = PolarGrid\nRadarChart.PolarAngleAxis = PolarAngleAxis\nRadarChart.PolarRadiusAxis = PolarRadiusAxis\nRadarChart.Tooltip = Tooltip\nRadarChart.Legend = Legend\nRadarChart.Background = Background\n\nexport default RadarChart\n"],"mappings":"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","names":[]}
|
|
@@ -2,9 +2,9 @@ import * as react from 'react';
|
|
|
2
2
|
import { ComponentProps, ReactNode } from 'react';
|
|
3
3
|
import { RadialBar, RadialBarChart } from 'recharts';
|
|
4
4
|
import { ChartConfig } from '../lib/chart.js';
|
|
5
|
+
import { ChartEmptyProps } from '../lib/empty.js';
|
|
5
6
|
import { ChartBackgroundVariant } from '../lib/background.js';
|
|
6
7
|
import { ChartTooltipSlotProps, ChartLegendSlotProps } from '../AreaChart/AreaChart.js';
|
|
7
|
-
import '../lib/empty.js';
|
|
8
8
|
import '../lib/values.js';
|
|
9
9
|
import '../lib/sonify-control.js';
|
|
10
10
|
import '../lib/sonify.js';
|
|
@@ -66,8 +66,15 @@ interface RadialChartProps<TData extends Record<string, unknown>> {
|
|
|
66
66
|
innerRadius?: number | string;
|
|
67
67
|
/** Where the arc ends. */
|
|
68
68
|
outerRadius?: number | string;
|
|
69
|
-
/** The bar lit on first render. */
|
|
69
|
+
/** The bar lit on first render, when the chart keeps its own selection. */
|
|
70
70
|
defaultSelectedBar?: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* The selected bar, driven from outside.
|
|
73
|
+
*
|
|
74
|
+
* Give this and the chart follows it; leave it undefined and the chart keeps
|
|
75
|
+
* its own, starting from `defaultSelectedBar`.
|
|
76
|
+
*/
|
|
77
|
+
selectedBar?: string | null;
|
|
71
78
|
/** Fires when the selection changes, and with null when it is cleared. */
|
|
72
79
|
onSelectionChange?: (selection: {
|
|
73
80
|
name: string;
|
|
@@ -78,13 +85,24 @@ interface RadialChartProps<TData extends Record<string, unknown>> {
|
|
|
78
85
|
* the page does not jump when the data lands.
|
|
79
86
|
*/
|
|
80
87
|
isLoading?: boolean;
|
|
81
|
-
/**
|
|
88
|
+
/**
|
|
89
|
+
* The row field holding each bar's number, for the table view and for the
|
|
90
|
+
* selection the legend reports.
|
|
91
|
+
*
|
|
92
|
+
* Falls back to the `dataKey` of the composed `<RadialChart.RadialBar>`, so
|
|
93
|
+
* the usual call site needs neither.
|
|
94
|
+
*/
|
|
82
95
|
valueKey?: keyof TData & string;
|
|
83
96
|
/**
|
|
84
97
|
* Drops the hidden table view. Only correct when the page prints the data
|
|
85
98
|
* itself.
|
|
86
99
|
*/
|
|
87
100
|
hideDataTable?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* What the chart shows when it has nothing to draw. `false` keeps the empty
|
|
103
|
+
* plot, for a chart whose emptiness is itself the reading.
|
|
104
|
+
*/
|
|
105
|
+
empty?: ChartEmptyProps | false;
|
|
88
106
|
}
|
|
89
107
|
/**
|
|
90
108
|
* Values on an arc — a gauge, or a small set of totals against one scale.
|
|
@@ -100,7 +118,7 @@ interface RadialChartProps<TData extends Record<string, unknown>> {
|
|
|
100
118
|
* <RadialChart.Tooltip />
|
|
101
119
|
* </RadialChart>
|
|
102
120
|
*/
|
|
103
|
-
declare function RadialChart<TData extends Record<string, unknown>>({ config, data, nameKey, title, showTitle, description, children, className, chartProps, variant, max, innerRadius, outerRadius, defaultSelectedBar, onSelectionChange, isLoading, valueKey, hideDataTable, }: RadialChartProps<TData>): react.JSX.Element;
|
|
121
|
+
declare function RadialChart<TData extends Record<string, unknown>>({ config, data, nameKey, title, showTitle, description, children, className, chartProps, variant, max, innerRadius, outerRadius, defaultSelectedBar, selectedBar: controlledBar, onSelectionChange, isLoading, valueKey, hideDataTable, empty, }: RadialChartProps<TData>): react.JSX.Element;
|
|
104
122
|
declare namespace RadialChart {
|
|
105
123
|
var RadialBar: ({ dataKey, cornerRadius, barSize, showTrack, isClickable, radialBarProps, }: RadialBarProps) => react.JSX.Element | null;
|
|
106
124
|
var Tooltip: ({ variant, roundness, defaultIndex }: ChartTooltipSlotProps) => react.JSX.Element | null;
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
} from "recharts";
|
|
18
18
|
import { useReducedMotion } from "motion/react";
|
|
19
19
|
import { ChartContainer, colorStops, cssName } from "../lib/chart.js";
|
|
20
|
+
import { findSlot } from "../lib/values.js";
|
|
20
21
|
import { ChartFigure } from "../lib/figure.js";
|
|
22
|
+
import {} from "../lib/empty.js";
|
|
23
|
+
import { useChartSelection } from "../lib/selection.js";
|
|
21
24
|
import { ChartBackground } from "../lib/background.js";
|
|
22
25
|
import { ChartLegend, ChartLegendContent } from "../lib/legend.js";
|
|
23
26
|
import { ChartTooltip, ChartTooltipContent } from "../lib/tooltip.js";
|
|
@@ -33,6 +36,9 @@ const ARC = {
|
|
|
33
36
|
full: { startAngle: 90, endAngle: -270, cx: "50%", cy: "50%" },
|
|
34
37
|
semi: { startAngle: 180, endAngle: 0, cx: "50%", cy: "70%" }
|
|
35
38
|
};
|
|
39
|
+
function valueField(valueKey, children) {
|
|
40
|
+
return valueKey ?? findSlot(children, RadialBar)?.dataKey ?? null;
|
|
41
|
+
}
|
|
36
42
|
const RadialChartContext = createContext(null);
|
|
37
43
|
function useRadialChart() {
|
|
38
44
|
const context = useContext(RadialChartContext);
|
|
@@ -54,21 +60,35 @@ function RadialChart({
|
|
|
54
60
|
innerRadius = "30%",
|
|
55
61
|
outerRadius = "100%",
|
|
56
62
|
defaultSelectedBar = null,
|
|
63
|
+
selectedBar: controlledBar,
|
|
57
64
|
onSelectionChange,
|
|
58
65
|
isLoading = false,
|
|
59
66
|
valueKey,
|
|
60
|
-
hideDataTable = false
|
|
67
|
+
hideDataTable = false,
|
|
68
|
+
empty
|
|
61
69
|
}) {
|
|
62
70
|
const chartId = useId().replace(/:/g, "");
|
|
63
|
-
const [selectedBar, setSelectedBar] = useState(defaultSelectedBar);
|
|
64
71
|
const loadingData = useLoadingBars(isLoading);
|
|
65
72
|
const arc = ARC[variant];
|
|
73
|
+
const valueSource = valueField(valueKey, children);
|
|
74
|
+
const [selectedBar, setSelection] = useChartSelection(
|
|
75
|
+
controlledBar,
|
|
76
|
+
defaultSelectedBar,
|
|
77
|
+
void 0
|
|
78
|
+
);
|
|
66
79
|
const selectBar = useCallback(
|
|
67
80
|
(name, value) => {
|
|
68
|
-
|
|
69
|
-
onSelectionChange
|
|
81
|
+
setSelection(name);
|
|
82
|
+
if (!onSelectionChange) return;
|
|
83
|
+
if (name === null) {
|
|
84
|
+
onSelectionChange(null);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const row = valueSource ? data.find((entry) => String(entry[nameKey]) === name) : void 0;
|
|
88
|
+
const resolved = value ?? (row && valueSource ? Number(row[valueSource]) : Number.NaN);
|
|
89
|
+
onSelectionChange({ name, value: Number.isFinite(resolved) ? resolved : 0 });
|
|
70
90
|
},
|
|
71
|
-
[onSelectionChange]
|
|
91
|
+
[data, nameKey, onSelectionChange, setSelection, valueSource]
|
|
72
92
|
);
|
|
73
93
|
const painted = useMemo(
|
|
74
94
|
() => data.map((row) => ({
|
|
@@ -88,7 +108,9 @@ function RadialChart({
|
|
|
88
108
|
showTitle,
|
|
89
109
|
description,
|
|
90
110
|
className,
|
|
91
|
-
table: hideDataTable || !
|
|
111
|
+
table: hideDataTable || !valueSource ? false : { rows: data, rowKey: nameKey, columns: [{ key: valueSource, label: "Value" }] },
|
|
112
|
+
isEmpty: !isLoading && data.length === 0,
|
|
113
|
+
empty,
|
|
92
114
|
children: [
|
|
93
115
|
/* @__PURE__ */ jsx(ChartContainer, { config, children: /* @__PURE__ */ jsxs(
|
|
94
116
|
RechartsRadialBarChart,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/charts/RadialChart/RadialChart.tsx"],"sourcesContent":["'use client'\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n type ComponentProps,\n type ReactNode,\n} from 'react'\nimport {\n PolarAngleAxis,\n RadialBar as RechartsRadialBar,\n RadialBarChart as RechartsRadialBarChart,\n Sector,\n} from 'recharts'\nimport type { SectorProps } from 'recharts'\nimport type { TypedDataKey } from 'recharts/types/util/typedDataKey'\nimport { useReducedMotion } from 'motion/react'\nimport { ChartContainer, colorStops, cssName, type ChartConfig } from '../lib/chart'\nimport { ChartFigure } from '../lib/figure'\nimport { ChartBackground, type ChartBackgroundVariant } from '../lib/background'\nimport { ChartLegend, ChartLegendContent } from '../lib/legend'\nimport { ChartTooltip, ChartTooltipContent } from '../lib/tooltip'\nimport { LoadingIndicator } from '../lib/loading'\nimport { ColorStops } from '../lib/paint'\nimport type { ChartLegendSlotProps, ChartTooltipSlotProps } from '../AreaChart/AreaChart'\n\nconst CORNER_RADIUS = 5\nconst BAR_SIZE = 14\nconst LOADING_BARS = 5\n\n/** One skeleton morph, in milliseconds. */\nconst LOADING_CYCLE = 1500\n\n/**\n * The arc the bars are drawn on.\n *\n * `full` is a ring; `semi` is the half-circle a gauge wants, with the centre\n * dropped so the arc sits in the middle of its own box rather than at the top.\n */\nexport type RadialVariant = 'full' | 'semi'\n\n/** The angles and centre each arc shape needs. */\nconst ARC: Record<RadialVariant, { startAngle: number; endAngle: number; cx: string; cy: string }> =\n {\n // Starting at 90° and running to -270° draws the ring clockwise from noon,\n // which is the direction a reader expects a total to fill in.\n full: { startAngle: 90, endAngle: -270, cx: '50%', cy: '50%' },\n semi: { startAngle: 180, endAngle: 0, cx: '50%', cy: '70%' },\n }\n\ninterface RadialChartContextValue {\n config: ChartConfig\n nameKey: string\n isLoading: boolean\n selectedBar: string | null\n selectBar: (name: string | null, value?: number) => void\n}\n\nconst RadialChartContext = createContext<RadialChartContextValue | null>(null)\n\nfunction useRadialChart(): RadialChartContextValue {\n const context = useContext(RadialChartContext)\n if (!context) throw new Error('A radial chart part must be rendered inside <RadialChart>')\n return context\n}\n\nexport interface RadialChartProps<TData extends Record<string, unknown>> {\n /** Bar names → their label and paint. Keys must match the `nameKey` values. */\n config: ChartConfig\n /** The rows the chart draws. One entry per point, bar or category. */\n data: TData[]\n /** The row field naming each bar. */\n nameKey: keyof TData & string\n /**\n * What the chart shows, in a sentence a reader could act on. Required, and\n * announced to a screen reader even when it is not printed.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n /**\n * The composed parts — axes, grid, tooltip, legend, and the marks\n * themselves.\n */\n children: ReactNode\n /** Merged onto the figure, last, so a call site can size or space it. */\n className?: string\n /** Escape hatch onto the raw Recharts chart element. */\n chartProps?: ComponentProps<typeof RechartsRadialBarChart>\n /** The arc shape — a full ring, or the half circle a gauge wants. */\n variant?: RadialVariant\n /**\n * What a full sweep is worth.\n *\n * Without it the scale comes from the data, so the largest bar always fills\n * the arc — which is right for a comparison and wrong for a gauge. Set it\n * (typically 100) whenever a single value has to read against a fixed total,\n * or \"62%\" and \"98%\" will look identical.\n */\n max?: number\n /** Where the arc starts, from the centre. */\n innerRadius?: number | string\n /** Where the arc ends. */\n outerRadius?: number | string\n /** The bar lit on first render. */\n defaultSelectedBar?: string | null\n /** Fires when the selection changes, and with null when it is cleared. */\n onSelectionChange?: (selection: { name: string; value: number } | null) => void\n /**\n * Swaps the marks for an animated skeleton, keeping the measured height so\n * the page does not jump when the data lands.\n */\n isLoading?: boolean\n /** The row field holding each bar's number. Used by the table view. */\n valueKey?: keyof TData & string\n /**\n * Drops the hidden table view. Only correct when the page prints the data\n * itself.\n */\n hideDataTable?: boolean\n}\n\n/**\n * Values on an arc — a gauge, or a small set of totals against one scale.\n *\n * The caveat worth knowing before reaching for it: a radial bar's LENGTH is its\n * value, but its RADIUS is not, so an inner bar and an outer bar of the same\n * value are drawn different lengths. That makes it a poor comparison and a good\n * single-value gauge; past about four bars, a `<BarChart>` is the honest choice.\n *\n * @example\n * <RadialChart title=\"Storage used\" config={config} data={data} nameKey=\"tier\" max={100}>\n * <RadialChart.RadialBar dataKey=\"used\" />\n * <RadialChart.Tooltip />\n * </RadialChart>\n */\nexport function RadialChart<TData extends Record<string, unknown>>({\n config,\n data,\n nameKey,\n title,\n showTitle,\n description,\n children,\n className,\n chartProps,\n variant = 'full',\n max,\n innerRadius = '30%',\n outerRadius = '100%',\n defaultSelectedBar = null,\n onSelectionChange,\n isLoading = false,\n valueKey,\n hideDataTable = false,\n}: RadialChartProps<TData>) {\n const chartId = useId().replace(/:/g, '')\n const [selectedBar, setSelectedBar] = useState<string | null>(defaultSelectedBar)\n const loadingData = useLoadingBars(isLoading)\n const arc = ARC[variant]\n\n const selectBar = useCallback(\n (name: string | null, value?: number) => {\n setSelectedBar(name)\n onSelectionChange?.(name === null ? null : { name, value: value ?? 0 })\n },\n [onSelectionChange],\n )\n\n const painted = useMemo(\n () =>\n data.map((row) => ({\n ...row,\n fill: `url(#${chartId}-colors-${cssName(String(row[nameKey]))})`,\n })),\n [chartId, data, nameKey],\n )\n\n const context = useMemo<RadialChartContextValue>(\n () => ({ config, nameKey, isLoading, selectedBar, selectBar }),\n [config, isLoading, nameKey, selectBar, selectedBar],\n )\n\n return (\n <RadialChartContext.Provider value={context}>\n <ChartFigure\n title={title}\n showTitle={showTitle}\n description={description}\n className={className}\n table={\n hideDataTable || !valueKey\n ? false\n : { rows: data, rowKey: nameKey, columns: [{ key: valueKey, label: 'Value' }] }\n }\n >\n <ChartContainer config={config}>\n <RechartsRadialBarChart\n id={chartId}\n data={isLoading ? loadingData : painted}\n innerRadius={innerRadius}\n outerRadius={outerRadius}\n startAngle={arc.startAngle}\n endAngle={arc.endAngle}\n cx={arc.cx}\n cy={arc.cy}\n {...chartProps}\n >\n {max != null && max > 0 && (\n <PolarAngleAxis type=\"number\" domain={[0, max]} tick={false} axisLine={false} />\n )}\n {children}\n {isLoading && <LoadingRadialBar />}\n <defs>\n {Object.entries(config).map(([key, series]) => (\n <linearGradient\n key={key}\n id={`${chartId}-colors-${cssName(key)}`}\n x1=\"0\"\n y1=\"0\"\n x2=\"1\"\n y2=\"1\"\n >\n <ColorStops dataKey={cssName(key)} stops={colorStops(series)} />\n </linearGradient>\n ))}\n </defs>\n </RechartsRadialBarChart>\n </ChartContainer>\n <LoadingIndicator isLoading={isLoading} />\n </ChartFigure>\n </RadialChartContext.Provider>\n )\n}\n\nexport interface RadialBarProps {\n /** The row field holding each bar's number. */\n dataKey: string\n cornerRadius?: number\n /** How thick each arc is, in pixels. */\n barSize?: number\n /** Draws the unfilled remainder behind each bar. Required reading for a gauge. */\n showTrack?: boolean\n isClickable?: boolean\n radialBarProps?: Omit<ComponentProps<typeof RechartsRadialBar>, 'dataKey'>\n}\n\n/** The arcs. One per row. */\nfunction RadialBar({\n dataKey,\n cornerRadius = CORNER_RADIUS,\n barSize = BAR_SIZE,\n showTrack = true,\n isClickable = false,\n radialBarProps,\n}: RadialBarProps) {\n const { nameKey, isLoading, selectedBar, selectBar } = useRadialChart()\n\n if (isLoading) return null\n\n return (\n <RechartsRadialBar\n dataKey={dataKey as TypedDataKey<Record<string, unknown>>}\n cornerRadius={cornerRadius}\n barSize={barSize}\n background={showTrack}\n style={isClickable ? { cursor: 'pointer' } : undefined}\n onClick={(payload, index) => {\n if (!isClickable) return\n const row = payload as Record<string, unknown>\n const name = (row?.[nameKey] as string | undefined) ?? String(index)\n selectBar(selectedBar === name ? null : name, Number(row?.[dataKey] ?? 0))\n }}\n shape={(props: SectorProps) => {\n const name = (props as unknown as Record<string, unknown>)[nameKey] as string\n const isSelected = selectedBar === null || selectedBar === name\n\n return (\n <Sector\n {...props}\n opacity={isClickable && !isSelected ? 0.15 : 1}\n className=\"transition-opacity duration-(--duration-base)\"\n />\n )\n }}\n {...radialBarProps}\n />\n )\n}\n\n/** The hover panel. No heading: the bar's own name is the row label. */\nfunction Tooltip({ variant, roundness, defaultIndex }: ChartTooltipSlotProps) {\n const { nameKey, isLoading } = useRadialChart()\n if (isLoading) return null\n\n return (\n <ChartTooltip\n defaultIndex={defaultIndex}\n cursor={false}\n content={\n <ChartTooltipContent nameKey={nameKey} hideLabel roundness={roundness} variant={variant} />\n }\n />\n )\n}\n\n/** The key. Under the arc and centred — nothing else names the bars. */\nfunction Legend({\n variant,\n align = 'center',\n verticalAlign = 'bottom',\n isClickable = false,\n}: ChartLegendSlotProps) {\n const { nameKey, isLoading, selectedBar, selectBar } = useRadialChart()\n if (isLoading) return null\n\n return (\n <ChartLegend\n verticalAlign={verticalAlign}\n align={align}\n content={\n <ChartLegendContent\n selected={selectedBar}\n onSelectChange={selectBar}\n isClickable={isClickable}\n nameKey={nameKey}\n variant={variant}\n />\n }\n />\n )\n}\n\n/** The decorative plate behind the arc. */\nfunction Background({ variant = 'dots' }: { variant?: ChartBackgroundVariant }) {\n const { isLoading } = useRadialChart()\n if (isLoading) return null\n return <ChartBackground variant={variant} />\n}\n\n/** A deterministic bar length, so the server and the client agree. */\nfunction sweep(index: number, cycle: number): number {\n const x = Math.sin(index * 53.9 + cycle * 173.1) * 13_759.3\n return 40 + (x - Math.floor(x)) * 60\n}\n\n/** The skeleton's arcs, re-rolled once per cycle. */\nfunction useLoadingBars(isLoading: boolean) {\n const [cycle, setCycle] = useState(0)\n\n useEffect(() => {\n if (!isLoading) return\n const timer = setInterval(() => setCycle((previous) => previous + 1), LOADING_CYCLE)\n return () => clearInterval(timer)\n }, [isLoading])\n\n return useMemo(\n () =>\n Array.from({ length: LOADING_BARS }, (_, index) => ({\n name: `loading-${index}`,\n value: sweep(index, cycle),\n })),\n [cycle],\n )\n}\n\n/** The skeleton arcs, morphing while the data is in flight. */\nfunction LoadingRadialBar() {\n const reduceMotion = useReducedMotion()\n\n return (\n <RechartsRadialBar\n dataKey=\"value\"\n cornerRadius={CORNER_RADIUS}\n barSize={BAR_SIZE}\n background\n className=\"text-(--ink)\"\n isAnimationActive={!reduceMotion}\n animationDuration={LOADING_CYCLE}\n animationEasing=\"ease-in-out\"\n shape={(props: SectorProps) => <Sector {...props} fill=\"currentColor\" fillOpacity={0.25} />}\n />\n )\n}\n\nRadialChart.RadialBar = RadialBar\nRadialChart.Tooltip = Tooltip\nRadialChart.Legend = Legend\nRadialChart.Background = Background\n\nexport default RadialChart\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/charts/RadialChart/RadialChart.tsx"],"sourcesContent":["'use client'\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useState,\n type ComponentProps,\n type ReactNode,\n} from 'react'\nimport {\n PolarAngleAxis,\n RadialBar as RechartsRadialBar,\n RadialBarChart as RechartsRadialBarChart,\n Sector,\n} from 'recharts'\nimport type { SectorProps } from 'recharts'\nimport type { TypedDataKey } from 'recharts/types/util/typedDataKey'\nimport { useReducedMotion } from 'motion/react'\nimport { ChartContainer, colorStops, cssName, type ChartConfig } from '../lib/chart'\nimport { findSlot } from '../lib/values'\nimport { ChartFigure } from '../lib/figure'\nimport { type ChartEmptyProps } from '../lib/empty'\nimport { useChartSelection } from '../lib/selection'\nimport { ChartBackground, type ChartBackgroundVariant } from '../lib/background'\nimport { ChartLegend, ChartLegendContent } from '../lib/legend'\nimport { ChartTooltip, ChartTooltipContent } from '../lib/tooltip'\nimport { LoadingIndicator } from '../lib/loading'\nimport { ColorStops } from '../lib/paint'\nimport type { ChartLegendSlotProps, ChartTooltipSlotProps } from '../AreaChart/AreaChart'\n\nconst CORNER_RADIUS = 5\nconst BAR_SIZE = 14\nconst LOADING_BARS = 5\n\n/** One skeleton morph, in milliseconds. */\nconst LOADING_CYCLE = 1500\n\n/**\n * The arc the bars are drawn on.\n *\n * `full` is a ring; `semi` is the half-circle a gauge wants, with the centre\n * dropped so the arc sits in the middle of its own box rather than at the top.\n */\nexport type RadialVariant = 'full' | 'semi'\n\n/** The angles and centre each arc shape needs. */\nconst ARC: Record<RadialVariant, { startAngle: number; endAngle: number; cx: string; cy: string }> =\n {\n // Starting at 90° and running to -270° draws the ring clockwise from noon,\n // which is the direction a reader expects a total to fill in.\n full: { startAngle: 90, endAngle: -270, cx: '50%', cy: '50%' },\n semi: { startAngle: 180, endAngle: 0, cx: '50%', cy: '70%' },\n }\n\ninterface RadialChartContextValue {\n config: ChartConfig\n nameKey: string\n isLoading: boolean\n selectedBar: string | null\n selectBar: (name: string | null, value?: number) => void\n}\n\n/**\n * The row field holding each bar's number, however the call site said it.\n *\n * `valueKey` is the explicit answer; the arc's own `dataKey` is the one every\n * call site already gives. Reading the slot means the legend reports the same\n * number the arc does, and the table gets built for a chart that named neither\n * — which used to render no table at all.\n */\nfunction valueField(valueKey: string | undefined, children: ReactNode): string | null {\n return valueKey ?? findSlot<RadialBarProps>(children, RadialBar)?.dataKey ?? null\n}\n\nconst RadialChartContext = createContext<RadialChartContextValue | null>(null)\n\nfunction useRadialChart(): RadialChartContextValue {\n const context = useContext(RadialChartContext)\n if (!context) throw new Error('A radial chart part must be rendered inside <RadialChart>')\n return context\n}\n\nexport interface RadialChartProps<TData extends Record<string, unknown>> {\n /** Bar names → their label and paint. Keys must match the `nameKey` values. */\n config: ChartConfig\n /** The rows the chart draws. One entry per point, bar or category. */\n data: TData[]\n /** The row field naming each bar. */\n nameKey: keyof TData & string\n /**\n * What the chart shows, in a sentence a reader could act on. Required, and\n * announced to a screen reader even when it is not printed.\n */\n title: string\n /** Prints the title above the plot instead of hiding it from sight. */\n showTitle?: boolean\n /** A line under the title — the unit, the window, the caveat. */\n description?: ReactNode\n /**\n * The composed parts — axes, grid, tooltip, legend, and the marks\n * themselves.\n */\n children: ReactNode\n /** Merged onto the figure, last, so a call site can size or space it. */\n className?: string\n /** Escape hatch onto the raw Recharts chart element. */\n chartProps?: ComponentProps<typeof RechartsRadialBarChart>\n /** The arc shape — a full ring, or the half circle a gauge wants. */\n variant?: RadialVariant\n /**\n * What a full sweep is worth.\n *\n * Without it the scale comes from the data, so the largest bar always fills\n * the arc — which is right for a comparison and wrong for a gauge. Set it\n * (typically 100) whenever a single value has to read against a fixed total,\n * or \"62%\" and \"98%\" will look identical.\n */\n max?: number\n /** Where the arc starts, from the centre. */\n innerRadius?: number | string\n /** Where the arc ends. */\n outerRadius?: number | string\n /** The bar lit on first render, when the chart keeps its own selection. */\n defaultSelectedBar?: string | null\n /**\n * The selected bar, driven from outside.\n *\n * Give this and the chart follows it; leave it undefined and the chart keeps\n * its own, starting from `defaultSelectedBar`.\n */\n selectedBar?: string | null\n /** Fires when the selection changes, and with null when it is cleared. */\n onSelectionChange?: (selection: { name: string; value: number } | null) => void\n /**\n * Swaps the marks for an animated skeleton, keeping the measured height so\n * the page does not jump when the data lands.\n */\n isLoading?: boolean\n /**\n * The row field holding each bar's number, for the table view and for the\n * selection the legend reports.\n *\n * Falls back to the `dataKey` of the composed `<RadialChart.RadialBar>`, so\n * the usual call site needs neither.\n */\n valueKey?: keyof TData & string\n /**\n * Drops the hidden table view. Only correct when the page prints the data\n * itself.\n */\n hideDataTable?: boolean\n /**\n * What the chart shows when it has nothing to draw. `false` keeps the empty\n * plot, for a chart whose emptiness is itself the reading.\n */\n empty?: ChartEmptyProps | false\n}\n\n/**\n * Values on an arc — a gauge, or a small set of totals against one scale.\n *\n * The caveat worth knowing before reaching for it: a radial bar's LENGTH is its\n * value, but its RADIUS is not, so an inner bar and an outer bar of the same\n * value are drawn different lengths. That makes it a poor comparison and a good\n * single-value gauge; past about four bars, a `<BarChart>` is the honest choice.\n *\n * @example\n * <RadialChart title=\"Storage used\" config={config} data={data} nameKey=\"tier\" max={100}>\n * <RadialChart.RadialBar dataKey=\"used\" />\n * <RadialChart.Tooltip />\n * </RadialChart>\n */\nexport function RadialChart<TData extends Record<string, unknown>>({\n config,\n data,\n nameKey,\n title,\n showTitle,\n description,\n children,\n className,\n chartProps,\n variant = 'full',\n max,\n innerRadius = '30%',\n outerRadius = '100%',\n defaultSelectedBar = null,\n selectedBar: controlledBar,\n onSelectionChange,\n isLoading = false,\n valueKey,\n hideDataTable = false,\n empty,\n}: RadialChartProps<TData>) {\n const chartId = useId().replace(/:/g, '')\n const loadingData = useLoadingBars(isLoading)\n const arc = ARC[variant]\n const valueSource = valueField(valueKey, children)\n\n const [selectedBar, setSelection] = useChartSelection(\n controlledBar,\n defaultSelectedBar,\n undefined,\n )\n\n const selectBar = useCallback(\n (name: string | null, value?: number) => {\n setSelection(name)\n if (!onSelectionChange) return\n if (name === null) {\n onSelectionChange(null)\n return\n }\n // The arc hands its own number in; the legend has only a name, and used\n // to be given a zero on its behalf — so one selection reported two\n // different values depending on which control the reader used. The row is\n // where the legend's number comes from now.\n const row = valueSource ? data.find((entry) => String(entry[nameKey]) === name) : undefined\n const resolved = value ?? (row && valueSource ? Number(row[valueSource]) : Number.NaN)\n onSelectionChange({ name, value: Number.isFinite(resolved) ? resolved : 0 })\n },\n [data, nameKey, onSelectionChange, setSelection, valueSource],\n )\n\n const painted = useMemo(\n () =>\n data.map((row) => ({\n ...row,\n fill: `url(#${chartId}-colors-${cssName(String(row[nameKey]))})`,\n })),\n [chartId, data, nameKey],\n )\n\n const context = useMemo<RadialChartContextValue>(\n () => ({ config, nameKey, isLoading, selectedBar, selectBar }),\n [config, isLoading, nameKey, selectBar, selectedBar],\n )\n\n return (\n <RadialChartContext.Provider value={context}>\n <ChartFigure\n title={title}\n showTitle={showTitle}\n description={description}\n className={className}\n table={\n hideDataTable || !valueSource\n ? false\n : { rows: data, rowKey: nameKey, columns: [{ key: valueSource, label: 'Value' }] }\n }\n isEmpty={!isLoading && data.length === 0}\n empty={empty}\n >\n <ChartContainer config={config}>\n <RechartsRadialBarChart\n id={chartId}\n data={isLoading ? loadingData : painted}\n innerRadius={innerRadius}\n outerRadius={outerRadius}\n startAngle={arc.startAngle}\n endAngle={arc.endAngle}\n cx={arc.cx}\n cy={arc.cy}\n {...chartProps}\n >\n {max != null && max > 0 && (\n <PolarAngleAxis type=\"number\" domain={[0, max]} tick={false} axisLine={false} />\n )}\n {children}\n {isLoading && <LoadingRadialBar />}\n <defs>\n {Object.entries(config).map(([key, series]) => (\n <linearGradient\n key={key}\n id={`${chartId}-colors-${cssName(key)}`}\n x1=\"0\"\n y1=\"0\"\n x2=\"1\"\n y2=\"1\"\n >\n <ColorStops dataKey={cssName(key)} stops={colorStops(series)} />\n </linearGradient>\n ))}\n </defs>\n </RechartsRadialBarChart>\n </ChartContainer>\n <LoadingIndicator isLoading={isLoading} />\n </ChartFigure>\n </RadialChartContext.Provider>\n )\n}\n\nexport interface RadialBarProps {\n /** The row field holding each bar's number. */\n dataKey: string\n cornerRadius?: number\n /** How thick each arc is, in pixels. */\n barSize?: number\n /** Draws the unfilled remainder behind each bar. Required reading for a gauge. */\n showTrack?: boolean\n isClickable?: boolean\n radialBarProps?: Omit<ComponentProps<typeof RechartsRadialBar>, 'dataKey'>\n}\n\n/** The arcs. One per row. */\nfunction RadialBar({\n dataKey,\n cornerRadius = CORNER_RADIUS,\n barSize = BAR_SIZE,\n showTrack = true,\n isClickable = false,\n radialBarProps,\n}: RadialBarProps) {\n const { nameKey, isLoading, selectedBar, selectBar } = useRadialChart()\n\n if (isLoading) return null\n\n return (\n <RechartsRadialBar\n dataKey={dataKey as TypedDataKey<Record<string, unknown>>}\n cornerRadius={cornerRadius}\n barSize={barSize}\n background={showTrack}\n style={isClickable ? { cursor: 'pointer' } : undefined}\n onClick={(payload, index) => {\n if (!isClickable) return\n const row = payload as Record<string, unknown>\n const name = (row?.[nameKey] as string | undefined) ?? String(index)\n selectBar(selectedBar === name ? null : name, Number(row?.[dataKey] ?? 0))\n }}\n shape={(props: SectorProps) => {\n const name = (props as unknown as Record<string, unknown>)[nameKey] as string\n const isSelected = selectedBar === null || selectedBar === name\n\n return (\n <Sector\n {...props}\n opacity={isClickable && !isSelected ? 0.15 : 1}\n className=\"transition-opacity duration-(--duration-base)\"\n />\n )\n }}\n {...radialBarProps}\n />\n )\n}\n\n/** The hover panel. No heading: the bar's own name is the row label. */\nfunction Tooltip({ variant, roundness, defaultIndex }: ChartTooltipSlotProps) {\n const { nameKey, isLoading } = useRadialChart()\n if (isLoading) return null\n\n return (\n <ChartTooltip\n defaultIndex={defaultIndex}\n cursor={false}\n content={\n <ChartTooltipContent nameKey={nameKey} hideLabel roundness={roundness} variant={variant} />\n }\n />\n )\n}\n\n/** The key. Under the arc and centred — nothing else names the bars. */\nfunction Legend({\n variant,\n align = 'center',\n verticalAlign = 'bottom',\n isClickable = false,\n}: ChartLegendSlotProps) {\n const { nameKey, isLoading, selectedBar, selectBar } = useRadialChart()\n if (isLoading) return null\n\n return (\n <ChartLegend\n verticalAlign={verticalAlign}\n align={align}\n content={\n <ChartLegendContent\n selected={selectedBar}\n onSelectChange={selectBar}\n isClickable={isClickable}\n nameKey={nameKey}\n variant={variant}\n />\n }\n />\n )\n}\n\n/** The decorative plate behind the arc. */\nfunction Background({ variant = 'dots' }: { variant?: ChartBackgroundVariant }) {\n const { isLoading } = useRadialChart()\n if (isLoading) return null\n return <ChartBackground variant={variant} />\n}\n\n/** A deterministic bar length, so the server and the client agree. */\nfunction sweep(index: number, cycle: number): number {\n const x = Math.sin(index * 53.9 + cycle * 173.1) * 13_759.3\n return 40 + (x - Math.floor(x)) * 60\n}\n\n/** The skeleton's arcs, re-rolled once per cycle. */\nfunction useLoadingBars(isLoading: boolean) {\n const [cycle, setCycle] = useState(0)\n\n useEffect(() => {\n if (!isLoading) return\n const timer = setInterval(() => setCycle((previous) => previous + 1), LOADING_CYCLE)\n return () => clearInterval(timer)\n }, [isLoading])\n\n return useMemo(\n () =>\n Array.from({ length: LOADING_BARS }, (_, index) => ({\n name: `loading-${index}`,\n value: sweep(index, cycle),\n })),\n [cycle],\n )\n}\n\n/** The skeleton arcs, morphing while the data is in flight. */\nfunction LoadingRadialBar() {\n const reduceMotion = useReducedMotion()\n\n return (\n <RechartsRadialBar\n dataKey=\"value\"\n cornerRadius={CORNER_RADIUS}\n barSize={BAR_SIZE}\n background\n className=\"text-(--ink)\"\n isAnimationActive={!reduceMotion}\n animationDuration={LOADING_CYCLE}\n animationEasing=\"ease-in-out\"\n shape={(props: SectorProps) => <Sector {...props} fill=\"currentColor\" fillOpacity={0.25} />}\n />\n )\n}\n\nRadialChart.RadialBar = RadialBar\nRadialChart.Tooltip = Tooltip\nRadialChart.Legend = Legend\nRadialChart.Background = Background\n\nexport default RadialChart\n"],"mappings":"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","names":[]}
|