react-native-metrify 0.1.0-alpha.1 → 0.1.0-alpha.3
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/README.md +111 -51
- package/package.json +1 -2
- package/src/core/animation/index.ts +0 -113
- package/src/core/animation/index.web.ts +0 -112
- package/src/core/hooks/index.ts +0 -66
- package/src/core/index.ts +0 -26
- package/src/core/layout/index.ts +0 -101
- package/src/core/math/index.ts +0 -72
- package/src/core/package.json +0 -13
- package/src/core/theme/ThemeProvider.tsx +0 -36
- package/src/core/theme/index.ts +0 -5
- package/src/core/theme/themes.ts +0 -132
- package/src/core/types/index.ts +0 -164
- package/src/core/utils/responsive.ts +0 -203
- package/src/core/utils/time.ts +0 -100
- package/src/index.ts +0 -13
- package/src/renderer-svg/adapters/index.ts +0 -84
- package/src/renderer-svg/index.ts +0 -8
- package/src/renderer-svg/package.json +0 -17
- package/src/renderer-svg/paths/arc.ts +0 -93
- package/src/renderer-svg/paths/index.ts +0 -6
- package/src/renderer-svg/paths/line.ts +0 -83
- package/src/renderer-svg/paths/rect.ts +0 -80
- package/src/renderer-svg/primitives/AnimatedCircle.tsx +0 -48
- package/src/renderer-svg/primitives/AnimatedPath.tsx +0 -48
- package/src/renderer-svg/primitives/Text.tsx +0 -73
- package/src/renderer-svg/primitives/index.ts +0 -6
- package/src/widgets/AreaChart/AreaChart.tsx +0 -213
- package/src/widgets/AreaChart/index.ts +0 -2
- package/src/widgets/AreaChart/types.ts +0 -34
- package/src/widgets/BarChart/BarChart.tsx +0 -249
- package/src/widgets/BarChart/index.ts +0 -10
- package/src/widgets/BarChart/types.ts +0 -27
- package/src/widgets/BoxPlot/BoxPlot.tsx +0 -252
- package/src/widgets/BoxPlot/index.ts +0 -2
- package/src/widgets/BoxPlot/types.ts +0 -27
- package/src/widgets/BubbleChart/BubbleChart.tsx +0 -175
- package/src/widgets/BubbleChart/index.ts +0 -2
- package/src/widgets/BubbleChart/types.ts +0 -33
- package/src/widgets/CandlestickChart/CandlestickChart.tsx +0 -204
- package/src/widgets/CandlestickChart/index.ts +0 -2
- package/src/widgets/CandlestickChart/types.ts +0 -29
- package/src/widgets/FunnelChart/FunnelChart.tsx +0 -172
- package/src/widgets/FunnelChart/index.ts +0 -2
- package/src/widgets/FunnelChart/types.ts +0 -22
- package/src/widgets/Gauge/Gauge.tsx +0 -235
- package/src/widgets/Gauge/index.ts +0 -5
- package/src/widgets/Gauge/types.ts +0 -19
- package/src/widgets/GroupedBarChart/GroupedBarChart.tsx +0 -190
- package/src/widgets/GroupedBarChart/index.ts +0 -2
- package/src/widgets/GroupedBarChart/types.ts +0 -30
- package/src/widgets/Heatmap/Heatmap.tsx +0 -216
- package/src/widgets/Heatmap/index.ts +0 -2
- package/src/widgets/Heatmap/types.ts +0 -27
- package/src/widgets/Histogram/Histogram.tsx +0 -173
- package/src/widgets/Histogram/index.ts +0 -2
- package/src/widgets/Histogram/types.ts +0 -18
- package/src/widgets/HorizontalBarChart/HorizontalBarChart.tsx +0 -125
- package/src/widgets/HorizontalBarChart/index.ts +0 -2
- package/src/widgets/HorizontalBarChart/types.ts +0 -23
- package/src/widgets/KPI/KPI.tsx +0 -222
- package/src/widgets/KPI/index.ts +0 -5
- package/src/widgets/KPI/types.ts +0 -19
- package/src/widgets/LineChart/LineChart.tsx +0 -364
- package/src/widgets/LineChart/index.ts +0 -10
- package/src/widgets/LineChart/types.ts +0 -34
- package/src/widgets/MultiLineSparkline/MultiLineSparkline.tsx +0 -234
- package/src/widgets/MultiLineSparkline/index.ts +0 -10
- package/src/widgets/MultiLineSparkline/types.ts +0 -25
- package/src/widgets/PieChart/PieChart.tsx +0 -275
- package/src/widgets/PieChart/index.ts +0 -10
- package/src/widgets/PieChart/types.ts +0 -26
- package/src/widgets/Progress/Progress.tsx +0 -201
- package/src/widgets/Progress/index.ts +0 -5
- package/src/widgets/Progress/types.ts +0 -19
- package/src/widgets/RadarChart/RadarChart.tsx +0 -213
- package/src/widgets/RadarChart/index.ts +0 -2
- package/src/widgets/RadarChart/types.ts +0 -29
- package/src/widgets/SankeyDiagram/SankeyDiagram.tsx +0 -272
- package/src/widgets/SankeyDiagram/index.ts +0 -2
- package/src/widgets/SankeyDiagram/types.ts +0 -29
- package/src/widgets/ScatterPlot/ScatterPlot.tsx +0 -167
- package/src/widgets/ScatterPlot/index.ts +0 -2
- package/src/widgets/ScatterPlot/types.ts +0 -32
- package/src/widgets/Sparkline/Sparkline.tsx +0 -203
- package/src/widgets/Sparkline/index.ts +0 -5
- package/src/widgets/Sparkline/types.ts +0 -18
- package/src/widgets/StackedBarChart/StackedBarChart.tsx +0 -181
- package/src/widgets/StackedBarChart/index.ts +0 -2
- package/src/widgets/StackedBarChart/types.ts +0 -29
- package/src/widgets/SunburstChart/SunburstChart.tsx +0 -176
- package/src/widgets/SunburstChart/index.ts +0 -2
- package/src/widgets/SunburstChart/types.ts +0 -22
- package/src/widgets/Treemap/Treemap.tsx +0 -191
- package/src/widgets/Treemap/index.ts +0 -2
- package/src/widgets/Treemap/types.ts +0 -23
- package/src/widgets/WaterfallChart/WaterfallChart.tsx +0 -226
- package/src/widgets/WaterfallChart/index.ts +0 -2
- package/src/widgets/WaterfallChart/types.ts +0 -26
- package/src/widgets/index.ts +0 -40
- package/src/widgets/package.json +0 -18
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RadarChart Widget - Multi-axis circular chart
|
|
3
|
-
*/
|
|
4
|
-
import React, { memo, useMemo } from 'react';
|
|
5
|
-
import { View, Text as RNText, StyleSheet } from 'react-native';
|
|
6
|
-
import Svg, { Polygon, Line as SvgLine, Circle } from 'react-native-svg';
|
|
7
|
-
import { useWidgetDimensions, useWidgetTheme, polarToCartesian, normalize } from '../../core';
|
|
8
|
-
import { Text } from '../../renderer-svg/primitives';
|
|
9
|
-
import { RadarChartWidgetProps } from './types';
|
|
10
|
-
|
|
11
|
-
export const RadarChart = memo<RadarChartWidgetProps>(({
|
|
12
|
-
data: widgetData,
|
|
13
|
-
width,
|
|
14
|
-
height,
|
|
15
|
-
loading = false,
|
|
16
|
-
theme: themeOverride,
|
|
17
|
-
showLabels = true,
|
|
18
|
-
showLegend = true,
|
|
19
|
-
showGrid = true,
|
|
20
|
-
gridLevels = 5,
|
|
21
|
-
size: customSize,
|
|
22
|
-
testID,
|
|
23
|
-
}) => {
|
|
24
|
-
const theme = useWidgetTheme(themeOverride);
|
|
25
|
-
const dimensions = useWidgetDimensions(width, height, 350, 350);
|
|
26
|
-
|
|
27
|
-
if (loading) {
|
|
28
|
-
return (
|
|
29
|
-
<View style={[styles.container, { width: dimensions.width, height: dimensions.height, backgroundColor: theme.colors.surface, borderRadius: theme.radius.md }]} testID={`${testID}-loading`}>
|
|
30
|
-
<RNText style={[styles.loadingText, { color: theme.colors.textSecondary }]}>Loading...</RNText>
|
|
31
|
-
</View>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (!widgetData || !widgetData.series || widgetData.series.length === 0) {
|
|
36
|
-
return (
|
|
37
|
-
<View style={[styles.container, { width: dimensions.width, height: dimensions.height, backgroundColor: theme.colors.surface, borderRadius: theme.radius.md }]} testID={`${testID}-empty`}>
|
|
38
|
-
<RNText style={[styles.emptyText, { color: theme.colors.textSecondary }]}>No data</RNText>
|
|
39
|
-
</View>
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const { series, title, maxValue: customMaxValue } = widgetData;
|
|
44
|
-
const padding = theme.spacing.md;
|
|
45
|
-
const titleHeight = title ? theme.fontScale.md + theme.spacing.sm : 0;
|
|
46
|
-
const legendHeight = showLegend ? 30 : 0;
|
|
47
|
-
const labelPadding = showLabels ? 40 : 10;
|
|
48
|
-
|
|
49
|
-
const availableSize = Math.min(
|
|
50
|
-
dimensions.width - padding * 2 - labelPadding * 2,
|
|
51
|
-
dimensions.height - padding * 2 - titleHeight - legendHeight - labelPadding * 2
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
const chartSize = customSize || availableSize;
|
|
55
|
-
const center = chartSize / 2 + labelPadding;
|
|
56
|
-
const radius = chartSize / 2;
|
|
57
|
-
|
|
58
|
-
// Get all axis names
|
|
59
|
-
const axes = series[0].data.map(d => d.axis);
|
|
60
|
-
const axisCount = axes.length;
|
|
61
|
-
|
|
62
|
-
// Find max value
|
|
63
|
-
const maxValue = customMaxValue || Math.max(...series.flatMap(s => s.data.map(d => d.value)));
|
|
64
|
-
|
|
65
|
-
// Calculate angle for each axis
|
|
66
|
-
const angleStep = (2 * Math.PI) / axisCount;
|
|
67
|
-
|
|
68
|
-
// Grid circles
|
|
69
|
-
const gridCircles = useMemo(() => {
|
|
70
|
-
return Array.from({ length: gridLevels }, (_, i) => {
|
|
71
|
-
const r = ((i + 1) / gridLevels) * radius;
|
|
72
|
-
return { radius: r, value: ((i + 1) / gridLevels) * maxValue };
|
|
73
|
-
});
|
|
74
|
-
}, [gridLevels, radius, maxValue]);
|
|
75
|
-
|
|
76
|
-
// Axis lines
|
|
77
|
-
const axisLines = useMemo(() => {
|
|
78
|
-
return axes.map((axis, index) => {
|
|
79
|
-
const angle = index * angleStep - Math.PI / 2; // Start from top
|
|
80
|
-
const point = polarToCartesian(center, center, radius, (angle * 180) / Math.PI + 90);
|
|
81
|
-
|
|
82
|
-
// Calculate label position (outside the circle)
|
|
83
|
-
const labelDistance = radius + 20;
|
|
84
|
-
const labelPoint = polarToCartesian(center, center, labelDistance, (angle * 180) / Math.PI + 90);
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
x1: center,
|
|
88
|
-
y1: center,
|
|
89
|
-
x2: point.x,
|
|
90
|
-
y2: point.y,
|
|
91
|
-
labelX: labelPoint.x,
|
|
92
|
-
labelY: labelPoint.y,
|
|
93
|
-
label: axis,
|
|
94
|
-
};
|
|
95
|
-
});
|
|
96
|
-
}, [axes, center, radius, angleStep]);
|
|
97
|
-
|
|
98
|
-
// Data polygons for each series
|
|
99
|
-
const dataPolygons = useMemo(() => {
|
|
100
|
-
return series.map(s => {
|
|
101
|
-
const points = s.data.map((point, index) => {
|
|
102
|
-
const angle = index * angleStep - Math.PI / 2;
|
|
103
|
-
const normalizedValue = normalize(point.value, 0, maxValue);
|
|
104
|
-
const r = normalizedValue * radius;
|
|
105
|
-
const coord = polarToCartesian(center, center, r, (angle * 180) / Math.PI + 90);
|
|
106
|
-
return `${coord.x},${coord.y}`;
|
|
107
|
-
}).join(' ');
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
points,
|
|
111
|
-
color: s.color,
|
|
112
|
-
label: s.label,
|
|
113
|
-
};
|
|
114
|
-
});
|
|
115
|
-
}, [series, center, radius, maxValue, angleStep]);
|
|
116
|
-
|
|
117
|
-
const svgSize = chartSize + labelPadding * 2;
|
|
118
|
-
|
|
119
|
-
return (
|
|
120
|
-
<View style={[styles.wrapper, { width: dimensions.width, height: dimensions.height, backgroundColor: theme.colors.surface, borderRadius: theme.radius.md, padding }]} testID={testID}>
|
|
121
|
-
{title && (
|
|
122
|
-
<RNText style={[styles.title, { color: theme.colors.text, fontSize: theme.fontScale.md, fontWeight: 'bold', marginBottom: theme.spacing.sm }]}>
|
|
123
|
-
{title}
|
|
124
|
-
</RNText>
|
|
125
|
-
)}
|
|
126
|
-
|
|
127
|
-
<View style={styles.chartContainer}>
|
|
128
|
-
<Svg width={svgSize} height={svgSize}>
|
|
129
|
-
{/* Grid circles */}
|
|
130
|
-
{showGrid && gridCircles.map((circle, index) => (
|
|
131
|
-
<Circle
|
|
132
|
-
key={`grid-${index}`}
|
|
133
|
-
cx={center}
|
|
134
|
-
cy={center}
|
|
135
|
-
r={circle.radius}
|
|
136
|
-
stroke={theme.colors.borderLight}
|
|
137
|
-
strokeWidth={1}
|
|
138
|
-
fill="transparent"
|
|
139
|
-
/>
|
|
140
|
-
))}
|
|
141
|
-
|
|
142
|
-
{/* Axis lines */}
|
|
143
|
-
{axisLines.map((line, index) => (
|
|
144
|
-
<SvgLine
|
|
145
|
-
key={`axis-${index}`}
|
|
146
|
-
x1={line.x1}
|
|
147
|
-
y1={line.y1}
|
|
148
|
-
x2={line.x2}
|
|
149
|
-
y2={line.y2}
|
|
150
|
-
stroke={theme.colors.border}
|
|
151
|
-
strokeWidth={1}
|
|
152
|
-
/>
|
|
153
|
-
))}
|
|
154
|
-
|
|
155
|
-
{/* Data polygons */}
|
|
156
|
-
{dataPolygons.map((polygon, index) => (
|
|
157
|
-
<Polygon
|
|
158
|
-
key={`polygon-${index}`}
|
|
159
|
-
points={polygon.points}
|
|
160
|
-
fill={polygon.color}
|
|
161
|
-
fillOpacity={0.2}
|
|
162
|
-
stroke={polygon.color}
|
|
163
|
-
strokeWidth={2}
|
|
164
|
-
/>
|
|
165
|
-
))}
|
|
166
|
-
|
|
167
|
-
{/* Axis labels */}
|
|
168
|
-
{showLabels && axisLines.map((line, index) => (
|
|
169
|
-
<Text
|
|
170
|
-
key={`label-${index}`}
|
|
171
|
-
x={line.labelX}
|
|
172
|
-
y={line.labelY}
|
|
173
|
-
text={line.label}
|
|
174
|
-
fontSize={theme.fontScale.xs}
|
|
175
|
-
fill={theme.colors.text}
|
|
176
|
-
textAnchor="middle"
|
|
177
|
-
/>
|
|
178
|
-
))}
|
|
179
|
-
</Svg>
|
|
180
|
-
</View>
|
|
181
|
-
|
|
182
|
-
{showLegend && (
|
|
183
|
-
<View style={styles.legend}>
|
|
184
|
-
{dataPolygons.map((polygon, index) => (
|
|
185
|
-
polygon.label && (
|
|
186
|
-
<View key={`legend-${index}`} style={styles.legendItem}>
|
|
187
|
-
<View style={[styles.legendColor, { backgroundColor: polygon.color }]} />
|
|
188
|
-
<RNText style={[styles.legendText, { color: theme.colors.textSecondary, fontSize: theme.fontScale.xs }]}>
|
|
189
|
-
{polygon.label}
|
|
190
|
-
</RNText>
|
|
191
|
-
</View>
|
|
192
|
-
)
|
|
193
|
-
))}
|
|
194
|
-
</View>
|
|
195
|
-
)}
|
|
196
|
-
</View>
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
RadarChart.displayName = 'RadarChart';
|
|
201
|
-
|
|
202
|
-
const styles = StyleSheet.create({
|
|
203
|
-
wrapper: { justifyContent: 'flex-start', alignItems: 'center' },
|
|
204
|
-
container: { justifyContent: 'center', alignItems: 'center' },
|
|
205
|
-
loadingText: { fontSize: 16 },
|
|
206
|
-
emptyText: { fontSize: 16 },
|
|
207
|
-
title: { textAlign: 'center', width: '100%' },
|
|
208
|
-
chartContainer: { alignItems: 'center', justifyContent: 'center' },
|
|
209
|
-
legend: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', marginTop: 12, gap: 12 },
|
|
210
|
-
legendItem: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
|
211
|
-
legendColor: { width: 12, height: 12, borderRadius: 2 },
|
|
212
|
-
legendText: { textTransform: 'uppercase', letterSpacing: 0.5 },
|
|
213
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RadarChart Widget types
|
|
3
|
-
*/
|
|
4
|
-
import { BaseWidgetProps } from '../../core';
|
|
5
|
-
|
|
6
|
-
export interface RadarDataPoint {
|
|
7
|
-
axis: string;
|
|
8
|
-
value: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface RadarSeries {
|
|
12
|
-
data: RadarDataPoint[];
|
|
13
|
-
color: string;
|
|
14
|
-
label?: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface RadarChartData {
|
|
18
|
-
series: RadarSeries[];
|
|
19
|
-
title?: string;
|
|
20
|
-
maxValue?: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface RadarChartWidgetProps extends BaseWidgetProps<RadarChartData> {
|
|
24
|
-
showLabels?: boolean;
|
|
25
|
-
showLegend?: boolean;
|
|
26
|
-
showGrid?: boolean;
|
|
27
|
-
gridLevels?: number;
|
|
28
|
-
size?: number;
|
|
29
|
-
}
|
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SankeyDiagram Widget - Flow visualization showing how values flow between nodes
|
|
3
|
-
*/
|
|
4
|
-
import React, { memo, useMemo } from 'react';
|
|
5
|
-
import { View, Text as RNText, StyleSheet } from 'react-native';
|
|
6
|
-
import Svg, { Rect, Path } from 'react-native-svg';
|
|
7
|
-
import { useWidgetDimensions, useWidgetTheme } from '../../core';
|
|
8
|
-
import { Text } from '../../renderer-svg/primitives';
|
|
9
|
-
import { SankeyDiagramWidgetProps } from './types';
|
|
10
|
-
|
|
11
|
-
interface LayoutNode {
|
|
12
|
-
id: string;
|
|
13
|
-
label: string;
|
|
14
|
-
x: number;
|
|
15
|
-
y: number;
|
|
16
|
-
height: number;
|
|
17
|
-
totalValue: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface LayoutLink {
|
|
21
|
-
source: LayoutNode;
|
|
22
|
-
target: LayoutNode;
|
|
23
|
-
value: number;
|
|
24
|
-
sourceY: number;
|
|
25
|
-
targetY: number;
|
|
26
|
-
color: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const SankeyDiagram = memo<SankeyDiagramWidgetProps>(({
|
|
30
|
-
data: widgetData,
|
|
31
|
-
width,
|
|
32
|
-
height,
|
|
33
|
-
loading = false,
|
|
34
|
-
theme: themeOverride,
|
|
35
|
-
nodeWidth = 20,
|
|
36
|
-
nodePadding = 20,
|
|
37
|
-
showLabels = true,
|
|
38
|
-
showValues = false,
|
|
39
|
-
testID,
|
|
40
|
-
}) => {
|
|
41
|
-
const theme = useWidgetTheme(themeOverride);
|
|
42
|
-
const dimensions = useWidgetDimensions(width, height, 400, 300);
|
|
43
|
-
|
|
44
|
-
if (loading) {
|
|
45
|
-
return (
|
|
46
|
-
<View style={[styles.container, { width: dimensions.width, height: dimensions.height, backgroundColor: theme.colors.surface, borderRadius: theme.radius.md }]} testID={`${testID}-loading`}>
|
|
47
|
-
<RNText style={[styles.loadingText, { color: theme.colors.textSecondary }]}>Loading...</RNText>
|
|
48
|
-
</View>
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!widgetData || !widgetData.nodes || widgetData.nodes.length === 0) {
|
|
53
|
-
return (
|
|
54
|
-
<View style={[styles.container, { width: dimensions.width, height: dimensions.height, backgroundColor: theme.colors.surface, borderRadius: theme.radius.md }]} testID={`${testID}-empty`}>
|
|
55
|
-
<RNText style={[styles.emptyText, { color: theme.colors.textSecondary }]}>No data</RNText>
|
|
56
|
-
</View>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const { nodes, links, title } = widgetData;
|
|
61
|
-
const padding = theme.spacing.md;
|
|
62
|
-
const titleHeight = title ? theme.fontScale.md + theme.spacing.sm : 0;
|
|
63
|
-
const labelWidth = showLabels ? 60 : 0;
|
|
64
|
-
|
|
65
|
-
const chartWidth = dimensions.width - padding * 2 - labelWidth * 2;
|
|
66
|
-
const chartHeight = dimensions.height - padding * 2 - titleHeight;
|
|
67
|
-
|
|
68
|
-
const defaultColors = [
|
|
69
|
-
theme.colors.chartPrimary,
|
|
70
|
-
theme.colors.chartSecondary,
|
|
71
|
-
theme.colors.chartTertiary,
|
|
72
|
-
theme.colors.chartQuaternary,
|
|
73
|
-
];
|
|
74
|
-
|
|
75
|
-
// Simple Sankey layout algorithm
|
|
76
|
-
const { layoutNodes, layoutLinks } = useMemo(() => {
|
|
77
|
-
// Calculate node values (sum of inputs/outputs)
|
|
78
|
-
const nodeValues = new Map<string, number>();
|
|
79
|
-
nodes.forEach(node => nodeValues.set(node.id, 0));
|
|
80
|
-
|
|
81
|
-
links.forEach(link => {
|
|
82
|
-
const sourceVal = nodeValues.get(link.source) || 0;
|
|
83
|
-
const targetVal = nodeValues.get(link.target) || 0;
|
|
84
|
-
nodeValues.set(link.source, Math.max(sourceVal, link.value));
|
|
85
|
-
nodeValues.set(link.target, targetVal + link.value);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Determine node layers (simple left-to-right)
|
|
89
|
-
const layers = new Map<string, number>();
|
|
90
|
-
const sourceNodes = new Set(links.map(l => l.source));
|
|
91
|
-
const targetNodes = new Set(links.map(l => l.target));
|
|
92
|
-
|
|
93
|
-
// Layer 0: nodes that are only sources
|
|
94
|
-
nodes.forEach(node => {
|
|
95
|
-
if (sourceNodes.has(node.id) && !targetNodes.has(node.id)) {
|
|
96
|
-
layers.set(node.id, 0);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Layer 1: nodes that receive from layer 0
|
|
101
|
-
nodes.forEach(node => {
|
|
102
|
-
if (!layers.has(node.id)) {
|
|
103
|
-
const hasLayer0Source = links.some(
|
|
104
|
-
l => l.target === node.id && layers.get(l.source) === 0
|
|
105
|
-
);
|
|
106
|
-
if (hasLayer0Source) {
|
|
107
|
-
layers.set(node.id, 1);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Layer 2: everything else
|
|
113
|
-
nodes.forEach(node => {
|
|
114
|
-
if (!layers.has(node.id)) {
|
|
115
|
-
layers.set(node.id, 2);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const maxLayer = Math.max(...Array.from(layers.values()));
|
|
120
|
-
const layerWidth = chartWidth / (maxLayer + 1);
|
|
121
|
-
|
|
122
|
-
// Position nodes
|
|
123
|
-
const positionedNodes: LayoutNode[] = [];
|
|
124
|
-
const nodesByLayer = new Map<number, string[]>();
|
|
125
|
-
|
|
126
|
-
layers.forEach((layer, nodeId) => {
|
|
127
|
-
if (!nodesByLayer.has(layer)) {
|
|
128
|
-
nodesByLayer.set(layer, []);
|
|
129
|
-
}
|
|
130
|
-
nodesByLayer.get(layer)!.push(nodeId);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
nodesByLayer.forEach((nodeIds, layer) => {
|
|
134
|
-
const layerHeight = chartHeight - (nodeIds.length - 1) * nodePadding;
|
|
135
|
-
const totalValue = nodeIds.reduce((sum, id) => sum + (nodeValues.get(id) || 0), 0);
|
|
136
|
-
|
|
137
|
-
let currentY = 0;
|
|
138
|
-
nodeIds.forEach(nodeId => {
|
|
139
|
-
const node = nodes.find(n => n.id === nodeId)!;
|
|
140
|
-
const nodeValue = nodeValues.get(nodeId) || 0;
|
|
141
|
-
const nodeHeight = Math.max((nodeValue / totalValue) * layerHeight, 20);
|
|
142
|
-
|
|
143
|
-
positionedNodes.push({
|
|
144
|
-
id: nodeId,
|
|
145
|
-
label: node.label,
|
|
146
|
-
x: layer * layerWidth + labelWidth,
|
|
147
|
-
y: currentY,
|
|
148
|
-
height: nodeHeight,
|
|
149
|
-
totalValue: nodeValue,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
currentY += nodeHeight + nodePadding;
|
|
153
|
-
});
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Create link paths
|
|
157
|
-
const positionedLinks: LayoutLink[] = [];
|
|
158
|
-
links.forEach((link, index) => {
|
|
159
|
-
const sourceNode = positionedNodes.find(n => n.id === link.source);
|
|
160
|
-
const targetNode = positionedNodes.find(n => n.id === link.target);
|
|
161
|
-
|
|
162
|
-
if (sourceNode && targetNode) {
|
|
163
|
-
// Calculate vertical positions within nodes
|
|
164
|
-
const sourceLinks = links.filter(l => l.source === link.source);
|
|
165
|
-
const targetLinks = links.filter(l => l.target === link.target);
|
|
166
|
-
|
|
167
|
-
const sourceLinkIndex = sourceLinks.findIndex(l => l === link);
|
|
168
|
-
const targetLinkIndex = targetLinks.findIndex(l => l === link);
|
|
169
|
-
|
|
170
|
-
const sourceTotal = sourceLinks.reduce((sum, l) => sum + l.value, 0);
|
|
171
|
-
const targetTotal = targetLinks.reduce((sum, l) => sum + l.value, 0);
|
|
172
|
-
|
|
173
|
-
const sourceOffsetBefore = sourceLinks
|
|
174
|
-
.slice(0, sourceLinkIndex)
|
|
175
|
-
.reduce((sum, l) => sum + l.value, 0);
|
|
176
|
-
const targetOffsetBefore = targetLinks
|
|
177
|
-
.slice(0, targetLinkIndex)
|
|
178
|
-
.reduce((sum, l) => sum + l.value, 0);
|
|
179
|
-
|
|
180
|
-
const sourceY = sourceNode.y + (sourceOffsetBefore / sourceTotal) * sourceNode.height;
|
|
181
|
-
const targetY = targetNode.y + (targetOffsetBefore / targetTotal) * targetNode.height;
|
|
182
|
-
const linkHeight = (link.value / sourceTotal) * sourceNode.height;
|
|
183
|
-
|
|
184
|
-
positionedLinks.push({
|
|
185
|
-
source: sourceNode,
|
|
186
|
-
target: targetNode,
|
|
187
|
-
value: link.value,
|
|
188
|
-
sourceY,
|
|
189
|
-
targetY,
|
|
190
|
-
color: link.color || defaultColors[index % defaultColors.length],
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
return { layoutNodes: positionedNodes, layoutLinks: positionedLinks };
|
|
196
|
-
}, [nodes, links, chartWidth, chartHeight, nodeWidth, nodePadding, labelWidth, defaultColors]);
|
|
197
|
-
|
|
198
|
-
// Create curved paths for links
|
|
199
|
-
function createLinkPath(link: LayoutLink, linkHeight: number): string {
|
|
200
|
-
const sourceX = link.source.x + nodeWidth;
|
|
201
|
-
const targetX = link.target.x;
|
|
202
|
-
const midX = (sourceX + targetX) / 2;
|
|
203
|
-
|
|
204
|
-
return `
|
|
205
|
-
M ${sourceX} ${link.sourceY}
|
|
206
|
-
C ${midX} ${link.sourceY}, ${midX} ${link.targetY}, ${targetX} ${link.targetY}
|
|
207
|
-
L ${targetX} ${link.targetY + linkHeight}
|
|
208
|
-
C ${midX} ${link.targetY + linkHeight}, ${midX} ${link.sourceY + linkHeight}, ${sourceX} ${link.sourceY + linkHeight}
|
|
209
|
-
Z
|
|
210
|
-
`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return (
|
|
214
|
-
<View style={[styles.wrapper, { width: dimensions.width, height: dimensions.height, backgroundColor: theme.colors.surface, borderRadius: theme.radius.md, padding }]} testID={testID}>
|
|
215
|
-
{title && (
|
|
216
|
-
<RNText style={[styles.title, { color: theme.colors.text, fontSize: theme.fontScale.md, fontWeight: 'bold', marginBottom: theme.spacing.sm }]}>
|
|
217
|
-
{title}
|
|
218
|
-
</RNText>
|
|
219
|
-
)}
|
|
220
|
-
|
|
221
|
-
<Svg width={chartWidth + labelWidth * 2} height={chartHeight}>
|
|
222
|
-
{/* Links */}
|
|
223
|
-
{layoutLinks.map((link, index) => {
|
|
224
|
-
const linkHeight = (link.value / link.source.totalValue) * link.source.height;
|
|
225
|
-
return (
|
|
226
|
-
<Path
|
|
227
|
-
key={`link-${index}`}
|
|
228
|
-
d={createLinkPath(link, linkHeight)}
|
|
229
|
-
fill={link.color}
|
|
230
|
-
opacity={0.4}
|
|
231
|
-
/>
|
|
232
|
-
);
|
|
233
|
-
})}
|
|
234
|
-
|
|
235
|
-
{/* Nodes */}
|
|
236
|
-
{layoutNodes.map((node, index) => (
|
|
237
|
-
<React.Fragment key={`node-${index}`}>
|
|
238
|
-
<Rect
|
|
239
|
-
x={node.x}
|
|
240
|
-
y={node.y}
|
|
241
|
-
width={nodeWidth}
|
|
242
|
-
height={node.height}
|
|
243
|
-
fill={theme.colors.chartPrimary}
|
|
244
|
-
rx={theme.radius.sm}
|
|
245
|
-
ry={theme.radius.sm}
|
|
246
|
-
/>
|
|
247
|
-
{showLabels && (
|
|
248
|
-
<Text
|
|
249
|
-
x={node.x < chartWidth / 2 ? node.x - 5 : node.x + nodeWidth + 5}
|
|
250
|
-
y={node.y + node.height / 2}
|
|
251
|
-
text={node.label}
|
|
252
|
-
fontSize={theme.fontScale.xs}
|
|
253
|
-
fill={theme.colors.text}
|
|
254
|
-
textAnchor={node.x < chartWidth / 2 ? 'end' : 'start'}
|
|
255
|
-
/>
|
|
256
|
-
)}
|
|
257
|
-
</React.Fragment>
|
|
258
|
-
))}
|
|
259
|
-
</Svg>
|
|
260
|
-
</View>
|
|
261
|
-
);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
SankeyDiagram.displayName = 'SankeyDiagram';
|
|
265
|
-
|
|
266
|
-
const styles = StyleSheet.create({
|
|
267
|
-
wrapper: { justifyContent: 'flex-start', alignItems: 'center' },
|
|
268
|
-
container: { justifyContent: 'center', alignItems: 'center' },
|
|
269
|
-
loadingText: { fontSize: 16 },
|
|
270
|
-
emptyText: { fontSize: 16 },
|
|
271
|
-
title: { textAlign: 'center', width: '100%' },
|
|
272
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SankeyDiagram Widget types - Flow visualization
|
|
3
|
-
*/
|
|
4
|
-
import { BaseWidgetProps } from '../../core';
|
|
5
|
-
|
|
6
|
-
export interface SankeyNode {
|
|
7
|
-
id: string;
|
|
8
|
-
label: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface SankeyLink {
|
|
12
|
-
source: string;
|
|
13
|
-
target: string;
|
|
14
|
-
value: number;
|
|
15
|
-
color?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface SankeyDiagramData {
|
|
19
|
-
nodes: SankeyNode[];
|
|
20
|
-
links: SankeyLink[];
|
|
21
|
-
title?: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface SankeyDiagramWidgetProps extends BaseWidgetProps<SankeyDiagramData> {
|
|
25
|
-
nodeWidth?: number;
|
|
26
|
-
nodePadding?: number;
|
|
27
|
-
showLabels?: boolean;
|
|
28
|
-
showValues?: boolean;
|
|
29
|
-
}
|