@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,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BubbleDataPoint,
|
|
3
|
+
CategoryScale,
|
|
4
|
+
Chart as ChartJS,
|
|
5
|
+
ChartOptions,
|
|
6
|
+
Legend,
|
|
7
|
+
LinearScale,
|
|
8
|
+
PointElement,
|
|
9
|
+
Title,
|
|
10
|
+
Tooltip,
|
|
11
|
+
} from 'chart.js';
|
|
12
|
+
import React, { useMemo, useState } from 'react';
|
|
13
|
+
import { Theme, webLightTheme } from '@fluentui/react-components';
|
|
14
|
+
|
|
15
|
+
import { Bubble } from 'react-chartjs-2';
|
|
16
|
+
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
|
17
|
+
import RenderLegend from '../../components/RenderLegend/RenderLegend';
|
|
18
|
+
import { useGraphGlobalStyles } from '../../graphGlobalStyles/useGraphGlobalStyles';
|
|
19
|
+
import { useGraphUtils } from '../../hooks/useGraphUtils';
|
|
20
|
+
|
|
21
|
+
ChartJS.register(
|
|
22
|
+
CategoryScale,
|
|
23
|
+
LinearScale,
|
|
24
|
+
PointElement,
|
|
25
|
+
Tooltip,
|
|
26
|
+
Legend,
|
|
27
|
+
ChartDataLabels,
|
|
28
|
+
Title
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
export interface BubbleChartProps<T> {
|
|
32
|
+
data: { label: string; data: T[] }[];
|
|
33
|
+
getPrimary: (datum: T) => string | number | Date;
|
|
34
|
+
getSecondary: (datum: T) => number;
|
|
35
|
+
getRadius: (datum: T) => number;
|
|
36
|
+
title?: string;
|
|
37
|
+
showDataLabels?: boolean;
|
|
38
|
+
theme?: Theme;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function BubbleChart<T extends object>({
|
|
42
|
+
data,
|
|
43
|
+
getPrimary,
|
|
44
|
+
getSecondary,
|
|
45
|
+
getRadius,
|
|
46
|
+
showDataLabels,
|
|
47
|
+
title,
|
|
48
|
+
theme = webLightTheme,
|
|
49
|
+
}: BubbleChartProps<T>) {
|
|
50
|
+
const [visibleSeries, setVisibleSeries] = useState(() =>
|
|
51
|
+
data.length > 1 ? data.map(s => s.label) : [data[0]?.label]
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const { lightenColor, getFluentPalette, createFluentTooltip } = useGraphUtils(
|
|
55
|
+
theme
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const styles = useGraphGlobalStyles();
|
|
59
|
+
const seriesColors = useMemo(() => {
|
|
60
|
+
return data.reduce((acc, series, idx) => {
|
|
61
|
+
const base = getFluentPalette(theme)[
|
|
62
|
+
idx % getFluentPalette(theme).length
|
|
63
|
+
];
|
|
64
|
+
const color = lightenColor(base, 0.3);
|
|
65
|
+
acc[series.label] = color;
|
|
66
|
+
return acc;
|
|
67
|
+
}, {} as Record<string, string>);
|
|
68
|
+
}, [data, theme]);
|
|
69
|
+
|
|
70
|
+
const toggleSeries = (label: string) => {
|
|
71
|
+
setVisibleSeries(prev => {
|
|
72
|
+
const isVisible = prev.includes(label);
|
|
73
|
+
const next = isVisible ? prev.filter(l => l !== label) : [...prev, label];
|
|
74
|
+
return next.length === 0 && data.length > 0 ? [data[0].label] : next;
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const chartData = useMemo(() => {
|
|
79
|
+
return {
|
|
80
|
+
datasets: data
|
|
81
|
+
.filter(series => visibleSeries.includes(series.label))
|
|
82
|
+
.map(series => ({
|
|
83
|
+
label: series.label,
|
|
84
|
+
data: series.data.map(d => ({
|
|
85
|
+
x: getPrimary(d),
|
|
86
|
+
y: getSecondary(d),
|
|
87
|
+
r: getRadius(d),
|
|
88
|
+
})) as BubbleDataPoint[],
|
|
89
|
+
backgroundColor: seriesColors[series.label],
|
|
90
|
+
borderColor: theme.colorNeutralStroke1,
|
|
91
|
+
borderWidth: 1,
|
|
92
|
+
hoverBorderWidth: 2,
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
}, [data, visibleSeries, getPrimary, getSecondary, getRadius, seriesColors]);
|
|
96
|
+
|
|
97
|
+
const { fontFamily, fontSize, labelColor, gridColor } = useMemo(() => ({
|
|
98
|
+
fontFamily: theme.fontFamilyBase,
|
|
99
|
+
fontSize: parseInt(theme.fontSizeBase200.replace('px', '')) || 14,
|
|
100
|
+
labelColor: theme.colorNeutralForeground1,
|
|
101
|
+
gridColor: theme.colorNeutralStroke2,
|
|
102
|
+
}), [theme]);
|
|
103
|
+
|
|
104
|
+
const options: ChartOptions<'bubble'> = useMemo(() => ({
|
|
105
|
+
responsive: true,
|
|
106
|
+
maintainAspectRatio: false,
|
|
107
|
+
plugins: {
|
|
108
|
+
title: {
|
|
109
|
+
display: !!title,
|
|
110
|
+
text: title,
|
|
111
|
+
font: {
|
|
112
|
+
size: 14,
|
|
113
|
+
family: theme.fontFamilyBase,
|
|
114
|
+
weight: theme.fontWeightSemibold,
|
|
115
|
+
},
|
|
116
|
+
color: theme.colorNeutralForeground1,
|
|
117
|
+
padding: {
|
|
118
|
+
top: 20,
|
|
119
|
+
bottom: 20,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
datalabels: {
|
|
123
|
+
display: showDataLabels,
|
|
124
|
+
color: theme.colorNeutralForeground1,
|
|
125
|
+
font: {
|
|
126
|
+
family: theme.fontFamilyBase,
|
|
127
|
+
size: parseInt(theme.fontSizeBase200.replace('px', '')) || 14,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
legend: { display: false },
|
|
131
|
+
tooltip: createFluentTooltip<'bubble'>(theme),
|
|
132
|
+
},
|
|
133
|
+
scales: {
|
|
134
|
+
x: {
|
|
135
|
+
type:
|
|
136
|
+
typeof getPrimary(data[0]?.data[0]) === 'number'
|
|
137
|
+
? 'linear'
|
|
138
|
+
: 'category',
|
|
139
|
+
ticks: {
|
|
140
|
+
color: labelColor,
|
|
141
|
+
font: { family: fontFamily, size: fontSize },
|
|
142
|
+
},
|
|
143
|
+
grid: { color: gridColor },
|
|
144
|
+
},
|
|
145
|
+
y: {
|
|
146
|
+
ticks: {
|
|
147
|
+
color: labelColor,
|
|
148
|
+
font: { family: fontFamily, size: fontSize },
|
|
149
|
+
},
|
|
150
|
+
grid: { color: gridColor },
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
}), [
|
|
154
|
+
title,
|
|
155
|
+
showDataLabels,
|
|
156
|
+
theme,
|
|
157
|
+
getPrimary,
|
|
158
|
+
getSecondary,
|
|
159
|
+
data,
|
|
160
|
+
fontFamily,
|
|
161
|
+
fontSize,
|
|
162
|
+
labelColor,
|
|
163
|
+
gridColor,
|
|
164
|
+
createFluentTooltip,
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
return (
|
|
168
|
+
<>
|
|
169
|
+
<div className={styles.chartWithLegend}>
|
|
170
|
+
<div className={styles.chartArea}>
|
|
171
|
+
<Bubble data={chartData} options={options} />
|
|
172
|
+
</div>
|
|
173
|
+
<div className={styles.legendArea}>
|
|
174
|
+
<RenderLegend
|
|
175
|
+
data={data}
|
|
176
|
+
visibleSeries={visibleSeries}
|
|
177
|
+
seriesColors={seriesColors}
|
|
178
|
+
toggleSeries={toggleSeries}
|
|
179
|
+
/>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
# BubbleChart Component
|
|
2
|
+
|
|
3
|
+
A powerful bubble chart component built with Chart.js and Fluent UI React. This component visualizes three-dimensional data where each bubble represents a data point with X and Y coordinates, and the bubble size represents a third dimension, making it perfect for complex data analysis and correlation visualization.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Three-Dimensional Data Visualization**: Display X, Y, and size (radius) data in a single chart
|
|
8
|
+
- **Multiple Series Support**: Compare multiple datasets with different colored bubble series
|
|
9
|
+
- **Interactive Legend**: Toggle series visibility with click interactions
|
|
10
|
+
- **Flexible Data Types**: Support for numeric, string, and Date values on axes
|
|
11
|
+
- **Fluent UI Integration**: Seamless integration with Fluent UI themes and design system
|
|
12
|
+
- **Data Labels**: Optional display of values directly on bubbles
|
|
13
|
+
- **Responsive Design**: Automatically adapts to container dimensions
|
|
14
|
+
- **TypeScript Support**: Full TypeScript support with generic types
|
|
15
|
+
- **Custom Tooltips**: Rich tooltips showing all three data dimensions
|
|
16
|
+
- **Dynamic Scaling**: Automatic axis scaling based on data ranges
|
|
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 { BubbleChart } from './components/bubbleChart/BubbleChart';
|
|
29
|
+
import { webLightTheme } from '@fluentui/react-components';
|
|
30
|
+
|
|
31
|
+
interface SalesData {
|
|
32
|
+
product: string;
|
|
33
|
+
revenue: number;
|
|
34
|
+
profit: number;
|
|
35
|
+
marketShare: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const salesData: SalesData[] = [
|
|
39
|
+
{ product: 'Product A', revenue: 150000, profit: 45000, marketShare: 15 },
|
|
40
|
+
{ product: 'Product B', revenue: 200000, profit: 60000, marketShare: 22 },
|
|
41
|
+
{ product: 'Product C', revenue: 120000, profit: 30000, marketShare: 12 },
|
|
42
|
+
{ product: 'Product D', revenue: 180000, profit: 50000, marketShare: 18 },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function App() {
|
|
46
|
+
return (
|
|
47
|
+
<div style={{ width: '800px', height: '600px' }}>
|
|
48
|
+
<BubbleChart
|
|
49
|
+
data={[
|
|
50
|
+
{ label: 'Product Performance', data: salesData }
|
|
51
|
+
]}
|
|
52
|
+
getPrimary={(datum) => datum.revenue} // X-axis: Revenue
|
|
53
|
+
getSecondary={(datum) => datum.profit} // Y-axis: Profit
|
|
54
|
+
getRadius={(datum) => datum.marketShare} // Bubble size: Market Share
|
|
55
|
+
title="Product Performance Analysis"
|
|
56
|
+
theme={webLightTheme}
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Props
|
|
64
|
+
|
|
65
|
+
### BubbleChartProps<T>
|
|
66
|
+
|
|
67
|
+
| Prop | Type | Required | Default | Description |
|
|
68
|
+
|------|------|----------|---------|-------------|
|
|
69
|
+
| `data` | `{ label: string; data: T[] }[]` | Yes | - | Array of data series with labels and data points |
|
|
70
|
+
| `getPrimary` | `(datum: T) => string \| number \| Date` | Yes | - | Function to extract the X-axis value from each data point |
|
|
71
|
+
| `getSecondary` | `(datum: T) => number` | Yes | - | Function to extract the Y-axis value from each data point |
|
|
72
|
+
| `getRadius` | `(datum: T) => number` | Yes | - | Function to extract the bubble size from each data point |
|
|
73
|
+
| `title` | `string` | No | - | Chart title displayed at the top |
|
|
74
|
+
| `showDataLabels` | `boolean` | No | `false` | Whether to show data labels on bubbles |
|
|
75
|
+
| `theme` | `Theme` | No | `webLightTheme` | Fluent UI theme object for styling |
|
|
76
|
+
|
|
77
|
+
## Advanced Usage
|
|
78
|
+
|
|
79
|
+
### Multiple Series Comparison
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
interface CompanyMetrics {
|
|
83
|
+
company: string;
|
|
84
|
+
employees: number;
|
|
85
|
+
revenue: number;
|
|
86
|
+
satisfaction: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const techCompanies: CompanyMetrics[] = [
|
|
90
|
+
{ company: 'TechCorp', employees: 5000, revenue: 2000000, satisfaction: 85 },
|
|
91
|
+
{ company: 'DataSoft', employees: 3000, revenue: 1500000, satisfaction: 78 },
|
|
92
|
+
{ company: 'CloudTech', employees: 8000, revenue: 3500000, satisfaction: 92 },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
const startups: CompanyMetrics[] = [
|
|
96
|
+
{ company: 'StartupA', employees: 50, revenue: 100000, satisfaction: 88 },
|
|
97
|
+
{ company: 'StartupB', employees: 80, revenue: 150000, satisfaction: 82 },
|
|
98
|
+
{ company: 'StartupC', employees: 120, revenue: 200000, satisfaction: 90 },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
<BubbleChart
|
|
102
|
+
data={[
|
|
103
|
+
{ label: 'Established Companies', data: techCompanies },
|
|
104
|
+
{ label: 'Startups', data: startups }
|
|
105
|
+
]}
|
|
106
|
+
getPrimary={(datum) => datum.employees} // X-axis: Number of employees
|
|
107
|
+
getSecondary={(datum) => datum.revenue} // Y-axis: Revenue
|
|
108
|
+
getRadius={(datum) => datum.satisfaction} // Bubble size: Satisfaction score
|
|
109
|
+
title="Company Performance: Size vs Revenue vs Satisfaction"
|
|
110
|
+
showDataLabels={true}
|
|
111
|
+
theme={webLightTheme}
|
|
112
|
+
/>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Financial Portfolio Analysis
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
interface Investment {
|
|
119
|
+
asset: string;
|
|
120
|
+
risk: number; // 1-10 scale
|
|
121
|
+
return: number; // Percentage
|
|
122
|
+
allocation: number; // Portfolio percentage
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const portfolioData: Investment[] = [
|
|
126
|
+
{ asset: 'Stocks', risk: 7, return: 12.5, allocation: 40 },
|
|
127
|
+
{ asset: 'Bonds', risk: 3, return: 4.2, allocation: 30 },
|
|
128
|
+
{ asset: 'Real Estate', risk: 5, return: 8.1, allocation: 20 },
|
|
129
|
+
{ asset: 'Commodities', risk: 8, return: 6.8, allocation: 10 },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
<BubbleChart
|
|
133
|
+
data={[{ label: 'Investment Portfolio', data: portfolioData }]}
|
|
134
|
+
getPrimary={(datum) => datum.risk} // X-axis: Risk level
|
|
135
|
+
getSecondary={(datum) => datum.return} // Y-axis: Expected return
|
|
136
|
+
getRadius={(datum) => datum.allocation} // Bubble size: Portfolio allocation
|
|
137
|
+
title="Portfolio Risk vs Return Analysis"
|
|
138
|
+
showDataLabels={true}
|
|
139
|
+
theme={webLightTheme}
|
|
140
|
+
/>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Time-based Analysis
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
interface ProjectData {
|
|
147
|
+
project: string;
|
|
148
|
+
startDate: Date;
|
|
149
|
+
duration: number; // Days
|
|
150
|
+
budget: number; // Dollars
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const projects: ProjectData[] = [
|
|
154
|
+
{ project: 'Project Alpha', startDate: new Date('2024-01-15'), duration: 90, budget: 150000 },
|
|
155
|
+
{ project: 'Project Beta', startDate: new Date('2024-02-01'), duration: 120, budget: 200000 },
|
|
156
|
+
{ project: 'Project Gamma', startDate: new Date('2024-03-10'), duration: 60, budget: 100000 },
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
<BubbleChart
|
|
160
|
+
data={[{ label: 'Project Timeline', data: projects }]}
|
|
161
|
+
getPrimary={(datum) => datum.startDate} // X-axis: Start date
|
|
162
|
+
getSecondary={(datum) => datum.duration} // Y-axis: Duration
|
|
163
|
+
getRadius={(datum) => datum.budget / 5000} // Bubble size: Budget (scaled)
|
|
164
|
+
title="Project Timeline vs Duration vs Budget"
|
|
165
|
+
theme={webLightTheme}
|
|
166
|
+
/>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Data Structure
|
|
170
|
+
|
|
171
|
+
The component expects data in the following format:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
interface ChartSeries<T> {
|
|
175
|
+
label: string; // Series name (appears in legend)
|
|
176
|
+
data: T[]; // Array of data points
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Each data point `T` should contain:
|
|
181
|
+
|
|
182
|
+
- A primary value (X-axis) - string, number, or Date
|
|
183
|
+
- A secondary value (Y-axis) - number
|
|
184
|
+
- A radius value (bubble size) - number
|
|
185
|
+
|
|
186
|
+
## Use Cases
|
|
187
|
+
|
|
188
|
+
Bubble charts are particularly effective for:
|
|
189
|
+
|
|
190
|
+
### Business Intelligence
|
|
191
|
+
|
|
192
|
+
- **Market Analysis**: Market size vs growth rate vs market share
|
|
193
|
+
- **Product Portfolio**: Price vs demand vs profit margin
|
|
194
|
+
- **Customer Segmentation**: Spend vs frequency vs lifetime value
|
|
195
|
+
|
|
196
|
+
### Scientific Data
|
|
197
|
+
|
|
198
|
+
- **Research Results**: Variable A vs Variable B vs sample size
|
|
199
|
+
- **Performance Metrics**: Speed vs accuracy vs resource usage
|
|
200
|
+
- **Correlation Analysis**: Three-variable relationship visualization
|
|
201
|
+
|
|
202
|
+
### Financial Analysis
|
|
203
|
+
|
|
204
|
+
- **Investment Comparison**: Risk vs return vs investment amount
|
|
205
|
+
- **Asset Allocation**: Volatility vs yield vs portfolio weight
|
|
206
|
+
- **Performance Tracking**: Time vs value vs volume
|
|
207
|
+
|
|
208
|
+
## Styling and Theme Integration
|
|
209
|
+
|
|
210
|
+
The component automatically applies Fluent UI theme styles:
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
// Bubble styling
|
|
214
|
+
backgroundColor: Derived from theme palette
|
|
215
|
+
borderColor: theme.colorNeutralStroke1
|
|
216
|
+
borderWidth: 1
|
|
217
|
+
hoverBorderWidth: 2
|
|
218
|
+
|
|
219
|
+
// Typography
|
|
220
|
+
fontFamily: theme.fontFamilyBase
|
|
221
|
+
fontSize: theme.fontSizeBase200
|
|
222
|
+
fontWeight: theme.fontWeightSemibold
|
|
223
|
+
|
|
224
|
+
// Colors
|
|
225
|
+
labelColor: theme.colorNeutralForeground1
|
|
226
|
+
gridColor: theme.colorNeutralStroke2
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## Interactive Features
|
|
230
|
+
|
|
231
|
+
### Legend Controls
|
|
232
|
+
|
|
233
|
+
- Click legend items to show/hide data series
|
|
234
|
+
- Visual feedback on hover states
|
|
235
|
+
- At least one series must remain visible
|
|
236
|
+
- Colors automatically assigned from theme palette
|
|
237
|
+
|
|
238
|
+
### Bubble Interactions
|
|
239
|
+
|
|
240
|
+
- Hover effects with border highlighting
|
|
241
|
+
- Rich tooltips showing all three data dimensions
|
|
242
|
+
- Smooth transitions and animations
|
|
243
|
+
|
|
244
|
+
### Responsive Scaling
|
|
245
|
+
|
|
246
|
+
- Automatic axis scaling based on data ranges
|
|
247
|
+
- Dynamic bubble size scaling
|
|
248
|
+
- Responsive layout for different screen sizes
|
|
249
|
+
|
|
250
|
+
## Performance Optimizations
|
|
251
|
+
|
|
252
|
+
The component includes several React optimizations:
|
|
253
|
+
|
|
254
|
+
````tsx
|
|
255
|
+
// Memoized color calculations
|
|
256
|
+
const seriesColors = useMemo(() => {
|
|
257
|
+
// Color generation logic
|
|
258
|
+
}, [data, theme]);
|
|
259
|
+
|
|
260
|
+
// Memoized chart data transformation
|
|
261
|
+
const chartData = useMemo(() => {
|
|
262
|
+
return {
|
|
263
|
+
datasets: data
|
|
264
|
+
.filter(series => visibleSeries.includes(series.label))
|
|
265
|
+
.map(series => ({
|
|
266
|
+
label: series.label,
|
|
267
|
+
data: series.data.map(d => ({
|
|
268
|
+
x: getPrimary(d),
|
|
269
|
+
y: getSecondary(d),
|
|
270
|
+
r: getRadius(d),
|
|
271
|
+
})) as BubbleDataPoint[],
|
|
272
|
+
// ... styling
|
|
273
|
+
})),
|
|
274
|
+
};
|
|
275
|
+
}, [data, visibleSeries, getPrimary, getSecondary, getRadius, seriesColors]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './BubbleChart';
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BarElement,
|
|
3
|
+
CategoryScale,
|
|
4
|
+
Chart as ChartJS,
|
|
5
|
+
ChartOptions,
|
|
6
|
+
Legend,
|
|
7
|
+
LinearScale,
|
|
8
|
+
Title,
|
|
9
|
+
Tooltip,
|
|
10
|
+
} from 'chart.js';
|
|
11
|
+
import React, { useMemo, useState } from 'react';
|
|
12
|
+
import { Theme, webLightTheme } from '@fluentui/react-components';
|
|
13
|
+
|
|
14
|
+
import { Bar } from 'react-chartjs-2';
|
|
15
|
+
import ChartDataLabels from 'chartjs-plugin-datalabels';
|
|
16
|
+
import RenderLegend from '../../components/RenderLegend/RenderLegend';
|
|
17
|
+
import { useGraphGlobalStyles } from '../../graphGlobalStyles/useGraphGlobalStyles';
|
|
18
|
+
import { useGraphUtils } from '../../hooks/useGraphUtils';
|
|
19
|
+
|
|
20
|
+
ChartJS.register(ChartDataLabels);
|
|
21
|
+
ChartJS.register(
|
|
22
|
+
CategoryScale,
|
|
23
|
+
LinearScale,
|
|
24
|
+
BarElement,
|
|
25
|
+
Tooltip,
|
|
26
|
+
Legend,
|
|
27
|
+
Title
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
export interface FloatingBarChartProps<T> {
|
|
31
|
+
data: { label: string; data: T[] }[];
|
|
32
|
+
getPrimary: (datum: T) => string | number;
|
|
33
|
+
getRange: (datum: T) => [number, number]; // e.g., [min, max]
|
|
34
|
+
title?: string;
|
|
35
|
+
showDataLabels?: boolean;
|
|
36
|
+
theme?: Theme;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default function FloatingBarChart<T extends object>({
|
|
40
|
+
data,
|
|
41
|
+
getPrimary,
|
|
42
|
+
getRange,
|
|
43
|
+
title,
|
|
44
|
+
showDataLabels = false,
|
|
45
|
+
theme = webLightTheme,
|
|
46
|
+
}: FloatingBarChartProps<T>) {
|
|
47
|
+
const [visibleSeries, setVisibleSeries] = useState(() =>
|
|
48
|
+
data.length > 1 ? data.map(s => s.label) : [data[0]?.label]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const styles = useGraphGlobalStyles();
|
|
52
|
+
const { lightenColor, getFluentPalette, createFluentTooltip } = useGraphUtils(
|
|
53
|
+
theme
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const seriesColors = useMemo(() => {
|
|
57
|
+
return data.reduce((acc, series, idx) => {
|
|
58
|
+
const base = getFluentPalette(theme)[
|
|
59
|
+
idx % getFluentPalette(theme).length
|
|
60
|
+
];
|
|
61
|
+
const color = lightenColor(base, 0.3);
|
|
62
|
+
acc[series.label] = color;
|
|
63
|
+
return acc;
|
|
64
|
+
}, {} as Record<string, string>);
|
|
65
|
+
}, [data, theme]);
|
|
66
|
+
|
|
67
|
+
const toggleSeries = (label: string) => {
|
|
68
|
+
setVisibleSeries(prev => {
|
|
69
|
+
const isVisible = prev.includes(label);
|
|
70
|
+
const next = isVisible ? prev.filter(l => l !== label) : [...prev, label];
|
|
71
|
+
return next.length === 0 && data.length > 0 ? [data[0].label] : next;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const allCategories = useMemo(() => {
|
|
76
|
+
const set = new Set<string | number>();
|
|
77
|
+
data.forEach(series => {
|
|
78
|
+
series.data.forEach(d => set.add(getPrimary(d)));
|
|
79
|
+
});
|
|
80
|
+
return Array.from(set);
|
|
81
|
+
}, [data, getPrimary]);
|
|
82
|
+
|
|
83
|
+
const chartData = useMemo(() => {
|
|
84
|
+
return {
|
|
85
|
+
labels: allCategories,
|
|
86
|
+
datasets: data
|
|
87
|
+
.filter(series => visibleSeries.includes(series.label))
|
|
88
|
+
.map(series => ({
|
|
89
|
+
label: series.label,
|
|
90
|
+
data: allCategories.map(cat => {
|
|
91
|
+
const match = series.data.find(d => getPrimary(d) === cat);
|
|
92
|
+
return match ? getRange(match) : [0, 0];
|
|
93
|
+
}),
|
|
94
|
+
backgroundColor: seriesColors[series.label],
|
|
95
|
+
borderRadius: 2,
|
|
96
|
+
})),
|
|
97
|
+
};
|
|
98
|
+
}, [data, visibleSeries, allCategories, getPrimary, getRange, seriesColors]);
|
|
99
|
+
|
|
100
|
+
const { fontFamily, fontSize, labelColor, gridColor } = useMemo(() => ({
|
|
101
|
+
fontFamily: theme.fontFamilyBase,
|
|
102
|
+
fontSize: parseInt(theme.fontSizeBase200.replace('px', '')) || 14,
|
|
103
|
+
labelColor: theme.colorNeutralForeground1,
|
|
104
|
+
gridColor: theme.colorNeutralStroke2,
|
|
105
|
+
}), [theme]);
|
|
106
|
+
|
|
107
|
+
const options: ChartOptions<'bar'> = useMemo(() => ({
|
|
108
|
+
responsive: true,
|
|
109
|
+
maintainAspectRatio: false,
|
|
110
|
+
plugins: {
|
|
111
|
+
title: {
|
|
112
|
+
display: !!title,
|
|
113
|
+
text: title,
|
|
114
|
+
font: {
|
|
115
|
+
size: 14,
|
|
116
|
+
family: theme.fontFamilyBase,
|
|
117
|
+
weight: theme.fontWeightSemibold,
|
|
118
|
+
},
|
|
119
|
+
color: theme.colorNeutralForeground1,
|
|
120
|
+
padding: {
|
|
121
|
+
top: 20,
|
|
122
|
+
bottom: 20,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
datalabels: {
|
|
126
|
+
display: showDataLabels,
|
|
127
|
+
color: theme.colorNeutralForeground1,
|
|
128
|
+
font: {
|
|
129
|
+
family: theme.fontFamilyBase,
|
|
130
|
+
size: parseInt(theme.fontSizeBase200.replace('px', '')) || 14,
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
legend: { display: false },
|
|
134
|
+
tooltip: createFluentTooltip<'bar'>(theme),
|
|
135
|
+
},
|
|
136
|
+
scales: {
|
|
137
|
+
x: {
|
|
138
|
+
ticks: {
|
|
139
|
+
color: labelColor,
|
|
140
|
+
font: { family: fontFamily, size: fontSize },
|
|
141
|
+
},
|
|
142
|
+
grid: { color: gridColor },
|
|
143
|
+
},
|
|
144
|
+
y: {
|
|
145
|
+
ticks: {
|
|
146
|
+
color: labelColor,
|
|
147
|
+
font: { family: fontFamily, size: fontSize },
|
|
148
|
+
},
|
|
149
|
+
grid: { color: gridColor },
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
}), [
|
|
153
|
+
title,
|
|
154
|
+
theme,
|
|
155
|
+
showDataLabels,
|
|
156
|
+
labelColor,
|
|
157
|
+
fontFamily,
|
|
158
|
+
fontSize,
|
|
159
|
+
gridColor,
|
|
160
|
+
createFluentTooltip
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div className={styles.chartWithLegend}>
|
|
165
|
+
<div className={styles.chartArea}>
|
|
166
|
+
<Bar data={chartData} options={options} />
|
|
167
|
+
</div>
|
|
168
|
+
<div className={styles.legendArea}>
|
|
169
|
+
<RenderLegend
|
|
170
|
+
data={data}
|
|
171
|
+
visibleSeries={visibleSeries}
|
|
172
|
+
seriesColors={seriesColors}
|
|
173
|
+
toggleSeries={toggleSeries}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|