@bashem/rn-charts 0.0.2
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 +20 -0
- package/README.md +35 -0
- package/lib/module/index.js +14 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/skia/AreaChart/AreaChart.js +122 -0
- package/lib/module/skia/AreaChart/AreaChart.js.map +1 -0
- package/lib/module/skia/AreaChart/useAreaChart.js +141 -0
- package/lib/module/skia/AreaChart/useAreaChart.js.map +1 -0
- package/lib/module/skia/BarChart/BarChart.js +127 -0
- package/lib/module/skia/BarChart/BarChart.js.map +1 -0
- package/lib/module/skia/BarChart/useBarChart.js +172 -0
- package/lib/module/skia/BarChart/useBarChart.js.map +1 -0
- package/lib/module/skia/Common/VerticalLabel.js +73 -0
- package/lib/module/skia/Common/VerticalLabel.js.map +1 -0
- package/lib/module/skia/HeatMap/HeatMap.js +76 -0
- package/lib/module/skia/HeatMap/HeatMap.js.map +1 -0
- package/lib/module/skia/HeatMap/useHeatMap.js +139 -0
- package/lib/module/skia/HeatMap/useHeatMap.js.map +1 -0
- package/lib/module/skia/PieChart/PieChart.js +96 -0
- package/lib/module/skia/PieChart/PieChart.js.map +1 -0
- package/lib/module/skia/PieChart/usePieChart.js +103 -0
- package/lib/module/skia/PieChart/usePieChart.js.map +1 -0
- package/lib/module/skia/Popup.js +58 -0
- package/lib/module/skia/Popup.js.map +1 -0
- package/lib/module/skia/Progress/LinearProgress.js +69 -0
- package/lib/module/skia/Progress/LinearProgress.js.map +1 -0
- package/lib/module/skia/Progress/SemiCircleProgress.js +70 -0
- package/lib/module/skia/Progress/SemiCircleProgress.js.map +1 -0
- package/lib/module/skia/RadarChart/RadarChart.js +98 -0
- package/lib/module/skia/RadarChart/RadarChart.js.map +1 -0
- package/lib/module/skia/RadarChart/useRadarChart.js +164 -0
- package/lib/module/skia/RadarChart/useRadarChart.js.map +1 -0
- package/lib/module/skia/common.js +65 -0
- package/lib/module/skia/common.js.map +1 -0
- package/lib/module/util/colors.js +182 -0
- package/lib/module/util/colors.js.map +1 -0
- package/lib/module/util/util.js +71 -0
- package/lib/module/util/util.js.map +1 -0
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/index.d.ts +10 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/skia/AreaChart/AreaChart.d.ts +25 -0
- package/lib/typescript/src/skia/AreaChart/AreaChart.d.ts.map +1 -0
- package/lib/typescript/src/skia/AreaChart/useAreaChart.d.ts +47 -0
- package/lib/typescript/src/skia/AreaChart/useAreaChart.d.ts.map +1 -0
- package/lib/typescript/src/skia/BarChart/BarChart.d.ts +30 -0
- package/lib/typescript/src/skia/BarChart/BarChart.d.ts.map +1 -0
- package/lib/typescript/src/skia/BarChart/useBarChart.d.ts +41 -0
- package/lib/typescript/src/skia/BarChart/useBarChart.d.ts.map +1 -0
- package/lib/typescript/src/skia/Common/VerticalLabel.d.ts +17 -0
- package/lib/typescript/src/skia/Common/VerticalLabel.d.ts.map +1 -0
- package/lib/typescript/src/skia/HeatMap/HeatMap.d.ts +33 -0
- package/lib/typescript/src/skia/HeatMap/HeatMap.d.ts.map +1 -0
- package/lib/typescript/src/skia/HeatMap/useHeatMap.d.ts +25 -0
- package/lib/typescript/src/skia/HeatMap/useHeatMap.d.ts.map +1 -0
- package/lib/typescript/src/skia/PieChart/PieChart.d.ts +27 -0
- package/lib/typescript/src/skia/PieChart/PieChart.d.ts.map +1 -0
- package/lib/typescript/src/skia/PieChart/usePieChart.d.ts +13 -0
- package/lib/typescript/src/skia/PieChart/usePieChart.d.ts.map +1 -0
- package/lib/typescript/src/skia/Popup.d.ts +26 -0
- package/lib/typescript/src/skia/Popup.d.ts.map +1 -0
- package/lib/typescript/src/skia/Progress/LinearProgress.d.ts +18 -0
- package/lib/typescript/src/skia/Progress/LinearProgress.d.ts.map +1 -0
- package/lib/typescript/src/skia/Progress/SemiCircleProgress.d.ts +18 -0
- package/lib/typescript/src/skia/Progress/SemiCircleProgress.d.ts.map +1 -0
- package/lib/typescript/src/skia/RadarChart/RadarChart.d.ts +27 -0
- package/lib/typescript/src/skia/RadarChart/RadarChart.d.ts.map +1 -0
- package/lib/typescript/src/skia/RadarChart/useRadarChart.d.ts +41 -0
- package/lib/typescript/src/skia/RadarChart/useRadarChart.d.ts.map +1 -0
- package/lib/typescript/src/skia/common.d.ts +31 -0
- package/lib/typescript/src/skia/common.d.ts.map +1 -0
- package/lib/typescript/src/util/colors.d.ts +4 -0
- package/lib/typescript/src/util/colors.d.ts.map +1 -0
- package/lib/typescript/src/util/util.d.ts +33 -0
- package/lib/typescript/src/util/util.d.ts.map +1 -0
- package/package.json +172 -0
- package/src/index.tsx +12 -0
- package/src/skia/AreaChart/AreaChart.tsx +140 -0
- package/src/skia/AreaChart/useAreaChart.ts +180 -0
- package/src/skia/BarChart/BarChart.tsx +190 -0
- package/src/skia/BarChart/useBarChart.ts +210 -0
- package/src/skia/Common/VerticalLabel.tsx +91 -0
- package/src/skia/HeatMap/HeatMap.tsx +106 -0
- package/src/skia/HeatMap/useHeatMap.ts +175 -0
- package/src/skia/PieChart/PieChart.tsx +114 -0
- package/src/skia/PieChart/usePieChart.ts +156 -0
- package/src/skia/Popup.tsx +125 -0
- package/src/skia/Progress/LinearProgress.tsx +84 -0
- package/src/skia/Progress/SemiCircleProgress.tsx +82 -0
- package/src/skia/RadarChart/RadarChart.tsx +159 -0
- package/src/skia/RadarChart/useRadarChart.ts +208 -0
- package/src/skia/common.ts +82 -0
- package/src/util/colors.ts +186 -0
- package/src/util/util.ts +89 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { View, Button, useWindowDimensions } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
Canvas,
|
|
5
|
+
LinearGradient,
|
|
6
|
+
Rect,
|
|
7
|
+
RoundedRect,
|
|
8
|
+
} from '@shopify/react-native-skia';
|
|
9
|
+
import type { CommonStyle } from '../common';
|
|
10
|
+
|
|
11
|
+
export interface LinearProgressStyles extends CommonStyle {
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
radius?: number;
|
|
15
|
+
tintColor?: string;
|
|
16
|
+
backgroundColor?: string;
|
|
17
|
+
tintColors?: string[];
|
|
18
|
+
tineColorsPositions?: number[];
|
|
19
|
+
backgroundColors?: string[];
|
|
20
|
+
backgroundColorsPositions?: number[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface LinearProgressProps {
|
|
24
|
+
style: LinearProgressStyles;
|
|
25
|
+
progress: number; // value between 0 and 1
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default function LinearProgress({
|
|
29
|
+
progress,
|
|
30
|
+
style,
|
|
31
|
+
}: LinearProgressProps) {
|
|
32
|
+
const width = style.width ?? 300;
|
|
33
|
+
const height = style.height ?? 20;
|
|
34
|
+
const radius = style.radius ?? 0;
|
|
35
|
+
const tintColor = style.tintColor ?? '#4A90E2';
|
|
36
|
+
|
|
37
|
+
// Animate progress
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
// runTiming(progress, 0.7, { duration: 1000 }); // progress to 70%
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View>
|
|
44
|
+
<Canvas style={{ width, height }}>
|
|
45
|
+
{/* Background bar */}
|
|
46
|
+
<RoundedRect
|
|
47
|
+
x={0}
|
|
48
|
+
y={0}
|
|
49
|
+
width={width}
|
|
50
|
+
height={height}
|
|
51
|
+
r={radius}
|
|
52
|
+
color={style.backgroundColor}
|
|
53
|
+
>
|
|
54
|
+
{style.backgroundColors && (
|
|
55
|
+
<LinearGradient
|
|
56
|
+
start={{ x: 0, y: 0 }}
|
|
57
|
+
end={{ x: width, y: 0 }}
|
|
58
|
+
colors={style.backgroundColors}
|
|
59
|
+
positions={style.backgroundColorsPositions}
|
|
60
|
+
/>
|
|
61
|
+
)}
|
|
62
|
+
</RoundedRect>
|
|
63
|
+
{/* Foreground progress */}
|
|
64
|
+
<RoundedRect
|
|
65
|
+
x={0}
|
|
66
|
+
y={0}
|
|
67
|
+
width={progress * width} // animate width
|
|
68
|
+
height={height}
|
|
69
|
+
r={radius}
|
|
70
|
+
color={style.tintColor}
|
|
71
|
+
>
|
|
72
|
+
{style.tintColors && (
|
|
73
|
+
<LinearGradient
|
|
74
|
+
start={{ x: 0, y: 0 }}
|
|
75
|
+
end={{ x: width, y: 0 }}
|
|
76
|
+
colors={style.tintColors}
|
|
77
|
+
positions={style.tineColorsPositions}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</RoundedRect>
|
|
81
|
+
</Canvas>
|
|
82
|
+
</View>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { View } from 'react-native';
|
|
2
|
+
import {
|
|
3
|
+
Canvas,
|
|
4
|
+
LinearGradient,
|
|
5
|
+
RoundedRect,
|
|
6
|
+
} from '@shopify/react-native-skia';
|
|
7
|
+
import type { CommonStyle } from '../common';
|
|
8
|
+
|
|
9
|
+
export interface SemiCircleProgressStyles extends CommonStyle {
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
radius?: number;
|
|
13
|
+
tintColor?: string;
|
|
14
|
+
backgroundColor?: string;
|
|
15
|
+
tintColors?: string[];
|
|
16
|
+
tineColorsPositions?: number[];
|
|
17
|
+
backgroundColors?: string[];
|
|
18
|
+
backgroundColorsPositions?: number[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SemiCircleProgressProps {
|
|
22
|
+
style: SemiCircleProgressStyles;
|
|
23
|
+
progress: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default function SemiCircleProgress({
|
|
27
|
+
progress,
|
|
28
|
+
style,
|
|
29
|
+
}: SemiCircleProgressProps) {
|
|
30
|
+
const width = style.width ?? 300;
|
|
31
|
+
const height = style.height ?? 20;
|
|
32
|
+
const radius = style.radius ?? 0;
|
|
33
|
+
const tintColor = style.tintColor ?? '#4A90E2';
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<View
|
|
37
|
+
onLayout={(event) => {
|
|
38
|
+
const { width, height } = event.nativeEvent.layout;
|
|
39
|
+
console.log('Layout width, height:', width, height);
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Canvas style={{ width, height }}>
|
|
43
|
+
{/* Background bar */}
|
|
44
|
+
<RoundedRect
|
|
45
|
+
x={0}
|
|
46
|
+
y={0}
|
|
47
|
+
width={width}
|
|
48
|
+
height={height}
|
|
49
|
+
r={radius}
|
|
50
|
+
color={style.backgroundColor}
|
|
51
|
+
>
|
|
52
|
+
{style.backgroundColors && (
|
|
53
|
+
<LinearGradient
|
|
54
|
+
start={{ x: 0, y: 0 }}
|
|
55
|
+
end={{ x: width, y: 0 }}
|
|
56
|
+
colors={style.backgroundColors}
|
|
57
|
+
positions={style.backgroundColorsPositions}
|
|
58
|
+
/>
|
|
59
|
+
)}
|
|
60
|
+
</RoundedRect>
|
|
61
|
+
{/* Foreground progress */}
|
|
62
|
+
<RoundedRect
|
|
63
|
+
x={0}
|
|
64
|
+
y={0}
|
|
65
|
+
width={progress * width} // animate width
|
|
66
|
+
height={height}
|
|
67
|
+
r={radius}
|
|
68
|
+
color={style.tintColor}
|
|
69
|
+
>
|
|
70
|
+
{style.tintColors && (
|
|
71
|
+
<LinearGradient
|
|
72
|
+
start={{ x: 0, y: 0 }}
|
|
73
|
+
end={{ x: width, y: 0 }}
|
|
74
|
+
colors={style.tintColors}
|
|
75
|
+
positions={style.tineColorsPositions}
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</RoundedRect>
|
|
79
|
+
</Canvas>
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React, { cloneElement, useMemo, type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
type ViewStyle,
|
|
7
|
+
type TextStyle,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import {
|
|
10
|
+
Canvas,
|
|
11
|
+
Path,
|
|
12
|
+
Group,
|
|
13
|
+
Circle,
|
|
14
|
+
} from '@shopify/react-native-skia';
|
|
15
|
+
import { type CommonStyle } from '../common';
|
|
16
|
+
import useRadarChart from './useRadarChart';
|
|
17
|
+
|
|
18
|
+
export interface RadarDatum {
|
|
19
|
+
values: number[];
|
|
20
|
+
backgroundColor?: string;
|
|
21
|
+
strokeColor?: string;
|
|
22
|
+
strokeWidth?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RadarChartStyle extends CommonStyle {
|
|
26
|
+
size?: number;
|
|
27
|
+
strokeWidth?: number;
|
|
28
|
+
strokeColor?: string;
|
|
29
|
+
centerDotRadius?: number;
|
|
30
|
+
centerDotColor?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface RadarChartProps {
|
|
34
|
+
data: RadarDatum[];
|
|
35
|
+
labels?: string[];
|
|
36
|
+
levels?: number;
|
|
37
|
+
labelViews?: ReactNode[];
|
|
38
|
+
maxValue?: number;
|
|
39
|
+
minValue?: number;
|
|
40
|
+
style?: RadarChartStyle;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function RadarChart(props: RadarChartProps) {
|
|
44
|
+
const {
|
|
45
|
+
size,
|
|
46
|
+
cx,
|
|
47
|
+
cy,
|
|
48
|
+
gridPaths,
|
|
49
|
+
axisPaths,
|
|
50
|
+
dataPaths,
|
|
51
|
+
formattedLabels,
|
|
52
|
+
formattedLabelViews,
|
|
53
|
+
strokeWidth,
|
|
54
|
+
strokeColor,
|
|
55
|
+
centerDotRadius,
|
|
56
|
+
centerDotColor,
|
|
57
|
+
} = useRadarChart(props);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<View
|
|
61
|
+
style={[
|
|
62
|
+
{
|
|
63
|
+
width: size,
|
|
64
|
+
height: size,
|
|
65
|
+
backgroundColor: props.style?.backgroundColor,
|
|
66
|
+
},
|
|
67
|
+
]}
|
|
68
|
+
>
|
|
69
|
+
<Canvas style={{ width: size, height: size }}>
|
|
70
|
+
<Group>
|
|
71
|
+
{/* grid polygons */}
|
|
72
|
+
{gridPaths.map((gridPath, index) => (
|
|
73
|
+
<Path
|
|
74
|
+
key={index}
|
|
75
|
+
path={gridPath}
|
|
76
|
+
style="stroke"
|
|
77
|
+
strokeWidth={strokeWidth}
|
|
78
|
+
color={strokeColor}
|
|
79
|
+
/>
|
|
80
|
+
))}
|
|
81
|
+
|
|
82
|
+
{axisPaths.map((ap, index) => (
|
|
83
|
+
<Path
|
|
84
|
+
key={index}
|
|
85
|
+
path={ap}
|
|
86
|
+
style="stroke"
|
|
87
|
+
strokeWidth={strokeWidth}
|
|
88
|
+
color={strokeColor}
|
|
89
|
+
/>
|
|
90
|
+
))}
|
|
91
|
+
|
|
92
|
+
{dataPaths.map((pathDatum, index) => (
|
|
93
|
+
<Group key={index}>
|
|
94
|
+
<Path
|
|
95
|
+
path={pathDatum.path}
|
|
96
|
+
style="fill"
|
|
97
|
+
color={pathDatum.backgroundColor}
|
|
98
|
+
/>
|
|
99
|
+
<Path
|
|
100
|
+
path={pathDatum.path}
|
|
101
|
+
style="stroke"
|
|
102
|
+
strokeWidth={pathDatum.strokeWidth}
|
|
103
|
+
color={pathDatum.strokeColor}
|
|
104
|
+
/>
|
|
105
|
+
</Group>
|
|
106
|
+
))}
|
|
107
|
+
|
|
108
|
+
{/* center dot */}
|
|
109
|
+
<Circle cx={cx} cy={cy} r={centerDotRadius} color={centerDotColor} />
|
|
110
|
+
</Group>
|
|
111
|
+
</Canvas>
|
|
112
|
+
|
|
113
|
+
{/* Labels overlay */}
|
|
114
|
+
<View
|
|
115
|
+
style={[
|
|
116
|
+
StyleSheet.absoluteFill,
|
|
117
|
+
{ justifyContent: 'center', alignItems: 'center' },
|
|
118
|
+
]}
|
|
119
|
+
pointerEvents="none"
|
|
120
|
+
>
|
|
121
|
+
{formattedLabelViews &&
|
|
122
|
+
formattedLabelViews.map((viewDataum, index) => {
|
|
123
|
+
let style: ViewStyle = {
|
|
124
|
+
position: 'absolute',
|
|
125
|
+
bottom: viewDataum.y,
|
|
126
|
+
};
|
|
127
|
+
if (viewDataum.align === 'left') style.left = viewDataum.x;
|
|
128
|
+
if (viewDataum.align === 'right') style.right = viewDataum.x;
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<View style={style} key={index}>
|
|
132
|
+
{viewDataum.view}
|
|
133
|
+
</View>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
{formattedLabels &&
|
|
137
|
+
formattedLabels.map((formatedlabel, index) => {
|
|
138
|
+
let style: TextStyle = {
|
|
139
|
+
position: 'absolute',
|
|
140
|
+
bottom: formatedlabel.y,
|
|
141
|
+
fontSize: 12,
|
|
142
|
+
includeFontPadding: false,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (formatedlabel.align === 'left') style.left = formatedlabel.x;
|
|
146
|
+
if (formatedlabel.align === 'right') style.right = formatedlabel.x;
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<Text key={index} style={style}>
|
|
150
|
+
{formatedlabel.label}
|
|
151
|
+
</Text>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</View>
|
|
155
|
+
</View>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default RadarChart;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import { Skia, type SkPath } from "@shopify/react-native-skia";
|
|
3
|
+
import { arrayFrom } from "../../util/util";
|
|
4
|
+
import { getPaddings, type CommonStyle } from "../common";
|
|
5
|
+
import type { RadarChartProps } from "./RadarChart";
|
|
6
|
+
|
|
7
|
+
type Point = { x: number; y: number };
|
|
8
|
+
|
|
9
|
+
export interface RadarDatum {
|
|
10
|
+
values: number[];
|
|
11
|
+
backgroundColor?: string;
|
|
12
|
+
strokeColor?: string;
|
|
13
|
+
strokeWidth?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface RadarChartStyle extends CommonStyle {
|
|
17
|
+
size?: number;
|
|
18
|
+
strokeWidth?: number;
|
|
19
|
+
strokeColor?: string;
|
|
20
|
+
centerDotRadius?: number;
|
|
21
|
+
centerDotColor?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function useRadarChart({
|
|
25
|
+
data,
|
|
26
|
+
labels,
|
|
27
|
+
labelViews,
|
|
28
|
+
levels = 5,
|
|
29
|
+
maxValue,
|
|
30
|
+
minValue = 0,
|
|
31
|
+
style,
|
|
32
|
+
}: RadarChartProps) {
|
|
33
|
+
const size = style?.size ?? 200;
|
|
34
|
+
const { paddingHorizontal, paddingVertical } = getPaddings(style);
|
|
35
|
+
|
|
36
|
+
const radius = size / 2 - Math.max(paddingHorizontal, paddingVertical) / 2;
|
|
37
|
+
const cx = size / 2;
|
|
38
|
+
const cy = size / 2;
|
|
39
|
+
|
|
40
|
+
// Compute max value
|
|
41
|
+
const safeMax = useMemo(() => {
|
|
42
|
+
if (maxValue !== undefined) return maxValue;
|
|
43
|
+
let m = minValue;
|
|
44
|
+
for (let datum of data)
|
|
45
|
+
for (let v of datum.values) m = Math.max(m, v);
|
|
46
|
+
return m;
|
|
47
|
+
}, [data, maxValue]);
|
|
48
|
+
|
|
49
|
+
// Determine number of axes
|
|
50
|
+
const angles = useMemo(() => {
|
|
51
|
+
let len = data[0]?.values.length ?? 0;
|
|
52
|
+
if (len === 0 && labels) len = labels.length;
|
|
53
|
+
if (len === 0 && labelViews) len = labelViews.length;
|
|
54
|
+
if (len === 0) return [];
|
|
55
|
+
|
|
56
|
+
return arrayFrom(len, 1).map(
|
|
57
|
+
(i) => -Math.PI / 2 + (i * 2 * Math.PI) / len
|
|
58
|
+
);
|
|
59
|
+
}, [data[0]?.values.length, labels?.length, labelViews?.length]);
|
|
60
|
+
|
|
61
|
+
const pointFor = (angle: number, r: number): Point => ({
|
|
62
|
+
x: cx + Math.cos(angle) * r,
|
|
63
|
+
y: cy + Math.sin(angle) * r,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ------------------------------
|
|
67
|
+
// Grid paths
|
|
68
|
+
// ------------------------------
|
|
69
|
+
const gridPaths = useMemo(() => {
|
|
70
|
+
const arr: SkPath[] = [];
|
|
71
|
+
for (let lvl = 1; lvl <= levels; lvl++) {
|
|
72
|
+
const r = (radius * lvl) / levels;
|
|
73
|
+
const p = Skia.Path.Make();
|
|
74
|
+
angles.forEach((angle, i) => {
|
|
75
|
+
const pt = pointFor(angle, r);
|
|
76
|
+
if (i === 0) p.moveTo(pt.x, pt.y);
|
|
77
|
+
else p.lineTo(pt.x, pt.y);
|
|
78
|
+
});
|
|
79
|
+
p.close();
|
|
80
|
+
arr.push(p);
|
|
81
|
+
}
|
|
82
|
+
return arr;
|
|
83
|
+
}, [levels, radius, angles]);
|
|
84
|
+
|
|
85
|
+
// ------------------------------
|
|
86
|
+
// Axis paths
|
|
87
|
+
// ------------------------------
|
|
88
|
+
const axisPaths = useMemo(
|
|
89
|
+
() =>
|
|
90
|
+
angles.map((angle) => {
|
|
91
|
+
const p = Skia.Path.Make();
|
|
92
|
+
const outer = pointFor(angle, radius);
|
|
93
|
+
p.moveTo(cx, cy);
|
|
94
|
+
p.lineTo(outer.x, outer.y);
|
|
95
|
+
return p;
|
|
96
|
+
}),
|
|
97
|
+
[angles, radius]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// ------------------------------
|
|
101
|
+
// Data polygons
|
|
102
|
+
// ------------------------------
|
|
103
|
+
const dataPaths = useMemo(() => {
|
|
104
|
+
return data.map(({ values, ...rest }) => {
|
|
105
|
+
const path = Skia.Path.Make();
|
|
106
|
+
values.forEach((v, i) => {
|
|
107
|
+
const r = Math.max(0, Math.min(1, v / safeMax)) * radius;
|
|
108
|
+
const pt = pointFor(angles[i]!, r);
|
|
109
|
+
if (i === 0) path.moveTo(pt.x, pt.y);
|
|
110
|
+
else path.lineTo(pt.x, pt.y);
|
|
111
|
+
});
|
|
112
|
+
path.close();
|
|
113
|
+
return { path, ...rest };
|
|
114
|
+
});
|
|
115
|
+
}, [data, radius, safeMax, angles]);
|
|
116
|
+
|
|
117
|
+
// ------------------------------
|
|
118
|
+
// Labels
|
|
119
|
+
// ------------------------------
|
|
120
|
+
const formattedLabels = useMemo(() => {
|
|
121
|
+
if (!labels) return undefined;
|
|
122
|
+
|
|
123
|
+
const arr: {
|
|
124
|
+
x: number;
|
|
125
|
+
y: number;
|
|
126
|
+
label: string;
|
|
127
|
+
align: "left" | "center" | "right";
|
|
128
|
+
}[] = [];
|
|
129
|
+
|
|
130
|
+
const r = radius + 12;
|
|
131
|
+
|
|
132
|
+
angles.forEach((angle, i) => {
|
|
133
|
+
const p = pointFor(angle, r);
|
|
134
|
+
const deg = (angle * 180) / Math.PI;
|
|
135
|
+
|
|
136
|
+
let align: "left" | "center" | "right" = "center";
|
|
137
|
+
if (deg > -90 && deg < 90) align = "left";
|
|
138
|
+
else if (deg < -90 || deg > 90) {
|
|
139
|
+
align = "right";
|
|
140
|
+
p.x = size - p.x;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
arr.push({
|
|
144
|
+
x: p.x,
|
|
145
|
+
y: size - p.y,
|
|
146
|
+
label: labels[i]!,
|
|
147
|
+
align,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return arr;
|
|
152
|
+
}, [labels, angles, radius]);
|
|
153
|
+
|
|
154
|
+
// ------------------------------
|
|
155
|
+
// Label Views
|
|
156
|
+
// ------------------------------
|
|
157
|
+
const formattedLabelViews = useMemo(() => {
|
|
158
|
+
if (!labelViews) return undefined;
|
|
159
|
+
|
|
160
|
+
const arr: {
|
|
161
|
+
x: number;
|
|
162
|
+
y: number;
|
|
163
|
+
view: React.ReactNode;
|
|
164
|
+
align: "left" | "center" | "right";
|
|
165
|
+
}[] = [];
|
|
166
|
+
|
|
167
|
+
const r = radius + 12;
|
|
168
|
+
|
|
169
|
+
angles.forEach((angle, i) => {
|
|
170
|
+
const p = pointFor(angle, r);
|
|
171
|
+
const deg = (angle * 180) / Math.PI;
|
|
172
|
+
|
|
173
|
+
let align: "left" | "center" | "right" = "center";
|
|
174
|
+
if (deg > -90 && deg < 90) align = "left";
|
|
175
|
+
else if (deg < -90 || deg > 90) {
|
|
176
|
+
align = "right";
|
|
177
|
+
p.x = size - p.x;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
arr.push({
|
|
181
|
+
x: p.x,
|
|
182
|
+
y: size - p.y,
|
|
183
|
+
view: labelViews[i]!,
|
|
184
|
+
align,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return arr;
|
|
189
|
+
}, [labelViews, angles, radius]);
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
size,
|
|
193
|
+
cx,
|
|
194
|
+
cy,
|
|
195
|
+
radius,
|
|
196
|
+
safeMax,
|
|
197
|
+
angles,
|
|
198
|
+
gridPaths,
|
|
199
|
+
axisPaths,
|
|
200
|
+
dataPaths,
|
|
201
|
+
formattedLabels,
|
|
202
|
+
formattedLabelViews,
|
|
203
|
+
strokeWidth: style?.strokeWidth ?? 2,
|
|
204
|
+
strokeColor: style?.strokeColor ?? "gray",
|
|
205
|
+
centerDotRadius: style?.centerDotRadius ?? 2,
|
|
206
|
+
centerDotColor: style?.centerDotColor ?? style?.strokeColor ?? "gray",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { listFontFamilies, matchFont, Skia, TextAlign, type SkFont, type SkParagraph, type SkTextStyle } from "@shopify/react-native-skia";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
|
|
4
|
+
export interface CommonStyle {
|
|
5
|
+
padding?: number;
|
|
6
|
+
paddingTop?: number;
|
|
7
|
+
paddingBottom?: number;
|
|
8
|
+
paddingStart?: number;
|
|
9
|
+
paddingEnd?: number;
|
|
10
|
+
paddingLeft?: number;
|
|
11
|
+
paddingRight?: number;
|
|
12
|
+
backgroundColor?: string;
|
|
13
|
+
disableRTL?: boolean;
|
|
14
|
+
font?: SkFont;
|
|
15
|
+
fontSize?: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getCommonStyleFont(style?: CommonStyle) {
|
|
19
|
+
const fontSize = style?.font?.getSize() ?? style?.fontSize ?? 12;
|
|
20
|
+
const font = style?.font ?? getFont(fontSize);
|
|
21
|
+
return { font, fontSize };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getPaddings(style?: CommonStyle) {
|
|
25
|
+
const paddingTop = style?.paddingTop ?? style?.padding ?? 0;
|
|
26
|
+
const paddingBottom = style?.paddingBottom ?? style?.padding ?? 0;
|
|
27
|
+
const paddingLeft = style?.paddingLeft ?? style?.paddingStart ?? style?.padding ?? 0;
|
|
28
|
+
const paddingRight = style?.paddingRight ?? style?.paddingEnd ?? style?.padding ?? 0;
|
|
29
|
+
const paddingHorizontal = paddingLeft + paddingRight;
|
|
30
|
+
const paddingVertical = paddingTop + paddingBottom;
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
paddingTop,
|
|
34
|
+
paddingBottom,
|
|
35
|
+
paddingLeft,
|
|
36
|
+
paddingRight,
|
|
37
|
+
paddingHorizontal,
|
|
38
|
+
paddingVertical
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getRandomRGBColor() {
|
|
43
|
+
const r = Math.floor(Math.random() * 256);
|
|
44
|
+
const g = Math.floor(Math.random() * 256);
|
|
45
|
+
const b = Math.floor(Math.random() * 256);
|
|
46
|
+
return `rgb(${r},${g},${b})`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const systemFontFamilies = Platform.select({
|
|
50
|
+
ios: ["Helvetica", "Arial", "Courier"],
|
|
51
|
+
android: ["roboto-flex", "sans-serif", "Roboto", "serif", "monospace"],
|
|
52
|
+
default: ["sans-serif", "serif"],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export function getFont(size: number = 14): SkFont {
|
|
56
|
+
const fontFamily = systemFontFamilies[0];
|
|
57
|
+
// console.log("Available system font families:", listFontFamilies());
|
|
58
|
+
// console.log("Using font family:", fontFamily, systemFontFamilies);
|
|
59
|
+
const font = matchFont({
|
|
60
|
+
fontFamily,
|
|
61
|
+
fontSize: size,
|
|
62
|
+
fontStyle: "normal",
|
|
63
|
+
fontWeight: "normal",
|
|
64
|
+
});
|
|
65
|
+
return font;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const font = getFont();
|
|
69
|
+
|
|
70
|
+
function createParagraph(text: string): SkParagraph {
|
|
71
|
+
const paragraphStyle = {
|
|
72
|
+
textAlign: TextAlign.Center,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const fontStyle: SkTextStyle = {
|
|
76
|
+
fontFamilies: systemFontFamilies,
|
|
77
|
+
fontSize: 14,
|
|
78
|
+
};
|
|
79
|
+
const paragraph = Skia.ParagraphBuilder.Make(paragraphStyle).pushStyle(fontStyle).addText(text).pop().build();
|
|
80
|
+
|
|
81
|
+
return paragraph;
|
|
82
|
+
}
|