@dhiraj0720/report1chart 2.5.7 → 2.5.8
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
CHANGED
|
@@ -1,60 +1,160 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { View, Text, ScrollView, StyleSheet } from 'react-native';
|
|
3
|
-
import { formatNumber } from '../utils/formatNumber';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
// Helper: Format numbers with commas (e.g., 1083203 → 1,083,203)
|
|
5
|
+
const formatNumber = (value) => {
|
|
6
|
+
if (value === null || value === undefined || value === '') return '-';
|
|
7
|
+
return Number(value).toLocaleString('en-US');
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// Percentage cell with up/down arrow and color
|
|
11
|
+
const PercentCell = ({ value }) => {
|
|
12
|
+
if (value === null || value === undefined) return <Text>-</Text>;
|
|
13
|
+
const positive = value >= 0;
|
|
14
|
+
return (
|
|
15
|
+
<Text style={[
|
|
16
|
+
styles.percentText,
|
|
17
|
+
{ color: positive ? '#2e7d32' : '#d32f2f' }
|
|
18
|
+
]}>
|
|
19
|
+
{positive ? '↑' : '↓'} {Math.abs(value)}%
|
|
20
|
+
</Text>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
10
23
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
24
|
+
// Reusable cell component
|
|
25
|
+
const Cell = ({ children, bold = false, highlight = false }) => (
|
|
26
|
+
<View style={[
|
|
27
|
+
styles.cell,
|
|
28
|
+
bold && styles.bold,
|
|
29
|
+
highlight && styles.highlightCell
|
|
30
|
+
]}>
|
|
31
|
+
<Text style={[styles.cellText, bold && styles.boldText]}>
|
|
32
|
+
{children}
|
|
33
|
+
</Text>
|
|
14
34
|
</View>
|
|
15
35
|
);
|
|
16
36
|
|
|
17
|
-
const FrozenTableReport2A = ({ rows }) =>
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
{rows.map((r, i) => <Cell key={i} bold>{r.monthLabel}</Cell>)}
|
|
22
|
-
</View>
|
|
37
|
+
const FrozenTableReport2A = ({ rows = [] }) => {
|
|
38
|
+
if (!rows || rows.length === 0) {
|
|
39
|
+
return <Text>No data available</Text>;
|
|
40
|
+
}
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
42
|
+
return (
|
|
43
|
+
<View style={styles.container}>
|
|
44
|
+
{/* Frozen Left Column - AY (Month) */}
|
|
45
|
+
<View style={styles.frozenColumn}>
|
|
46
|
+
<Cell bold>AY</Cell>
|
|
47
|
+
{rows.map((row, index) => {
|
|
48
|
+
const isTotal = row.monthLabel === 'Total';
|
|
49
|
+
return (
|
|
50
|
+
<Cell
|
|
51
|
+
key={index}
|
|
52
|
+
bold={isTotal}
|
|
53
|
+
highlight={isTotal}
|
|
54
|
+
>
|
|
55
|
+
{row.monthLabel}
|
|
56
|
+
</Cell>
|
|
57
|
+
);
|
|
58
|
+
})}
|
|
59
|
+
</View>
|
|
31
60
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
61
|
+
{/* Scrollable Columns */}
|
|
62
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
63
|
+
<View>
|
|
64
|
+
{/* Header Row */}
|
|
65
|
+
<View style={styles.headerRow}>
|
|
66
|
+
{[
|
|
67
|
+
'2024 TEU', '2025 TEU', 'TEU %',
|
|
68
|
+
'2024 Kar', '2025 Kar', 'Kar %',
|
|
69
|
+
'2025 Bütçe', 'Bütçe %'
|
|
70
|
+
].map((header) => (
|
|
71
|
+
<Cell key={header} bold>
|
|
72
|
+
{header}
|
|
73
|
+
</Cell>
|
|
74
|
+
))}
|
|
37
75
|
</View>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
76
|
+
|
|
77
|
+
{/* Data Rows */}
|
|
78
|
+
{rows.map((row, index) => {
|
|
79
|
+
const isTotal = row.monthLabel === 'Total';
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<View key={index} style={styles.dataRow}>
|
|
83
|
+
<Cell highlight={isTotal}>{formatNumber(row.teu2024)}</Cell>
|
|
84
|
+
<Cell highlight={isTotal}>{formatNumber(row.teu2025)}</Cell>
|
|
85
|
+
<Cell highlight={isTotal}>
|
|
86
|
+
<PercentCell value={row.teuChangePercent} />
|
|
87
|
+
</Cell>
|
|
88
|
+
|
|
89
|
+
<Cell highlight={isTotal}>{formatNumber(row.profitUsd2024)}</Cell>
|
|
90
|
+
<Cell highlight={isTotal}>{formatNumber(row.profitUsd2025)}</Cell>
|
|
91
|
+
<Cell highlight={isTotal}>
|
|
92
|
+
<PercentCell value={row.profitChangePercent} />
|
|
93
|
+
</Cell>
|
|
94
|
+
|
|
95
|
+
<Cell highlight={isTotal}>{formatNumber(row.budgetProfitUsd2025)}</Cell>
|
|
96
|
+
<Cell highlight={isTotal}>
|
|
97
|
+
<PercentCell value={row.budgetChangePercent} />
|
|
98
|
+
</Cell>
|
|
99
|
+
</View>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</View>
|
|
103
|
+
</ScrollView>
|
|
104
|
+
</View>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
43
107
|
|
|
44
108
|
export default FrozenTableReport2A;
|
|
45
109
|
|
|
46
110
|
const styles = StyleSheet.create({
|
|
47
|
-
container: {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
111
|
+
container: {
|
|
112
|
+
flexDirection: 'row',
|
|
113
|
+
borderWidth: 1,
|
|
114
|
+
borderColor: '#ddd',
|
|
115
|
+
borderRadius: 8,
|
|
116
|
+
overflow: 'hidden',
|
|
117
|
+
marginVertical: 12,
|
|
118
|
+
backgroundColor: '#fff',
|
|
119
|
+
},
|
|
120
|
+
frozenColumn: {
|
|
121
|
+
width: 120,
|
|
122
|
+
backgroundColor: '#f4f6f8',
|
|
123
|
+
borderRightWidth: 1,
|
|
55
124
|
borderColor: '#ddd',
|
|
125
|
+
},
|
|
126
|
+
headerRow: {
|
|
127
|
+
flexDirection: 'row',
|
|
128
|
+
backgroundColor: '#f4f6f8',
|
|
129
|
+
},
|
|
130
|
+
dataRow: {
|
|
131
|
+
flexDirection: 'row',
|
|
132
|
+
},
|
|
133
|
+
cell: {
|
|
134
|
+
width: 130,
|
|
135
|
+
paddingVertical: 10,
|
|
136
|
+
paddingHorizontal: 6,
|
|
56
137
|
justifyContent: 'center',
|
|
57
|
-
|
|
138
|
+
borderBottomWidth: 1,
|
|
139
|
+
borderColor: '#e0e0e0',
|
|
140
|
+
},
|
|
141
|
+
cellText: {
|
|
142
|
+
fontSize: 12,
|
|
143
|
+
textAlign: 'center',
|
|
144
|
+
color: '#333',
|
|
145
|
+
},
|
|
146
|
+
bold: {
|
|
147
|
+
backgroundColor: '#e9f0f8',
|
|
148
|
+
},
|
|
149
|
+
boldText: {
|
|
150
|
+
fontWeight: '700',
|
|
151
|
+
},
|
|
152
|
+
highlightCell: {
|
|
153
|
+
backgroundColor: '#e9f0f8',
|
|
154
|
+
},
|
|
155
|
+
percentText: {
|
|
156
|
+
fontWeight: '700',
|
|
157
|
+
fontSize: 12,
|
|
158
|
+
textAlign: 'center',
|
|
58
159
|
},
|
|
59
|
-
|
|
60
|
-
});
|
|
160
|
+
});
|
|
@@ -6,107 +6,125 @@ import { formatNumber } from '../utils/formatNumber';
|
|
|
6
6
|
const SvgBarLineChartCompact = ({ data }) => {
|
|
7
7
|
if (!data?.series || !data.labels?.length) return null;
|
|
8
8
|
|
|
9
|
-
const height =
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const paddingLeft = 50;
|
|
13
|
-
const paddingRight = 10;
|
|
9
|
+
const height = 240;
|
|
10
|
+
const paddingLeft = 60;
|
|
11
|
+
const paddingRight = 20;
|
|
14
12
|
const paddingTop = 20;
|
|
15
|
-
const paddingBottom =
|
|
13
|
+
const paddingBottom = 50;
|
|
16
14
|
|
|
17
15
|
const chartHeight = height - paddingTop - paddingBottom;
|
|
16
|
+
const graphWidth = Math.max(400, data.labels.length * 90);
|
|
17
|
+
|
|
18
|
+
const MAX = 1279958; // Slightly above max in image
|
|
19
|
+
const barWidth = 16;
|
|
20
|
+
const groupGap = 20;
|
|
21
|
+
const groupWidth = barWidth * 2 + 4 + groupGap;
|
|
18
22
|
|
|
19
|
-
const
|
|
20
|
-
const barWidth = 14;
|
|
23
|
+
const y = (v) => paddingTop + ((MAX - v) / MAX) * chartHeight;
|
|
21
24
|
|
|
22
|
-
const
|
|
23
|
-
paddingTop + ((MAX - v) / MAX) * chartHeight;
|
|
25
|
+
const getX = (i) => paddingLeft + i * groupWidth + groupGap / 2;
|
|
24
26
|
|
|
25
27
|
return (
|
|
26
28
|
<View style={styles.container}>
|
|
27
29
|
<Text style={styles.title}>{data.title}</Text>
|
|
28
30
|
|
|
29
31
|
<View style={{ flexDirection: 'row' }}>
|
|
30
|
-
{/* FIXED Y AXIS */}
|
|
31
32
|
<Svg width={paddingLeft} height={height}>
|
|
32
|
-
{[1000000, 500000, 0].map((v
|
|
33
|
+
{[1000000, 500000, 0].map((v) => (
|
|
33
34
|
<SvgText
|
|
34
|
-
key={
|
|
35
|
-
x={paddingLeft -
|
|
36
|
-
y={y(v) +
|
|
37
|
-
fontSize="
|
|
35
|
+
key={v}
|
|
36
|
+
x={paddingLeft - 8}
|
|
37
|
+
y={y(v) + 5}
|
|
38
|
+
fontSize="11"
|
|
38
39
|
textAnchor="end"
|
|
39
40
|
fill="#444"
|
|
40
41
|
>
|
|
41
|
-
{(v /
|
|
42
|
+
{(v / 1000000).toFixed(1)}M
|
|
42
43
|
</SvgText>
|
|
43
44
|
))}
|
|
44
45
|
</Svg>
|
|
45
46
|
|
|
46
|
-
{/* SCROLLABLE GRAPH */}
|
|
47
47
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
48
48
|
<Svg width={graphWidth} height={height}>
|
|
49
|
-
{/*
|
|
50
|
-
{[1000000, 500000, 0].map((v
|
|
51
|
-
<Line
|
|
52
|
-
key={i}
|
|
53
|
-
x1={0}
|
|
54
|
-
x2={graphWidth}
|
|
55
|
-
y1={y(v)}
|
|
56
|
-
y2={y(v)}
|
|
57
|
-
stroke="#ccc"
|
|
58
|
-
strokeDasharray="4"
|
|
59
|
-
/>
|
|
49
|
+
{/* Grid */}
|
|
50
|
+
{[1000000, 500000, 0].map((v) => (
|
|
51
|
+
<Line key={v} x1={0} x2={graphWidth} y1={y(v)} y2={y(v)} stroke="#eee" strokeDasharray="5,5" />
|
|
60
52
|
))}
|
|
61
53
|
|
|
62
54
|
{data.labels.map((label, i) => {
|
|
63
|
-
const
|
|
55
|
+
const xCenter = getX(i);
|
|
56
|
+
const bar2024 = data.series[0].data[i] || 0;
|
|
57
|
+
const bar2025 = data.series[1].data[i] || 0;
|
|
58
|
+
const budget = data.series[2].data[i] || 0;
|
|
59
|
+
|
|
60
|
+
const height2024 = chartHeight - (y(bar2024) - paddingTop);
|
|
61
|
+
const height2025 = chartHeight - (y(bar2025) - paddingTop);
|
|
64
62
|
|
|
65
63
|
return (
|
|
66
64
|
<React.Fragment key={i}>
|
|
67
|
-
{/* 2024
|
|
65
|
+
{/* 2024 Bar */}
|
|
68
66
|
<Rect
|
|
69
|
-
x={
|
|
70
|
-
y={y(
|
|
67
|
+
x={xCenter - barWidth - 2}
|
|
68
|
+
y={y(bar2024)}
|
|
71
69
|
width={barWidth}
|
|
72
|
-
height={
|
|
70
|
+
height={height2024}
|
|
73
71
|
fill="#E07A3F"
|
|
74
72
|
/>
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
{bar2024 > MAX * 0.1 && (
|
|
74
|
+
<SvgText
|
|
75
|
+
x={xCenter - barWidth - 2 + barWidth / 2}
|
|
76
|
+
y={y(bar2024) + height2024 / 2 + 5}
|
|
77
|
+
fontSize="9"
|
|
78
|
+
textAnchor="middle"
|
|
79
|
+
fill="white"
|
|
80
|
+
fontWeight="700"
|
|
81
|
+
>
|
|
82
|
+
{formatNumber(bar2024)}
|
|
83
|
+
</SvgText>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{/* 2025 Bar */}
|
|
77
87
|
<Rect
|
|
78
|
-
x={
|
|
79
|
-
y={y(
|
|
88
|
+
x={xCenter + 2}
|
|
89
|
+
y={y(bar2025)}
|
|
80
90
|
width={barWidth}
|
|
81
|
-
height={
|
|
91
|
+
height={height2025}
|
|
82
92
|
fill="#4E79A7"
|
|
83
93
|
/>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
{bar2025 > MAX * 0.1 && (
|
|
95
|
+
<SvgText
|
|
96
|
+
x={xCenter + 2 + barWidth / 2}
|
|
97
|
+
y={y(bar2025) + height2025 / 2 + 5}
|
|
98
|
+
fontSize="9"
|
|
99
|
+
textAnchor="middle"
|
|
100
|
+
fill="white"
|
|
101
|
+
fontWeight="700"
|
|
102
|
+
>
|
|
103
|
+
{formatNumber(bar2025)}
|
|
104
|
+
</SvgText>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{/* Budget Line + Dot */}
|
|
108
|
+
<Circle cx={xCenter} cy={y(budget)} r={5} fill="#8AB6E8" stroke="#fff" strokeWidth={2} />
|
|
94
109
|
<SvgText
|
|
95
|
-
x={
|
|
96
|
-
y={y(
|
|
97
|
-
fontSize="
|
|
110
|
+
x={xCenter}
|
|
111
|
+
y={y(budget) - 10}
|
|
112
|
+
fontSize="10"
|
|
98
113
|
textAnchor="middle"
|
|
114
|
+
fill="#333"
|
|
115
|
+
fontWeight="700"
|
|
99
116
|
>
|
|
100
|
-
{formatNumber(
|
|
117
|
+
{formatNumber(budget)}
|
|
101
118
|
</SvgText>
|
|
102
119
|
|
|
103
|
-
{/* X
|
|
120
|
+
{/* X Label */}
|
|
104
121
|
<SvgText
|
|
105
|
-
x={
|
|
106
|
-
y={height -
|
|
107
|
-
fontSize="
|
|
122
|
+
x={xCenter}
|
|
123
|
+
y={height - 20}
|
|
124
|
+
fontSize="11"
|
|
108
125
|
textAnchor="middle"
|
|
109
|
-
|
|
126
|
+
fill="#555"
|
|
127
|
+
transform={`rotate(-45 ${xCenter} ${height - 20})`}
|
|
110
128
|
>
|
|
111
129
|
{label}
|
|
112
130
|
</SvgText>
|
|
@@ -117,15 +135,10 @@ const SvgBarLineChartCompact = ({ data }) => {
|
|
|
117
135
|
</ScrollView>
|
|
118
136
|
</View>
|
|
119
137
|
|
|
120
|
-
{/* LEGEND */}
|
|
121
138
|
<View style={styles.legend}>
|
|
122
|
-
<Text style={{ color: '#E07A3F' }}>● {data.series[0].name}</Text>
|
|
123
|
-
<Text style={{ color: '#4E79A7',
|
|
124
|
-
|
|
125
|
-
</Text>
|
|
126
|
-
<Text style={{ color: '#8AB6E8', marginLeft: 12 }}>
|
|
127
|
-
● {data.series[2].name}
|
|
128
|
-
</Text>
|
|
139
|
+
<Text style={{ color: '#E07A3F', marginRight: 16 }}>● {data.series[0].name}</Text>
|
|
140
|
+
<Text style={{ color: '#4E79A7', marginRight: 16 }}>● {data.series[1].name}</Text>
|
|
141
|
+
<Text style={{ color: '#8AB6E8' }}>● {data.series[2].name}</Text>
|
|
129
142
|
</View>
|
|
130
143
|
</View>
|
|
131
144
|
);
|
|
@@ -7,43 +7,38 @@ const SvgLineChartCompact = ({ data }) => {
|
|
|
7
7
|
if (!data?.series || !data.labels?.length) return null;
|
|
8
8
|
|
|
9
9
|
const height = 200;
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const paddingLeft = 50; // Y axis space
|
|
13
|
-
const paddingRight = 10;
|
|
10
|
+
const paddingLeft = 60;
|
|
11
|
+
const paddingRight = 20;
|
|
14
12
|
const paddingTop = 20;
|
|
15
|
-
const paddingBottom =
|
|
13
|
+
const paddingBottom = 40;
|
|
14
|
+
|
|
15
|
+
const chartHeight = height - paddingTop - paddingBottom;
|
|
16
|
+
const graphWidth = Math.max(400, data.labels.length * 80);
|
|
16
17
|
|
|
17
18
|
// FIXED AXIS (as per image)
|
|
18
19
|
const MIN = 5000;
|
|
19
20
|
const MAX = 10000;
|
|
20
21
|
|
|
21
|
-
const
|
|
22
|
-
const xStep =
|
|
23
|
-
data.labels.length === 1
|
|
24
|
-
? 0
|
|
25
|
-
: (graphWidth - paddingLeft - paddingRight) /
|
|
26
|
-
(data.labels.length - 1);
|
|
22
|
+
const y = (v) => paddingTop + ((MAX - Math.max(MIN, Math.min(MAX, v))) / (MAX - MIN)) * chartHeight;
|
|
27
23
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
// Fix for single point: use fixed spacing
|
|
25
|
+
const totalPoints = data.labels.length;
|
|
26
|
+
const xStep = totalPoints === 1 ? 0 : (graphWidth - paddingLeft - paddingRight) / (totalPoints - 1);
|
|
31
27
|
|
|
32
|
-
const colors = ['#E07A3F', '#4E79A7']
|
|
28
|
+
const colors = ['#E07A3F', '#4E79A7'];// orange / blue
|
|
33
29
|
|
|
34
30
|
return (
|
|
35
31
|
<View style={styles.container}>
|
|
36
32
|
<Text style={styles.title}>{data.title}</Text>
|
|
37
33
|
|
|
38
34
|
<View style={{ flexDirection: 'row' }}>
|
|
39
|
-
{/* FIXED Y AXIS */}
|
|
40
35
|
<Svg width={paddingLeft} height={height}>
|
|
41
|
-
{[10000, 5000].map((v
|
|
36
|
+
{[10000, 5000].map((v) => (
|
|
42
37
|
<SvgText
|
|
43
|
-
key={
|
|
44
|
-
x={paddingLeft -
|
|
45
|
-
y={y(v) +
|
|
46
|
-
fontSize="
|
|
38
|
+
key={v}
|
|
39
|
+
x={paddingLeft - 8}
|
|
40
|
+
y={y(v) + 5}
|
|
41
|
+
fontSize="11"
|
|
47
42
|
textAnchor="end"
|
|
48
43
|
fill="#444"
|
|
49
44
|
>
|
|
@@ -52,55 +47,33 @@ const SvgLineChartCompact = ({ data }) => {
|
|
|
52
47
|
))}
|
|
53
48
|
</Svg>
|
|
54
49
|
|
|
55
|
-
{/* SCROLLABLE GRAPH */}
|
|
56
50
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
57
51
|
<Svg width={graphWidth} height={height}>
|
|
58
|
-
{/*
|
|
59
|
-
{[10000, 5000].map((v
|
|
60
|
-
<Line
|
|
61
|
-
key={i}
|
|
62
|
-
x1={0}
|
|
63
|
-
x2={graphWidth}
|
|
64
|
-
y1={y(v)}
|
|
65
|
-
y2={y(v)}
|
|
66
|
-
stroke="#ccc"
|
|
67
|
-
strokeDasharray="4"
|
|
68
|
-
/>
|
|
52
|
+
{/* Grid */}
|
|
53
|
+
{[10000, 5000].map((v) => (
|
|
54
|
+
<Line key={v} x1={0} x2={graphWidth} y1={y(v)} y2={y(v)} stroke="#eee" strokeDasharray="5,5" />
|
|
69
55
|
))}
|
|
70
56
|
|
|
71
|
-
{/*
|
|
72
|
-
{data.series.map((
|
|
73
|
-
|
|
74
|
-
const x =
|
|
75
|
-
paddingLeft + (data.labels.length === 1 ? 0 : i * xStep);
|
|
76
|
-
const yVal = y(val);
|
|
57
|
+
{/* Series */}
|
|
58
|
+
{data.series.map((series, si) =>
|
|
59
|
+
series.data.map((val, i) => {
|
|
60
|
+
const x = paddingLeft + i * (totalPoints === 1 ? graphWidth / 2 - paddingLeft : xStep);
|
|
77
61
|
|
|
78
62
|
return (
|
|
79
63
|
<React.Fragment key={`${si}-${i}`}>
|
|
80
|
-
{
|
|
81
|
-
{i < s.data.length - 1 && (
|
|
64
|
+
{i > 0 && (
|
|
82
65
|
<Line
|
|
83
|
-
x1={
|
|
84
|
-
y1={
|
|
85
|
-
x2={
|
|
86
|
-
y2={y(
|
|
66
|
+
x1={paddingLeft + (i - 1) * (totalPoints === 1 ? graphWidth / 2 - paddingLeft : xStep)}
|
|
67
|
+
y1={y(series.data[i - 1])}
|
|
68
|
+
x2={x}
|
|
69
|
+
y2={y(val)}
|
|
87
70
|
stroke={colors[si]}
|
|
88
|
-
strokeWidth={2}
|
|
89
|
-
strokeDasharray={si === 0 ? '4' : '0'}
|
|
71
|
+
strokeWidth={2.5}
|
|
72
|
+
strokeDasharray={si === 0 ? '6,4' : '0'}
|
|
90
73
|
/>
|
|
91
74
|
)}
|
|
92
|
-
|
|
93
|
-
{
|
|
94
|
-
<Circle cx={x} cy={yVal} r={4} fill={colors[si]} />
|
|
95
|
-
|
|
96
|
-
{/* VALUE */}
|
|
97
|
-
<SvgText
|
|
98
|
-
x={x}
|
|
99
|
-
y={yVal - 8}
|
|
100
|
-
fontSize="9"
|
|
101
|
-
textAnchor="middle"
|
|
102
|
-
fill="#333"
|
|
103
|
-
>
|
|
75
|
+
<Circle cx={x} cy={y(val)} r={5} fill={colors[si]} />
|
|
76
|
+
<SvgText x={x} y={y(val) - 10} fontSize="10" textAnchor="middle" fill="#333">
|
|
104
77
|
{formatNumber(val)}
|
|
105
78
|
</SvgText>
|
|
106
79
|
</React.Fragment>
|
|
@@ -108,18 +81,18 @@ const SvgLineChartCompact = ({ data }) => {
|
|
|
108
81
|
})
|
|
109
82
|
)}
|
|
110
83
|
|
|
111
|
-
{/* X
|
|
84
|
+
{/* X Labels */}
|
|
112
85
|
{data.labels.map((label, i) => {
|
|
113
|
-
const x =
|
|
114
|
-
paddingLeft + (data.labels.length === 1 ? 0 : i * xStep);
|
|
86
|
+
const x = paddingLeft + i * (totalPoints === 1 ? graphWidth / 2 - paddingLeft : xStep);
|
|
115
87
|
return (
|
|
116
88
|
<SvgText
|
|
117
89
|
key={i}
|
|
118
90
|
x={x}
|
|
119
|
-
y={height -
|
|
120
|
-
fontSize="
|
|
91
|
+
y={height - 15}
|
|
92
|
+
fontSize="11"
|
|
121
93
|
textAnchor="middle"
|
|
122
|
-
|
|
94
|
+
fill="#555"
|
|
95
|
+
transform={`rotate(-45 ${x} ${height - 15})`}
|
|
123
96
|
>
|
|
124
97
|
{label}
|
|
125
98
|
</SvgText>
|
|
@@ -129,12 +102,12 @@ const SvgLineChartCompact = ({ data }) => {
|
|
|
129
102
|
</ScrollView>
|
|
130
103
|
</View>
|
|
131
104
|
|
|
132
|
-
{/* LEGEND */}
|
|
133
105
|
<View style={styles.legend}>
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
106
|
+
{data.series.map((s, i) => (
|
|
107
|
+
<Text key={i} style={{ color: colors[i], marginRight: 16 }}>
|
|
108
|
+
● {s.name}
|
|
109
|
+
</Text>
|
|
110
|
+
))}
|
|
138
111
|
</View>
|
|
139
112
|
</View>
|
|
140
113
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
|
|
3
|
-
|
|
3
|
+
import { filterChartByMonths } from '../utils/filterChartByMonths';
|
|
4
4
|
import { getDivisions, getTable, getLine, getBar } from '../api/report2Fetcher';
|
|
5
5
|
import MonthFilterModal from '../components/MonthFilterModal';
|
|
6
6
|
import DivisionFilterModal from '../components/DivisionFilterModal';
|
|
@@ -24,12 +24,13 @@ const Report2AScreen = ({ api, token, onBack }) => {
|
|
|
24
24
|
useEffect(() => {
|
|
25
25
|
getDivisions(api.divisions, token).then(d => {
|
|
26
26
|
setDivisions(d);
|
|
27
|
-
setDivision(d[0]?.code);
|
|
27
|
+
if (d.length > 0) setDivision(d[0]?.code);
|
|
28
28
|
});
|
|
29
|
-
}, []);
|
|
29
|
+
}, [api.divisions, token]);
|
|
30
30
|
|
|
31
31
|
useEffect(() => {
|
|
32
32
|
if (!division) return;
|
|
33
|
+
|
|
33
34
|
Promise.all([
|
|
34
35
|
getTable(api.table, division, token),
|
|
35
36
|
getLine(api.line, division, token),
|
|
@@ -39,44 +40,50 @@ const Report2AScreen = ({ api, token, onBack }) => {
|
|
|
39
40
|
setLine(l);
|
|
40
41
|
setBar(b);
|
|
41
42
|
setMonths(l.labels);
|
|
42
|
-
setSelectedMonths(l.labels);
|
|
43
|
+
setSelectedMonths(l.labels); // default: all selected
|
|
43
44
|
});
|
|
44
|
-
}, [division]);
|
|
45
|
+
}, [division, api, token]);
|
|
45
46
|
|
|
46
|
-
if (!table || !line || !bar) return null;
|
|
47
|
+
if (!table || !line || !bar || !division) return null;
|
|
47
48
|
|
|
48
|
-
const
|
|
49
|
+
const filteredRows = table.rows.filter(r =>
|
|
49
50
|
selectedMonths.includes(r.monthLabel)
|
|
50
51
|
);
|
|
51
52
|
|
|
53
|
+
const filteredLine = filterChartByMonths(line, selectedMonths);
|
|
54
|
+
const filteredBar = filterChartByMonths(bar, selectedMonths);
|
|
55
|
+
|
|
52
56
|
return (
|
|
53
57
|
<ScrollView style={{ padding: 12 }}>
|
|
54
|
-
<Text onPress={onBack}
|
|
58
|
+
<Text onPress={onBack} style={{ fontSize: 18, marginBottom: 12 }}>
|
|
59
|
+
‹ Back
|
|
60
|
+
</Text>
|
|
55
61
|
|
|
56
|
-
{/* FILTER
|
|
57
|
-
<View style={{ flexDirection: 'row', marginVertical:
|
|
62
|
+
{/* FILTER BUTTONS */}
|
|
63
|
+
<View style={{ flexDirection: 'row', marginVertical: 10, gap: 16 }}>
|
|
58
64
|
<TouchableOpacity onPress={() => setMonthsModal(true)}>
|
|
59
|
-
<Text>Months ⛃</Text>
|
|
65
|
+
<Text style={{ fontSize: 16, color: '#1e88e5' }}>Months ⛃</Text>
|
|
60
66
|
</TouchableOpacity>
|
|
61
67
|
|
|
62
|
-
<TouchableOpacity
|
|
63
|
-
|
|
64
|
-
style={{ marginLeft: 16 }}
|
|
65
|
-
>
|
|
66
|
-
<Text>Divisions ⛃</Text>
|
|
68
|
+
<TouchableOpacity onPress={() => setDivisionModal(true)}>
|
|
69
|
+
<Text style={{ fontSize: 16, color: '#1e88e5' }}>Divisions ⛃</Text>
|
|
67
70
|
</TouchableOpacity>
|
|
68
71
|
</View>
|
|
69
72
|
|
|
70
|
-
|
|
73
|
+
{/* TABLE */}
|
|
74
|
+
<FrozenTableReport2A rows={filteredRows} />
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
{/* LINE CHART */}
|
|
77
|
+
<View style={{ borderWidth: 1, borderColor: '#ddd', borderRadius: 8, marginVertical: 12 }}>
|
|
78
|
+
<SvgLineChartCompact data={filteredLine} />
|
|
74
79
|
</View>
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
{/* BAR + LINE CHART */}
|
|
82
|
+
<View style={{ borderWidth: 1, borderColor: '#ddd', borderRadius: 8, marginVertical: 12 }}>
|
|
83
|
+
<SvgBarLineChartCompact data={filteredBar} />
|
|
78
84
|
</View>
|
|
79
85
|
|
|
86
|
+
{/* MODALS */}
|
|
80
87
|
<MonthFilterModal
|
|
81
88
|
visible={monthsModal}
|
|
82
89
|
months={months}
|
|
@@ -96,4 +103,4 @@ const Report2AScreen = ({ api, token, onBack }) => {
|
|
|
96
103
|
);
|
|
97
104
|
};
|
|
98
105
|
|
|
99
|
-
export default Report2AScreen;
|
|
106
|
+
export default Report2AScreen;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const filterChartByMonths = (chart, selectedMonths) => {
|
|
2
|
+
if (!selectedMonths || selectedMonths.length === 0) return chart;
|
|
3
|
+
|
|
4
|
+
const indices = chart.labels
|
|
5
|
+
.map((label, i) => (selectedMonths.includes(label) ? i : -1))
|
|
6
|
+
.filter(i => i !== -1);
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
...chart,
|
|
10
|
+
labels: indices.map(i => chart.labels[i]),
|
|
11
|
+
series: chart.series.map(s => ({
|
|
12
|
+
...s,
|
|
13
|
+
data: indices.map(i => s.data[i]),
|
|
14
|
+
})),
|
|
15
|
+
};
|
|
16
|
+
};
|