@wallarm-org/design-system 0.31.1 → 0.32.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/dist/components/SimpleCharts/BarList/BarListItem.js +1 -5
- package/dist/components/SimpleCharts/PieChart/LegendDot.d.ts +5 -0
- package/dist/components/SimpleCharts/PieChart/LegendDot.js +18 -0
- package/dist/components/SimpleCharts/PieChart/PieChart.d.ts +35 -0
- package/dist/components/SimpleCharts/PieChart/PieChart.figma.d.ts +1 -0
- package/dist/components/SimpleCharts/PieChart/PieChart.figma.js +120 -0
- package/dist/components/SimpleCharts/PieChart/PieChart.js +117 -0
- package/dist/components/SimpleCharts/PieChart/PieChartCenter.d.ts +13 -0
- package/dist/components/SimpleCharts/PieChart/PieChartCenter.js +39 -0
- package/dist/components/SimpleCharts/PieChart/PieChartContext.d.ts +60 -0
- package/dist/components/SimpleCharts/PieChart/PieChartContext.js +11 -0
- package/dist/components/SimpleCharts/PieChart/PieChartDonut.d.ts +15 -0
- package/dist/components/SimpleCharts/PieChart/PieChartDonut.js +112 -0
- package/dist/components/SimpleCharts/PieChart/PieChartLegend.d.ts +5 -0
- package/dist/components/SimpleCharts/PieChart/PieChartLegend.js +16 -0
- package/dist/components/SimpleCharts/PieChart/PieChartLegendItem.d.ts +26 -0
- package/dist/components/SimpleCharts/PieChart/PieChartLegendItem.js +116 -0
- package/dist/components/SimpleCharts/PieChart/PieChartLegendPercent.d.ts +15 -0
- package/dist/components/SimpleCharts/PieChart/PieChartLegendPercent.js +34 -0
- package/dist/components/SimpleCharts/PieChart/PieChartLegendValue.d.ts +5 -0
- package/dist/components/SimpleCharts/PieChart/PieChartLegendValue.js +16 -0
- package/dist/components/SimpleCharts/PieChart/PieChartSkeleton.d.ts +8 -0
- package/dist/components/SimpleCharts/PieChart/PieChartSkeleton.js +49 -0
- package/dist/components/SimpleCharts/PieChart/classes.d.ts +22 -0
- package/dist/components/SimpleCharts/PieChart/classes.js +62 -0
- package/dist/components/SimpleCharts/PieChart/constants.d.ts +9 -0
- package/dist/components/SimpleCharts/PieChart/constants.js +22 -0
- package/dist/components/SimpleCharts/PieChart/index.d.ts +10 -0
- package/dist/components/SimpleCharts/PieChart/index.js +10 -0
- package/dist/components/SimpleCharts/index.d.ts +2 -0
- package/dist/components/SimpleCharts/index.js +2 -1
- package/dist/components/SimpleCharts/lib/clamp01.d.ts +1 -0
- package/dist/components/SimpleCharts/lib/clamp01.js +6 -0
- package/dist/metadata/components.json +3099 -2
- package/package.json +2 -1
|
@@ -2,13 +2,9 @@ import { jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useContext, useMemo } from "react";
|
|
3
3
|
import { cn } from "../../../utils/cn.js";
|
|
4
4
|
import { useTestId } from "../../../utils/testId.js";
|
|
5
|
+
import { clamp01 } from "../lib/clamp01.js";
|
|
5
6
|
import { BarListContext, BarListItemContext } from "./BarListContext.js";
|
|
6
7
|
import { barListItemVariants } from "./classes.js";
|
|
7
|
-
const clamp01 = (n)=>{
|
|
8
|
-
if (n < 0) return 0;
|
|
9
|
-
if (n > 1) return 1;
|
|
10
|
-
return n;
|
|
11
|
-
};
|
|
12
8
|
const BarListItem = ({ value, selected = false, className, children, ref, onClick, onKeyDown, ...props })=>{
|
|
13
9
|
const testId = useTestId('item');
|
|
14
10
|
const listCtx = useContext(BarListContext);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../../utils/cn.js";
|
|
3
|
+
import { useTestId } from "../../../utils/testId.js";
|
|
4
|
+
import { legendDotClasses } from "./classes.js";
|
|
5
|
+
const LegendDot = ({ className, children, ref, ...props })=>{
|
|
6
|
+
const testId = useTestId('legend-dot');
|
|
7
|
+
return /*#__PURE__*/ jsx("span", {
|
|
8
|
+
...props,
|
|
9
|
+
ref: ref,
|
|
10
|
+
"data-slot": "legend-dot",
|
|
11
|
+
"data-testid": testId,
|
|
12
|
+
"aria-hidden": "true",
|
|
13
|
+
className: cn(legendDotClasses, className),
|
|
14
|
+
children: children ?? '·'
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
LegendDot.displayName = 'LegendDot';
|
|
18
|
+
export { LegendDot };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type FC, type HTMLAttributes, type Ref } from 'react';
|
|
2
|
+
import { type TestableProps } from '../../../utils/testId';
|
|
3
|
+
import { type PieChartDatum } from './PieChartContext';
|
|
4
|
+
export interface PieChartProps extends HTMLAttributes<HTMLDivElement>, TestableProps {
|
|
5
|
+
ref?: Ref<HTMLDivElement>;
|
|
6
|
+
/**
|
|
7
|
+
* Each datum's `name` is the join key used by `PieChartLegendItem` to sync hover/active state.
|
|
8
|
+
* Non-finite or negative `value`s are coerced to 0 so recharts can draw them.
|
|
9
|
+
*/
|
|
10
|
+
data: PieChartDatum[];
|
|
11
|
+
/**
|
|
12
|
+
* Override for the value used to compute percentages and the centre total.
|
|
13
|
+
* Defaults to `sum(data.value)`. Pass an explicit value when the centre label
|
|
14
|
+
* should reflect a different denominator — e.g. the unfiltered total while
|
|
15
|
+
* the chart shows a filtered view.
|
|
16
|
+
*/
|
|
17
|
+
total?: number;
|
|
18
|
+
/**
|
|
19
|
+
* Controlled active slice. When provided, the component does not manage hover state
|
|
20
|
+
* internally — the parent must update it via `onActiveNameChange`.
|
|
21
|
+
*/
|
|
22
|
+
activeName?: string | null;
|
|
23
|
+
onActiveNameChange?: (name: string | null) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Multi-selection set. When non-empty, donut slices and legend rows whose `name`
|
|
26
|
+
* is not in the set fade so emphasis lands on the chosen group. Hover
|
|
27
|
+
* (`activeName`) wins — while the user points at a specific slice, only that one
|
|
28
|
+
* stays bright. Names not present in `data` are ignored.
|
|
29
|
+
*
|
|
30
|
+
* Pass a stable reference (`useMemo`/state) — an inline array literal recreates
|
|
31
|
+
* on every parent render and invalidates the internal Set memo.
|
|
32
|
+
*/
|
|
33
|
+
selectedNames?: string[];
|
|
34
|
+
}
|
|
35
|
+
export declare const PieChart: FC<PieChartProps>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import code_connect from "@figma/code-connect";
|
|
3
|
+
import { Settings } from "../../../icons/index.js";
|
|
4
|
+
import { Badge } from "../../Badge/index.js";
|
|
5
|
+
import { Button } from "../../Button/index.js";
|
|
6
|
+
import { Chart } from "../Chart/Chart.js";
|
|
7
|
+
import { ChartActions } from "../Chart/ChartActions.js";
|
|
8
|
+
import { ChartHeader } from "../Chart/ChartHeader.js";
|
|
9
|
+
import { ChartTitle } from "../Chart/ChartTitle.js";
|
|
10
|
+
import { LegendDot } from "./LegendDot.js";
|
|
11
|
+
import { PieChart } from "./PieChart.js";
|
|
12
|
+
import { PieChartCenter, PieChartCenterLabel, PieChartCenterValue } from "./PieChartCenter.js";
|
|
13
|
+
import { PieChartDonut } from "./PieChartDonut.js";
|
|
14
|
+
import { PieChartLegend } from "./PieChartLegend.js";
|
|
15
|
+
import { PieChartLegendItem } from "./PieChartLegendItem.js";
|
|
16
|
+
import { PieChartLegendPercent } from "./PieChartLegendPercent.js";
|
|
17
|
+
import { PieChartLegendValue } from "./PieChartLegendValue.js";
|
|
18
|
+
const figmaNodeUrl = 'https://www.figma.com/design/VKb5gW46uSGw0rqrhZsbXT/WADS-Components?node-id=7490-122167&m=dev';
|
|
19
|
+
const sampleData = [
|
|
20
|
+
{
|
|
21
|
+
name: '4XX',
|
|
22
|
+
value: 35,
|
|
23
|
+
color: 'amber',
|
|
24
|
+
badgeColor: 'amber'
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: '2XX',
|
|
28
|
+
value: 30,
|
|
29
|
+
color: 'green',
|
|
30
|
+
badgeColor: 'green'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: '5XX',
|
|
34
|
+
value: 15,
|
|
35
|
+
color: 'red',
|
|
36
|
+
badgeColor: 'red'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: '3XX',
|
|
40
|
+
value: 12,
|
|
41
|
+
color: 'blue',
|
|
42
|
+
badgeColor: 'blue'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: '1XX',
|
|
46
|
+
value: 8,
|
|
47
|
+
color: 'slate',
|
|
48
|
+
badgeColor: 'slate'
|
|
49
|
+
}
|
|
50
|
+
];
|
|
51
|
+
const sampleTotal = sampleData.reduce((sum, d)=>sum + d.value, 0);
|
|
52
|
+
code_connect.connect(PieChart, figmaNodeUrl, {
|
|
53
|
+
props: {
|
|
54
|
+
title: code_connect.string('Title'),
|
|
55
|
+
state: code_connect["enum"]('State', {
|
|
56
|
+
Default: 'default',
|
|
57
|
+
Hovered: 'hovered',
|
|
58
|
+
Filtered: 'filtered'
|
|
59
|
+
})
|
|
60
|
+
},
|
|
61
|
+
example: ({ title, state })=>/*#__PURE__*/ jsxs(Chart, {
|
|
62
|
+
children: [
|
|
63
|
+
/*#__PURE__*/ jsxs(ChartHeader, {
|
|
64
|
+
children: [
|
|
65
|
+
/*#__PURE__*/ jsx(ChartTitle, {
|
|
66
|
+
children: title
|
|
67
|
+
}),
|
|
68
|
+
/*#__PURE__*/ jsx(ChartActions, {
|
|
69
|
+
children: /*#__PURE__*/ jsx(Button, {
|
|
70
|
+
variant: "ghost",
|
|
71
|
+
color: "neutral",
|
|
72
|
+
size: "small",
|
|
73
|
+
"aria-label": "Settings",
|
|
74
|
+
children: /*#__PURE__*/ jsx(Settings, {})
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
]
|
|
78
|
+
}),
|
|
79
|
+
/*#__PURE__*/ jsxs(PieChart, {
|
|
80
|
+
data: sampleData,
|
|
81
|
+
total: sampleTotal,
|
|
82
|
+
children: [
|
|
83
|
+
/*#__PURE__*/ jsx(PieChartDonut, {
|
|
84
|
+
children: /*#__PURE__*/ jsxs(PieChartCenter, {
|
|
85
|
+
children: [
|
|
86
|
+
/*#__PURE__*/ jsx(PieChartCenterValue, {
|
|
87
|
+
children: sampleTotal
|
|
88
|
+
}),
|
|
89
|
+
/*#__PURE__*/ jsx(PieChartCenterLabel, {
|
|
90
|
+
children: "requests"
|
|
91
|
+
})
|
|
92
|
+
]
|
|
93
|
+
})
|
|
94
|
+
}),
|
|
95
|
+
/*#__PURE__*/ jsx(PieChartLegend, {
|
|
96
|
+
children: sampleData.map((d)=>/*#__PURE__*/ jsxs(PieChartLegendItem, {
|
|
97
|
+
name: d.name,
|
|
98
|
+
selected: 'filtered' === state && d.name === sampleData[0]?.name,
|
|
99
|
+
children: [
|
|
100
|
+
/*#__PURE__*/ jsx(Badge, {
|
|
101
|
+
color: d.badgeColor,
|
|
102
|
+
type: "secondary",
|
|
103
|
+
textVariant: "code",
|
|
104
|
+
children: d.name
|
|
105
|
+
}),
|
|
106
|
+
/*#__PURE__*/ jsxs(PieChartLegendValue, {
|
|
107
|
+
children: [
|
|
108
|
+
d.value,
|
|
109
|
+
/*#__PURE__*/ jsx(LegendDot, {}),
|
|
110
|
+
/*#__PURE__*/ jsx(PieChartLegendPercent, {})
|
|
111
|
+
]
|
|
112
|
+
})
|
|
113
|
+
]
|
|
114
|
+
}, d.name))
|
|
115
|
+
})
|
|
116
|
+
]
|
|
117
|
+
})
|
|
118
|
+
]
|
|
119
|
+
})
|
|
120
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
3
|
+
import { useControlled } from "../../../hooks/index.js";
|
|
4
|
+
import { cn } from "../../../utils/cn.js";
|
|
5
|
+
import { TestIdProvider } from "../../../utils/testId.js";
|
|
6
|
+
import { pieChartRootClasses } from "./classes.js";
|
|
7
|
+
import { EMPTY_SELECTION, PieChartActiveContext, PieChartDataContext, PieChartSelectionContext } from "./PieChartContext.js";
|
|
8
|
+
const sanitizeValue = (n)=>{
|
|
9
|
+
if ('number' != typeof n || !Number.isFinite(n) || n < 0) return 0;
|
|
10
|
+
return n;
|
|
11
|
+
};
|
|
12
|
+
const PieChart = ({ data, total, activeName: controlledActiveName, onActiveNameChange, selectedNames, className, children, ref, 'data-testid': testId, ...props })=>{
|
|
13
|
+
const sanitizedData = useMemo(()=>data.map((d)=>({
|
|
14
|
+
...d,
|
|
15
|
+
value: sanitizeValue(d.value)
|
|
16
|
+
})), [
|
|
17
|
+
data
|
|
18
|
+
]);
|
|
19
|
+
const { byName, computedTotal, hasDuplicateNames } = useMemo(()=>{
|
|
20
|
+
const map = new Map();
|
|
21
|
+
let sum = 0;
|
|
22
|
+
let duplicates = false;
|
|
23
|
+
for (const d of sanitizedData){
|
|
24
|
+
if (map.has(d.name)) duplicates = true;
|
|
25
|
+
map.set(d.name, d);
|
|
26
|
+
sum += d.value;
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
byName: map,
|
|
30
|
+
computedTotal: sum,
|
|
31
|
+
hasDuplicateNames: duplicates
|
|
32
|
+
};
|
|
33
|
+
}, [
|
|
34
|
+
sanitizedData
|
|
35
|
+
]);
|
|
36
|
+
useEffect(()=>{
|
|
37
|
+
if (hasDuplicateNames && 'production' !== process.env.NODE_ENV) console.warn("[PieChart] `data` contains duplicate `name` values. Names are used as the join key for percent lookup, hover sync, and React reconciliation — duplicates will produce incorrect percentages, ambiguous hover, and unstable rendering. Provide unique names.");
|
|
38
|
+
}, [
|
|
39
|
+
hasDuplicateNames
|
|
40
|
+
]);
|
|
41
|
+
const resolvedTotal = 'number' == typeof total && Number.isFinite(total) ? total : computedTotal;
|
|
42
|
+
const isValidTotal = resolvedTotal > 0;
|
|
43
|
+
const [activeNameValue, setInternalActiveName] = useControlled({
|
|
44
|
+
controlled: controlledActiveName,
|
|
45
|
+
default: null
|
|
46
|
+
});
|
|
47
|
+
const rawActiveName = activeNameValue ?? null;
|
|
48
|
+
const activeName = null !== rawActiveName && byName.has(rawActiveName) ? rawActiveName : null;
|
|
49
|
+
useEffect(()=>{
|
|
50
|
+
if (null != controlledActiveName && !byName.has(controlledActiveName)) onActiveNameChange?.(null);
|
|
51
|
+
}, [
|
|
52
|
+
controlledActiveName,
|
|
53
|
+
byName,
|
|
54
|
+
onActiveNameChange
|
|
55
|
+
]);
|
|
56
|
+
const setActive = useCallback((name)=>{
|
|
57
|
+
setInternalActiveName(name);
|
|
58
|
+
onActiveNameChange?.(name);
|
|
59
|
+
}, [
|
|
60
|
+
setInternalActiveName,
|
|
61
|
+
onActiveNameChange
|
|
62
|
+
]);
|
|
63
|
+
const selectedSet = useMemo(()=>{
|
|
64
|
+
if (!selectedNames?.length) return EMPTY_SELECTION;
|
|
65
|
+
const set = new Set();
|
|
66
|
+
for (const name of selectedNames)if (byName.has(name)) set.add(name);
|
|
67
|
+
return set.size > 0 ? set : EMPTY_SELECTION;
|
|
68
|
+
}, [
|
|
69
|
+
selectedNames,
|
|
70
|
+
byName
|
|
71
|
+
]);
|
|
72
|
+
const dataValue = useMemo(()=>({
|
|
73
|
+
data: sanitizedData,
|
|
74
|
+
byName,
|
|
75
|
+
total: resolvedTotal,
|
|
76
|
+
isValidTotal,
|
|
77
|
+
setActive
|
|
78
|
+
}), [
|
|
79
|
+
sanitizedData,
|
|
80
|
+
byName,
|
|
81
|
+
resolvedTotal,
|
|
82
|
+
isValidTotal,
|
|
83
|
+
setActive
|
|
84
|
+
]);
|
|
85
|
+
const activeValue = useMemo(()=>({
|
|
86
|
+
activeName
|
|
87
|
+
}), [
|
|
88
|
+
activeName
|
|
89
|
+
]);
|
|
90
|
+
const selectionValue = useMemo(()=>({
|
|
91
|
+
selectedSet
|
|
92
|
+
}), [
|
|
93
|
+
selectedSet
|
|
94
|
+
]);
|
|
95
|
+
return /*#__PURE__*/ jsx(PieChartDataContext.Provider, {
|
|
96
|
+
value: dataValue,
|
|
97
|
+
children: /*#__PURE__*/ jsx(PieChartActiveContext.Provider, {
|
|
98
|
+
value: activeValue,
|
|
99
|
+
children: /*#__PURE__*/ jsx(PieChartSelectionContext.Provider, {
|
|
100
|
+
value: selectionValue,
|
|
101
|
+
children: /*#__PURE__*/ jsx(TestIdProvider, {
|
|
102
|
+
value: testId,
|
|
103
|
+
children: /*#__PURE__*/ jsx("div", {
|
|
104
|
+
...props,
|
|
105
|
+
ref: ref,
|
|
106
|
+
"data-slot": "pie-chart",
|
|
107
|
+
"data-testid": testId,
|
|
108
|
+
className: cn(pieChartRootClasses, className),
|
|
109
|
+
children: children
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
PieChart.displayName = 'PieChart';
|
|
117
|
+
export { PieChart };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FC, HTMLAttributes, Ref } from 'react';
|
|
2
|
+
export interface PieChartCenterProps extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
ref?: Ref<HTMLDivElement>;
|
|
4
|
+
}
|
|
5
|
+
export declare const PieChartCenter: FC<PieChartCenterProps>;
|
|
6
|
+
export interface PieChartCenterValueProps extends HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
ref?: Ref<HTMLSpanElement>;
|
|
8
|
+
}
|
|
9
|
+
export declare const PieChartCenterValue: FC<PieChartCenterValueProps>;
|
|
10
|
+
export interface PieChartCenterLabelProps extends HTMLAttributes<HTMLSpanElement> {
|
|
11
|
+
ref?: Ref<HTMLSpanElement>;
|
|
12
|
+
}
|
|
13
|
+
export declare const PieChartCenterLabel: FC<PieChartCenterLabelProps>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../../utils/cn.js";
|
|
3
|
+
import { useTestId } from "../../../utils/testId.js";
|
|
4
|
+
import { pieChartCenterClasses, pieChartCenterLabelClasses, pieChartCenterValueClasses } from "./classes.js";
|
|
5
|
+
const PieChartCenter = ({ className, ref, ...props })=>{
|
|
6
|
+
const testId = useTestId('center');
|
|
7
|
+
return /*#__PURE__*/ jsx("div", {
|
|
8
|
+
...props,
|
|
9
|
+
ref: ref,
|
|
10
|
+
"data-slot": "pie-chart-center",
|
|
11
|
+
"data-testid": testId,
|
|
12
|
+
"aria-hidden": "true",
|
|
13
|
+
className: cn(pieChartCenterClasses, className)
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
PieChartCenter.displayName = 'PieChartCenter';
|
|
17
|
+
const PieChartCenterValue = ({ className, ref, ...props })=>{
|
|
18
|
+
const testId = useTestId('center-value');
|
|
19
|
+
return /*#__PURE__*/ jsx("span", {
|
|
20
|
+
...props,
|
|
21
|
+
ref: ref,
|
|
22
|
+
"data-slot": "pie-chart-center-value",
|
|
23
|
+
"data-testid": testId,
|
|
24
|
+
className: cn(pieChartCenterValueClasses, className)
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
PieChartCenterValue.displayName = 'PieChartCenterValue';
|
|
28
|
+
const PieChartCenterLabel = ({ className, ref, ...props })=>{
|
|
29
|
+
const testId = useTestId('center-label');
|
|
30
|
+
return /*#__PURE__*/ jsx("span", {
|
|
31
|
+
...props,
|
|
32
|
+
ref: ref,
|
|
33
|
+
"data-slot": "pie-chart-center-label",
|
|
34
|
+
"data-testid": testId,
|
|
35
|
+
className: cn(pieChartCenterLabelClasses, className)
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
PieChartCenterLabel.displayName = 'PieChartCenterLabel';
|
|
39
|
+
export { PieChartCenter, PieChartCenterLabel, PieChartCenterValue };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ChartColor } from '../types';
|
|
2
|
+
export interface PieChartDatum {
|
|
3
|
+
/**
|
|
4
|
+
* Stable identity string — **never rendered by the component**. Used as the join key
|
|
5
|
+
* for percent lookup, bidirectional hover sync between slice and legend row, recharts'
|
|
6
|
+
* sector reconciliation, and the `data-name` E2E hook on slice + row DOM.
|
|
7
|
+
*
|
|
8
|
+
* Must be unique within `data` — duplicates trigger a dev-only warning and produce
|
|
9
|
+
* incorrect percentages, ambiguous hover, and unstable rendering. The visible label
|
|
10
|
+
* comes from caller JSX inside `<PieChartLegendItem>` (e.g. `<Badge>{row.name}</Badge>`),
|
|
11
|
+
* so this string can be a slug, ID, or any opaque key when display ≠ identity.
|
|
12
|
+
*/
|
|
13
|
+
name: string;
|
|
14
|
+
/**
|
|
15
|
+
* Slice magnitude. Non-finite or negative values are coerced to `0` before reaching
|
|
16
|
+
* recharts (which rejects negatives). Percent = `value / total`.
|
|
17
|
+
*/
|
|
18
|
+
value: number;
|
|
19
|
+
/**
|
|
20
|
+
* Built-in palette token. Resolves to a slice-fill CSS variable via `PIE_SLICE_FILL`
|
|
21
|
+
* in `constants.ts` — most colours map to `--color-{color}-500`, with documented
|
|
22
|
+
* exceptions for `'brand'` and `'slate'`. Ignored when `className` is set.
|
|
23
|
+
*/
|
|
24
|
+
color?: ChartColor;
|
|
25
|
+
/**
|
|
26
|
+
* Tailwind `fill-*` utility applied directly to the slice `<path>`. Wins over `color`.
|
|
27
|
+
* `bg-*` does NOT work — slices are SVG paths, not divs.
|
|
28
|
+
*/
|
|
29
|
+
className?: string;
|
|
30
|
+
}
|
|
31
|
+
/** Static chart shape — recomputes only when data/total changes. */
|
|
32
|
+
export interface PieChartDataContextValue {
|
|
33
|
+
data: PieChartDatum[];
|
|
34
|
+
byName: Map<string, PieChartDatum>;
|
|
35
|
+
total: number;
|
|
36
|
+
isValidTotal: boolean;
|
|
37
|
+
setActive: (name: string | null) => void;
|
|
38
|
+
}
|
|
39
|
+
/** Volatile hover state — recomputes on every hover, kept separate so static
|
|
40
|
+
* consumers (donut layout, legend rows that don't care about active) skip
|
|
41
|
+
* re-rendering when only the hover target changes. */
|
|
42
|
+
export interface PieChartActiveContextValue {
|
|
43
|
+
activeName: string | null;
|
|
44
|
+
}
|
|
45
|
+
/** Multi-selection set pushed by the root. Empty when no selection is active. */
|
|
46
|
+
export interface PieChartSelectionContextValue {
|
|
47
|
+
selectedSet: Set<string>;
|
|
48
|
+
}
|
|
49
|
+
export interface PieChartItemContextValue {
|
|
50
|
+
ratio: number;
|
|
51
|
+
selected: boolean;
|
|
52
|
+
interactive: boolean;
|
|
53
|
+
name: string;
|
|
54
|
+
active: boolean;
|
|
55
|
+
}
|
|
56
|
+
export declare const EMPTY_SELECTION: Set<string>;
|
|
57
|
+
export declare const PieChartDataContext: import("react").Context<PieChartDataContextValue | null>;
|
|
58
|
+
export declare const PieChartActiveContext: import("react").Context<PieChartActiveContextValue>;
|
|
59
|
+
export declare const PieChartSelectionContext: import("react").Context<PieChartSelectionContextValue>;
|
|
60
|
+
export declare const PieChartItemContext: import("react").Context<PieChartItemContextValue | null>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createContext } from "react";
|
|
2
|
+
const EMPTY_SELECTION = new Set();
|
|
3
|
+
const PieChartDataContext = createContext(null);
|
|
4
|
+
const PieChartActiveContext = createContext({
|
|
5
|
+
activeName: null
|
|
6
|
+
});
|
|
7
|
+
const PieChartSelectionContext = createContext({
|
|
8
|
+
selectedSet: EMPTY_SELECTION
|
|
9
|
+
});
|
|
10
|
+
const PieChartItemContext = createContext(null);
|
|
11
|
+
export { EMPTY_SELECTION, PieChartActiveContext, PieChartDataContext, PieChartItemContext, PieChartSelectionContext };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type FC, type HTMLAttributes, type Ref } from 'react';
|
|
2
|
+
export interface PieChartDonutProps extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
ref?: Ref<HTMLDivElement>;
|
|
4
|
+
/**
|
|
5
|
+
* Opacity applied to slices that are not currently hovered. Defaults to `0.3`.
|
|
6
|
+
* Set to `1` to disable the hover-dim effect.
|
|
7
|
+
*/
|
|
8
|
+
inactiveOpacity?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Disable the recharts mount/transition animation. Defaults to `false`.
|
|
11
|
+
* Set `true` in test/screenshot environments where deterministic frames matter.
|
|
12
|
+
*/
|
|
13
|
+
disableAnimation?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare const PieChartDonut: FC<PieChartDonutProps>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useContext } from "react";
|
|
3
|
+
import { Pie, PieChart, Sector } from "recharts";
|
|
4
|
+
import { cn } from "../../../utils/cn.js";
|
|
5
|
+
import { useTestId } from "../../../utils/testId.js";
|
|
6
|
+
import { pieChartDonutClasses } from "./classes.js";
|
|
7
|
+
import { PIE_DONUT_ANIMATION_BEGIN, PIE_DONUT_ANIMATION_DURATION, PIE_DONUT_CORNER_RADIUS, PIE_DONUT_INNER_RADIUS, PIE_DONUT_OUTER_RADIUS, PIE_DONUT_PADDING_ANGLE, PIE_DONUT_SIZE, PIE_SLICE_FILL } from "./constants.js";
|
|
8
|
+
import { PieChartActiveContext, PieChartDataContext, PieChartSelectionContext } from "./PieChartContext.js";
|
|
9
|
+
const PLACEHOLDER_DATA = [
|
|
10
|
+
{
|
|
11
|
+
name: '',
|
|
12
|
+
value: 1
|
|
13
|
+
}
|
|
14
|
+
];
|
|
15
|
+
const PieChartDonut = ({ inactiveOpacity = 0.3, disableAnimation = false, className, children, ref, ...props })=>{
|
|
16
|
+
const testId = useTestId('donut');
|
|
17
|
+
const dataCtx = useContext(PieChartDataContext);
|
|
18
|
+
const { activeName } = useContext(PieChartActiveContext);
|
|
19
|
+
const { selectedSet } = useContext(PieChartSelectionContext);
|
|
20
|
+
const handleEnter = useCallback((_payload, index)=>{
|
|
21
|
+
if (!dataCtx?.isValidTotal) return;
|
|
22
|
+
const datum = dataCtx.data[index];
|
|
23
|
+
if (datum) dataCtx.setActive(datum.name);
|
|
24
|
+
}, [
|
|
25
|
+
dataCtx
|
|
26
|
+
]);
|
|
27
|
+
const handleLeave = useCallback(()=>{
|
|
28
|
+
dataCtx?.setActive(null);
|
|
29
|
+
}, [
|
|
30
|
+
dataCtx
|
|
31
|
+
]);
|
|
32
|
+
const isValidTotal = !!dataCtx?.isValidTotal;
|
|
33
|
+
const isMultiSlice = isValidTotal && dataCtx.data.length > 1;
|
|
34
|
+
const renderSlice = useCallback((sectorProps)=>{
|
|
35
|
+
const { isActive: _isActive, index: _index, payload, ...sectorRest } = sectorProps;
|
|
36
|
+
if (!isValidTotal) return /*#__PURE__*/ jsx(Sector, {
|
|
37
|
+
...sectorRest,
|
|
38
|
+
fill: "var(--color-border-primary-light)",
|
|
39
|
+
opacity: 1,
|
|
40
|
+
stroke: "none"
|
|
41
|
+
});
|
|
42
|
+
const datum = payload;
|
|
43
|
+
const name = datum?.name ?? '';
|
|
44
|
+
const fill = datum?.className ? void 0 : PIE_SLICE_FILL[datum?.color ?? 'slate'];
|
|
45
|
+
const isActive = activeName === name;
|
|
46
|
+
const isSelected = selectedSet.has(name);
|
|
47
|
+
let opacity = 1;
|
|
48
|
+
if (null !== activeName) opacity = isActive ? 1 : inactiveOpacity;
|
|
49
|
+
else if (selectedSet.size > 0) opacity = isSelected ? 1 : inactiveOpacity;
|
|
50
|
+
return /*#__PURE__*/ jsx(Sector, {
|
|
51
|
+
...sectorRest,
|
|
52
|
+
fill: fill,
|
|
53
|
+
opacity: opacity,
|
|
54
|
+
stroke: "none",
|
|
55
|
+
className: cn('outline-none transition-opacity duration-150 ease-out', datum?.className),
|
|
56
|
+
"data-slot": "pie-chart-slice",
|
|
57
|
+
"data-name": name,
|
|
58
|
+
"data-active": isActive ? 'true' : void 0
|
|
59
|
+
});
|
|
60
|
+
}, [
|
|
61
|
+
isValidTotal,
|
|
62
|
+
activeName,
|
|
63
|
+
inactiveOpacity,
|
|
64
|
+
selectedSet
|
|
65
|
+
]);
|
|
66
|
+
const pieData = isValidTotal ? dataCtx.data : PLACEHOLDER_DATA;
|
|
67
|
+
return /*#__PURE__*/ jsxs("div", {
|
|
68
|
+
...props,
|
|
69
|
+
ref: ref,
|
|
70
|
+
"data-slot": "pie-chart-donut",
|
|
71
|
+
"data-testid": testId,
|
|
72
|
+
"aria-hidden": "true",
|
|
73
|
+
className: cn(pieChartDonutClasses, className),
|
|
74
|
+
children: [
|
|
75
|
+
/*#__PURE__*/ jsx(PieChart, {
|
|
76
|
+
width: PIE_DONUT_SIZE,
|
|
77
|
+
height: PIE_DONUT_SIZE,
|
|
78
|
+
margin: {
|
|
79
|
+
top: 0,
|
|
80
|
+
right: 0,
|
|
81
|
+
bottom: 0,
|
|
82
|
+
left: 0
|
|
83
|
+
},
|
|
84
|
+
accessibilityLayer: false,
|
|
85
|
+
children: /*#__PURE__*/ jsx(Pie, {
|
|
86
|
+
data: pieData,
|
|
87
|
+
dataKey: "value",
|
|
88
|
+
nameKey: "name",
|
|
89
|
+
cx: "50%",
|
|
90
|
+
cy: "50%",
|
|
91
|
+
innerRadius: PIE_DONUT_INNER_RADIUS,
|
|
92
|
+
outerRadius: PIE_DONUT_OUTER_RADIUS,
|
|
93
|
+
startAngle: 90,
|
|
94
|
+
endAngle: -270,
|
|
95
|
+
cornerRadius: isMultiSlice ? PIE_DONUT_CORNER_RADIUS : 0,
|
|
96
|
+
paddingAngle: isMultiSlice ? PIE_DONUT_PADDING_ANGLE : 0,
|
|
97
|
+
stroke: "none",
|
|
98
|
+
isAnimationActive: disableAnimation ? false : 'auto',
|
|
99
|
+
animationBegin: PIE_DONUT_ANIMATION_BEGIN,
|
|
100
|
+
animationDuration: PIE_DONUT_ANIMATION_DURATION,
|
|
101
|
+
animationEasing: "ease-out",
|
|
102
|
+
onMouseEnter: handleEnter,
|
|
103
|
+
onMouseLeave: handleLeave,
|
|
104
|
+
shape: renderSlice
|
|
105
|
+
})
|
|
106
|
+
}),
|
|
107
|
+
children
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
PieChartDonut.displayName = 'PieChartDonut';
|
|
112
|
+
export { PieChartDonut };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../../../utils/cn.js";
|
|
3
|
+
import { useTestId } from "../../../utils/testId.js";
|
|
4
|
+
import { pieChartLegendClasses } from "./classes.js";
|
|
5
|
+
const PieChartLegend = ({ className, ref, ...props })=>{
|
|
6
|
+
const testId = useTestId('legend');
|
|
7
|
+
return /*#__PURE__*/ jsx("div", {
|
|
8
|
+
...props,
|
|
9
|
+
ref: ref,
|
|
10
|
+
"data-slot": "pie-chart-legend",
|
|
11
|
+
"data-testid": testId,
|
|
12
|
+
className: cn(pieChartLegendClasses, className)
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
PieChartLegend.displayName = 'PieChartLegend';
|
|
16
|
+
export { PieChartLegend };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type FC, type HTMLAttributes, type Ref } from 'react';
|
|
2
|
+
export interface PieChartLegendItemProps extends HTMLAttributes<HTMLDivElement> {
|
|
3
|
+
ref?: Ref<HTMLDivElement>;
|
|
4
|
+
/**
|
|
5
|
+
* Identifier matching a `PieChartDatum.name` from the root `data` prop.
|
|
6
|
+
* Drives both the percent calculation and the bidirectional hover sync with the donut slice.
|
|
7
|
+
*/
|
|
8
|
+
name: string;
|
|
9
|
+
/**
|
|
10
|
+
* Optional override for the value used in the percent calculation. Defaults to looking
|
|
11
|
+
* the value up from the root `data` array by `name`. Pass an explicit value when the
|
|
12
|
+
* legend renders entries that are not part of the donut (e.g. a synthetic "Other" row).
|
|
13
|
+
*/
|
|
14
|
+
value?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Visually marks the row as the selected/filtered datum — applies `bg-states-primary-active`
|
|
17
|
+
* and `aria-current`. When omitted, the row falls back to membership in the root's
|
|
18
|
+
* `selectedNames` set (if provided). When passed explicitly (`true` or `false`) it
|
|
19
|
+
* fully overrides the context — useful when the legend renders rows that aren't part
|
|
20
|
+
* of the chart's multi-selection (e.g. a single filtered view). The resolved value
|
|
21
|
+
* also feeds the dim calculation: a row resolved to `selected=false` while *some*
|
|
22
|
+
* peer is selected will fade like any other non-selected row.
|
|
23
|
+
*/
|
|
24
|
+
selected?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare const PieChartLegendItem: FC<PieChartLegendItemProps>;
|