@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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +462 -0
  3. package/dist/charts/BarChart/BarChart.d.ts +16 -0
  4. package/dist/charts/BarChart/index.d.ts +1 -0
  5. package/dist/charts/ComboChart/ComboChart.d.ts +16 -0
  6. package/dist/charts/ComboChart/index.d.ts +1 -0
  7. package/dist/charts/Doughnut/DoughnutChart.d.ts +14 -0
  8. package/dist/charts/Doughnut/index.d.ts +1 -0
  9. package/dist/charts/PieChart/PieChart.d.ts +14 -0
  10. package/dist/charts/PieChart/index.d.ts +1 -0
  11. package/dist/charts/areaChart/AreaChart.d.ts +15 -0
  12. package/dist/charts/areaChart/index.d.ts +1 -0
  13. package/dist/charts/barHorizontalChart/BarHotizontalChart.d.ts +15 -0
  14. package/dist/charts/barHorizontalChart/index.d.ts +1 -0
  15. package/dist/charts/bubbleChart/BubbleChart.d.ts +15 -0
  16. package/dist/charts/bubbleChart/index.d.ts +1 -0
  17. package/dist/charts/floatBarChart/FloatBarChart.d.ts +14 -0
  18. package/dist/charts/floatBarChart/index.d.ts +1 -0
  19. package/dist/charts/lineChart/LineChart.d.ts +14 -0
  20. package/dist/charts/lineChart/index.d.ts +1 -0
  21. package/dist/charts/polarChart/PolarChart.d.ts +14 -0
  22. package/dist/charts/polarChart/index.d.ts +1 -0
  23. package/dist/charts/radarChart/RadarChart.d.ts +14 -0
  24. package/dist/charts/radarChart/index.d.ts +1 -0
  25. package/dist/charts/scatterChart/ScatterChart.d.ts +14 -0
  26. package/dist/charts/scatterChart/index.d.ts +1 -0
  27. package/dist/charts/stackedLineChart/StackedLineChart.d.ts +14 -0
  28. package/dist/charts/stackedLineChart/index.d.ts +1 -0
  29. package/dist/charts/steamChart/SteamChart.d.ts +14 -0
  30. package/dist/charts/steamChart/index.d.ts +1 -0
  31. package/dist/components/DashBoard.d.ts +3 -0
  32. package/dist/components/RenderLegend/RenderLegend.d.ts +11 -0
  33. package/dist/components/RenderTooltip/RenderTooltip.d.ts +14 -0
  34. package/dist/components/buttonMenu/ButtonMenu.d.ts +3 -0
  35. package/dist/components/buttonMenu/IButtonMenuOption.d.ts +10 -0
  36. package/dist/components/buttonMenu/IButtonMenuProps.d.ts +37 -0
  37. package/dist/components/index.d.ts +15 -0
  38. package/dist/components/legendContainer/LegendContainer.d.ts +16 -0
  39. package/dist/components/legendeButton/LegendButton.d.ts +11 -0
  40. package/dist/components/renderSliceLegend/RenderSliceLegend.d.ts +9 -0
  41. package/dist/components/renderValueLegend/RenderValueLegend.d.ts +13 -0
  42. package/dist/components/stack/IStackProps.d.ts +76 -0
  43. package/dist/components/stack/Stack.d.ts +8 -0
  44. package/dist/components/themeProvider/ThemeProvider.d.ts +15 -0
  45. package/dist/constants/Constants.d.ts +1 -0
  46. package/dist/fluentui-react-charts.cjs.development.js +2916 -0
  47. package/dist/fluentui-react-charts.cjs.development.js.map +1 -0
  48. package/dist/fluentui-react-charts.cjs.production.min.js +2 -0
  49. package/dist/fluentui-react-charts.cjs.production.min.js.map +1 -0
  50. package/dist/fluentui-react-charts.esm.js +2905 -0
  51. package/dist/fluentui-react-charts.esm.js.map +1 -0
  52. package/dist/graphGlobalStyles/useGraphGlobalStyles.d.ts +5 -0
  53. package/dist/hooks/index.d.ts +1 -0
  54. package/dist/hooks/useGraphUtils.d.ts +38 -0
  55. package/dist/hooks/useResponsiveLegend.d.ts +8 -0
  56. package/dist/index.d.ts +3 -0
  57. package/dist/index.js +8 -0
  58. package/dist/models/IChart.d.ts +25 -0
  59. package/dist/models/index.d.ts +1 -0
  60. package/package.json +66 -0
  61. package/src/assets/sample1.png +0 -0
  62. package/src/assets/sample2.png +0 -0
  63. package/src/assets/sample3.png +0 -0
  64. package/src/charts/BarChart/BarChart.tsx +227 -0
  65. package/src/charts/BarChart/README.MD +335 -0
  66. package/src/charts/BarChart/index.ts +1 -0
  67. package/src/charts/ComboChart/ComboChart.tsx +209 -0
  68. package/src/charts/ComboChart/README.MD +347 -0
  69. package/src/charts/ComboChart/index.ts +1 -0
  70. package/src/charts/Doughnut/DoughnutChart.tsx +152 -0
  71. package/src/charts/Doughnut/README.MD +296 -0
  72. package/src/charts/Doughnut/index.ts +1 -0
  73. package/src/charts/PieChart/PieChart.tsx +148 -0
  74. package/src/charts/PieChart/README.MD +315 -0
  75. package/src/charts/PieChart/index.ts +1 -0
  76. package/src/charts/areaChart/AreaChart.tsx +195 -0
  77. package/src/charts/areaChart/README.MD +236 -0
  78. package/src/charts/areaChart/index.ts +1 -0
  79. package/src/charts/barHorizontalChart/BarHotizontalChart.tsx +200 -0
  80. package/src/charts/barHorizontalChart/README.MD +278 -0
  81. package/src/charts/barHorizontalChart/index.ts +2 -0
  82. package/src/charts/bubbleChart/BubbleChart.tsx +184 -0
  83. package/src/charts/bubbleChart/README.MD +275 -0
  84. package/src/charts/bubbleChart/index.ts +1 -0
  85. package/src/charts/floatBarChart/FloatBarChart.tsx +178 -0
  86. package/src/charts/floatBarChart/README.MD +354 -0
  87. package/src/charts/floatBarChart/index.ts +1 -0
  88. package/src/charts/lineChart/LineChart.tsx +200 -0
  89. package/src/charts/lineChart/README.MD +354 -0
  90. package/src/charts/lineChart/index.ts +1 -0
  91. package/src/charts/polarChart/PolarChart.tsx +161 -0
  92. package/src/charts/polarChart/README.MD +336 -0
  93. package/src/charts/polarChart/index.ts +1 -0
  94. package/src/charts/radarChart/README.MD +388 -0
  95. package/src/charts/radarChart/RadarChart.tsx +173 -0
  96. package/src/charts/radarChart/index.ts +1 -0
  97. package/src/charts/scatterChart/README.MD +335 -0
  98. package/src/charts/scatterChart/ScatterChart.tsx +155 -0
  99. package/src/charts/scatterChart/index.ts +1 -0
  100. package/src/charts/stackedLineChart/README.MD +396 -0
  101. package/src/charts/stackedLineChart/StackedLineChart.tsx +188 -0
  102. package/src/charts/stackedLineChart/index.ts +1 -0
  103. package/src/charts/steamChart/README.MD +414 -0
  104. package/src/charts/steamChart/SteamChart.tsx +236 -0
  105. package/src/charts/steamChart/index.ts +1 -0
  106. package/src/components/DashBoard.tsx +409 -0
  107. package/src/components/RenderLegend/RenderLegend.tsx +40 -0
  108. package/src/components/RenderTooltip/RenderTooltip.tsx +111 -0
  109. package/src/components/buttonMenu/ButtonMenu.tsx +186 -0
  110. package/src/components/buttonMenu/IButtonMenuOption.ts +9 -0
  111. package/src/components/buttonMenu/IButtonMenuProps.tsx +40 -0
  112. package/src/components/index.ts +15 -0
  113. package/src/components/legendContainer/LegendContainer.tsx +118 -0
  114. package/src/components/legendeButton/LegendButton.tsx +57 -0
  115. package/src/components/renderSliceLegend/RenderSliceLegend.tsx +46 -0
  116. package/src/components/renderValueLegend/RenderValueLegend.tsx +43 -0
  117. package/src/components/stack/IStackProps.tsx +94 -0
  118. package/src/components/stack/Stack.tsx +103 -0
  119. package/src/components/themeProvider/ThemeProvider.tsx +48 -0
  120. package/src/constants/Constants.tsx +23 -0
  121. package/src/graphGlobalStyles/useGraphGlobalStyles.ts +28 -0
  122. package/src/hooks/index.ts +1 -0
  123. package/src/hooks/useGraphUtils.tsx +314 -0
  124. package/src/hooks/useResponsiveLegend.ts +35 -0
  125. package/src/index.tsx +4 -0
  126. package/src/models/IChart.ts +50 -0
  127. package/src/models/index.ts +1 -0
