@dhiraj0720/report1chart 2.2.9 → 2.3.1
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/package.json +1 -1
- package/src/components/FrozenTableReport2.jsx +25 -6
- package/src/components/FrozenTableReport3.jsx +21 -3
- package/src/components/SafeScreen.jsx +29 -0
- package/src/components/SvgBarLineChart.jsx +155 -16
- package/src/components/SvgLineChart.jsx +111 -33
- package/src/index.jsx +10 -2
- package/src/screens/Report1Screen.jsx +3 -0
- package/src/screens/Report2Screen.jsx +3 -1
- package/src/screens/Report3Screen.jsx +2 -0
- package/src/utils/formatNumber.js +12 -0
package/package.json
CHANGED
|
@@ -5,6 +5,16 @@ const Cell = ({ children, bold }) => (
|
|
|
5
5
|
<Text style={[styles.cell, bold && styles.bold]}>{children}</Text>
|
|
6
6
|
);
|
|
7
7
|
|
|
8
|
+
const PercentCell = ({ value }) => {
|
|
9
|
+
const positive = value >= 0;
|
|
10
|
+
return (
|
|
11
|
+
<Text style={{ color: positive ? '#2e7d32' : '#d32f2f', fontWeight: '700' }}>
|
|
12
|
+
{positive ? '↑' : '↓'} {Math.abs(value)}%
|
|
13
|
+
</Text>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
|
|
8
18
|
const FrozenTableReport2 = ({ rows }) => (
|
|
9
19
|
<View style={styles.container}>
|
|
10
20
|
<View style={styles.frozen}>
|
|
@@ -18,9 +28,9 @@ const FrozenTableReport2 = ({ rows }) => (
|
|
|
18
28
|
<View>
|
|
19
29
|
<View style={styles.headerRow}>
|
|
20
30
|
{[
|
|
21
|
-
'2024 TEU','2025 TEU','TEU %',
|
|
22
|
-
'2024 Kar','2025 Kar','Kar %',
|
|
23
|
-
'2025 Bütçe','Bütçe %'
|
|
31
|
+
'2024 TEU', '2025 TEU', 'TEU %',
|
|
32
|
+
'2024 Kar', '2025 Kar', 'Kar %',
|
|
33
|
+
'2025 Bütçe', 'Bütçe %'
|
|
24
34
|
].map(h => <Cell key={h} bold>{h}</Cell>)}
|
|
25
35
|
</View>
|
|
26
36
|
|
|
@@ -28,12 +38,21 @@ const FrozenTableReport2 = ({ rows }) => (
|
|
|
28
38
|
<View key={i} style={styles.row}>
|
|
29
39
|
<Cell>{r.teu2024}</Cell>
|
|
30
40
|
<Cell>{r.teu2025}</Cell>
|
|
31
|
-
<Cell>
|
|
41
|
+
<Cell>
|
|
42
|
+
<PercentCell value={r.teuChangePercent} />
|
|
43
|
+
</Cell>
|
|
44
|
+
|
|
32
45
|
<Cell>{r.profitUsd2024}</Cell>
|
|
33
46
|
<Cell>{r.profitUsd2025}</Cell>
|
|
34
|
-
<Cell>
|
|
47
|
+
<Cell>
|
|
48
|
+
<PercentCell value={r.profitChangePercent} />
|
|
49
|
+
</Cell>
|
|
50
|
+
|
|
35
51
|
<Cell>{r.budgetProfitUsd2025}</Cell>
|
|
36
|
-
|
|
52
|
+
|
|
53
|
+
<Cell>
|
|
54
|
+
<PercentCell value={r.budgetChangePercent} />
|
|
55
|
+
</Cell>
|
|
37
56
|
</View>
|
|
38
57
|
))}
|
|
39
58
|
</View>
|
|
@@ -5,6 +5,15 @@ const Cell = ({ children, bold }) => (
|
|
|
5
5
|
<Text style={[styles.cell, bold && styles.bold]}>{children}</Text>
|
|
6
6
|
);
|
|
7
7
|
|
|
8
|
+
const PercentCell = ({ value }) => {
|
|
9
|
+
const positive = value >= 0;
|
|
10
|
+
return (
|
|
11
|
+
<Text style={{ color: positive ? '#2e7d32' : '#d32f2f', fontWeight: '700' }}>
|
|
12
|
+
{positive ? '↑' : '↓'} {Math.abs(value)}%
|
|
13
|
+
</Text>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
8
17
|
const FrozenTableReport3 = ({ rows }) => (
|
|
9
18
|
<View style={styles.container}>
|
|
10
19
|
<View style={styles.frozen}>
|
|
@@ -28,12 +37,21 @@ const FrozenTableReport3 = ({ rows }) => (
|
|
|
28
37
|
<View key={i} style={styles.row}>
|
|
29
38
|
<Cell>{r.loadCount2024}</Cell>
|
|
30
39
|
<Cell>{r.loadCount2025}</Cell>
|
|
31
|
-
|
|
40
|
+
|
|
41
|
+
<Cell>
|
|
42
|
+
<PercentCell value={r.loadCountChangePercent} />
|
|
43
|
+
</Cell>
|
|
32
44
|
<Cell>{r.revenueTl2024}</Cell>
|
|
33
45
|
<Cell>{r.revenueTl2025}</Cell>
|
|
34
|
-
|
|
46
|
+
|
|
47
|
+
<Cell>
|
|
48
|
+
<PercentCell value={r.revenueChangePercent} />
|
|
49
|
+
</Cell>
|
|
35
50
|
<Cell>{r.budgetRevenueTl2025}</Cell>
|
|
36
|
-
|
|
51
|
+
|
|
52
|
+
<Cell>
|
|
53
|
+
<PercentCell value={r.budgetChangePercent} />
|
|
54
|
+
</Cell>
|
|
37
55
|
</View>
|
|
38
56
|
))}
|
|
39
57
|
</View>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
SafeAreaView,
|
|
4
|
+
View,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Platform,
|
|
7
|
+
} from 'react-native';
|
|
8
|
+
|
|
9
|
+
const SafeScreen = ({ children }) => {
|
|
10
|
+
return (
|
|
11
|
+
<SafeAreaView style={styles.safe}>
|
|
12
|
+
<View style={styles.container}>{children}</View>
|
|
13
|
+
</SafeAreaView>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default SafeScreen;
|
|
18
|
+
|
|
19
|
+
const styles = StyleSheet.create({
|
|
20
|
+
safe: {
|
|
21
|
+
flex: 1,
|
|
22
|
+
backgroundColor: '#fff',
|
|
23
|
+
},
|
|
24
|
+
container: {
|
|
25
|
+
flex: 1,
|
|
26
|
+
paddingBottom: Platform.OS === 'android' ? 24 : 0,
|
|
27
|
+
paddingTop: Platform.OS === 'android' ? 24 : 0,
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -1,16 +1,30 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import { View, Text, ScrollView } from 'react-native';
|
|
3
|
-
import Svg, {
|
|
3
|
+
import Svg, {
|
|
4
|
+
Rect,
|
|
5
|
+
Line,
|
|
6
|
+
Circle,
|
|
7
|
+
Text as SvgText,
|
|
8
|
+
} from 'react-native-svg';
|
|
9
|
+
import { formatNumber } from '../utils/formatNumber';
|
|
4
10
|
|
|
5
11
|
const SvgBarLineChart = ({ data }) => {
|
|
6
|
-
if (!data?.series) return null;
|
|
12
|
+
if (!data?.series || !data.labels?.length) return null;
|
|
13
|
+
|
|
14
|
+
const [activePoint, setActivePoint] = useState(null);
|
|
15
|
+
|
|
7
16
|
|
|
8
17
|
const width = Math.max(360, data.labels.length * 90);
|
|
9
18
|
const height = 240;
|
|
10
19
|
const padding = 40;
|
|
20
|
+
const chartHeight = height - padding * 2;
|
|
11
21
|
|
|
12
22
|
const max = Math.max(...data.series.flatMap(s => s.data));
|
|
13
23
|
const barWidth = 14;
|
|
24
|
+
const step = 70;
|
|
25
|
+
|
|
26
|
+
const scaleY = v => (v / max) * chartHeight;
|
|
27
|
+
const yPos = v => height - padding - scaleY(v);
|
|
14
28
|
|
|
15
29
|
return (
|
|
16
30
|
<View style={{ marginVertical: 16 }}>
|
|
@@ -19,56 +33,181 @@ const SvgBarLineChart = ({ data }) => {
|
|
|
19
33
|
</Text>
|
|
20
34
|
|
|
21
35
|
<ScrollView horizontal>
|
|
22
|
-
<Svg width={width} height={height}>
|
|
36
|
+
<Svg width={width} height={height} onPress={() => setActivePoint(null)}>
|
|
37
|
+
{/* Y AXIS GRID + LABELS */}
|
|
38
|
+
{[0, 0.5, 1].map((p, i) => {
|
|
39
|
+
const y = height - padding - chartHeight * p;
|
|
40
|
+
return (
|
|
41
|
+
<React.Fragment key={i}>
|
|
42
|
+
<Line
|
|
43
|
+
x1={padding}
|
|
44
|
+
x2={width - padding}
|
|
45
|
+
y1={y}
|
|
46
|
+
y2={y}
|
|
47
|
+
stroke="#ccc"
|
|
48
|
+
strokeDasharray="4"
|
|
49
|
+
/>
|
|
50
|
+
<SvgText
|
|
51
|
+
x={padding - 8}
|
|
52
|
+
y={y + 4}
|
|
53
|
+
fontSize="10"
|
|
54
|
+
textAnchor="end"
|
|
55
|
+
>
|
|
56
|
+
{formatNumber(max * p)}
|
|
57
|
+
</SvgText>
|
|
58
|
+
</React.Fragment>
|
|
59
|
+
);
|
|
60
|
+
})}
|
|
61
|
+
|
|
62
|
+
{/* BARS + BUDGET LINE */}
|
|
23
63
|
{data.labels.map((label, i) => {
|
|
24
|
-
const x = padding + i *
|
|
64
|
+
const x = padding + i * step;
|
|
25
65
|
|
|
26
66
|
return (
|
|
27
67
|
<React.Fragment key={i}>
|
|
28
|
-
{/* 2024
|
|
68
|
+
{/* 2024 BAR */}
|
|
29
69
|
<Rect
|
|
30
70
|
x={x}
|
|
31
|
-
y={
|
|
71
|
+
y={yPos(data.series[0].data[i])}
|
|
32
72
|
width={barWidth}
|
|
33
|
-
height={(data.series[0].data[i]
|
|
73
|
+
height={scaleY(data.series[0].data[i])}
|
|
34
74
|
fill="#E07A3F"
|
|
75
|
+
onPress={() =>
|
|
76
|
+
setActivePoint({
|
|
77
|
+
x: x + barWidth / 2,
|
|
78
|
+
y: yPos(data.series[0].data[i]),
|
|
79
|
+
value: data.series[0].data[i],
|
|
80
|
+
})
|
|
81
|
+
}
|
|
35
82
|
/>
|
|
83
|
+
<SvgText
|
|
84
|
+
x={x + barWidth / 2}
|
|
85
|
+
y={yPos(data.series[0].data[i]) - 6}
|
|
86
|
+
fontSize="10"
|
|
87
|
+
textAnchor="middle"
|
|
88
|
+
>
|
|
89
|
+
{formatNumber(data.series[0].data[i])}
|
|
90
|
+
</SvgText>
|
|
36
91
|
|
|
37
|
-
{/* 2025
|
|
92
|
+
{/* 2025 BAR */}
|
|
38
93
|
<Rect
|
|
39
94
|
x={x + barWidth + 4}
|
|
40
|
-
y={
|
|
95
|
+
y={yPos(data.series[1].data[i])}
|
|
41
96
|
width={barWidth}
|
|
42
|
-
height={(data.series[1].data[i]
|
|
97
|
+
height={scaleY(data.series[1].data[i])}
|
|
43
98
|
fill="#4E79A7"
|
|
99
|
+
onPress={() =>
|
|
100
|
+
setActivePoint({
|
|
101
|
+
x: x + barWidth + 4 + barWidth / 2,
|
|
102
|
+
y: yPos(data.series[1].data[i]),
|
|
103
|
+
value: data.series[1].data[i],
|
|
104
|
+
})
|
|
105
|
+
}
|
|
44
106
|
/>
|
|
107
|
+
<SvgText
|
|
108
|
+
x={x + barWidth + 4 + barWidth / 2}
|
|
109
|
+
y={yPos(data.series[1].data[i]) - 6}
|
|
110
|
+
fontSize="10"
|
|
111
|
+
textAnchor="middle"
|
|
112
|
+
>
|
|
113
|
+
{formatNumber(data.series[1].data[i])}
|
|
114
|
+
</SvgText>
|
|
45
115
|
|
|
46
|
-
{/*
|
|
116
|
+
{/* BUDGET DOT */}
|
|
47
117
|
<Circle
|
|
48
|
-
cx={x + barWidth}
|
|
49
|
-
cy={
|
|
118
|
+
cx={x + barWidth + 2}
|
|
119
|
+
cy={yPos(data.series[2].data[i])}
|
|
50
120
|
r={4}
|
|
51
121
|
fill="#8AB6E8"
|
|
122
|
+
onPress={() =>
|
|
123
|
+
setActivePoint({
|
|
124
|
+
x: x + barWidth + 2,
|
|
125
|
+
y: yPos(data.series[2].data[i]),
|
|
126
|
+
value: data.series[2].data[i],
|
|
127
|
+
})
|
|
128
|
+
}
|
|
52
129
|
/>
|
|
53
130
|
|
|
131
|
+
{/* BUDGET VALUE */}
|
|
132
|
+
<SvgText
|
|
133
|
+
x={x + barWidth + 2}
|
|
134
|
+
y={yPos(data.series[2].data[i]) - 8}
|
|
135
|
+
fontSize="10"
|
|
136
|
+
textAnchor="middle"
|
|
137
|
+
>
|
|
138
|
+
{formatNumber(data.series[2].data[i])}
|
|
139
|
+
</SvgText>
|
|
140
|
+
|
|
141
|
+
{/* X LABEL */}
|
|
54
142
|
<SvgText
|
|
55
143
|
x={x + barWidth}
|
|
56
|
-
y={height -
|
|
144
|
+
y={height - 10}
|
|
57
145
|
fontSize="10"
|
|
58
146
|
textAnchor="middle"
|
|
147
|
+
transform={`rotate(-35 ${x + barWidth} ${height - 10})`}
|
|
59
148
|
>
|
|
60
149
|
{label}
|
|
61
150
|
</SvgText>
|
|
62
151
|
</React.Fragment>
|
|
63
152
|
);
|
|
64
153
|
})}
|
|
154
|
+
|
|
155
|
+
{activePoint && (
|
|
156
|
+
<>
|
|
157
|
+
<Rect
|
|
158
|
+
x={activePoint.x - 32}
|
|
159
|
+
y={activePoint.y - 44}
|
|
160
|
+
width={64}
|
|
161
|
+
height={26}
|
|
162
|
+
rx={6}
|
|
163
|
+
fill="#000"
|
|
164
|
+
opacity={0.8}
|
|
165
|
+
/>
|
|
166
|
+
<SvgText
|
|
167
|
+
x={activePoint.x}
|
|
168
|
+
y={activePoint.y - 26}
|
|
169
|
+
fontSize="10"
|
|
170
|
+
fill="#fff"
|
|
171
|
+
textAnchor="middle"
|
|
172
|
+
>
|
|
173
|
+
{formatNumber(activePoint.value)}
|
|
174
|
+
</SvgText>
|
|
175
|
+
</>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
{/* BUDGET DOTTED LINE */}
|
|
180
|
+
{data.series[2].data.map((v, i) => {
|
|
181
|
+
if (i === data.series[2].data.length - 1) return null;
|
|
182
|
+
return (
|
|
183
|
+
<Line
|
|
184
|
+
key={`line-${i}`}
|
|
185
|
+
x1={padding + i * step + barWidth + 2}
|
|
186
|
+
y1={yPos(v)}
|
|
187
|
+
x2={padding + (i + 1) * step + barWidth + 2}
|
|
188
|
+
y2={yPos(data.series[2].data[i + 1])}
|
|
189
|
+
stroke="#8AB6E8"
|
|
190
|
+
strokeWidth={2}
|
|
191
|
+
strokeDasharray="4"
|
|
192
|
+
/>
|
|
193
|
+
);
|
|
194
|
+
})}
|
|
65
195
|
</Svg>
|
|
66
196
|
</ScrollView>
|
|
67
197
|
|
|
198
|
+
{/* LEGEND */}
|
|
68
199
|
<View style={{ flexDirection: 'row', marginTop: 8 }}>
|
|
69
200
|
{data.series.map((s, i) => (
|
|
70
201
|
<Text key={i} style={{ marginRight: 16 }}>
|
|
71
|
-
|
|
202
|
+
<Text
|
|
203
|
+
style={{
|
|
204
|
+
color:
|
|
205
|
+
i === 0 ? '#E07A3F' : i === 1 ? '#4E79A7' : '#8AB6E8',
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
●
|
|
209
|
+
</Text>{' '}
|
|
210
|
+
{s.name}
|
|
72
211
|
</Text>
|
|
73
212
|
))}
|
|
74
213
|
</View>
|
|
@@ -1,23 +1,35 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
2
|
import { View, Text, ScrollView } from 'react-native';
|
|
3
|
-
import Svg, {
|
|
3
|
+
import Svg, {
|
|
4
|
+
Line,
|
|
5
|
+
Circle,
|
|
6
|
+
Rect,
|
|
7
|
+
Text as SvgText,
|
|
8
|
+
} from 'react-native-svg';
|
|
9
|
+
import { formatNumber } from '../utils/formatNumber';
|
|
4
10
|
|
|
5
11
|
const SvgLineChart = ({ data }) => {
|
|
6
|
-
if (!data?.series) return null;
|
|
12
|
+
if (!data?.series || !data.labels?.length) return null;
|
|
13
|
+
|
|
14
|
+
const [activePoint, setActivePoint] = useState(null);
|
|
7
15
|
|
|
8
16
|
const width = Math.max(360, data.labels.length * 80);
|
|
9
17
|
const height = 220;
|
|
10
18
|
const padding = 40;
|
|
19
|
+
const chartHeight = height - padding * 2;
|
|
11
20
|
|
|
12
21
|
const values = data.series.flatMap(s => s.data);
|
|
13
22
|
const max = Math.max(...values);
|
|
14
|
-
const min =
|
|
23
|
+
const min = 0;
|
|
15
24
|
|
|
16
25
|
const y = v =>
|
|
17
|
-
height - padding -
|
|
18
|
-
|
|
26
|
+
height - padding - ((v - min) / (max - min || 1)) * chartHeight;
|
|
27
|
+
|
|
28
|
+
const xStep =
|
|
29
|
+
data.labels.length === 1
|
|
30
|
+
? 0
|
|
31
|
+
: (width - padding * 2) / (data.labels.length - 1);
|
|
19
32
|
|
|
20
|
-
const xStep = (width - padding * 2) / (data.labels.length - 1 || 1);
|
|
21
33
|
const colors = ['#E07A3F', '#4E79A7'];
|
|
22
34
|
|
|
23
35
|
return (
|
|
@@ -27,55 +39,121 @@ const SvgLineChart = ({ data }) => {
|
|
|
27
39
|
</Text>
|
|
28
40
|
|
|
29
41
|
<ScrollView horizontal>
|
|
30
|
-
<Svg width={width} height={height}>
|
|
31
|
-
{/*
|
|
32
|
-
{[0.25, 0.5, 0.75].map((p, i) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
<Svg width={width} height={height} onPress={() => setActivePoint(null)}>
|
|
43
|
+
{/* GRID + Y LABELS */}
|
|
44
|
+
{[0.25, 0.5, 0.75].map((p, i) => {
|
|
45
|
+
const yPos = padding + chartHeight * p;
|
|
46
|
+
const labelValue = Math.round(max * (1 - p));
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<React.Fragment key={i}>
|
|
50
|
+
<Line
|
|
51
|
+
x1={padding}
|
|
52
|
+
x2={width - padding}
|
|
53
|
+
y1={yPos}
|
|
54
|
+
y2={yPos}
|
|
55
|
+
stroke="#ccc"
|
|
56
|
+
strokeDasharray="4"
|
|
57
|
+
/>
|
|
58
|
+
<SvgText
|
|
59
|
+
x={padding - 8}
|
|
60
|
+
y={yPos + 4}
|
|
61
|
+
fontSize="10"
|
|
62
|
+
textAnchor="end"
|
|
63
|
+
>
|
|
64
|
+
{formatNumber(labelValue)}
|
|
65
|
+
</SvgText>
|
|
66
|
+
</React.Fragment>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
43
69
|
|
|
70
|
+
{/* LINES + DOTS */}
|
|
44
71
|
{data.series.map((s, si) =>
|
|
45
72
|
s.data.map((v, i) => {
|
|
46
73
|
const x = padding + i * xStep;
|
|
47
74
|
const yVal = y(v);
|
|
48
75
|
|
|
49
|
-
if (i === s.data.length - 1) return null;
|
|
50
|
-
|
|
51
76
|
return (
|
|
52
77
|
<React.Fragment key={`${si}-${i}`}>
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
78
|
+
{i < s.data.length - 1 && (
|
|
79
|
+
<Line
|
|
80
|
+
x1={x}
|
|
81
|
+
y1={yVal}
|
|
82
|
+
x2={padding + (i + 1) * xStep}
|
|
83
|
+
y2={y(s.data[i + 1])}
|
|
84
|
+
stroke={colors[si]}
|
|
85
|
+
strokeWidth={2}
|
|
86
|
+
strokeDasharray={si === 1 ? '4' : '0'}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<Circle
|
|
91
|
+
cx={x}
|
|
92
|
+
cy={yVal}
|
|
93
|
+
r={5}
|
|
94
|
+
fill={colors[si]}
|
|
95
|
+
onPress={() =>
|
|
96
|
+
setActivePoint({ x, y: yVal, value: v })
|
|
97
|
+
}
|
|
61
98
|
/>
|
|
62
|
-
|
|
99
|
+
|
|
63
100
|
<SvgText
|
|
64
101
|
x={x}
|
|
65
102
|
y={yVal - 8}
|
|
66
103
|
fontSize="10"
|
|
67
104
|
textAnchor="middle"
|
|
68
105
|
>
|
|
69
|
-
{v
|
|
106
|
+
{formatNumber(v)}
|
|
70
107
|
</SvgText>
|
|
71
108
|
</React.Fragment>
|
|
72
109
|
);
|
|
73
110
|
})
|
|
74
111
|
)}
|
|
112
|
+
|
|
113
|
+
{/* TOOLTIP (ONLY ONCE) */}
|
|
114
|
+
{activePoint && (
|
|
115
|
+
<>
|
|
116
|
+
<Rect
|
|
117
|
+
x={activePoint.x - 30}
|
|
118
|
+
y={activePoint.y - 40}
|
|
119
|
+
width={60}
|
|
120
|
+
height={24}
|
|
121
|
+
rx={6}
|
|
122
|
+
fill="#000"
|
|
123
|
+
opacity={0.75}
|
|
124
|
+
/>
|
|
125
|
+
<SvgText
|
|
126
|
+
x={activePoint.x}
|
|
127
|
+
y={activePoint.y - 24}
|
|
128
|
+
fontSize="10"
|
|
129
|
+
fill="#fff"
|
|
130
|
+
textAnchor="middle"
|
|
131
|
+
>
|
|
132
|
+
{formatNumber(activePoint.value)}
|
|
133
|
+
</SvgText>
|
|
134
|
+
</>
|
|
135
|
+
)}
|
|
136
|
+
|
|
137
|
+
{/* X LABELS */}
|
|
138
|
+
{data.labels.map((label, i) => {
|
|
139
|
+
const x = padding + i * xStep;
|
|
140
|
+
return (
|
|
141
|
+
<SvgText
|
|
142
|
+
key={i}
|
|
143
|
+
x={x}
|
|
144
|
+
y={height - 10}
|
|
145
|
+
fontSize="10"
|
|
146
|
+
textAnchor="middle"
|
|
147
|
+
transform={`rotate(-35 ${x} ${height - 10})`}
|
|
148
|
+
>
|
|
149
|
+
{label}
|
|
150
|
+
</SvgText>
|
|
151
|
+
);
|
|
152
|
+
})}
|
|
75
153
|
</Svg>
|
|
76
154
|
</ScrollView>
|
|
77
155
|
|
|
78
|
-
{/*
|
|
156
|
+
{/* LEGEND */}
|
|
79
157
|
<View style={{ flexDirection: 'row', marginTop: 8 }}>
|
|
80
158
|
{data.series.map((s, i) => (
|
|
81
159
|
<Text key={i} style={{ marginRight: 16, color: colors[i] }}>
|
package/src/index.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
2
|
import { View, ActivityIndicator } from 'react-native';
|
|
3
|
-
|
|
3
|
+
import SafeScreen from './components/SafeScreen';
|
|
4
4
|
import ReportListScreen from './screens/ReportListScreen';
|
|
5
5
|
import Report1Screen from './screens/Report1Screen';
|
|
6
6
|
import Report2Screen from './screens/Report2Screen';
|
|
@@ -36,39 +36,47 @@ const AnalyticsReports = ({ config }) => {
|
|
|
36
36
|
|
|
37
37
|
// 👉 AFTER LOADER → SHOW REPORT LIST
|
|
38
38
|
if (!active) {
|
|
39
|
-
|
|
39
|
+
<SafeScreen>
|
|
40
|
+
<ReportListScreen onSelect={setActive} />
|
|
41
|
+
</SafeScreen>
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
// 👉 REPORT 1
|
|
43
45
|
if (active === 1) {
|
|
44
46
|
return (
|
|
47
|
+
<SafeScreen>
|
|
45
48
|
<Report1Screen
|
|
46
49
|
endpoint={config.report1.url}
|
|
47
50
|
token={config.token}
|
|
48
51
|
onBack={() => setActive(null)}
|
|
49
52
|
/>
|
|
53
|
+
</SafeScreen>
|
|
50
54
|
);
|
|
51
55
|
}
|
|
52
56
|
|
|
53
57
|
// 👉 REPORT 2
|
|
54
58
|
if (active === 2) {
|
|
55
59
|
return (
|
|
60
|
+
<SafeScreen>
|
|
56
61
|
<Report2Screen
|
|
57
62
|
api={config.report2}
|
|
58
63
|
token={config.token}
|
|
59
64
|
onBack={() => setActive(null)}
|
|
60
65
|
/>
|
|
66
|
+
</SafeScreen>
|
|
61
67
|
);
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
// 👉 REPORT 3
|
|
65
71
|
if (active === 3) {
|
|
66
72
|
return (
|
|
73
|
+
<SafeScreen>
|
|
67
74
|
<Report3Screen
|
|
68
75
|
api={config.report3}
|
|
69
76
|
token={config.token}
|
|
70
77
|
onBack={() => setActive(null)}
|
|
71
78
|
/>
|
|
79
|
+
</SafeScreen>
|
|
72
80
|
);
|
|
73
81
|
}
|
|
74
82
|
|
|
@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|
|
2
2
|
import { ScrollView, Text, ActivityIndicator } from 'react-native';
|
|
3
3
|
import fetchReport1 from '../api/report1Fetcher';
|
|
4
4
|
import Report1Card from '../components/Report1Card';
|
|
5
|
+
import SafeScreen from '../components/SafeScreen';
|
|
5
6
|
|
|
6
7
|
const Report1Screen = ({ endpoint, token, onBack }) => {
|
|
7
8
|
const [rows, setRows] = useState(null);
|
|
@@ -13,6 +14,7 @@ const Report1Screen = ({ endpoint, token, onBack }) => {
|
|
|
13
14
|
if (!rows) return <ActivityIndicator />;
|
|
14
15
|
|
|
15
16
|
return (
|
|
17
|
+
<SafeScreen>
|
|
16
18
|
<ScrollView style={{ padding: 16 }}>
|
|
17
19
|
<Text onPress={onBack} style={{ marginBottom: 12 }}>‹ Back</Text>
|
|
18
20
|
<Text style={{ fontSize: 18, fontWeight: '700', marginBottom: 12 }}>
|
|
@@ -23,6 +25,7 @@ const Report1Screen = ({ endpoint, token, onBack }) => {
|
|
|
23
25
|
<Report1Card key={i} item={r} />
|
|
24
26
|
))}
|
|
25
27
|
</ScrollView>
|
|
28
|
+
</SafeScreen>
|
|
26
29
|
);
|
|
27
30
|
};
|
|
28
31
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { ScrollView, Text, ActivityIndicator } from 'react-native';
|
|
3
|
-
|
|
3
|
+
import SafeScreen from '../components/SafeScreen';
|
|
4
4
|
import { getDivisions, getTable, getLine, getBar } from '../api/report2Fetcher';
|
|
5
5
|
import MonthSelector from '../components/MonthSelector';
|
|
6
6
|
import DivisionSelector from '../components/DivisionSelector';
|
|
@@ -65,6 +65,7 @@ const filteredBar = filterChartByMonth(bar, month);
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
return (
|
|
68
|
+
<SafeScreen>
|
|
68
69
|
<ScrollView style={{ padding: 16 }}>
|
|
69
70
|
<Text onPress={onBack}>‹ Back</Text>
|
|
70
71
|
|
|
@@ -91,6 +92,7 @@ const filteredBar = filterChartByMonth(bar, month);
|
|
|
91
92
|
|
|
92
93
|
|
|
93
94
|
</ScrollView>
|
|
95
|
+
</SafeScreen>
|
|
94
96
|
);
|
|
95
97
|
};
|
|
96
98
|
|
|
@@ -53,6 +53,7 @@ const filteredBar = filterChartByMonth(bar, month);
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
return (
|
|
56
|
+
<SafeScreen>
|
|
56
57
|
<ScrollView style={{ padding: 16 }}>
|
|
57
58
|
<Text onPress={onBack}>‹ Back</Text>
|
|
58
59
|
|
|
@@ -71,6 +72,7 @@ const filteredBar = filterChartByMonth(bar, month);
|
|
|
71
72
|
<SvgLineChart data={filteredLine} />
|
|
72
73
|
<SvgBarLineChart data={filteredBar} />
|
|
73
74
|
</ScrollView>
|
|
75
|
+
</SafeScreen>
|
|
74
76
|
);
|
|
75
77
|
};
|
|
76
78
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const formatNumber = (value) => {
|
|
2
|
+
if (value >= 1_000_000_000) {
|
|
3
|
+
return (value / 1_000_000_000).toFixed(1) + 'bn';
|
|
4
|
+
}
|
|
5
|
+
if (value >= 1_000_000) {
|
|
6
|
+
return (value / 1_000_000).toFixed(1) + 'M';
|
|
7
|
+
}
|
|
8
|
+
if (value >= 1_000) {
|
|
9
|
+
return (value / 1_000).toFixed(0) + 'k';
|
|
10
|
+
}
|
|
11
|
+
return value.toString();
|
|
12
|
+
};
|