@dhiraj0720/report1chart 2.2.8 → 2.3.0
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/SvgBarLineChart.jsx +200 -68
- package/src/components/SvgLineChart.jsx +146 -64
- package/src/index.jsx +31 -1
- package/src/screens/Report2Screen.jsx +27 -5
- package/src/screens/Report3Screen.jsx +21 -2
- 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>
|
|
@@ -1,84 +1,216 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { View, Text } from 'react-native';
|
|
3
|
-
import Svg, {
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, ScrollView } from 'react-native';
|
|
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?.length) return null;
|
|
12
|
+
if (!data?.series || !data.labels?.length) return null;
|
|
7
13
|
|
|
8
|
-
const
|
|
9
|
-
const height = 190;
|
|
10
|
-
const padding = 30;
|
|
11
|
-
const barWidth = 12;
|
|
14
|
+
const [activePoint, setActivePoint] = useState(null);
|
|
12
15
|
|
|
13
|
-
const max = Math.max(...data.series.flatMap(s => s.data));
|
|
14
16
|
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
+
const width = Math.max(360, data.labels.length * 90);
|
|
18
|
+
const height = 240;
|
|
19
|
+
const padding = 40;
|
|
20
|
+
const chartHeight = height - padding * 2;
|
|
21
|
+
|
|
22
|
+
const max = Math.max(...data.series.flatMap(s => s.data));
|
|
23
|
+
const barWidth = 14;
|
|
24
|
+
const step = 70;
|
|
17
25
|
|
|
18
|
-
const scaleY = v =>
|
|
19
|
-
|
|
26
|
+
const scaleY = v => (v / max) * chartHeight;
|
|
27
|
+
const yPos = v => height - padding - scaleY(v);
|
|
20
28
|
|
|
21
29
|
return (
|
|
22
|
-
<View style={{
|
|
23
|
-
<Text style={{ fontWeight: '700',
|
|
30
|
+
<View style={{ marginVertical: 16 }}>
|
|
31
|
+
<Text style={{ fontWeight: '700', marginBottom: 8 }}>
|
|
24
32
|
{data.title}
|
|
25
33
|
</Text>
|
|
26
34
|
|
|
27
|
-
<
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
35
|
+
<ScrollView horizontal>
|
|
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 */}
|
|
63
|
+
{data.labels.map((label, i) => {
|
|
64
|
+
const x = padding + i * step;
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<React.Fragment key={i}>
|
|
68
|
+
{/* 2024 BAR */}
|
|
69
|
+
<Rect
|
|
70
|
+
x={x}
|
|
71
|
+
y={yPos(data.series[0].data[i])}
|
|
72
|
+
width={barWidth}
|
|
73
|
+
height={scaleY(dat.series[0].data[i])}
|
|
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
|
+
}
|
|
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>
|
|
91
|
+
|
|
92
|
+
{/* 2025 BAR */}
|
|
93
|
+
<Rect
|
|
94
|
+
x={x + barWidth + 4}
|
|
95
|
+
y={yPos(data.series[1].data[i])}
|
|
96
|
+
width={barWidth}
|
|
97
|
+
height={scaleY(data.series[1].data[i])}
|
|
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
|
+
}
|
|
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>
|
|
115
|
+
|
|
116
|
+
{/* BUDGET DOT */}
|
|
117
|
+
<Circle
|
|
118
|
+
cx={x + barWidth + 2}
|
|
119
|
+
cy={yPos(data.series[2].data[i])}
|
|
120
|
+
r={4}
|
|
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
|
+
}
|
|
129
|
+
/>
|
|
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 */}
|
|
142
|
+
<SvgText
|
|
143
|
+
x={x + barWidth}
|
|
144
|
+
y={height - 10}
|
|
145
|
+
fontSize="10"
|
|
146
|
+
textAnchor="middle"
|
|
147
|
+
transform={`rotate(-35 ${x + barWidth} ${height - 10})`}
|
|
148
|
+
>
|
|
149
|
+
{label}
|
|
150
|
+
</SvgText>
|
|
151
|
+
</React.Fragment>
|
|
152
|
+
);
|
|
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}
|
|
63
165
|
/>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
})}
|
|
195
|
+
</Svg>
|
|
196
|
+
</ScrollView>
|
|
197
|
+
|
|
198
|
+
{/* LEGEND */}
|
|
199
|
+
<View style={{ flexDirection: 'row', marginTop: 8 }}>
|
|
200
|
+
{data.series.map((s, i) => (
|
|
201
|
+
<Text key={i} style={{ marginRight: 16 }}>
|
|
202
|
+
<Text
|
|
203
|
+
style={{
|
|
204
|
+
color:
|
|
205
|
+
i === 0 ? '#E07A3F' : i === 1 ? '#4E79A7' : '#8AB6E8',
|
|
206
|
+
}}
|
|
207
|
+
>
|
|
208
|
+
●
|
|
209
|
+
</Text>{' '}
|
|
210
|
+
{s.name}
|
|
211
|
+
</Text>
|
|
80
212
|
))}
|
|
81
|
-
</
|
|
213
|
+
</View>
|
|
82
214
|
</View>
|
|
83
215
|
);
|
|
84
216
|
};
|
|
@@ -1,84 +1,166 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { View, Text } from 'react-native';
|
|
3
|
-
import Svg, {
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { View, Text, ScrollView } from 'react-native';
|
|
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?.length) return null;
|
|
12
|
+
if (!data?.series || !data.labels?.length) return null;
|
|
7
13
|
|
|
8
|
-
const
|
|
9
|
-
const height = 180;
|
|
10
|
-
const padding = 30;
|
|
14
|
+
const [activePoint, setActivePoint] = useState(null);
|
|
11
15
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
16
|
+
const width = Math.max(360, data.labels.length * 80);
|
|
17
|
+
const height = 220;
|
|
18
|
+
const padding = 40;
|
|
19
|
+
const chartHeight = height - padding * 2;
|
|
15
20
|
|
|
16
|
-
const
|
|
17
|
-
|
|
21
|
+
const values = data.series.flatMap(s => s.data);
|
|
22
|
+
const max = Math.max(...values);
|
|
23
|
+
const min = 0;
|
|
18
24
|
|
|
19
|
-
const
|
|
20
|
-
height - padding - ((v - min) / (max - min)) *
|
|
25
|
+
const y = v =>
|
|
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);
|
|
32
|
+
|
|
33
|
+
const colors = ['#E07A3F', '#4E79A7'];
|
|
21
34
|
|
|
22
35
|
return (
|
|
23
|
-
<View style={{
|
|
24
|
-
<Text style={{ fontWeight: '700',
|
|
36
|
+
<View style={{ marginVertical: 16 }}>
|
|
37
|
+
<Text style={{ fontWeight: '700', marginBottom: 8 }}>
|
|
25
38
|
{data.title}
|
|
26
39
|
</Text>
|
|
27
40
|
|
|
28
|
-
<
|
|
29
|
-
{
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
<ScrollView horizontal>
|
|
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
|
+
})}
|
|
69
|
+
|
|
70
|
+
{/* LINES + DOTS */}
|
|
71
|
+
{data.series.map((s, si) =>
|
|
72
|
+
s.data.map((v, i) => {
|
|
73
|
+
const x = padding + i * xStep;
|
|
74
|
+
const yVal = y(v);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<React.Fragment key={`${si}-${i}`}>
|
|
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
|
+
}
|
|
98
|
+
/>
|
|
99
|
+
|
|
100
|
+
<SvgText
|
|
101
|
+
x={x}
|
|
102
|
+
y={yVal - 8}
|
|
103
|
+
fontSize="10"
|
|
104
|
+
textAnchor="middle"
|
|
105
|
+
>
|
|
106
|
+
{formatNumber(v)}
|
|
107
|
+
</SvgText>
|
|
108
|
+
</React.Fragment>
|
|
109
|
+
);
|
|
110
|
+
})
|
|
111
|
+
)}
|
|
41
112
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
stroke={idx === 0 ? '#FB8C00' : '#1E88E5'}
|
|
54
|
-
strokeWidth="2"
|
|
55
|
-
/>
|
|
56
|
-
);
|
|
57
|
-
})}
|
|
58
|
-
|
|
59
|
-
{/* Points + Labels */}
|
|
60
|
-
{data.series.map((s, idx) =>
|
|
61
|
-
s.data.map((v, i) => (
|
|
62
|
-
<React.Fragment key={`${idx}-${i}`}>
|
|
63
|
-
<Circle
|
|
64
|
-
cx={scaleX(i)}
|
|
65
|
-
cy={scaleY(v)}
|
|
66
|
-
r="4"
|
|
67
|
-
fill={idx === 0 ? '#FB8C00' : '#1E88E5'}
|
|
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}
|
|
68
124
|
/>
|
|
69
125
|
<SvgText
|
|
70
|
-
x={
|
|
71
|
-
y={
|
|
72
|
-
fontSize="
|
|
73
|
-
fill="#
|
|
126
|
+
x={activePoint.x}
|
|
127
|
+
y={activePoint.y - 24}
|
|
128
|
+
fontSize="10"
|
|
129
|
+
fill="#fff"
|
|
74
130
|
textAnchor="middle"
|
|
75
131
|
>
|
|
76
|
-
{
|
|
132
|
+
{formatNumber(activePoint.value)}
|
|
77
133
|
</SvgText>
|
|
78
|
-
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
})}
|
|
153
|
+
</Svg>
|
|
154
|
+
</ScrollView>
|
|
155
|
+
|
|
156
|
+
{/* LEGEND */}
|
|
157
|
+
<View style={{ flexDirection: 'row', marginTop: 8 }}>
|
|
158
|
+
{data.series.map((s, i) => (
|
|
159
|
+
<Text key={i} style={{ marginRight: 16, color: colors[i] }}>
|
|
160
|
+
● {s.name}
|
|
161
|
+
</Text>
|
|
162
|
+
))}
|
|
163
|
+
</View>
|
|
82
164
|
</View>
|
|
83
165
|
);
|
|
84
166
|
};
|
package/src/index.jsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { View, ActivityIndicator } from 'react-native';
|
|
2
3
|
|
|
3
4
|
import ReportListScreen from './screens/ReportListScreen';
|
|
4
5
|
import Report1Screen from './screens/Report1Screen';
|
|
@@ -7,11 +8,38 @@ import Report3Screen from './screens/Report3Screen';
|
|
|
7
8
|
|
|
8
9
|
const AnalyticsReports = ({ config }) => {
|
|
9
10
|
const [active, setActive] = useState(null);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
10
12
|
|
|
13
|
+
// SPLASH / LOADER (5 seconds)
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const timer = setTimeout(() => {
|
|
16
|
+
setLoading(false);
|
|
17
|
+
}, 5000);
|
|
18
|
+
|
|
19
|
+
return () => clearTimeout(timer);
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
// 👉 SHOW LOADER FIRST (BEFORE REPORT LIST)
|
|
23
|
+
if (loading) {
|
|
24
|
+
return (
|
|
25
|
+
<View
|
|
26
|
+
style={{
|
|
27
|
+
flex: 1,
|
|
28
|
+
justifyContent: 'center',
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
<ActivityIndicator size="large" />
|
|
33
|
+
</View>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 👉 AFTER LOADER → SHOW REPORT LIST
|
|
11
38
|
if (!active) {
|
|
12
39
|
return <ReportListScreen onSelect={setActive} />;
|
|
13
40
|
}
|
|
14
41
|
|
|
42
|
+
// 👉 REPORT 1
|
|
15
43
|
if (active === 1) {
|
|
16
44
|
return (
|
|
17
45
|
<Report1Screen
|
|
@@ -22,6 +50,7 @@ const AnalyticsReports = ({ config }) => {
|
|
|
22
50
|
);
|
|
23
51
|
}
|
|
24
52
|
|
|
53
|
+
// 👉 REPORT 2
|
|
25
54
|
if (active === 2) {
|
|
26
55
|
return (
|
|
27
56
|
<Report2Screen
|
|
@@ -32,6 +61,7 @@ const AnalyticsReports = ({ config }) => {
|
|
|
32
61
|
);
|
|
33
62
|
}
|
|
34
63
|
|
|
64
|
+
// 👉 REPORT 3
|
|
35
65
|
if (active === 3) {
|
|
36
66
|
return (
|
|
37
67
|
<Report3Screen
|
|
@@ -39,11 +39,31 @@ const Report2Screen = ({ api, token, onBack }) => {
|
|
|
39
39
|
|
|
40
40
|
if (!table || !line || !bar) return <ActivityIndicator />;
|
|
41
41
|
|
|
42
|
+
const filterChartByMonth = (chart, month) => {
|
|
43
|
+
if (month === 'ALL') return chart;
|
|
44
|
+
const index = chart.labels.indexOf(month);
|
|
45
|
+
if (index === -1) return chart;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...chart,
|
|
49
|
+
labels: [chart.labels[index]],
|
|
50
|
+
series: chart.series.map(s => ({
|
|
51
|
+
...s,
|
|
52
|
+
data: [s.data[index]],
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
|
|
42
58
|
const rows =
|
|
43
59
|
month === 'ALL'
|
|
44
60
|
? table.rows
|
|
45
61
|
: table.rows.filter(r => r.monthLabel === month);
|
|
46
62
|
|
|
63
|
+
const filteredLine = filterChartByMonth(line, month);
|
|
64
|
+
const filteredBar = filterChartByMonth(bar, month);
|
|
65
|
+
|
|
66
|
+
|
|
47
67
|
return (
|
|
48
68
|
<ScrollView style={{ padding: 16 }}>
|
|
49
69
|
<Text onPress={onBack}>‹ Back</Text>
|
|
@@ -58,16 +78,18 @@ const Report2Screen = ({ api, token, onBack }) => {
|
|
|
58
78
|
onSelect={setMonth}
|
|
59
79
|
/>
|
|
60
80
|
|
|
61
|
-
<FrozenTableReport2 rows={rows} />
|
|
62
|
-
|
|
63
|
-
<SvgLineChart data={line} />
|
|
64
|
-
<SvgBarLineChart data={bar} />
|
|
65
|
-
|
|
66
81
|
<DivisionSelector
|
|
67
82
|
divisions={divisions}
|
|
68
83
|
selected={division}
|
|
69
84
|
onSelect={setDivision}
|
|
70
85
|
/>
|
|
86
|
+
|
|
87
|
+
<FrozenTableReport2 rows={rows} />
|
|
88
|
+
|
|
89
|
+
<SvgLineChart data={filteredLine} />
|
|
90
|
+
<SvgBarLineChart data={filteredBar} />
|
|
91
|
+
|
|
92
|
+
|
|
71
93
|
</ScrollView>
|
|
72
94
|
);
|
|
73
95
|
};
|
|
@@ -26,6 +26,21 @@ const Report3Screen = ({ api, token, onBack }) => {
|
|
|
26
26
|
|
|
27
27
|
if (!table || !line || !bar) return <ActivityIndicator />;
|
|
28
28
|
|
|
29
|
+
const filterChartByMonth = (chart, month) => {
|
|
30
|
+
if (month === 'ALL') return chart;
|
|
31
|
+
const index = chart.labels.indexOf(month);
|
|
32
|
+
if (index === -1) return chart;
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
...chart,
|
|
36
|
+
labels: [chart.labels[index]],
|
|
37
|
+
series: chart.series.map(s => ({
|
|
38
|
+
...s,
|
|
39
|
+
data: [s.data[index]],
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
29
44
|
const months = table.rows.filter(r => r.month !== 99).map(r => r.monthLabel);
|
|
30
45
|
|
|
31
46
|
const rows =
|
|
@@ -33,6 +48,10 @@ const Report3Screen = ({ api, token, onBack }) => {
|
|
|
33
48
|
? table.rows
|
|
34
49
|
: table.rows.filter(r => r.monthLabel === month);
|
|
35
50
|
|
|
51
|
+
const filteredLine = filterChartByMonth(line, month);
|
|
52
|
+
const filteredBar = filterChartByMonth(bar, month);
|
|
53
|
+
|
|
54
|
+
|
|
36
55
|
return (
|
|
37
56
|
<ScrollView style={{ padding: 16 }}>
|
|
38
57
|
<Text onPress={onBack}>‹ Back</Text>
|
|
@@ -49,8 +68,8 @@ const Report3Screen = ({ api, token, onBack }) => {
|
|
|
49
68
|
|
|
50
69
|
<FrozenTableReport3 rows={rows} />
|
|
51
70
|
|
|
52
|
-
<SvgLineChart data={
|
|
53
|
-
|
|
71
|
+
<SvgLineChart data={filteredLine} />
|
|
72
|
+
<SvgBarLineChart data={filteredBar} />
|
|
54
73
|
</ScrollView>
|
|
55
74
|
);
|
|
56
75
|
};
|
|
@@ -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
|
+
};
|