@@ -0,0 +1,414 @@
1
+ # SteamChart Component
2
+
3
+ A specialized stream chart (stacked area chart) component built with Chart.js and Fluent UI React. This component displays data as flowing, stacked areas that create a "stream" effect, making it perfect for visualizing how different categories contribute to a total over time, with smooth transitions and the ability to toggle between raw values and percentages.
4
+
5
+ ## Features
6
+
7
+ - **Stream Visualization**: Smooth, flowing stacked areas with curved boundaries
8
+ - **Percentage Toggle**: Switch between raw values and percentage view
9
+ - **Interactive Legend**: Toggle series visibility with click interactions
10
+ - **Smooth Animations**: Fluid transitions with configurable easing
11
+ - **Filled Areas**: Semi-transparent filled areas for better visual flow
12
+ - **Fluent UI Integration**: Seamless integration with Fluent UI themes and design system
13
+ - **Data Labels**: Optional display of values directly on data points
14
+ - **Responsive Design**: Automatically adapts to container dimensions
15
+ - **TypeScript Support**: Full TypeScript support with generic types
16
+ - **Custom Tooltips**: Rich tooltips showing detailed breakdown information
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install chart.js react-chartjs-2 chartjs-plugin-datalabels @fluentui/react-components
22
+ ```
23
+
24
+ ## Basic Usage
25
+
26
+ ```tsx
27
+ import React from 'react';
28
+ import { SteamChart } from './components/steamChart/SteamChart';
29
+ import { webLightTheme } from '@fluentui/react-components';
30
+
31
+ interface MusicStreamingData {
32
+ month: string;
33
+ pop: number;
34
+ rock: number;
35
+ jazz: number;
36
+ classical: number;
37
+ electronic: number;
38
+ }
39
+
40
+ const streamingData: MusicStreamingData[] = [
41
+ { month: 'Jan', pop: 45000, rock: 32000, jazz: 12000, classical: 8000, electronic: 18000 },
42
+ { month: 'Feb', pop: 48000, rock: 35000, jazz: 13000, classical: 9000, electronic: 20000 },
43
+ { month: 'Mar', pop: 52000, rock: 38000, jazz: 15000, classical: 10000, electronic: 22000 },
44
+ { month: 'Apr', pop: 49000, rock: 36000, jazz: 14000, classical: 11000, electronic: 25000 },
45
+ { month: 'May', pop: 55000, rock: 40000, jazz: 16000, classical: 12000, electronic: 28000 },
46
+ ];
47
+
48
+ function App() {
49
+ return (
50
+ <div style={{ width: '800px', height: '500px' }}>
51
+ <SteamChart
52
+ data={[
53
+ {
54
+ label: 'Pop',
55
+ data: streamingData.map(d => ({ month: d.month, streams: d.pop }))
56
+ },
57
+ {
58
+ label: 'Rock',
59
+ data: streamingData.map(d => ({ month: d.month, streams: d.rock }))
60
+ },
61
+ {
62
+ label: 'Jazz',
63
+ data: streamingData.map(d => ({ month: d.month, streams: d.jazz }))
64
+ },
65
+ {
66
+ label: 'Classical',
67
+ data: streamingData.map(d => ({ month: d.month, streams: d.classical }))
68
+ },
69
+ {
70
+ label: 'Electronic',
71
+ data: streamingData.map(d => ({ month: d.month, streams: d.electronic }))
72
+ }
73
+ ]}
74
+ getPrimary={(datum) => datum.month}
75
+ getSecondary={(datum) => datum.streams}
76
+ title="Music Streaming by Genre"
77
+ theme={webLightTheme}
78
+ />
79
+ </div>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ## Props
85
+
86
+ ### SteamChartProps<T>
87
+
88
+ | Prop | Type | Required | Default | Description |
89
+ |------|------|----------|---------|-------------|
90
+ | `data` | `{ label: string; data: T[] }[]` | Yes | - | Array of data series with labels and data points |
91
+ | `getPrimary` | `(datum: T) => string \| number` | Yes | - | Function to extract the x-axis category from each data point |
92
+ | `getSecondary` | `(datum: T) => number` | Yes | - | Function to extract the y-axis value from each data point |
93
+ | `title` | `string` | No | - | Chart title displayed at the top |
94
+ | `showDataLabels` | `boolean` | No | `false` | Whether to show data values on data points |
95
+ | `theme` | `Theme` | No | `webLightTheme` | Fluent UI theme object for styling |
96
+
97
+ ## Advanced Usage
98
+
99
+ ### Social Media Engagement
100
+
101
+ ```tsx
102
+ interface SocialEngagement {
103
+ week: string;
104
+ facebook: number;
105
+ instagram: number;
106
+ twitter: number;
107
+ linkedin: number;
108
+ tiktok: number;
109
+ }
110
+
111
+ const engagementData: SocialEngagement[] = [
112
+ { week: 'Week 1', facebook: 12000, instagram: 18000, twitter: 8000, linkedin: 3000, tiktok: 25000 },
113
+ { week: 'Week 2', facebook: 13500, instagram: 20000, twitter: 9000, linkedin: 3500, tiktok: 28000 },
114
+ { week: 'Week 3', facebook: 11000, instagram: 22000, twitter: 7500, linkedin: 4000, tiktok: 32000 },
115
+ { week: 'Week 4', facebook: 14000, instagram: 24000, twitter: 8500, linkedin: 4500, tiktok: 35000 },
116
+ ];
117
+
118
+ <SteamChart
119
+ data={[
120
+ {
121
+ label: 'Facebook',
122
+ data: engagementData.map(d => ({ week: d.week, engagement: d.facebook }))
123
+ },
124
+ {
125
+ label: 'Instagram',
126
+ data: engagementData.map(d => ({ week: d.week, engagement: d.instagram }))
127
+ },
128
+ {
129
+ label: 'Twitter',
130
+ data: engagementData.map(d => ({ week: d.week, engagement: d.twitter }))
131
+ },
132
+ {
133
+ label: 'LinkedIn',
134
+ data: engagementData.map(d => ({ week: d.week, engagement: d.linkedin }))
135
+ },
136
+ {
137
+ label: 'TikTok',
138
+ data: engagementData.map(d => ({ week: d.week, engagement: d.tiktok }))
139
+ }
140
+ ]}
141
+ getPrimary={(datum) => datum.week}
142
+ getSecondary={(datum) => datum.engagement}
143
+ title="Social Media Engagement by Platform"
144
+ showDataLabels={true}
145
+ theme={webLightTheme}
146
+ />
147
+ ```
148
+
149
+ ### E-commerce Sales Channels
150
+
151
+ ```tsx
152
+ interface SalesChannelData {
153
+ quarter: string;
154
+ website: number;
155
+ mobileApp: number;
156
+ retailStores: number;
157
+ marketplace: number;
158
+ wholesale: number;
159
+ }
160
+
161
+ const salesData: SalesChannelData[] = [
162
+ { quarter: 'Q1 2024', website: 250000, mobileApp: 180000, retailStores: 320000, marketplace: 150000, wholesale: 200000 },
163
+ { quarter: 'Q2 2024', website: 280000, mobileApp: 220000, retailStores: 310000, marketplace: 180000, wholesale: 220000 },
164
+ { quarter: 'Q3 2024', website: 320000, mobileApp: 260000, retailStores: 300000, marketplace: 210000, wholesale: 240000 },
165
+ { quarter: 'Q4 2024', website: 380000, mobileApp: 300000, retailStores: 290000, marketplace: 250000, wholesale: 260000 },
166
+ ];
167
+
168
+ <SteamChart
169
+ data={[
170
+ {
171
+ label: 'Website',
172
+ data: salesData.map(d => ({ quarter: d.quarter, sales: d.website }))
173
+ },
174
+ {
175
+ label: 'Mobile App',
176
+ data: salesData.map(d => ({ quarter: d.quarter, sales: d.mobileApp }))
177
+ },
178
+ {
179
+ label: 'Retail Stores',
180
+ data: salesData.map(d => ({ quarter: d.quarter, sales: d.retailStores }))
181
+ },
182
+ {
183
+ label: 'Marketplace',
184
+ data: salesData.map(d => ({ quarter: d.quarter, sales: d.marketplace }))
185
+ },
186
+ {
187
+ label: 'Wholesale',
188
+ data: salesData.map(d => ({ quarter: d.quarter, sales: d.wholesale }))
189
+ }
190
+ ]}
191
+ getPrimary={(datum) => datum.quarter}
192
+ getSecondary={(datum) => datum.sales}
193
+ title="Sales Performance by Channel"
194
+ theme={webLightTheme}
195
+ />
196
+ ```
197
+
198
+ ### Energy Production Sources
199
+
200
+ ```tsx
201
+ interface EnergyProduction {
202
+ month: string;
203
+ solar: number;
204
+ wind: number;
205
+ hydro: number;
206
+ nuclear: number;
207
+ coal: number;
208
+ naturalGas: number;
209
+ }
210
+
211
+ const energyData: EnergyProduction[] = [
212
+ { month: 'Jan', solar: 1200, wind: 2800, hydro: 3500, nuclear: 4200, coal: 2100, naturalGas: 3200 },
213
+ { month: 'Feb', solar: 1400, wind: 3200, hydro: 3200, nuclear: 4200, coal: 1900, naturalGas: 3100 },
214
+ { month: 'Mar', solar: 1800, wind: 3600, hydro: 3800, nuclear: 4200, coal: 1700, naturalGas: 2900 },
215
+ { month: 'Apr', solar: 2200, wind: 3400, hydro: 4200, nuclear: 4200, coal: 1500, naturalGas: 2700 },
216
+ ];
217
+
218
+ <SteamChart
219
+ data={[
220
+ {
221
+ label: 'Solar',
222
+ data: energyData.map(d => ({ month: d.month, production: d.solar }))
223
+ },
224
+ {
225
+ label: 'Wind',
226
+ data: energyData.map(d => ({ month: d.month, production: d.wind }))
227
+ },
228
+ {
229
+ label: 'Hydro',
230
+ data: energyData.map(d => ({ month: d.month, production: d.hydro }))
231
+ },
232
+ {
233
+ label: 'Nuclear',
234
+ data: energyData.map(d => ({ month: d.month, production: d.nuclear }))
235
+ },
236
+ {
237
+ label: 'Coal',
238
+ data: energyData.map(d => ({ month: d.month, production: d.coal }))
239
+ },
240
+ {
241
+ label: 'Natural Gas',
242
+ data: energyData.map(d => ({ month: d.month, production: d.naturalGas }))
243
+ }
244
+ ]}
245
+ getPrimary={(datum) => datum.month}
246
+ getSecondary={(datum) => datum.production}
247
+ title="Energy Production by Source (MWh)"
248
+ showDataLabels={true}
249
+ theme={webLightTheme}
250
+ />
251
+ ```
252
+
253
+ ## Data Structure
254
+
255
+ The component expects data in the following format:
256
+
257
+ ```tsx
258
+ interface ChartSeries<T> {
259
+ label: string; // Series name (appears in legend)
260
+ data: T[]; // Array of data points
261
+ }
262
+ ```
263
+
264
+ Each data point `T` should contain:
265
+
266
+ - A primary value (x-axis category) - string or number
267
+ - A secondary value (y-axis value) - number
268
+
269
+ ## Stream Chart Characteristics
270
+
271
+ ### Visual Flow
272
+
273
+ - **Smooth Curves**: High tension value (0.5) creates flowing stream effect
274
+ - **Stacked Areas**: Each category flows on top of the previous ones
275
+ - **No Borders**: Transparent borders create seamless flow between areas
276
+ - **Organic Shape**: Natural, river-like appearance
277
+
278
+ ### Percentage Toggle
279
+
280
+ - **Raw Values**: Shows actual numerical values
281
+ - **Percentage View**: Shows proportional contribution (0-100%)
282
+ - **Toggle Button**: Central button to switch between views
283
+ - **Dynamic Scaling**: Y-axis adjusts automatically
284
+
285
+ ## Use Cases
286
+
287
+ Steam charts are particularly effective for:
288
+
289
+ ### Content Consumption
290
+
291
+ - **Media Streaming**: Different genres or content types over time
292
+ - **Website Traffic**: Traffic sources and their evolution
293
+ - **App Usage**: Feature usage patterns across time periods
294
+
295
+ ### Market Analysis
296
+
297
+ - **Market Share**: How different players' shares flow over time
298
+ - **Product Mix**: Sales composition with smooth transitions
299
+ - **Demographic Changes**: Population segments over time
300
+
301
+ ### Resource Allocation
302
+
303
+ - **Budget Distribution**: How budget allocation changes over time
304
+ - **Team Productivity**: Contribution of different teams
305
+ - **Energy Mix**: Renewable vs non-renewable energy sources
306
+
307
+ ### Communication Channels
308
+
309
+ - **Social Media Engagement**: Platform-wise engagement trends
310
+ - **Customer Support**: Channel usage patterns
311
+ - **Marketing Channels**: Campaign performance across channels
312
+
313
+ ## Interactive Features
314
+
315
+ ### Percentage Toggle Button
316
+
317
+ - Central toggle button to switch between raw values and percentages
318
+ - Smooth transitions between view modes
319
+ - Y-axis automatically adjusts scale and labels
320
+ - Maintains visual flow during transitions
321
+
322
+ ### Legend Controls
323
+
324
+ - Click legend items to show/hide data series
325
+ - Visual feedback on hover states
326
+ - At least one series must remain visible
327
+ - Maintains stream flow when series are hidden
328
+
329
+ ### Stream Interactions
330
+
331
+ - Hover effects on stream areas
332
+ - Rich tooltips showing breakdown of values
333
+ - Smooth animations with easing effects
334
+ - Index-based interaction mode for better UX
335
+
336
+ ## Styling and Theme Integration
337
+
338
+ The component uses Fluent UI theme tokens:
339
+
340
+ ```tsx
341
+ // Stream styling
342
+ backgroundColor: seriesColor (semi-transparent)
343
+ borderColor: 'transparent'
344
+ borderWidth: 0
345
+ fill: true
346
+ tension: 0.5 (high curve smoothness)
347
+
348
+ // Animation
349
+ duration: 800ms
350
+ easing: 'easeOutQuart'
351
+
352
+ // Typography
353
+ fontFamily: theme.fontFamilyBase
354
+ fontSize: theme.fontSizeBase200
355
+ fontWeight: theme.fontWeightSemibold
356
+ color: theme.colorNeutralForeground1
357
+
358
+ // Toggle button
359
+ appearance: 'secondary'
360
+ shape: 'circular'
361
+ size: 'small'
362
+ width: '150px'
363
+ ```
364
+
365
+ ## Performance Optimizations
366
+
367
+ The component includes several React optimizations:
368
+
369
+ ````tsx
370
+ // Memoized color calculations
371
+ const seriesColors = useMemo(() => {
372
+ return data.reduce((acc, series, idx) => {
373
+ const base = getFluentPalette(theme)[
374
+ idx % getFluentPalette(theme).length
375
+ ];
376
+ acc[series.label] = lightenColor(base, 0.3);
377
+ return acc;
378
+ }, {} as Record<string, string>);
379
+ }, [data, getFluentPalette, lightenColor, theme]);
380
+
381
+ // Memoized total calculations for percentages
382
+ const totalPerPoint = useMemo(() => {
383
+ return allLabels.map(cat =>
384
+ data.reduce((sum, series) => {
385
+ const match = series.data.find(d => getPrimary(d) === cat);
386
+ return sum + (match ? getSecondary(match) : 0);
387
+ }, 0)
388
+ );
389
+ }, [allLabels, data, getPrimary, getSecondary]);
390
+
391
+ // Memoized chart data with percentage support
392
+ const chartData = useMemo(() => {
393
+ return {
394
+ labels: allLabels,
395
+ datasets: data
396
+ .filter(series => visibleSeries.includes(series.label))
397
+ .map((series) => ({
398
+ label: series.label,
399
+ fill: true,
400
+ backgroundColor: seriesColors[series.label],
401
+ borderColor: 'transparent',
402
+ borderWidth: 0,
403
+ data: allLabels.map((cat, index) => {
404
+ const match = series.data.find(d => getPrimary(d) === cat);
405
+ const rawValue = match ? getSecondary(match) : 0;
406
+ const total = totalPerPoint[index] || 1;
407
+ return showPercent
408
+ ? Math.round(((rawValue / total) * 100 + Number.EPSILON) * 100) / 100
409
+ : Math.round((rawValue + Number.EPSILON) * 100) / 100;
410
+ }),
411
+ tension: 0.5,
412
+ })),
413
+ };
414
+ }, [data, visibleSeries, allLabels, getPrimary, getSecondary, seriesColors, totalPerPoint, showPercent]);
@@ -0,0 +1,236 @@
1
+ import {
2
+ CategoryScale,
3
+ Chart as ChartJS,
4
+ ChartOptions,
5
+ Filler,
6
+ Legend,
7
+ LineElement,
8
+ LinearScale,
9
+ PointElement,
10
+ Title,
11
+ Tooltip,
12
+ } from 'chart.js';
13
+ import React, { useMemo, useState } from 'react';
14
+ import { Theme, ToggleButton, webLightTheme } from '@fluentui/react-components';
15
+ import { createFluentTooltip, useGraphUtils } from '../../hooks/useGraphUtils';
16
+
17
+ import ChartDataLabels from 'chartjs-plugin-datalabels';
18
+ import { Line } from 'react-chartjs-2';
19
+ import RenderLegend from '../../components/RenderLegend/RenderLegend';
20
+ import { Stack } from '../../components/stack/Stack';
21
+ import { useGraphGlobalStyles } from '../../graphGlobalStyles/useGraphGlobalStyles';
22
+
23
+ ChartJS.register(ChartDataLabels);
24
+ ChartJS.register(
25
+ LineElement,
26
+ PointElement,
27
+ Filler,
28
+ CategoryScale,
29
+ LinearScale,
30
+ Tooltip,
31
+ Legend,
32
+ Title
33
+ );
34
+
35
+ export interface SteamChartProps<T> {
36
+ data: { label: string; data: T[] }[];
37
+ getPrimary: (datum: T) => string | number;
38
+ getSecondary: (datum: T) => number;
39
+ title?: string;
40
+ showDataLabels?: boolean;
41
+ theme?: Theme;
42
+ }
43
+
44
+ export default function SteamChart<T extends object>({
45
+ data,
46
+ getPrimary,
47
+ getSecondary,
48
+ title,
49
+ showDataLabels = false,
50
+ theme = webLightTheme,
51
+ }: SteamChartProps<T>) {
52
+ const [visibleSeries, setVisibleSeries] = useState(() =>
53
+ data.length > 1 ? data.map(s => s.label) : [data[0]?.label]
54
+ );
55
+ const [showPercent, setShowPercent] = useState(false);
56
+ const styles = useGraphGlobalStyles();
57
+
58
+ const { lightenColor, getFluentPalette } = useGraphUtils(theme);
59
+
60
+ const seriesColors = useMemo(() => {
61
+ return data.reduce((acc, series, idx) => {
62
+ const base = getFluentPalette(theme)[
63
+ idx % getFluentPalette(theme).length
64
+ ];
65
+ acc[series.label] = lightenColor(base, 0.3);
66
+ return acc;
67
+ }, {} as Record<string, string>);
68
+ }, [data, getFluentPalette, lightenColor, theme]);
69
+
70
+ const toggleSeries = React.useCallback(
71
+ (label: string) => {
72
+ setVisibleSeries(prev => {
73
+ const isVisible = prev.includes(label);
74
+ const next = isVisible
75
+ ? prev.filter(l => l !== label)
76
+ : [...prev, label];
77
+ return next.length === 0 ? [data[0].label] : next;
78
+ });
79
+ },
80
+ [data]
81
+ );
82
+
83
+ const allLabels = useMemo(() => {
84
+ const set = new Set<string | number>();
85
+ data.forEach(series => {
86
+ series.data.forEach(d => set.add(getPrimary(d)));
87
+ });
88
+ return Array.from(set);
89
+ }, [data, getPrimary]);
90
+
91
+ const totalPerPoint = useMemo(() => {
92
+ return allLabels.map(cat =>
93
+ data.reduce((sum, series) => {
94
+ const match = series.data.find(d => getPrimary(d) === cat);
95
+ return sum + (match ? getSecondary(match) : 0);
96
+ }, 0)
97
+ );
98
+ }, [allLabels, data, getPrimary, getSecondary]);
99
+
100
+ const chartData = useMemo(() => {
101
+ return {
102
+ labels: allLabels,
103
+ datasets: data
104
+ .filter(series => visibleSeries.includes(series.label))
105
+ .map((series, ) => ({
106
+ label: series.label,
107
+ fill: true,
108
+ backgroundColor: seriesColors[series.label],
109
+ borderColor: 'transparent',
110
+ borderWidth: 0,
111
+ data: allLabels.map((cat, index) => {
112
+ const match = series.data.find(d => getPrimary(d) === cat);
113
+ const rawValue = match ? getSecondary(match) : 0;
114
+ const total = totalPerPoint[index] || 1;
115
+ return showPercent
116
+ ? Math.round(((rawValue / total) * 100 + Number.EPSILON) * 100) / 100
117
+ : Math.round((rawValue + Number.EPSILON) * 100) / 100;
118
+ }),
119
+ tension: 0.5,
120
+ })),
121
+ };
122
+ }, [
123
+ data,
124
+ visibleSeries,
125
+ allLabels,
126
+ getPrimary,
127
+ getSecondary,
128
+ seriesColors,
129
+ totalPerPoint,
130
+ showPercent,
131
+ ]);
132
+
133
+ const { fontFamily, fontSize, labelColor, gridColor } = useMemo(() => ({
134
+ fontFamily: theme.fontFamilyBase,
135
+ fontSize: parseInt(theme.fontSizeBase200.replace('px', '')) || 14,
136
+ labelColor: theme.colorNeutralForeground1,
137
+ gridColor: theme.colorNeutralStroke2,
138
+ }), [theme]);
139
+
140
+ const options = useMemo<ChartOptions<'line'>>(() => ({
141
+ responsive: true,
142
+ maintainAspectRatio: false,
143
+ animation: {
144
+ duration: 800,
145
+ easing: 'easeOutQuart',
146
+ },
147
+ plugins: {
148
+ title: {
149
+ display: !!title,
150
+ text: title,
151
+ font: {
152
+ size: 14,
153
+ family: theme.fontFamilyBase,
154
+ weight: theme.fontWeightSemibold,
155
+ },
156
+ color: theme.colorNeutralForeground1,
157
+ padding: {
158
+ top: 20,
159
+ bottom: 20,
160
+ },
161
+ },
162
+ datalabels: {
163
+ display: showDataLabels,
164
+ color: theme.colorNeutralForeground1,
165
+ font: {
166
+ family: theme.fontFamilyBase,
167
+ size: parseInt(theme.fontSizeBase200.replace('px', '')) || 14,
168
+ },
169
+ },
170
+ legend: { display: false },
171
+ tooltip: createFluentTooltip<'line'>(theme),
172
+ },
173
+ interaction: {
174
+ mode: 'index',
175
+ intersect: false,
176
+ },
177
+ scales: {
178
+ x: {
179
+ stacked: true,
180
+ ticks: {
181
+ color: labelColor,
182
+ font: { family: fontFamily, size: fontSize },
183
+ },
184
+ grid: { color: gridColor },
185
+ },
186
+ y: {
187
+ stacked: true,
188
+ ticks: {
189
+ callback: (value: string | number) => (showPercent ? `${value}%` : value),
190
+ color: labelColor,
191
+ font: { family: fontFamily, size: fontSize },
192
+ },
193
+ grid: { color: gridColor },
194
+ min: 0,
195
+ max: showPercent ? 100 : undefined,
196
+ },
197
+ },
198
+ }), [
199
+ theme,
200
+ title,
201
+ showDataLabels,
202
+ createFluentTooltip,
203
+ labelColor,
204
+ fontFamily,
205
+ fontSize,
206
+ gridColor,
207
+ showPercent,
208
+ ]);
209
+
210
+ return (
211
+ <div className={styles.chartWithLegend}>
212
+ <div className={styles.chartArea}>
213
+ <Line data={chartData} options={options} />
214
+ </div>
215
+ <Stack justifyContent="center" alignItems="center" margin="7px">
216
+ <ToggleButton
217
+ onClick={() => setShowPercent(p => !p)}
218
+ shape="circular"
219
+ appearance="secondary"
220
+ size="small"
221
+ style={{ width: '150px' }}
222
+ >
223
+ {showPercent ? 'Show Raw Values' : 'Show %'}
224
+ </ToggleButton>
225
+ </Stack>
226
+ <div className={styles.legendArea}>
227
+ <RenderLegend
228
+ data={data}
229
+ visibleSeries={visibleSeries}
230
+ seriesColors={seriesColors}
231
+ toggleSeries={toggleSeries}
232
+ />
233
+ </div>
234
+ </div>
235
+ );
236
+ }
@@ -0,0 +1 @@
1
+ export * from './SteamChart';