@spteck/fluentui-react-charts 0.1.8
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/LICENSE +21 -0
- package/README.md +462 -0
- package/dist/charts/BarChart/BarChart.d.ts +16 -0
- package/dist/charts/BarChart/index.d.ts +1 -0
- package/dist/charts/ComboChart/ComboChart.d.ts +16 -0
- package/dist/charts/ComboChart/index.d.ts +1 -0
- package/dist/charts/Doughnut/DoughnutChart.d.ts +14 -0
- package/dist/charts/Doughnut/index.d.ts +1 -0
- package/dist/charts/PieChart/PieChart.d.ts +14 -0
- package/dist/charts/PieChart/index.d.ts +1 -0
- package/dist/charts/areaChart/AreaChart.d.ts +15 -0
- package/dist/charts/areaChart/index.d.ts +1 -0
- package/dist/charts/barHorizontalChart/BarHotizontalChart.d.ts +15 -0
- package/dist/charts/barHorizontalChart/index.d.ts +1 -0
- package/dist/charts/bubbleChart/BubbleChart.d.ts +15 -0
- package/dist/charts/bubbleChart/index.d.ts +1 -0
- package/dist/charts/floatBarChart/FloatBarChart.d.ts +14 -0
- package/dist/charts/floatBarChart/index.d.ts +1 -0
- package/dist/charts/lineChart/LineChart.d.ts +14 -0
- package/dist/charts/lineChart/index.d.ts +1 -0
- package/dist/charts/polarChart/PolarChart.d.ts +14 -0
- package/dist/charts/polarChart/index.d.ts +1 -0
- package/dist/charts/radarChart/RadarChart.d.ts +14 -0
- package/dist/charts/radarChart/index.d.ts +1 -0
- package/dist/charts/scatterChart/ScatterChart.d.ts +14 -0
- package/dist/charts/scatterChart/index.d.ts +1 -0
- package/dist/charts/stackedLineChart/StackedLineChart.d.ts +14 -0
- package/dist/charts/stackedLineChart/index.d.ts +1 -0
- package/dist/charts/steamChart/SteamChart.d.ts +14 -0
- package/dist/charts/steamChart/index.d.ts +1 -0
- package/dist/components/DashBoard.d.ts +3 -0
- package/dist/components/RenderLegend/RenderLegend.d.ts +11 -0
- package/dist/components/RenderTooltip/RenderTooltip.d.ts +14 -0
- package/dist/components/buttonMenu/ButtonMenu.d.ts +3 -0
- package/dist/components/buttonMenu/IButtonMenuOption.d.ts +10 -0
- package/dist/components/buttonMenu/IButtonMenuProps.d.ts +37 -0
- package/dist/components/index.d.ts +15 -0
- package/dist/components/legendContainer/LegendContainer.d.ts +16 -0
- package/dist/components/legendeButton/LegendButton.d.ts +11 -0
- package/dist/components/renderSliceLegend/RenderSliceLegend.d.ts +9 -0
- package/dist/components/renderValueLegend/RenderValueLegend.d.ts +13 -0
- package/dist/components/stack/IStackProps.d.ts +76 -0
- package/dist/components/stack/Stack.d.ts +8 -0
- package/dist/components/themeProvider/ThemeProvider.d.ts +15 -0
- package/dist/constants/Constants.d.ts +1 -0
- package/dist/fluentui-react-charts.cjs.development.js +2916 -0
- package/dist/fluentui-react-charts.cjs.development.js.map +1 -0
- package/dist/fluentui-react-charts.cjs.production.min.js +2 -0
- package/dist/fluentui-react-charts.cjs.production.min.js.map +1 -0
- package/dist/fluentui-react-charts.esm.js +2905 -0
- package/dist/fluentui-react-charts.esm.js.map +1 -0
- package/dist/graphGlobalStyles/useGraphGlobalStyles.d.ts +5 -0
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/useGraphUtils.d.ts +38 -0
- package/dist/hooks/useResponsiveLegend.d.ts +8 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -0
- package/dist/models/IChart.d.ts +25 -0
- package/dist/models/index.d.ts +1 -0
- package/package.json +66 -0
- package/src/assets/sample1.png +0 -0
- package/src/assets/sample2.png +0 -0
- package/src/assets/sample3.png +0 -0
- package/src/charts/BarChart/BarChart.tsx +227 -0
- package/src/charts/BarChart/README.MD +335 -0
- package/src/charts/BarChart/index.ts +1 -0
- package/src/charts/ComboChart/ComboChart.tsx +209 -0
- package/src/charts/ComboChart/README.MD +347 -0
- package/src/charts/ComboChart/index.ts +1 -0
- package/src/charts/Doughnut/DoughnutChart.tsx +152 -0
- package/src/charts/Doughnut/README.MD +296 -0
- package/src/charts/Doughnut/index.ts +1 -0
- package/src/charts/PieChart/PieChart.tsx +148 -0
- package/src/charts/PieChart/README.MD +315 -0
- package/src/charts/PieChart/index.ts +1 -0
- package/src/charts/areaChart/AreaChart.tsx +195 -0
- package/src/charts/areaChart/README.MD +236 -0
- package/src/charts/areaChart/index.ts +1 -0
- package/src/charts/barHorizontalChart/BarHotizontalChart.tsx +200 -0
- package/src/charts/barHorizontalChart/README.MD +278 -0
- package/src/charts/barHorizontalChart/index.ts +2 -0
- package/src/charts/bubbleChart/BubbleChart.tsx +184 -0
- package/src/charts/bubbleChart/README.MD +275 -0
- package/src/charts/bubbleChart/index.ts +1 -0
- package/src/charts/floatBarChart/FloatBarChart.tsx +178 -0
- package/src/charts/floatBarChart/README.MD +354 -0
- package/src/charts/floatBarChart/index.ts +1 -0
- package/src/charts/lineChart/LineChart.tsx +200 -0
- package/src/charts/lineChart/README.MD +354 -0
- package/src/charts/lineChart/index.ts +1 -0
- package/src/charts/polarChart/PolarChart.tsx +161 -0
- package/src/charts/polarChart/README.MD +336 -0
- package/src/charts/polarChart/index.ts +1 -0
- package/src/charts/radarChart/README.MD +388 -0
- package/src/charts/radarChart/RadarChart.tsx +173 -0
- package/src/charts/radarChart/index.ts +1 -0
- package/src/charts/scatterChart/README.MD +335 -0
- package/src/charts/scatterChart/ScatterChart.tsx +155 -0
- package/src/charts/scatterChart/index.ts +1 -0
- package/src/charts/stackedLineChart/README.MD +396 -0
- package/src/charts/stackedLineChart/StackedLineChart.tsx +188 -0
- package/src/charts/stackedLineChart/index.ts +1 -0
- package/src/charts/steamChart/README.MD +414 -0
- package/src/charts/steamChart/SteamChart.tsx +236 -0
- package/src/charts/steamChart/index.ts +1 -0
- package/src/components/DashBoard.tsx +409 -0
- package/src/components/RenderLegend/RenderLegend.tsx +40 -0
- package/src/components/RenderTooltip/RenderTooltip.tsx +111 -0
- package/src/components/buttonMenu/ButtonMenu.tsx +186 -0
- package/src/components/buttonMenu/IButtonMenuOption.ts +9 -0
- package/src/components/buttonMenu/IButtonMenuProps.tsx +40 -0
- package/src/components/index.ts +15 -0
- package/src/components/legendContainer/LegendContainer.tsx +118 -0
- package/src/components/legendeButton/LegendButton.tsx +57 -0
- package/src/components/renderSliceLegend/RenderSliceLegend.tsx +46 -0
- package/src/components/renderValueLegend/RenderValueLegend.tsx +43 -0
- package/src/components/stack/IStackProps.tsx +94 -0
- package/src/components/stack/Stack.tsx +103 -0
- package/src/components/themeProvider/ThemeProvider.tsx +48 -0
- package/src/constants/Constants.tsx +23 -0
- package/src/graphGlobalStyles/useGraphGlobalStyles.ts +28 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useGraphUtils.tsx +314 -0
- package/src/hooks/useResponsiveLegend.ts +35 -0
- package/src/index.tsx +4 -0
- package/src/models/IChart.ts +50 -0
- package/src/models/index.ts +1 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# DoughnutChart Component
|
|
2
|
+
|
|
3
|
+
A customizable doughnut chart component built with Chart.js and Fluent UI React. This component displays data as a circular chart with a hollow center, making it perfect for showing proportions, percentages, and part-to-whole relationships with an elegant visual presentation.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Circular Data Visualization**: Display proportional data in an intuitive circular format
|
|
8
|
+
- **Value Aggregation**: Automatically aggregates values across multiple series for the same labels
|
|
9
|
+
- **Interactive Legend**: Toggle segments visibility with click interactions and value display
|
|
10
|
+
- **Fluent UI Integration**: Seamless integration with Fluent UI themes and design system
|
|
11
|
+
- **Data Labels**: Optional display of values directly on chart segments
|
|
12
|
+
- **Responsive Design**: Automatically adapts to container dimensions
|
|
13
|
+
- **TypeScript Support**: Full TypeScript support with generic types
|
|
14
|
+
- **Custom Tooltips**: Rich tooltips showing detailed segment information
|
|
15
|
+
- **Hollow Center**: Classic doughnut design with customizable center space
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install chart.js react-chartjs-2 chartjs-plugin-datalabels @fluentui/react-components
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Basic Usage
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import React from 'react';
|
|
27
|
+
import { DoughnutChart } from './components/Doughnut/DoughnutChart';
|
|
28
|
+
import { webLightTheme } from '@fluentui/react-components';
|
|
29
|
+
|
|
30
|
+
interface SalesData {
|
|
31
|
+
category: string;
|
|
32
|
+
amount: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const salesData: SalesData[] = [
|
|
36
|
+
{ category: 'Electronics', amount: 125000 },
|
|
37
|
+
{ category: 'Clothing', amount: 85000 },
|
|
38
|
+
{ category: 'Books', amount: 45000 },
|
|
39
|
+
{ category: 'Home & Garden', amount: 95000 },
|
|
40
|
+
{ category: 'Sports', amount: 65000 },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
function App() {
|
|
44
|
+
return (
|
|
45
|
+
<div style={{ width: '600px', height: '400px' }}>
|
|
46
|
+
<DoughnutChart
|
|
47
|
+
data={[
|
|
48
|
+
{ label: 'Sales by Category', data: salesData }
|
|
49
|
+
]}
|
|
50
|
+
getLabel={(datum) => datum.category}
|
|
51
|
+
getValue={(datum) => datum.amount}
|
|
52
|
+
title="Sales Distribution by Category"
|
|
53
|
+
theme={webLightTheme}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Props
|
|
61
|
+
|
|
62
|
+
### DoughnutChartProps<T>
|
|
63
|
+
|
|
64
|
+
| Prop | Type | Required | Default | Description |
|
|
65
|
+
|------|------|----------|---------|-------------|
|
|
66
|
+
| `data` | `{ label: string; data: T[] }[]` | Yes | - | Array of data series with labels and data points |
|
|
67
|
+
| `getLabel` | `(datum: T) => string` | Yes | - | Function to extract the segment label from each data point |
|
|
68
|
+
| `getValue` | `(datum: T) => number` | Yes | - | Function to extract the segment value from each data point |
|
|
69
|
+
| `title` | `string` | No | - | Chart title displayed at the top |
|
|
70
|
+
| `showDataLabels` | `boolean` | No | `true` | Whether to show data values on chart segments |
|
|
71
|
+
| `theme` | `Theme` | No | `webLightTheme` | Fluent UI theme object for styling |
|
|
72
|
+
|
|
73
|
+
## Advanced Usage
|
|
74
|
+
|
|
75
|
+
### Budget Allocation
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
interface BudgetItem {
|
|
79
|
+
department: string;
|
|
80
|
+
allocation: number;
|
|
81
|
+
priority: 'high' | 'medium' | 'low';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const budgetData: BudgetItem[] = [
|
|
85
|
+
{ department: 'Engineering', allocation: 450000, priority: 'high' },
|
|
86
|
+
{ department: 'Marketing', allocation: 280000, priority: 'high' },
|
|
87
|
+
{ department: 'Sales', allocation: 320000, priority: 'high' },
|
|
88
|
+
{ department: 'Operations', allocation: 180000, priority: 'medium' },
|
|
89
|
+
{ department: 'HR', allocation: 120000, priority: 'medium' },
|
|
90
|
+
{ department: 'Legal', allocation: 80000, priority: 'low' },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
<DoughnutChart
|
|
94
|
+
data={[{ label: 'Budget Allocation', data: budgetData }]}
|
|
95
|
+
getLabel={(datum) => datum.department}
|
|
96
|
+
getValue={(datum) => datum.allocation}
|
|
97
|
+
title="Annual Budget Allocation by Department"
|
|
98
|
+
showDataLabels={true}
|
|
99
|
+
theme={webLightTheme}
|
|
100
|
+
/>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Multiple Series Aggregation
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
interface RegionalSales {
|
|
107
|
+
product: string;
|
|
108
|
+
region: string;
|
|
109
|
+
sales: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const salesData: RegionalSales[] = [
|
|
113
|
+
{ product: 'Product A', region: 'North', sales: 15000 },
|
|
114
|
+
{ product: 'Product A', region: 'South', sales: 12000 },
|
|
115
|
+
{ product: 'Product B', region: 'North', sales: 18000 },
|
|
116
|
+
{ product: 'Product B', region: 'South', sales: 14000 },
|
|
117
|
+
{ product: 'Product C', region: 'North', sales: 10000 },
|
|
118
|
+
{ product: 'Product C', region: 'South', sales: 8000 },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
// This will automatically aggregate sales by product across all regions
|
|
122
|
+
<DoughnutChart
|
|
123
|
+
data={[
|
|
124
|
+
{ label: 'North Region', data: salesData.filter(d => d.region === 'North') },
|
|
125
|
+
{ label: 'South Region', data: salesData.filter(d => d.region === 'South') }
|
|
126
|
+
]}
|
|
127
|
+
getLabel={(datum) => datum.product}
|
|
128
|
+
getValue={(datum) => datum.sales}
|
|
129
|
+
title="Total Sales by Product (All Regions Combined)"
|
|
130
|
+
theme={webLightTheme}
|
|
131
|
+
/>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Survey Results
|
|
135
|
+
|
|
136
|
+
```tsx
|
|
137
|
+
interface SurveyResponse {
|
|
138
|
+
answer: string;
|
|
139
|
+
count: number;
|
|
140
|
+
percentage: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const surveyData: SurveyResponse[] = [
|
|
144
|
+
{ answer: 'Very Satisfied', count: 156, percentage: 39 },
|
|
145
|
+
{ answer: 'Satisfied', count: 124, percentage: 31 },
|
|
146
|
+
{ answer: 'Neutral', count: 68, percentage: 17 },
|
|
147
|
+
{ answer: 'Dissatisfied', count: 32, percentage: 8 },
|
|
148
|
+
{ answer: 'Very Dissatisfied', count: 20, percentage: 5 },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
<DoughnutChart
|
|
152
|
+
data={[{ label: 'Customer Satisfaction', data: surveyData }]}
|
|
153
|
+
getLabel={(datum) => datum.answer}
|
|
154
|
+
getValue={(datum) => datum.count}
|
|
155
|
+
title="Customer Satisfaction Survey Results"
|
|
156
|
+
showDataLabels={true}
|
|
157
|
+
theme={webLightTheme}
|
|
158
|
+
/>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Market Share Analysis
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
interface MarketShare {
|
|
165
|
+
company: string;
|
|
166
|
+
marketShare: number;
|
|
167
|
+
revenue: number;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const marketData: MarketShare[] = [
|
|
171
|
+
{ company: 'Company A', marketShare: 28.5, revenue: 2850000 },
|
|
172
|
+
{ company: 'Company B', marketShare: 22.1, revenue: 2210000 },
|
|
173
|
+
{ company: 'Company C', marketShare: 18.7, revenue: 1870000 },
|
|
174
|
+
{ company: 'Company D', marketShare: 15.3, revenue: 1530000 },
|
|
175
|
+
{ company: 'Others', marketShare: 15.4, revenue: 1540000 },
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
<DoughnutChart
|
|
179
|
+
data={[{ label: 'Market Share', data: marketData }]}
|
|
180
|
+
getLabel={(datum) => datum.company}
|
|
181
|
+
getValue={(datum) => datum.marketShare}
|
|
182
|
+
title="Industry Market Share Distribution"
|
|
183
|
+
showDataLabels={true}
|
|
184
|
+
theme={webLightTheme}
|
|
185
|
+
/>
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Data Structure
|
|
189
|
+
|
|
190
|
+
The component expects data in the following format:
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
interface ChartSeries<T> {
|
|
194
|
+
label: string; // Series name (for multiple data sources)
|
|
195
|
+
data: T[]; // Array of data points
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Each data point `T` should contain:
|
|
200
|
+
|
|
201
|
+
- A label value (segment name) - string
|
|
202
|
+
- A numeric value (segment size) - number
|
|
203
|
+
|
|
204
|
+
## Data Aggregation
|
|
205
|
+
|
|
206
|
+
The component automatically aggregates values when multiple data points have the same label:
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
// Input data
|
|
210
|
+
const data = [
|
|
211
|
+
{ category: 'Electronics', value: 100 },
|
|
212
|
+
{ category: 'Electronics', value: 50 }, // Will be aggregated
|
|
213
|
+
{ category: 'Clothing', value: 75 },
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
// Result: Electronics = 150, Clothing = 75
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
## Interactive Features
|
|
220
|
+
|
|
221
|
+
### Legend with Values
|
|
222
|
+
|
|
223
|
+
- Displays both label and actual value for each segment
|
|
224
|
+
- Click legend items to show/hide segments
|
|
225
|
+
- Visual feedback on hover states
|
|
226
|
+
- Maintains proportional relationships when segments are hidden
|
|
227
|
+
|
|
228
|
+
### Segment Interactions
|
|
229
|
+
|
|
230
|
+
- Hover effects on chart segments
|
|
231
|
+
- Rich tooltips showing detailed information
|
|
232
|
+
- Smooth animations and transitions
|
|
233
|
+
|
|
234
|
+
### Responsive Layout
|
|
235
|
+
|
|
236
|
+
- Chart automatically resizes to container dimensions
|
|
237
|
+
- Legend positioning adapts to available space
|
|
238
|
+
- Maintains readability across different screen sizes
|
|
239
|
+
|
|
240
|
+
## Styling and Theme Integration
|
|
241
|
+
|
|
242
|
+
The component uses Fluent UI theme tokens:
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
// Segment colors
|
|
246
|
+
backgroundColor: Derived from theme palette with lightening
|
|
247
|
+
borderWidth: 1
|
|
248
|
+
|
|
249
|
+
// Typography
|
|
250
|
+
fontFamily: theme.fontFamilyBase
|
|
251
|
+
fontSize: theme.fontSizeBase200
|
|
252
|
+
fontWeight: theme.fontWeightSemibold
|
|
253
|
+
color: theme.colorNeutralForeground1
|
|
254
|
+
|
|
255
|
+
// Data labels
|
|
256
|
+
color: theme.colorNeutralForeground1
|
|
257
|
+
font: theme font properties
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## Performance Optimizations
|
|
261
|
+
|
|
262
|
+
The component includes several React optimizations:
|
|
263
|
+
|
|
264
|
+
````tsx
|
|
265
|
+
// Memoized value aggregation
|
|
266
|
+
const valueMap = useMemo(() => {
|
|
267
|
+
const map = new Map<string, number>();
|
|
268
|
+
data.forEach(series => {
|
|
269
|
+
series.data.forEach(d => {
|
|
270
|
+
const label = getLabel(d);
|
|
271
|
+
const value = getValue(d);
|
|
272
|
+
map.set(label, (map.get(label) || 0) + value);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
return map;
|
|
276
|
+
}, [data, getLabel, getValue]);
|
|
277
|
+
|
|
278
|
+
// Memoized color calculations
|
|
279
|
+
const { allLabels, colors } = useMemo(() => {
|
|
280
|
+
const allLabels = Array.from(valueMap.keys());
|
|
281
|
+
const palette = getFluentPalette(theme);
|
|
282
|
+
const colors = allLabels.map((_, i) =>
|
|
283
|
+
lightenColor(palette[i % palette.length], 0.3)
|
|
284
|
+
);
|
|
285
|
+
return { allLabels, colors };
|
|
286
|
+
}, [valueMap, getFluentPalette, theme, lightenColor]);
|
|
287
|
+
|
|
288
|
+
// Memoized chart data
|
|
289
|
+
const chartData = useMemo(() => ({
|
|
290
|
+
labels: filteredLabels,
|
|
291
|
+
datasets: [{
|
|
292
|
+
data: values,
|
|
293
|
+
backgroundColor: visibleColors,
|
|
294
|
+
borderWidth: 1,
|
|
295
|
+
}],
|
|
296
|
+
}), [filteredLabels, values, visibleColors]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './DoughnutChart';
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArcElement,
|
|
3
|
+
Chart as ChartJS,
|
|
4
|
+
ChartOptions,
|
|
5
|
+
Legend,
|
|
6
|
+
Title,
|
|
7
|
+
Tooltip,
|
|
8
|
+
} from 'chart.js';
|
|
9
|
+
import React, { useMemo, useState } from 'react';
|
|
10
|
+
import { Theme, webLightTheme } from '@fluentui/react-components';
|
|
11
|
+
|
|
12
|
+
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
|
13
|
+
import { Pie } from 'react-chartjs-2';
|
|
14
|
+
import RenderValueLegend from '../../components/renderValueLegend/RenderValueLegend';
|
|
15
|
+
import { createFluentTooltip } from '../../hooks/useGraphUtils';
|
|
16
|
+
import { useGraphGlobalStyles } from '../../graphGlobalStyles/useGraphGlobalStyles';
|
|
17
|
+
import { useGraphUtils } from '../../hooks/useGraphUtils';
|
|
18
|
+
|
|
19
|
+
ChartJS.register(ChartDataLabels);
|
|
20
|
+
ChartJS.register(ArcElement, Tooltip, Legend, Title);
|
|
21
|
+
|
|
22
|
+
export interface PieChartProps<T> {
|
|
23
|
+
data: {
|
|
24
|
+
label: string;
|
|
25
|
+
data: T[];
|
|
26
|
+
}[];
|
|
27
|
+
getLabel: (datum: T) => string;
|
|
28
|
+
getValue: (datum: T) => number;
|
|
29
|
+
title?: string;
|
|
30
|
+
showDataLabels?: boolean;
|
|
31
|
+
theme?: Theme;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function PieChart<T extends object>({
|
|
35
|
+
data,
|
|
36
|
+
getLabel,
|
|
37
|
+
getValue,
|
|
38
|
+
title,
|
|
39
|
+
showDataLabels = false,
|
|
40
|
+
theme = webLightTheme,
|
|
41
|
+
}: PieChartProps<T>) {
|
|
42
|
+
const { getFluentPalette, lightenColor } = useGraphUtils(theme);
|
|
43
|
+
const [hiddenLabels, setHiddenLabels] = useState<string[]>([]);
|
|
44
|
+
const styles = useGraphGlobalStyles();
|
|
45
|
+
const toggleLabel = (label: string): void => {
|
|
46
|
+
setHiddenLabels(prev =>
|
|
47
|
+
prev.includes(label) ? prev.filter(l => l !== label) : [...prev, label]
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const valueMap = useMemo(() => {
|
|
52
|
+
const map = new Map<string, number>();
|
|
53
|
+
data.forEach(series => {
|
|
54
|
+
series.data.forEach(d => {
|
|
55
|
+
const label = getLabel(d);
|
|
56
|
+
const value = getValue(d);
|
|
57
|
+
map.set(label, (map.get(label) || 0) + value);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
return map;
|
|
61
|
+
}, [data, getLabel, getValue]);
|
|
62
|
+
|
|
63
|
+
const { allLabels, colors, filteredLabels, values, visibleColors } = useMemo(() => {
|
|
64
|
+
const allLabels = Array.from(valueMap.keys());
|
|
65
|
+
const palette = getFluentPalette(theme);
|
|
66
|
+
const colors = allLabels.map((_, i) =>
|
|
67
|
+
lightenColor(palette[i % palette.length], 0.3)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const filteredLabels = allLabels.filter(
|
|
71
|
+
label => !hiddenLabels.includes(label)
|
|
72
|
+
);
|
|
73
|
+
const values = filteredLabels.map(label => valueMap.get(label) || 0);
|
|
74
|
+
const visibleColors = filteredLabels.map(label => {
|
|
75
|
+
const idx = allLabels.indexOf(label);
|
|
76
|
+
return colors[idx];
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return { allLabels, colors, filteredLabels, values, visibleColors };
|
|
80
|
+
}, [valueMap, getFluentPalette, lightenColor, theme, hiddenLabels]);
|
|
81
|
+
|
|
82
|
+
const { chartData, legendEntries } = useMemo(() => {
|
|
83
|
+
const chartData = {
|
|
84
|
+
labels: filteredLabels,
|
|
85
|
+
datasets: [
|
|
86
|
+
{
|
|
87
|
+
data: values,
|
|
88
|
+
backgroundColor: visibleColors,
|
|
89
|
+
borderWidth: 1,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const legendEntries = allLabels.map((label, i) => ({
|
|
95
|
+
label,
|
|
96
|
+
value: valueMap.get(label) || 0,
|
|
97
|
+
color: colors[i],
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
return { chartData, legendEntries };
|
|
101
|
+
}, [filteredLabels, values, visibleColors, allLabels, valueMap, colors]);
|
|
102
|
+
|
|
103
|
+
const options = useMemo<ChartOptions<'pie'>>(() => ({
|
|
104
|
+
responsive: true,
|
|
105
|
+
maintainAspectRatio: false,
|
|
106
|
+
plugins: {
|
|
107
|
+
tooltip: createFluentTooltip<'pie'>(theme),
|
|
108
|
+
legend: { display: false },
|
|
109
|
+
title: {
|
|
110
|
+
display: !!title,
|
|
111
|
+
text: title,
|
|
112
|
+
font: {
|
|
113
|
+
size: 14,
|
|
114
|
+
family: theme.fontFamilyBase,
|
|
115
|
+
weight: theme.fontWeightSemibold,
|
|
116
|
+
},
|
|
117
|
+
color: theme.colorNeutralForeground1,
|
|
118
|
+
padding: {
|
|
119
|
+
top: 20,
|
|
120
|
+
bottom: 20,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
datalabels: {
|
|
124
|
+
display: showDataLabels,
|
|
125
|
+
color: theme.colorNeutralForeground1,
|
|
126
|
+
font: {
|
|
127
|
+
family: theme.fontFamilyBase,
|
|
128
|
+
size: parseInt(theme.fontSizeBase200.replace('px', '')) || 14,
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}), [theme, title, showDataLabels]);
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className={styles.chartWithLegend}>
|
|
136
|
+
<div className={styles.chartArea}>
|
|
137
|
+
<Pie data={chartData} options={options} />
|
|
138
|
+
</div>
|
|
139
|
+
<div className={styles.legendArea}>
|
|
140
|
+
<RenderValueLegend
|
|
141
|
+
entries={legendEntries}
|
|
142
|
+
visibleLabels={filteredLabels}
|
|
143
|
+
toggleLabel={toggleLabel}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|