@principal-ai/principal-view-react 0.14.14 → 0.14.16

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 (75) hide show
  1. package/dist/components/SequenceDiagramRenderer.d.ts +61 -0
  2. package/dist/components/SequenceDiagramRenderer.d.ts.map +1 -0
  3. package/dist/components/SequenceDiagramRenderer.js +184 -0
  4. package/dist/components/SequenceDiagramRenderer.js.map +1 -0
  5. package/dist/components/dashboard/DashboardRenderer.d.ts +9 -0
  6. package/dist/components/dashboard/DashboardRenderer.d.ts.map +1 -0
  7. package/dist/components/dashboard/DashboardRenderer.js +179 -0
  8. package/dist/components/dashboard/DashboardRenderer.js.map +1 -0
  9. package/dist/components/dashboard/MetricPanel.d.ts +9 -0
  10. package/dist/components/dashboard/MetricPanel.d.ts.map +1 -0
  11. package/dist/components/dashboard/MetricPanel.js +103 -0
  12. package/dist/components/dashboard/MetricPanel.js.map +1 -0
  13. package/dist/components/dashboard/MockDataProvider.d.ts +30 -0
  14. package/dist/components/dashboard/MockDataProvider.d.ts.map +1 -0
  15. package/dist/components/dashboard/MockDataProvider.js +270 -0
  16. package/dist/components/dashboard/MockDataProvider.js.map +1 -0
  17. package/dist/components/dashboard/components/BarChart.d.ts +9 -0
  18. package/dist/components/dashboard/components/BarChart.d.ts.map +1 -0
  19. package/dist/components/dashboard/components/BarChart.js +167 -0
  20. package/dist/components/dashboard/components/BarChart.js.map +1 -0
  21. package/dist/components/dashboard/components/LineChart.d.ts +9 -0
  22. package/dist/components/dashboard/components/LineChart.d.ts.map +1 -0
  23. package/dist/components/dashboard/components/LineChart.js +141 -0
  24. package/dist/components/dashboard/components/LineChart.js.map +1 -0
  25. package/dist/components/dashboard/components/MetricCard.d.ts +8 -0
  26. package/dist/components/dashboard/components/MetricCard.d.ts.map +1 -0
  27. package/dist/components/dashboard/components/MetricCard.js +163 -0
  28. package/dist/components/dashboard/components/MetricCard.js.map +1 -0
  29. package/dist/components/dashboard/components/SourceLink.d.ts +8 -0
  30. package/dist/components/dashboard/components/SourceLink.d.ts.map +1 -0
  31. package/dist/components/dashboard/components/SourceLink.js +39 -0
  32. package/dist/components/dashboard/components/SourceLink.js.map +1 -0
  33. package/dist/components/dashboard/components/TimeRangeSelector.d.ts +8 -0
  34. package/dist/components/dashboard/components/TimeRangeSelector.d.ts.map +1 -0
  35. package/dist/components/dashboard/components/TimeRangeSelector.js +167 -0
  36. package/dist/components/dashboard/components/TimeRangeSelector.js.map +1 -0
  37. package/dist/components/dashboard/components/index.d.ts +6 -0
  38. package/dist/components/dashboard/components/index.d.ts.map +1 -0
  39. package/dist/components/dashboard/components/index.js +6 -0
  40. package/dist/components/dashboard/components/index.js.map +1 -0
  41. package/dist/components/dashboard/index.d.ts +6 -0
  42. package/dist/components/dashboard/index.d.ts.map +1 -0
  43. package/dist/components/dashboard/index.js +8 -0
  44. package/dist/components/dashboard/index.js.map +1 -0
  45. package/dist/components/dashboard/types.d.ts +74 -0
  46. package/dist/components/dashboard/types.d.ts.map +1 -0
  47. package/dist/components/dashboard/types.js +8 -0
  48. package/dist/components/dashboard/types.js.map +1 -0
  49. package/dist/hooks/useSequenceLayout.d.ts +148 -0
  50. package/dist/hooks/useSequenceLayout.d.ts.map +1 -0
  51. package/dist/hooks/useSequenceLayout.js +225 -0
  52. package/dist/hooks/useSequenceLayout.js.map +1 -0
  53. package/dist/index.d.ts +6 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +4 -0
  56. package/dist/index.js.map +1 -1
  57. package/package.json +3 -3
  58. package/src/components/SequenceDiagramRenderer.tsx +459 -0
  59. package/src/components/dashboard/DashboardRenderer.tsx +317 -0
  60. package/src/components/dashboard/MetricPanel.tsx +254 -0
  61. package/src/components/dashboard/MockDataProvider.ts +330 -0
  62. package/src/components/dashboard/components/BarChart.tsx +299 -0
  63. package/src/components/dashboard/components/LineChart.tsx +279 -0
  64. package/src/components/dashboard/components/MetricCard.tsx +270 -0
  65. package/src/components/dashboard/components/SourceLink.tsx +63 -0
  66. package/src/components/dashboard/components/TimeRangeSelector.tsx +280 -0
  67. package/src/components/dashboard/components/index.ts +5 -0
  68. package/src/components/dashboard/index.ts +47 -0
  69. package/src/components/dashboard/types.ts +126 -0
  70. package/src/hooks/useSequenceLayout.ts +413 -0
  71. package/src/index.ts +62 -0
  72. package/src/stories/SequenceDiagram.stories.tsx +306 -0
  73. package/src/stories/dashboard/DashboardRenderer.stories.tsx +263 -0
  74. package/src/stories/dashboard/sample-dashboards/activity-feed-analytics.dashboard.json +300 -0
  75. package/src/stories/data/graph-converter-test-execution.json +50 -50
