@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,180 @@
|
|
|
1
|
+
import { Skia, type SkPath } from "@shopify/react-native-skia";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { getCommonStyleFont, getPaddings } from "../common";
|
|
4
|
+
import type { AreaChartProps, AreaChartStyle } from "./AreaChart";
|
|
5
|
+
import { isDefined } from "../../util/util";
|
|
6
|
+
|
|
7
|
+
export interface AreaData {
|
|
8
|
+
values: number[];
|
|
9
|
+
label?: string;
|
|
10
|
+
color?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Point {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PathData {
|
|
19
|
+
path: SkPath;
|
|
20
|
+
points: Point[];
|
|
21
|
+
values: number[];
|
|
22
|
+
color?: string;
|
|
23
|
+
label?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface XLable {
|
|
27
|
+
label: string;
|
|
28
|
+
xPosition: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TouchLine {
|
|
32
|
+
col: number;
|
|
33
|
+
x: number;
|
|
34
|
+
y: number[];
|
|
35
|
+
values: number[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function useAreaChart({
|
|
39
|
+
data,
|
|
40
|
+
xLabels,
|
|
41
|
+
maxValue,
|
|
42
|
+
minValue,
|
|
43
|
+
style,
|
|
44
|
+
}: AreaChartProps
|
|
45
|
+
) {
|
|
46
|
+
|
|
47
|
+
const height = style?.height ?? 200;
|
|
48
|
+
const width = style?.width ?? 200;
|
|
49
|
+
const {
|
|
50
|
+
paddingLeft,
|
|
51
|
+
paddingTop,
|
|
52
|
+
paddingHorizontal,
|
|
53
|
+
paddingVertical,
|
|
54
|
+
} = getPaddings(style);
|
|
55
|
+
|
|
56
|
+
const canvasHeight = height - paddingVertical;
|
|
57
|
+
const labelWidth = 30;
|
|
58
|
+
const chartWidth = width - labelWidth - paddingHorizontal;
|
|
59
|
+
const xLabelHeight = xLabels && xLabels.length > 0 ? (style?.fontSize ?? 12) + 5 : 0;
|
|
60
|
+
const areaCanvasHeight = canvasHeight - xLabelHeight;
|
|
61
|
+
|
|
62
|
+
const { font } = getCommonStyleFont(style);
|
|
63
|
+
|
|
64
|
+
const { maxValueCalculated, minValueCalculated } = useMemo(() => {
|
|
65
|
+
if (isDefined(maxValue) && isDefined(minValue)) {
|
|
66
|
+
return { maxValueCalculated: maxValue, minValueCalculated: minValue };
|
|
67
|
+
}
|
|
68
|
+
let maxValueCalculated = Number.MIN_VALUE;
|
|
69
|
+
let minValueCalculated = Number.MAX_VALUE;
|
|
70
|
+
|
|
71
|
+
data.forEach((datum) => {
|
|
72
|
+
datum.values.forEach((value) => {
|
|
73
|
+
if (value > maxValueCalculated && !isDefined(maxValue)) {
|
|
74
|
+
maxValueCalculated = value;
|
|
75
|
+
}
|
|
76
|
+
if (value < minValueCalculated && !isDefined(minValue)) {
|
|
77
|
+
minValueCalculated = value;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return { maxValueCalculated: maxValueCalculated + 10, minValueCalculated };
|
|
83
|
+
}, [data]);
|
|
84
|
+
|
|
85
|
+
const paths = useMemo(() => {
|
|
86
|
+
const pathData: PathData[] = [];
|
|
87
|
+
for (let datum of data) {
|
|
88
|
+
const areaData = datum.values;
|
|
89
|
+
const stepX = chartWidth / areaData.length;
|
|
90
|
+
const p = Skia.Path.Make();
|
|
91
|
+
|
|
92
|
+
p.moveTo(0, areaCanvasHeight);
|
|
93
|
+
const points: Point[] = [];
|
|
94
|
+
const values: number[] = [];
|
|
95
|
+
|
|
96
|
+
areaData.forEach((y, i) => {
|
|
97
|
+
const xPos = i * stepX;
|
|
98
|
+
const yPos = Math.max(0, areaCanvasHeight - ((y - minValueCalculated) / (maxValueCalculated - minValueCalculated)) * areaCanvasHeight);
|
|
99
|
+
|
|
100
|
+
points.push({ x: xPos, y: yPos });
|
|
101
|
+
values.push(y);
|
|
102
|
+
p.lineTo(xPos, yPos);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
p.lineTo(chartWidth, areaCanvasHeight);
|
|
106
|
+
p.close();
|
|
107
|
+
|
|
108
|
+
pathData.push({
|
|
109
|
+
path: p,
|
|
110
|
+
points,
|
|
111
|
+
values,
|
|
112
|
+
color: datum.color,
|
|
113
|
+
label: datum.label,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return pathData;
|
|
118
|
+
}, [data, chartWidth, maxValueCalculated, minValueCalculated]);
|
|
119
|
+
|
|
120
|
+
const xLabelsData: XLable[] = useMemo(() => {
|
|
121
|
+
if (!xLabels || xLabels.length === 0) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const stepX = chartWidth / xLabels.length;
|
|
126
|
+
const labels = xLabels.map((label, i) => {
|
|
127
|
+
return {
|
|
128
|
+
label,
|
|
129
|
+
xPosition: i * stepX + font.getSize(),
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
return labels;
|
|
133
|
+
}, [xLabels, chartWidth]);
|
|
134
|
+
|
|
135
|
+
const [touchLine, setTouchLine] = useState<TouchLine | undefined>(undefined);
|
|
136
|
+
|
|
137
|
+
const touchHandler = (touchedX: number, touchedY: number) => {
|
|
138
|
+
if (data.length === 0 || (data[0]?.values.length ?? 0) === 0 || touchedX < 0 || touchedY < 0 || touchedX >= chartWidth || touchedY >= areaCanvasHeight) {
|
|
139
|
+
setTouchLine(undefined);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const stepX = chartWidth / data[0]!.values.length;
|
|
144
|
+
const xIndex = Math.round(touchedX / stepX);
|
|
145
|
+
if (xIndex < 0 || xIndex >= data[0]!.values.length) {
|
|
146
|
+
setTouchLine(undefined);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const yValues: number[] = paths.map(path => path.points[xIndex]!.y);
|
|
151
|
+
const values: number[] = paths.map(path => path.values[xIndex]!);
|
|
152
|
+
|
|
153
|
+
setTouchLine({
|
|
154
|
+
col: xIndex,
|
|
155
|
+
x: xIndex * stepX,
|
|
156
|
+
y: yValues,
|
|
157
|
+
values
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
paths,
|
|
163
|
+
chartWidth,
|
|
164
|
+
canvasHeight,
|
|
165
|
+
paddingLeft,
|
|
166
|
+
paddingTop,
|
|
167
|
+
paddingHorizontal,
|
|
168
|
+
areaCanvasHeight,
|
|
169
|
+
labelWidth,
|
|
170
|
+
maxValue: maxValueCalculated,
|
|
171
|
+
minValue: minValueCalculated,
|
|
172
|
+
xLabelsData,
|
|
173
|
+
xLabelHeight,
|
|
174
|
+
font,
|
|
175
|
+
touchHandler,
|
|
176
|
+
touchLine
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default useAreaChart;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { Fragment, useState } from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import {
|
|
4
|
+
GestureDetector,
|
|
5
|
+
Gesture,
|
|
6
|
+
GestureHandlerRootView,
|
|
7
|
+
} from 'react-native-gesture-handler';
|
|
8
|
+
|
|
9
|
+
import { Canvas, Rect, Text, vec, Line } from '@shopify/react-native-skia';
|
|
10
|
+
import { type CommonStyle } from '../common';
|
|
11
|
+
import useBarChart from './useBarChart';
|
|
12
|
+
import VerticalLabel from '../Common/VerticalLabel';
|
|
13
|
+
import Popup, { type PopupStyle } from '../Popup';
|
|
14
|
+
|
|
15
|
+
export interface StackValue {
|
|
16
|
+
value: number;
|
|
17
|
+
label: string;
|
|
18
|
+
id?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BarData {
|
|
22
|
+
values: StackValue[];
|
|
23
|
+
label?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BarChartStyle extends CommonStyle {
|
|
27
|
+
width?: number;
|
|
28
|
+
height?: number;
|
|
29
|
+
barWidth?: number;
|
|
30
|
+
barSpacing?: number;
|
|
31
|
+
firstBarLeadingSpacing?: number;
|
|
32
|
+
lastBarTrailingSpacing?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BarChartProps {
|
|
36
|
+
data: BarData[];
|
|
37
|
+
colors?: Record<string, string>;
|
|
38
|
+
maxValue?: number;
|
|
39
|
+
minValue?: number;
|
|
40
|
+
popupStyle?: PopupStyle<StackValue>;
|
|
41
|
+
style?: BarChartStyle;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function BarChart(props: BarChartProps) {
|
|
45
|
+
const {
|
|
46
|
+
maxValueCalculated,
|
|
47
|
+
minValueCalculated,
|
|
48
|
+
canvasHeight,
|
|
49
|
+
canvasWidth,
|
|
50
|
+
paddingRight,
|
|
51
|
+
paddingLeft,
|
|
52
|
+
paddingBottom,
|
|
53
|
+
paddingTop,
|
|
54
|
+
rectangles,
|
|
55
|
+
verticalLabelWidth,
|
|
56
|
+
chartHeight,
|
|
57
|
+
strokeWidth,
|
|
58
|
+
tooltip,
|
|
59
|
+
bottomLabelHeight,
|
|
60
|
+
font,
|
|
61
|
+
onScroll,
|
|
62
|
+
touchHandler,
|
|
63
|
+
totalHeight,
|
|
64
|
+
totalWidth,
|
|
65
|
+
} = useBarChart(props);
|
|
66
|
+
|
|
67
|
+
const dragGesture = Gesture.Pan()
|
|
68
|
+
.runOnJS(true)
|
|
69
|
+
.onChange((event) => {
|
|
70
|
+
onScroll(-event.changeX);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const [viewOffset, setViewOffset] = useState({ x: 0, y: 0 });
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<GestureHandlerRootView>
|
|
77
|
+
<View
|
|
78
|
+
style={{
|
|
79
|
+
width: totalWidth,
|
|
80
|
+
flexDirection: 'row',
|
|
81
|
+
backgroundColor: props.style?.backgroundColor,
|
|
82
|
+
paddingLeft: paddingLeft,
|
|
83
|
+
paddingRight: paddingRight,
|
|
84
|
+
paddingTop: paddingTop,
|
|
85
|
+
paddingBottom: paddingBottom,
|
|
86
|
+
}}
|
|
87
|
+
ref={(view) => {
|
|
88
|
+
view?.measureInWindow((fx, fy) => {
|
|
89
|
+
setViewOffset((prev) => {
|
|
90
|
+
if (prev.x === fx && prev.y === fy) {
|
|
91
|
+
return prev;
|
|
92
|
+
}
|
|
93
|
+
return { x: fx, y: fy };
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
<VerticalLabel
|
|
99
|
+
maxValue={maxValueCalculated}
|
|
100
|
+
minValue={minValueCalculated}
|
|
101
|
+
labelCount={6}
|
|
102
|
+
styles={{
|
|
103
|
+
width: verticalLabelWidth,
|
|
104
|
+
height: chartHeight,
|
|
105
|
+
strokeWidth,
|
|
106
|
+
}}
|
|
107
|
+
/>
|
|
108
|
+
<GestureDetector gesture={dragGesture}>
|
|
109
|
+
<Canvas
|
|
110
|
+
style={{
|
|
111
|
+
width: canvasWidth,
|
|
112
|
+
height: canvasHeight,
|
|
113
|
+
paddingRight: 50,
|
|
114
|
+
backgroundColor: 'red',
|
|
115
|
+
}}
|
|
116
|
+
onTouchStart={(event) =>
|
|
117
|
+
touchHandler(
|
|
118
|
+
event.nativeEvent.locationX,
|
|
119
|
+
event.nativeEvent.locationY
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
>
|
|
123
|
+
{/* X axis */}
|
|
124
|
+
<Line
|
|
125
|
+
p1={vec(0, chartHeight)}
|
|
126
|
+
p2={vec(canvasWidth, chartHeight)}
|
|
127
|
+
color="white"
|
|
128
|
+
strokeWidth={strokeWidth}
|
|
129
|
+
/>
|
|
130
|
+
|
|
131
|
+
{/* Bars */}
|
|
132
|
+
|
|
133
|
+
{rectangles.map((bar, xIndex) => {
|
|
134
|
+
if (bar.bars.length === 0) return null;
|
|
135
|
+
return (
|
|
136
|
+
<Fragment key={xIndex}>
|
|
137
|
+
<Text
|
|
138
|
+
x={bar.bars[0]!.x}
|
|
139
|
+
y={
|
|
140
|
+
chartHeight +
|
|
141
|
+
font.getSize() +
|
|
142
|
+
(bottomLabelHeight - font.getSize()) / 2
|
|
143
|
+
}
|
|
144
|
+
text={bar.label ?? ''}
|
|
145
|
+
color="white"
|
|
146
|
+
font={font}
|
|
147
|
+
/>
|
|
148
|
+
{bar.bars.map((item, yIndex) => {
|
|
149
|
+
let currentData = props.data[xIndex]!.values[yIndex]!;
|
|
150
|
+
let color =
|
|
151
|
+
props?.colors?.[currentData.id ?? currentData.label] ||
|
|
152
|
+
'#4A90E2';
|
|
153
|
+
return (
|
|
154
|
+
<Rect
|
|
155
|
+
key={xIndex + '-' + yIndex}
|
|
156
|
+
x={item.x}
|
|
157
|
+
y={item.y}
|
|
158
|
+
width={item.width}
|
|
159
|
+
height={item.height}
|
|
160
|
+
color={color}
|
|
161
|
+
/>
|
|
162
|
+
);
|
|
163
|
+
})}
|
|
164
|
+
</Fragment>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</Canvas>
|
|
168
|
+
</GestureDetector>
|
|
169
|
+
{tooltip && (
|
|
170
|
+
<Popup
|
|
171
|
+
popupData={{
|
|
172
|
+
x: tooltip.centerX,
|
|
173
|
+
y: tooltip.centerY,
|
|
174
|
+
data: tooltip.data,
|
|
175
|
+
}}
|
|
176
|
+
popupStyle={props.popupStyle}
|
|
177
|
+
totalWidth={totalWidth}
|
|
178
|
+
totalHeight={totalHeight}
|
|
179
|
+
touchHandler={(x, y) =>
|
|
180
|
+
touchHandler(x - verticalLabelWidth - paddingLeft, y - paddingTop)
|
|
181
|
+
}
|
|
182
|
+
viewOffset={viewOffset}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
</View>
|
|
186
|
+
</GestureHandlerRootView>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export default BarChart;
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import { rect } from "@shopify/react-native-skia";
|
|
3
|
+
import { arrayFrom, isDefined } from "../../util/util";
|
|
4
|
+
import type { BarData, BarChartStyle, StackValue, BarChartProps } from "./BarChart";
|
|
5
|
+
import { useWindowDimensions } from "react-native";
|
|
6
|
+
import { getCommonStyleFont, getPaddings } from "../common";
|
|
7
|
+
|
|
8
|
+
export default function useBarChart(
|
|
9
|
+
{
|
|
10
|
+
data,
|
|
11
|
+
style,
|
|
12
|
+
maxValue,
|
|
13
|
+
minValue
|
|
14
|
+
}: BarChartProps
|
|
15
|
+
) {
|
|
16
|
+
const { maxValueCalculated, minValueCalculated } = useMemo(() => {
|
|
17
|
+
if (isDefined(maxValue) && isDefined(minValue)) {
|
|
18
|
+
return {
|
|
19
|
+
maxValueCalculated: maxValue,
|
|
20
|
+
minValueCalculated: minValue
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (data.length === 0) {
|
|
25
|
+
return { maxValueCalculated: maxValue ?? 100, minValueCalculated: minValue ?? 0 };
|
|
26
|
+
}
|
|
27
|
+
let maxValueCalculated = Number.MIN_VALUE;
|
|
28
|
+
let minValueCalculated = Number.MAX_VALUE;
|
|
29
|
+
|
|
30
|
+
data.forEach((item) => {
|
|
31
|
+
const currentValue = item.values.reduce(
|
|
32
|
+
(acc, value) => {
|
|
33
|
+
minValueCalculated = Math.min(minValueCalculated, value.value);
|
|
34
|
+
return acc + value.value;
|
|
35
|
+
},
|
|
36
|
+
0
|
|
37
|
+
);
|
|
38
|
+
maxValueCalculated = Math.max(maxValueCalculated, currentValue);
|
|
39
|
+
});
|
|
40
|
+
if (isDefined(maxValue))
|
|
41
|
+
maxValueCalculated = maxValue;
|
|
42
|
+
|
|
43
|
+
if (isDefined(minValue))
|
|
44
|
+
minValueCalculated = minValue;
|
|
45
|
+
|
|
46
|
+
return { maxValueCalculated, minValueCalculated };
|
|
47
|
+
}, [data, maxValue]);
|
|
48
|
+
|
|
49
|
+
const steps = useMemo(() => arrayFrom(1, 0.2), []);
|
|
50
|
+
const [tooltip, setTooltip] = useState<{ centerX: number, centerY: number, data: StackValue; } | undefined>(undefined);
|
|
51
|
+
const [startX, setStartX] = useState<number>(0);
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
paddingLeft,
|
|
55
|
+
paddingRight,
|
|
56
|
+
paddingTop,
|
|
57
|
+
paddingBottom
|
|
58
|
+
} = getPaddings(style);
|
|
59
|
+
|
|
60
|
+
const chartBarWidth = style?.barWidth ?? 100;
|
|
61
|
+
const chartBarSpacing = style?.barSpacing ?? 0;
|
|
62
|
+
const verticalLabelWidth = 35;
|
|
63
|
+
const chartHeight = style?.height ?? 200;
|
|
64
|
+
const strokeWidth = 2;
|
|
65
|
+
const bottomLabelHeight = 20;
|
|
66
|
+
const canvasHeight = chartHeight + bottomLabelHeight;
|
|
67
|
+
const { width: windowWidth } = useWindowDimensions();
|
|
68
|
+
const totalWidth = style?.width ?? windowWidth;
|
|
69
|
+
const totalHeight = chartHeight;
|
|
70
|
+
|
|
71
|
+
const initialSpacing = style?.firstBarLeadingSpacing ?? 0;
|
|
72
|
+
const endSpacing = style?.lastBarTrailingSpacing ?? chartBarSpacing;
|
|
73
|
+
|
|
74
|
+
const scrollAreaWidth = initialSpacing + data.length * chartBarWidth + (Math.max(0, data.length - 1) * chartBarSpacing) + endSpacing;
|
|
75
|
+
const canvasWidth = Math.min(scrollAreaWidth, totalWidth - verticalLabelWidth - paddingRight - paddingLeft);
|
|
76
|
+
const { font } = getCommonStyleFont(style);
|
|
77
|
+
|
|
78
|
+
const rectangles = useMemo(() => {
|
|
79
|
+
let leftBoundary = Math.max(0, startX);
|
|
80
|
+
let rightBoundary = startX + totalWidth;
|
|
81
|
+
|
|
82
|
+
let startArrayIndex = Math.floor(Math.max(leftBoundary - initialSpacing, 0) / (chartBarWidth + chartBarSpacing));
|
|
83
|
+
let endArrayIndex = Math.min(Math.ceil(rightBoundary / (chartBarWidth + chartBarSpacing)), data.length);
|
|
84
|
+
|
|
85
|
+
return data.slice(startArrayIndex, endArrayIndex)
|
|
86
|
+
.map((bar, xIndex) => {
|
|
87
|
+
let previousHeight = 0;
|
|
88
|
+
const x = initialSpacing + (xIndex + startArrayIndex) * (chartBarWidth + chartBarSpacing) - leftBoundary;
|
|
89
|
+
return {
|
|
90
|
+
bars: bar.values.map((item, yIndex) => {
|
|
91
|
+
const barHeight =
|
|
92
|
+
((item.value - minValueCalculated) /
|
|
93
|
+
(maxValueCalculated - minValueCalculated)) *
|
|
94
|
+
chartHeight;
|
|
95
|
+
|
|
96
|
+
const y =
|
|
97
|
+
chartHeight - barHeight - previousHeight - strokeWidth;
|
|
98
|
+
|
|
99
|
+
previousHeight += barHeight;
|
|
100
|
+
return rect(x, y, chartBarWidth, barHeight);
|
|
101
|
+
}),
|
|
102
|
+
label: bar.label,
|
|
103
|
+
dataIndex: xIndex + startArrayIndex,
|
|
104
|
+
x: x
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
}, [
|
|
108
|
+
data,
|
|
109
|
+
chartBarWidth,
|
|
110
|
+
chartBarSpacing,
|
|
111
|
+
maxValueCalculated,
|
|
112
|
+
minValueCalculated,
|
|
113
|
+
strokeWidth,
|
|
114
|
+
startX
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const touchHandler = (touchedX: number, touchedY: number) => {
|
|
118
|
+
if (rectangles.length === 0 || touchedX < 0 || touchedY < 0 || touchedX >= canvasWidth || touchedY >= chartHeight) {
|
|
119
|
+
setTooltip(undefined);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let xIndex = -1;
|
|
124
|
+
let startingXIndex = 0;
|
|
125
|
+
|
|
126
|
+
if (touchedX >= rectangles[0]!.x && touchedX <= rectangles[0]!.x + rectangles[0]!.bars[0]!.width) {
|
|
127
|
+
xIndex = 0;
|
|
128
|
+
startingXIndex = Math.max(0, rectangles[0]!.x);
|
|
129
|
+
} else if (touchedX >= rectangles[0]!.x) {
|
|
130
|
+
xIndex = Math.floor((touchedX - (rectangles[0]!.x + chartBarWidth) - chartBarSpacing) / (chartBarWidth + chartBarSpacing)) + 1;
|
|
131
|
+
startingXIndex = rectangles[xIndex]!.x;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (xIndex === -1 || (touchedX < rectangles[xIndex]!.x || touchedX > rectangles[xIndex]!.x + chartBarWidth)) {
|
|
135
|
+
console.log('Touch is outside the bar width, ignoring.');
|
|
136
|
+
setTooltip(undefined);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
xIndex = rectangles[xIndex]!.dataIndex;
|
|
140
|
+
|
|
141
|
+
let yIndex = 0;
|
|
142
|
+
let yPassed = 0;
|
|
143
|
+
let categoryData = data[xIndex]?.values || [];
|
|
144
|
+
let lastBarHeight = 0;
|
|
145
|
+
|
|
146
|
+
while (
|
|
147
|
+
yIndex < categoryData.length &&
|
|
148
|
+
yPassed < chartHeight - touchedY
|
|
149
|
+
) {
|
|
150
|
+
const barHeight =
|
|
151
|
+
((categoryData[yIndex]!.value - minValueCalculated) /
|
|
152
|
+
(maxValueCalculated - minValueCalculated)) *
|
|
153
|
+
chartHeight;
|
|
154
|
+
yPassed += barHeight;
|
|
155
|
+
lastBarHeight = barHeight;
|
|
156
|
+
yIndex++;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (yIndex === 0 || (yIndex === categoryData.length && touchedY < chartHeight - yPassed)) {
|
|
160
|
+
console.log('Touch is outside the bar height, ignoring.');
|
|
161
|
+
setTooltip(undefined);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
setTooltip({
|
|
166
|
+
centerX: startingXIndex + chartBarWidth / 2,
|
|
167
|
+
centerY:
|
|
168
|
+
chartHeight - yPassed - strokeWidth + lastBarHeight / 2,
|
|
169
|
+
data: categoryData[yIndex - 1]!,
|
|
170
|
+
});
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function onScroll(translateX: number) {
|
|
174
|
+
setTooltip(undefined);
|
|
175
|
+
setStartX((prev) => {
|
|
176
|
+
let newX = prev + translateX;
|
|
177
|
+
if (newX < 0) return 0;
|
|
178
|
+
if (newX + canvasWidth > scrollAreaWidth)
|
|
179
|
+
return Math.max(0, scrollAreaWidth - canvasWidth);
|
|
180
|
+
return newX;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
maxValueCalculated,
|
|
186
|
+
minValueCalculated,
|
|
187
|
+
canvasHeight,
|
|
188
|
+
canvasWidth,
|
|
189
|
+
steps,
|
|
190
|
+
scrollAreaWidth,
|
|
191
|
+
chartHeight,
|
|
192
|
+
paddingTop,
|
|
193
|
+
paddingBottom,
|
|
194
|
+
paddingLeft,
|
|
195
|
+
paddingRight,
|
|
196
|
+
verticalLabelWidth,
|
|
197
|
+
chartBarWidth,
|
|
198
|
+
chartBarSpacing,
|
|
199
|
+
strokeWidth,
|
|
200
|
+
rectangles,
|
|
201
|
+
tooltip,
|
|
202
|
+
bottomLabelHeight,
|
|
203
|
+
font,
|
|
204
|
+
setTooltip,
|
|
205
|
+
touchHandler,
|
|
206
|
+
onScroll,
|
|
207
|
+
totalHeight,
|
|
208
|
+
totalWidth
|
|
209
|
+
};
|
|
210
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Canvas, Line, Text, Skia } from '@shopify/react-native-skia';
|
|
2
|
+
import { getFont, type CommonStyle } from '../common';
|
|
3
|
+
|
|
4
|
+
interface VerticalLabelStyles extends CommonStyle {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
strokeWidth?: number;
|
|
8
|
+
strokeColor?: string;
|
|
9
|
+
textColor?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface VerticalLabelProps {
|
|
13
|
+
minValue: number;
|
|
14
|
+
maxValue: number;
|
|
15
|
+
labelCount: number;
|
|
16
|
+
styles: VerticalLabelStyles;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function VerticalLabel({
|
|
20
|
+
minValue,
|
|
21
|
+
maxValue,
|
|
22
|
+
labelCount,
|
|
23
|
+
styles,
|
|
24
|
+
}: VerticalLabelProps) {
|
|
25
|
+
const {
|
|
26
|
+
width,
|
|
27
|
+
height,
|
|
28
|
+
strokeWidth = 2,
|
|
29
|
+
strokeColor = 'white',
|
|
30
|
+
textColor = 'white',
|
|
31
|
+
paddingTop = 0,
|
|
32
|
+
paddingRight = 0,
|
|
33
|
+
paddingBottom = 0,
|
|
34
|
+
paddingLeft = 0,
|
|
35
|
+
fontSize = 12,
|
|
36
|
+
backgroundColor,
|
|
37
|
+
} = styles;
|
|
38
|
+
|
|
39
|
+
// Generate evenly spaced values
|
|
40
|
+
const stepValue =
|
|
41
|
+
labelCount > 1 ? (maxValue - minValue) / (labelCount - 1) : 0;
|
|
42
|
+
const labels = Array.from(
|
|
43
|
+
{ length: labelCount },
|
|
44
|
+
(_, i) => minValue + i * stepValue
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const font = getFont(fontSize);
|
|
48
|
+
|
|
49
|
+
const usableHeight = height - paddingTop - paddingBottom - fontSize;
|
|
50
|
+
const stepY = labelCount > 1 ? usableHeight / (labelCount - 1) : 0;
|
|
51
|
+
|
|
52
|
+
// Precompute text paint
|
|
53
|
+
const paint = Skia.Paint();
|
|
54
|
+
paint.setColor(Skia.Color(textColor));
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Canvas style={{ width, height, backgroundColor }}>
|
|
58
|
+
<Line
|
|
59
|
+
p1={{ x: width - paddingRight - strokeWidth / 2, y: paddingTop }}
|
|
60
|
+
p2={{
|
|
61
|
+
x: width - paddingRight - strokeWidth / 2,
|
|
62
|
+
y: height - paddingBottom,
|
|
63
|
+
}}
|
|
64
|
+
color={strokeColor}
|
|
65
|
+
strokeWidth={strokeWidth}
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
{labels.map((label, i) => {
|
|
69
|
+
const y = height - paddingBottom - stepY * i;
|
|
70
|
+
const text = label.toFixed(0);
|
|
71
|
+
|
|
72
|
+
// measure text width for right-align
|
|
73
|
+
const textWidth = font.measureText(text).width;
|
|
74
|
+
const x = width - paddingRight - strokeWidth - 4 - textWidth;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Text
|
|
78
|
+
key={i}
|
|
79
|
+
x={x}
|
|
80
|
+
y={y}
|
|
81
|
+
text={text}
|
|
82
|
+
font={font}
|
|
83
|
+
color={textColor}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</Canvas>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export default VerticalLabel;
|