@@ -0,0 +1,330 @@
1
+ /**
2
+ * MockDataProvider
3
+ *
4
+ * Provides mock data for dashboard metrics during prototyping.
5
+ * Can use inline _mockData from the dashboard definition or generate realistic data.
6
+ */
7
+
8
+ import type {
9
+ DashboardDefinition,
10
+ MetricDefinition,
11
+ MetricData,
12
+ DataProvider,
13
+ TimeSeriesPoint,
14
+ } from './types';
15
+
16
+ export class MockDataProvider implements DataProvider {
17
+ private data: Record<string, MetricData> = {};
18
+
19
+ constructor(dashboard: DashboardDefinition) {
20
+ for (const metric of dashboard.metrics) {
21
+ this.data[metric.id] = this.resolveData(metric);
22
+ }
23
+ }
24
+
25
+ get(metricId: string): MetricData {
26
+ return this.data[metricId] || { error: `No data for metric: ${metricId}` };
27
+ }
28
+
29
+ getAll(): Record<string, MetricData> {
30
+ return this.data;
31
+ }
32
+
33
+ private resolveData(metric: MetricDefinition): MetricData {
34
+ // Use inline mock data if provided
35
+ if (metric._mockData) {
36
+ return this.transformMockData(metric._mockData, metric);
37
+ }
38
+
39
+ // Generate data based on metric type and query
40
+ return this.generateData(metric);
41
+ }
42
+
43
+ private transformMockData(
44
+ mockData: NonNullable<MetricDefinition['_mockData']>,
45
+ _metric: MetricDefinition
46
+ ): MetricData {
47
+ const result: MetricData = {};
48
+
49
+ if (mockData.current !== undefined) {
50
+ result.current = mockData.current;
51
+ }
52
+
53
+ if (mockData.previous !== undefined) {
54
+ result.previous = mockData.previous;
55
+ // Calculate change percent if not provided
56
+ if (result.current !== undefined) {
57
+ result.changePercent =
58
+ ((result.current - mockData.previous) / mockData.previous) * 100;
59
+ result.trend =
60
+ result.changePercent > 0
61
+ ? 'up'
62
+ : result.changePercent < 0
63
+ ? 'down'
64
+ : 'flat';
65
+ }
66
+ }
67
+
68
+ if (mockData.trend) {
69
+ result.trend = mockData.trend;
70
+ }
71
+
72
+ if (mockData.series) {
73
+ result.series = mockData.series;
74
+ }
75
+
76
+ if (mockData.breakdown) {
77
+ result.breakdown = mockData.breakdown;
78
+ }
79
+
80
+ if (mockData.histogram) {
81
+ result.histogram = mockData.histogram;
82
+ }
83
+
84
+ return result;
85
+ }
86
+
87
+ private generateData(metric: MetricDefinition): MetricData {
88
+ const { type, query } = metric;
89
+
90
+ // Time series data
91
+ if (query.timeGroup) {
92
+ return this.generateTimeSeries(metric);
93
+ }
94
+
95
+ // Single value
96
+ if (type === 'gauge') {
97
+ return this.generateGaugeData(metric);
98
+ }
99
+
100
+ if (type === 'counter') {
101
+ return this.generateCounterData(metric);
102
+ }
103
+
104
+ if (type === 'histogram') {
105
+ return this.generateHistogramData(metric);
106
+ }
107
+
108
+ // Fallback
109
+ return {
110
+ current: Math.floor(Math.random() * 1000),
111
+ trend: 'flat',
112
+ };
113
+ }
114
+
115
+ private generateTimeSeries(metric: MetricDefinition): MetricData {
116
+ const { query } = metric;
117
+ const points = this.getPointCount(query.timeGroup);
118
+ const series: TimeSeriesPoint[] = [];
119
+
120
+ const baseValue = this.getBaseValue(metric);
121
+ const variance = 0.2;
122
+
123
+ // Generate dates
124
+ const dates = this.generateDates(query.timeGroup, points);
125
+
126
+ if (query.groupBy && query.groupBy.length > 0) {
127
+ // Grouped data (e.g., by isMobile)
128
+ const groups = this.getGroupValues(query.groupBy[0]);
129
+
130
+ for (let i = 0; i < points; i++) {
131
+ const point: TimeSeriesPoint = { date: dates[i] };
132
+ for (const group of groups) {
133
+ point[group] = Math.floor(
134
+ baseValue * (0.3 + Math.random() * 0.7) * (1 + (Math.random() - 0.5) * variance)
135
+ );
136
+ }
137
+ series.push(point);
138
+ }
139
+ } else {
140
+ // Simple time series
141
+ for (let i = 0; i < points; i++) {
142
+ series.push({
143
+ date: dates[i],
144
+ value: Math.floor(baseValue * (1 + (Math.random() - 0.5) * variance)),
145
+ });
146
+ }
147
+ }
148
+
149
+ return { series };
150
+ }
151
+
152
+ private generateGaugeData(metric: MetricDefinition): MetricData {
153
+ const { query } = metric;
154
+
155
+ // Percentage derivations
156
+ if (
157
+ query.derivation === 'percentage' ||
158
+ query.derivation === 'error_rate' ||
159
+ query.derivation === 'success_rate'
160
+ ) {
161
+ const current = Math.random() * 100;
162
+ const previous = Math.random() * 100;
163
+ return {
164
+ current: Math.round(current * 10) / 10,
165
+ previous: Math.round(previous * 10) / 10,
166
+ trend: current > previous ? 'up' : current < previous ? 'down' : 'flat',
167
+ changePercent: Math.round(((current - previous) / previous) * 100 * 10) / 10,
168
+ };
169
+ }
170
+
171
+ // Rate
172
+ if (query.derivation === 'rate') {
173
+ const current = Math.floor(Math.random() * 1000);
174
+ const previous = Math.floor(Math.random() * 1000);
175
+ return {
176
+ current,
177
+ previous,
178
+ trend: current > previous ? 'up' : current < previous ? 'down' : 'flat',
179
+ changePercent: Math.round(((current - previous) / previous) * 100),
180
+ };
181
+ }
182
+
183
+ const current = Math.floor(Math.random() * 10000);
184
+ return { current, trend: 'flat' };
185
+ }
186
+
187
+ private generateCounterData(_metric: MetricDefinition): MetricData {
188
+ const current = Math.floor(Math.random() * 100000);
189
+ const previous = Math.floor(Math.random() * 100000);
190
+ return {
191
+ current,
192
+ previous,
193
+ trend: current > previous ? 'up' : current < previous ? 'down' : 'flat',
194
+ changePercent: Math.round(((current - previous) / previous) * 100),
195
+ };
196
+ }
197
+
198
+ private generateHistogramData(metric: MetricDefinition): MetricData {
199
+ const { query } = metric;
200
+
201
+ // Percentile value
202
+ if (query.percentile) {
203
+ const values = [50, 120, 200, 350, 500, 800, 1200];
204
+ const percentileIndex = Math.min(
205
+ Math.floor((query.percentile / 100) * values.length),
206
+ values.length - 1
207
+ );
208
+ const current = values[percentileIndex] + Math.floor(Math.random() * 100);
209
+ const previous = values[percentileIndex] + Math.floor(Math.random() * 100);
210
+
211
+ return {
212
+ current,
213
+ previous,
214
+ trend: current > previous ? 'up' : current < previous ? 'down' : 'flat',
215
+ changePercent: Math.round(((current - previous) / previous) * 100),
216
+ };
217
+ }
218
+
219
+ // Full histogram
220
+ return {
221
+ histogram: {
222
+ buckets: ['0-50ms', '50-100ms', '100-200ms', '200-500ms', '500ms-1s', '>1s'],
223
+ counts: [
224
+ Math.floor(Math.random() * 1000),
225
+ Math.floor(Math.random() * 800),
226
+ Math.floor(Math.random() * 400),
227
+ Math.floor(Math.random() * 200),
228
+ Math.floor(Math.random() * 100),
229
+ Math.floor(Math.random() * 50),
230
+ ],
231
+ },
232
+ };
233
+ }
234
+
235
+ private getPointCount(timeGroup?: string): number {
236
+ switch (timeGroup) {
237
+ case 'minute':
238
+ return 60;
239
+ case 'hour':
240
+ return 24;
241
+ case 'day':
242
+ return 7;
243
+ case 'week':
244
+ return 12;
245
+ case 'month':
246
+ return 12;
247
+ default:
248
+ return 7;
249
+ }
250
+ }
251
+
252
+ private getBaseValue(metric: MetricDefinition): number {
253
+ // Estimate reasonable base value based on metric type
254
+ if (metric.query.derivation === 'percentage' || metric.query.derivation === 'error_rate') {
255
+ return 50;
256
+ }
257
+ if (metric.unit === 'views' || metric.unit === 'count') {
258
+ return 10000;
259
+ }
260
+ if (metric.unit === 'milliseconds' || metric.unit === 'ms') {
261
+ return 200;
262
+ }
263
+ return 1000;
264
+ }
265
+
266
+ private generateDates(timeGroup: string | undefined, count: number): string[] {
267
+ const dates: string[] = [];
268
+ const now = new Date();
269
+
270
+ for (let i = count - 1; i >= 0; i--) {
271
+ const date = new Date(now);
272
+
273
+ switch (timeGroup) {
274
+ case 'minute':
275
+ date.setMinutes(date.getMinutes() - i);
276
+ dates.push(date.toISOString().slice(11, 16)); // HH:MM
277
+ break;
278
+ case 'hour':
279
+ date.setHours(date.getHours() - i);
280
+ dates.push(date.toISOString().slice(11, 13) + ':00'); // HH:00
281
+ break;
282
+ case 'day':
283
+ date.setDate(date.getDate() - i);
284
+ dates.push(date.toISOString().slice(0, 10)); // YYYY-MM-DD
285
+ break;
286
+ case 'week':
287
+ date.setDate(date.getDate() - i * 7);
288
+ dates.push(`W${this.getWeekNumber(date)}`);
289
+ break;
290
+ case 'month':
291
+ date.setMonth(date.getMonth() - i);
292
+ dates.push(date.toISOString().slice(0, 7)); // YYYY-MM
293
+ break;
294
+ default:
295
+ date.setDate(date.getDate() - i);
296
+ dates.push(date.toISOString().slice(0, 10));
297
+ }
298
+ }
299
+
300
+ return dates;
301
+ }
302
+
303
+ private getWeekNumber(date: Date): number {
304
+ const firstDayOfYear = new Date(date.getFullYear(), 0, 1);
305
+ const pastDaysOfYear = (date.getTime() - firstDayOfYear.getTime()) / 86400000;
306
+ return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7);
307
+ }
308
+
309
+ private getGroupValues(groupBy: string): string[] {
310
+ // Common group values based on attribute name
311
+ if (groupBy === 'isMobile') {
312
+ return ['mobile', 'desktop'];
313
+ }
314
+ if (groupBy === 'status' || groupBy === 'status.code') {
315
+ return ['success', 'error'];
316
+ }
317
+ if (groupBy === 'user.tier') {
318
+ return ['free', 'pro', 'enterprise'];
319
+ }
320
+ // Default generic groups
321
+ return ['group_a', 'group_b'];
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Create a mock data provider from a dashboard definition
327
+ */
328
+ export function createMockDataProvider(dashboard: DashboardDefinition): DataProvider {
329
+ return new MockDataProvider(dashboard);
330
+ }
@@ -0,0 +1,299 @@
1
+ /**
2
+ * BarChart
3
+ *
4
+ * Simple SVG-based bar chart for grouped/stacked data.
5
+ * For prototyping - can be replaced with a more robust charting library later.
6
+ */
7
+
8
+ import { useMemo } from 'react';
9
+ import { useTheme } from '@principal-ade/industry-theme';
10
+ import type { BarChartProps } from '../types';
11
+
12
+ const CHART_COLORS = [
13
+ '#3b82f6', // blue
14
+ '#22c55e', // green
15
+ '#f59e0b', // amber
16
+ '#ef4444', // red
17
+ '#8b5cf6', // violet
18
+ '#ec4899', // pink
19
+ ];
20
+
21
+ export function BarChart({
22
+ title,
23
+ data,
24
+ xKey = 'date',
25
+ series,
26
+ stacked = false,
27
+ unit,
28
+ height = 200,
29
+ onClick,
30
+ }: BarChartProps) {
31
+ const { theme } = useTheme();
32
+
33
+ const padding = { top: 20, right: 20, bottom: 40, left: 60 };
34
+ const chartWidth = 400;
35
+ const chartHeight = height;
36
+
37
+ const { bars, yAxisLabels, xAxisLabels } = useMemo(() => {
38
+ if (!data || data.length === 0 || !series || series.length === 0) {
39
+ return { bars: [], yAxisLabels: [], xAxisLabels: [], maxValue: 0 };
40
+ }
41
+
42
+ // Find max value
43
+ let max = 0;
44
+ for (const point of data) {
45
+ if (stacked) {
46
+ // Sum all series for stacked bars
47
+ let sum = 0;
48
+ for (const key of series) {
49
+ const val = point[key];
50
+ if (typeof val === 'number') sum += val;
51
+ }
52
+ if (sum > max) max = sum;
53
+ } else {
54
+ // Max of individual values for grouped bars
55
+ for (const key of series) {
56
+ const val = point[key];
57
+ if (typeof val === 'number' && val > max) max = val;
58
+ }
59
+ }
60
+ }
61
+
62
+ max = max * 1.1; // Add padding
63
+
64
+ const innerWidth = chartWidth - padding.left - padding.right;
65
+ const innerHeight = chartHeight - padding.top - padding.bottom;
66
+
67
+ const barGroupWidth = innerWidth / data.length;
68
+ const barPadding = barGroupWidth * 0.2;
69
+ const barAreaWidth = barGroupWidth - barPadding;
70
+
71
+ const barsResult: Array<{
72
+ x: number;
73
+ y: number;
74
+ width: number;
75
+ height: number;
76
+ color: string;
77
+ label: string;
78
+ value: number;
79
+ }> = [];
80
+
81
+ data.forEach((point, i) => {
82
+ const groupX = padding.left + i * barGroupWidth + barPadding / 2;
83
+
84
+ if (stacked) {
85
+ // Stacked bars
86
+ let currentY = padding.top + innerHeight;
87
+
88
+ series.forEach((key, keyIndex) => {
89
+ const val = point[key];
90
+ if (typeof val === 'number') {
91
+ const barHeight = (val / max) * innerHeight;
92
+ currentY -= barHeight;
93
+
94
+ barsResult.push({
95
+ x: groupX,
96
+ y: currentY,
97
+ width: barAreaWidth,
98
+ height: barHeight,
99
+ color: CHART_COLORS[keyIndex % CHART_COLORS.length],
100
+ label: key,
101
+ value: val,
102
+ });
103
+ }
104
+ });
105
+ } else {
106
+ // Grouped bars
107
+ const singleBarWidth = barAreaWidth / series.length;
108
+
109
+ series.forEach((key, keyIndex) => {
110
+ const val = point[key];
111
+ if (typeof val === 'number') {
112
+ const barHeight = (val / max) * innerHeight;
113
+ const barX = groupX + keyIndex * singleBarWidth;
114
+ const barY = padding.top + innerHeight - barHeight;
115
+
116
+ barsResult.push({
117
+ x: barX,
118
+ y: barY,
119
+ width: singleBarWidth * 0.9,
120
+ height: barHeight,
121
+ color: CHART_COLORS[keyIndex % CHART_COLORS.length],
122
+ label: key,
123
+ value: val,
124
+ });
125
+ }
126
+ });
127
+ }
128
+ });
129
+
130
+ // Y-axis labels
131
+ const yLabels: Array<{ value: string; y: number }> = [];
132
+ for (let i = 0; i <= 4; i++) {
133
+ const value = (max / 4) * (4 - i);
134
+ const y = padding.top + (i / 4) * innerHeight;
135
+ yLabels.push({
136
+ value: formatNumber(value),
137
+ y,
138
+ });
139
+ }
140
+
141
+ // X-axis labels
142
+ const xLabels: Array<{ value: string; x: number }> = [];
143
+ data.forEach((point, i) => {
144
+ const x = padding.left + i * barGroupWidth + barGroupWidth / 2;
145
+ xLabels.push({
146
+ value: String(point[xKey] || '').slice(-5), // Show last 5 chars (e.g., MM-DD)
147
+ x,
148
+ });
149
+ });
150
+
151
+ return {
152
+ bars: barsResult,
153
+ yAxisLabels: yLabels,
154
+ xAxisLabels: xLabels,
155
+ maxValue: max,
156
+ };
157
+ }, [data, series, xKey, stacked, chartWidth, chartHeight]);
158
+
159
+ return (
160
+ <div
161
+ onClick={onClick}
162
+ style={{
163
+ backgroundColor: theme.colors.surface || theme.colors.background,
164
+ border: `1px solid ${theme.colors.border}`,
165
+ borderRadius: theme.radii?.[2] || 8,
166
+ padding: theme.space?.[3] || 16,
167
+ cursor: onClick ? 'pointer' : 'default',
168
+ fontFamily: theme.fonts.body,
169
+ }}
170
+ >
171
+ {/* Title */}
172
+ <div
173
+ style={{
174
+ fontSize: theme.fontSizes[1],
175
+ fontWeight: theme.fontWeights.medium,
176
+ color: theme.colors.text,
177
+ marginBottom: theme.space?.[3] || 16,
178
+ }}
179
+ >
180
+ {title}
181
+ {unit && (
182
+ <span style={{ color: theme.colors.textSecondary, fontWeight: theme.fontWeights.body }}>
183
+ {' '}
184
+ ({unit})
185
+ </span>
186
+ )}
187
+ </div>
188
+
189
+ {/* Chart */}
190
+ <svg
191
+ width="100%"
192
+ height={chartHeight}
193
+ viewBox={`0 0 ${chartWidth} ${chartHeight}`}
194
+ preserveAspectRatio="xMidYMid meet"
195
+ >
196
+ {/* Grid lines */}
197
+ {yAxisLabels.map((label, i) => (
198
+ <line
199
+ key={i}
200
+ x1={padding.left}
201
+ y1={label.y}
202
+ x2={chartWidth - padding.right}
203
+ y2={label.y}
204
+ stroke={theme.colors.border}
205
+ strokeDasharray="4 4"
206
+ opacity={0.5}
207
+ />
208
+ ))}
209
+
210
+ {/* Y-axis labels */}
211
+ {yAxisLabels.map((label, i) => (
212
+ <text
213
+ key={i}
214
+ x={padding.left - 8}
215
+ y={label.y}
216
+ textAnchor="end"
217
+ dominantBaseline="middle"
218
+ fontSize={theme.fontSizes[0]}
219
+ fontFamily={theme.fonts.monospace}
220
+ fill={theme.colors.textSecondary}
221
+ >
222
+ {label.value}
223
+ </text>
224
+ ))}
225
+
226
+ {/* X-axis labels */}
227
+ {xAxisLabels.map((label, i) => (
228
+ <text
229
+ key={i}
230
+ x={label.x}
231
+ y={chartHeight - padding.bottom + 20}
232
+ textAnchor="middle"
233
+ fontSize={theme.fontSizes[0]}
234
+ fontFamily={theme.fonts.monospace}
235
+ fill={theme.colors.textSecondary}
236
+ >
237
+ {label.value}
238
+ </text>
239
+ ))}
240
+
241
+ {/* Bars */}
242
+ {bars.map((bar, i) => (
243
+ <rect
244
+ key={i}
245
+ x={bar.x}
246
+ y={bar.y}
247
+ width={bar.width}
248
+ height={bar.height}
249
+ fill={bar.color}
250
+ rx={2}
251
+ />
252
+ ))}
253
+ </svg>
254
+
255
+ {/* Legend */}
256
+ {series && series.length > 1 && (
257
+ <div
258
+ style={{
259
+ display: 'flex',
260
+ gap: theme.space?.[3] || 16,
261
+ marginTop: theme.space?.[3] || 12,
262
+ justifyContent: 'center',
263
+ }}
264
+ >
265
+ {series.map((s, i) => (
266
+ <div
267
+ key={s}
268
+ style={{
269
+ display: 'flex',
270
+ alignItems: 'center',
271
+ gap: 6,
272
+ fontSize: theme.fontSizes[0],
273
+ color: theme.colors.textSecondary,
274
+ fontFamily: theme.fonts.body,
275
+ }}
276
+ >
277
+ <div
278
+ style={{
279
+ width: 12,
280
+ height: 12,
281
+ backgroundColor: CHART_COLORS[i % CHART_COLORS.length],
282
+ borderRadius: 2,
283
+ }}
284
+ />
285
+ {s}
286
+ </div>
287
+ ))}
288
+ </div>
289
+ )}
290
+ </div>
291
+ );
292
+ }
293
+
294
+ function formatNumber(value: number): string {
295
+ if (value >= 1000000) return `${(value / 1000000).toFixed(1)}M`;
296
+ if (value >= 1000) return `${(value / 1000).toFixed(1)}K`;
297
+ if (value % 1 !== 0) return value.toFixed(1);
298
+ return Math.round(value).toString();
299
+ }