@dhiraj0720/report1chart 2.2.2 → 2.2.4
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 +2 -6
- package/src/api/report1Fetcher.jsx +27 -0
- package/src/components/ProgressBar.jsx +27 -0
- package/src/components/Report1Card.jsx +99 -0
- package/src/index.jsx +2 -17
- package/src/screens/Report1Screen.jsx +80 -0
- package/src/api/ApiService.jsx +0 -138
- package/src/components/charts/BarChart.jsx +0 -116
- package/src/components/charts/LineChart.jsx +0 -134
- package/src/components/charts/PieChart.jsx +0 -120
- package/src/components/common/ErrorDisplay.jsx +0 -51
- package/src/components/common/LoadingSpinner.jsx +0 -32
- package/src/components/common/ReportCard.jsx +0 -84
- package/src/components/tables/CompactTable.jsx +0 -194
- package/src/components/tables/DefaultTable.jsx +0 -221
- package/src/components/tables/FreezeTable.jsx +0 -250
- package/src/screens/ReportDashboard.jsx +0 -121
- package/src/screens/ReportDetail.jsx +0 -222
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhiraj0720/report1chart",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"main": "src/index.jsx",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "echo 'No tests'"
|
|
@@ -22,9 +22,5 @@
|
|
|
22
22
|
"author": "Dhiraj",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"description": "A simple report chart and table package for React Native",
|
|
25
|
-
"devDependencies": {
|
|
26
|
-
"@babel/core": "^7.28.5",
|
|
27
|
-
"@babel/preset-env": "^7.28.5",
|
|
28
|
-
"@babel/preset-react": "^7.28.5"
|
|
29
|
-
}
|
|
25
|
+
"devDependencies": {}
|
|
30
26
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
const fetchReport1 = async (apiEndpoint, apiToken) => {
|
|
4
|
+
try {
|
|
5
|
+
const response = await axios.get(apiEndpoint, {
|
|
6
|
+
headers: {
|
|
7
|
+
Authorization: apiToken,
|
|
8
|
+
Accept: 'application/json',
|
|
9
|
+
},
|
|
10
|
+
timeout: 15000,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
if (!response.data || !response.data.rows) {
|
|
14
|
+
throw new Error('Invalid API response');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return response.data.rows;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
error?.response?.data?.message ||
|
|
21
|
+
error.message ||
|
|
22
|
+
'Failed to fetch report'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default fetchReport1;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, StyleSheet } from 'react-native';
|
|
3
|
+
|
|
4
|
+
const ProgressBar = ({ value }) => {
|
|
5
|
+
const width = Math.min(Math.max(value || 0, 0), 100);
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<View style={styles.track}>
|
|
9
|
+
<View style={[styles.fill, { width: `${width}%` }]} />
|
|
10
|
+
</View>
|
|
11
|
+
);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default ProgressBar;
|
|
15
|
+
|
|
16
|
+
const styles = StyleSheet.create({
|
|
17
|
+
track: {
|
|
18
|
+
height: 6,
|
|
19
|
+
backgroundColor: '#e0e0e0',
|
|
20
|
+
borderRadius: 4,
|
|
21
|
+
overflow: 'hidden',
|
|
22
|
+
},
|
|
23
|
+
fill: {
|
|
24
|
+
height: '100%',
|
|
25
|
+
backgroundColor: '#f57c00',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, Text, StyleSheet } from 'react-native';
|
|
3
|
+
import ProgressBar from './ProgressBar';
|
|
4
|
+
|
|
5
|
+
const format = (num) => (num ?? 0).toLocaleString();
|
|
6
|
+
|
|
7
|
+
const Trend = ({ value }) => {
|
|
8
|
+
const positive = value >= 0;
|
|
9
|
+
return (
|
|
10
|
+
<Text style={[styles.trend, positive ? styles.up : styles.down]}>
|
|
11
|
+
{positive ? '↑' : '↓'} {Math.abs(value)}%
|
|
12
|
+
</Text>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Report1Card = ({ item }) => {
|
|
17
|
+
return (
|
|
18
|
+
<View style={styles.card}>
|
|
19
|
+
<Text style={styles.title}>{item.name}</Text>
|
|
20
|
+
|
|
21
|
+
<Row label="2024 Actual" value={format(item.actual2024)} />
|
|
22
|
+
<Row label="2025 Actual" value={format(item.actual2025)} />
|
|
23
|
+
|
|
24
|
+
<View style={styles.row}>
|
|
25
|
+
<Text style={styles.label}>Change YoY</Text>
|
|
26
|
+
<Trend value={item.actualChangePercent} />
|
|
27
|
+
</View>
|
|
28
|
+
|
|
29
|
+
<Row label="2025 Budget" value={format(item.budget2025)} />
|
|
30
|
+
|
|
31
|
+
<View style={styles.row}>
|
|
32
|
+
<Text style={styles.label}>Budget Variance</Text>
|
|
33
|
+
<Trend value={item.budgetVariancePercent} />
|
|
34
|
+
</View>
|
|
35
|
+
|
|
36
|
+
{item.opexToGrossProfitPercent !== undefined && (
|
|
37
|
+
<>
|
|
38
|
+
<View style={styles.row}>
|
|
39
|
+
<Text style={styles.label}>OPEX / Gross Profit</Text>
|
|
40
|
+
<Text style={styles.value}>
|
|
41
|
+
{item.opexToGrossProfitPercent}%
|
|
42
|
+
</Text>
|
|
43
|
+
</View>
|
|
44
|
+
<ProgressBar value={item.opexToGrossProfitPercent} />
|
|
45
|
+
</>
|
|
46
|
+
)}
|
|
47
|
+
</View>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const Row = ({ label, value }) => (
|
|
52
|
+
<View style={styles.row}>
|
|
53
|
+
<Text style={styles.label}>{label}</Text>
|
|
54
|
+
<Text style={styles.value}>{value}</Text>
|
|
55
|
+
</View>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export default Report1Card;
|
|
59
|
+
|
|
60
|
+
const styles = StyleSheet.create({
|
|
61
|
+
card: {
|
|
62
|
+
backgroundColor: '#fff',
|
|
63
|
+
borderRadius: 12,
|
|
64
|
+
padding: 16,
|
|
65
|
+
marginBottom: 12,
|
|
66
|
+
borderWidth: 1,
|
|
67
|
+
borderColor: '#e0e0e0',
|
|
68
|
+
},
|
|
69
|
+
title: {
|
|
70
|
+
fontSize: 16,
|
|
71
|
+
fontWeight: '700',
|
|
72
|
+
marginBottom: 12,
|
|
73
|
+
color: '#000',
|
|
74
|
+
},
|
|
75
|
+
row: {
|
|
76
|
+
flexDirection: 'row',
|
|
77
|
+
justifyContent: 'space-between',
|
|
78
|
+
marginBottom: 6,
|
|
79
|
+
},
|
|
80
|
+
label: {
|
|
81
|
+
fontSize: 13,
|
|
82
|
+
color: '#666',
|
|
83
|
+
},
|
|
84
|
+
value: {
|
|
85
|
+
fontSize: 13,
|
|
86
|
+
fontWeight: '600',
|
|
87
|
+
color: '#000',
|
|
88
|
+
},
|
|
89
|
+
trend: {
|
|
90
|
+
fontSize: 13,
|
|
91
|
+
fontWeight: '700',
|
|
92
|
+
},
|
|
93
|
+
up: {
|
|
94
|
+
color: '#2e7d32',
|
|
95
|
+
},
|
|
96
|
+
down: {
|
|
97
|
+
color: '#d32f2f',
|
|
98
|
+
},
|
|
99
|
+
});
|
package/src/index.jsx
CHANGED
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
import
|
|
2
|
-
import ReportDashboard from './screens/ReportDashboard';
|
|
1
|
+
import Report1Screen from './screens/Report1Screen';
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
reports = [],
|
|
6
|
-
onReportSelect,
|
|
7
|
-
onClose
|
|
8
|
-
}) => {
|
|
9
|
-
return (
|
|
10
|
-
<ReportDashboard
|
|
11
|
-
reports={reports}
|
|
12
|
-
onReportSelect={onReportSelect}
|
|
13
|
-
onClose={onClose}
|
|
14
|
-
/>
|
|
15
|
-
);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export default ReportChart;
|
|
3
|
+
export default Report1Screen;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
ScrollView,
|
|
6
|
+
ActivityIndicator,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
import fetchReport1 from '../api/report1Fetcher';
|
|
10
|
+
import Report1Card from '../components/Report1Card';
|
|
11
|
+
|
|
12
|
+
const Report1Screen = ({ apiEndpoint, apiToken }) => {
|
|
13
|
+
const [loading, setLoading] = useState(true);
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
const [rows, setRows] = useState([]);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const loadData = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const data = await fetchReport1(apiEndpoint, apiToken);
|
|
21
|
+
setRows(data);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
setError(e.message);
|
|
24
|
+
} finally {
|
|
25
|
+
setLoading(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
loadData();
|
|
30
|
+
}, [apiEndpoint, apiToken]);
|
|
31
|
+
|
|
32
|
+
if (loading) {
|
|
33
|
+
return (
|
|
34
|
+
<View style={styles.center}>
|
|
35
|
+
<ActivityIndicator size="large" />
|
|
36
|
+
<Text style={styles.info}>Loading report...</Text>
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (error) {
|
|
42
|
+
return (
|
|
43
|
+
<View style={styles.center}>
|
|
44
|
+
<Text style={styles.error}>{error}</Text>
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<ScrollView style={styles.container}>
|
|
51
|
+
{rows.map((item, index) => (
|
|
52
|
+
<Report1Card key={index} item={item} />
|
|
53
|
+
))}
|
|
54
|
+
</ScrollView>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default Report1Screen;
|
|
59
|
+
|
|
60
|
+
const styles = StyleSheet.create({
|
|
61
|
+
container: {
|
|
62
|
+
backgroundColor: '#f5f5f5',
|
|
63
|
+
padding: 16,
|
|
64
|
+
},
|
|
65
|
+
center: {
|
|
66
|
+
flex: 1,
|
|
67
|
+
justifyContent: 'center',
|
|
68
|
+
alignItems: 'center',
|
|
69
|
+
padding: 20,
|
|
70
|
+
},
|
|
71
|
+
info: {
|
|
72
|
+
marginTop: 12,
|
|
73
|
+
color: '#666',
|
|
74
|
+
},
|
|
75
|
+
error: {
|
|
76
|
+
fontSize: 16,
|
|
77
|
+
color: '#d32f2f',
|
|
78
|
+
textAlign: 'center',
|
|
79
|
+
},
|
|
80
|
+
});
|
package/src/api/ApiService.jsx
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import axios from 'axios';
|
|
2
|
-
|
|
3
|
-
class ApiService {
|
|
4
|
-
constructor() {
|
|
5
|
-
this.api = axios.create({
|
|
6
|
-
timeout: 15000,
|
|
7
|
-
headers: {
|
|
8
|
-
'Content-Type': 'application/json',
|
|
9
|
-
'Accept': 'application/json'
|
|
10
|
-
}
|
|
11
|
-
});
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async fetchReportData(apiConfig) {
|
|
15
|
-
try {
|
|
16
|
-
const config = {
|
|
17
|
-
url: apiConfig.url,
|
|
18
|
-
method: apiConfig.method || 'GET',
|
|
19
|
-
timeout: apiConfig.timeout || 15000,
|
|
20
|
-
headers: {
|
|
21
|
-
'Content-Type': 'application/json',
|
|
22
|
-
'Accept': 'application/json'
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
if (apiConfig.token) {
|
|
27
|
-
config.headers.Authorization = `Bearer ${apiConfig.token}`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const response = await this.api(config);
|
|
31
|
-
return this.transformData(response.data, apiConfig.url);
|
|
32
|
-
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error('API Error:', error.message);
|
|
35
|
-
return this.getFallbackData(apiConfig.url);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
transformData(data, url) {
|
|
40
|
-
if (url.includes('cumulative-operating-profit')) {
|
|
41
|
-
return this.transformPerformanceData(data);
|
|
42
|
-
} else if (url.includes('revenue-by-mode')) {
|
|
43
|
-
return this.transformRevenueByMode(data);
|
|
44
|
-
} else if (url.includes('shipment-by-direction')) {
|
|
45
|
-
return this.transformShipmentByDirection(data);
|
|
46
|
-
}
|
|
47
|
-
return data;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
transformPerformanceData(data) {
|
|
51
|
-
if (!data || !data.data) return this.getPerformanceFallbackData();
|
|
52
|
-
|
|
53
|
-
return data.data.map(item => ({
|
|
54
|
-
name: item.name || 'Unknown',
|
|
55
|
-
actual2024: item.actual2024 || 0,
|
|
56
|
-
actual2025: item.actual2025 || 0,
|
|
57
|
-
budget2025: item.budget2025 || 0,
|
|
58
|
-
yoYChangePercent: this.calculatePercentageChange(item.actual2024 || 0, item.actual2025 || 0),
|
|
59
|
-
budgetVariancePercent: this.calculatePercentageChange(item.budget2025 || 0, item.actual2025 || 0),
|
|
60
|
-
grossMarginPercent: item.grossMarginPercent || 0,
|
|
61
|
-
isTotal: item.isTotal || false
|
|
62
|
-
}));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
transformRevenueByMode(data) {
|
|
66
|
-
if (Array.isArray(data)) {
|
|
67
|
-
return data.map(item => ({
|
|
68
|
-
mode: item.mode || 'Unknown',
|
|
69
|
-
revenue: item.revenue || 0
|
|
70
|
-
}));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (data.data && Array.isArray(data.data)) {
|
|
74
|
-
return data.data;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return this.getRevenueByModeFallbackData();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
transformShipmentByDirection(data) {
|
|
81
|
-
if (Array.isArray(data)) {
|
|
82
|
-
return data.map(item => ({
|
|
83
|
-
direction: item.direction || 'Unknown',
|
|
84
|
-
shipments: item.shipments || 0
|
|
85
|
-
}));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (data.data && Array.isArray(data.data)) {
|
|
89
|
-
return data.data;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return this.getShipmentFallbackData();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
calculatePercentageChange(oldValue, newValue) {
|
|
96
|
-
if (!oldValue || oldValue === 0) return 0;
|
|
97
|
-
return Number(((newValue - oldValue) / Math.abs(oldValue)) * 100).toFixed(1);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
getFallbackData(url) {
|
|
101
|
-
if (url.includes('cumulative-operating-profit')) {
|
|
102
|
-
return this.getPerformanceFallbackData();
|
|
103
|
-
} else if (url.includes('revenue-by-mode')) {
|
|
104
|
-
return this.getRevenueByModeFallbackData();
|
|
105
|
-
} else if (url.includes('shipment-by-direction')) {
|
|
106
|
-
return this.getShipmentFallbackData();
|
|
107
|
-
}
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
getPerformanceFallbackData() {
|
|
112
|
-
return [
|
|
113
|
-
{ name: "Ocean Transportation", actual2024: 45000000, actual2025: 52000000, budget2025: 50000000, grossMarginPercent: 25, isTotal: false },
|
|
114
|
-
{ name: "Air Freight", actual2024: 18000000, actual2025: 21000000, budget2025: 20000000, grossMarginPercent: 22, isTotal: false },
|
|
115
|
-
{ name: "Road Logistics", actual2024: 32000000, actual2025: 38000000, budget2025: 35000000, grossMarginPercent: 28, isTotal: false },
|
|
116
|
-
{ name: "Warehousing", actual2024: 12000000, actual2025: 15000000, budget2025: 14000000, grossMarginPercent: 35, isTotal: false },
|
|
117
|
-
{ name: "Customs Clearance", actual2024: 8000000, actual2025: 9500000, budget2025: 9000000, grossMarginPercent: 40, isTotal: false },
|
|
118
|
-
{ name: "TOTAL", actual2024: 115000000, actual2025: 135500000, budget2025: 128000000, grossMarginPercent: 28, isTotal: true }
|
|
119
|
-
];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
getRevenueByModeFallbackData() {
|
|
123
|
-
return [
|
|
124
|
-
{ mode: "Ocean", revenue: 101951456 },
|
|
125
|
-
{ mode: "Road", revenue: 89158437 },
|
|
126
|
-
{ mode: "Air", revenue: 40171032 }
|
|
127
|
-
];
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
getShipmentFallbackData() {
|
|
131
|
-
return [
|
|
132
|
-
{ direction: "Export", shipments: 568 },
|
|
133
|
-
{ direction: "Import", shipments: 546 }
|
|
134
|
-
];
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export default new ApiService();
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
View,
|
|
4
|
-
Text,
|
|
5
|
-
ScrollView,
|
|
6
|
-
StyleSheet,
|
|
7
|
-
} from 'react-native';
|
|
8
|
-
|
|
9
|
-
const BarChart = ({ data, title }) => {
|
|
10
|
-
const formatNumber = (num) => {
|
|
11
|
-
return num.toLocaleString();
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const getColor = (index) => {
|
|
15
|
-
const colors = ['#4CAF50', '#2196F3', '#FF9800', '#E91E63', '#9C27B0'];
|
|
16
|
-
return colors[index % colors.length];
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
if (!Array.isArray(data) || data.length === 0) {
|
|
20
|
-
return (
|
|
21
|
-
<View style={styles.noDataContainer}>
|
|
22
|
-
<Text style={styles.noDataText}>No data available</Text>
|
|
23
|
-
</View>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const values = data.map(d => d.revenue || d.value || 0);
|
|
28
|
-
const labels = data.map(d => d.mode || d.label || '');
|
|
29
|
-
|
|
30
|
-
const maxValue = Math.max(...values, 1);
|
|
31
|
-
const chartHeight = 200;
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<View style={styles.container}>
|
|
35
|
-
{title && <Text style={styles.title}>{title}</Text>}
|
|
36
|
-
<ScrollView
|
|
37
|
-
horizontal
|
|
38
|
-
showsHorizontalScrollIndicator={true}
|
|
39
|
-
>
|
|
40
|
-
<View style={[styles.chart, { height: chartHeight }]}>
|
|
41
|
-
{values.map((value, index) => {
|
|
42
|
-
const height = (value / maxValue) * chartHeight;
|
|
43
|
-
const label = labels[index] || `Item ${index + 1}`;
|
|
44
|
-
|
|
45
|
-
return (
|
|
46
|
-
<View key={index} style={styles.barWrapper}>
|
|
47
|
-
<View style={[styles.bar, {
|
|
48
|
-
height,
|
|
49
|
-
backgroundColor: getColor(index)
|
|
50
|
-
}]} />
|
|
51
|
-
<Text style={styles.barLabel} numberOfLines={1}>{label}</Text>
|
|
52
|
-
<Text style={styles.valueText}>{formatNumber(value)}</Text>
|
|
53
|
-
</View>
|
|
54
|
-
);
|
|
55
|
-
})}
|
|
56
|
-
</View>
|
|
57
|
-
</ScrollView>
|
|
58
|
-
</View>
|
|
59
|
-
);
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const styles = StyleSheet.create({
|
|
63
|
-
container: {
|
|
64
|
-
backgroundColor: '#fff',
|
|
65
|
-
borderRadius: 12,
|
|
66
|
-
padding: 16,
|
|
67
|
-
marginBottom: 16,
|
|
68
|
-
borderWidth: 1,
|
|
69
|
-
borderColor: '#e8e8e8',
|
|
70
|
-
},
|
|
71
|
-
title: {
|
|
72
|
-
fontSize: 18,
|
|
73
|
-
fontWeight: '700',
|
|
74
|
-
color: '#000',
|
|
75
|
-
marginBottom: 20,
|
|
76
|
-
textAlign: 'center',
|
|
77
|
-
},
|
|
78
|
-
chart: {
|
|
79
|
-
flexDirection: 'row',
|
|
80
|
-
alignItems: 'flex-end',
|
|
81
|
-
paddingHorizontal: 8,
|
|
82
|
-
minWidth: 400,
|
|
83
|
-
},
|
|
84
|
-
barWrapper: {
|
|
85
|
-
alignItems: 'center',
|
|
86
|
-
marginHorizontal: 12,
|
|
87
|
-
},
|
|
88
|
-
bar: {
|
|
89
|
-
width: 40,
|
|
90
|
-
borderRadius: 4,
|
|
91
|
-
},
|
|
92
|
-
barLabel: {
|
|
93
|
-
marginTop: 8,
|
|
94
|
-
fontSize: 12,
|
|
95
|
-
color: '#666',
|
|
96
|
-
textAlign: 'center',
|
|
97
|
-
width: 60,
|
|
98
|
-
},
|
|
99
|
-
valueText: {
|
|
100
|
-
fontSize: 11,
|
|
101
|
-
color: '#999',
|
|
102
|
-
marginTop: 4,
|
|
103
|
-
fontWeight: '500',
|
|
104
|
-
},
|
|
105
|
-
noDataContainer: {
|
|
106
|
-
padding: 40,
|
|
107
|
-
alignItems: 'center',
|
|
108
|
-
},
|
|
109
|
-
noDataText: {
|
|
110
|
-
fontSize: 16,
|
|
111
|
-
color: '#999',
|
|
112
|
-
textAlign: 'center',
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
export default BarChart;
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
View,
|
|
4
|
-
Text,
|
|
5
|
-
ScrollView,
|
|
6
|
-
StyleSheet,
|
|
7
|
-
} from 'react-native';
|
|
8
|
-
|
|
9
|
-
const LineChart = ({ data, title }) => {
|
|
10
|
-
const formatNumber = (num) => {
|
|
11
|
-
return num.toLocaleString();
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
if (!data || (!Array.isArray(data) && !data.values)) {
|
|
15
|
-
return (
|
|
16
|
-
<View style={styles.noDataContainer}>
|
|
17
|
-
<Text style={styles.noDataText}>No data available</Text>
|
|
18
|
-
</View>
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let values, labels;
|
|
23
|
-
|
|
24
|
-
if (Array.isArray(data)) {
|
|
25
|
-
values = data.map(d => d.value || 0);
|
|
26
|
-
labels = data.map(d => d.label || '');
|
|
27
|
-
} else {
|
|
28
|
-
values = data.values || [];
|
|
29
|
-
labels = data.labels || [];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (values.length === 0) {
|
|
33
|
-
return (
|
|
34
|
-
<View style={styles.noDataContainer}>
|
|
35
|
-
<Text style={styles.noDataText}>No data available</Text>
|
|
36
|
-
</View>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const maxValue = Math.max(...values, 1);
|
|
41
|
-
const chartHeight = 200;
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
<View style={styles.container}>
|
|
45
|
-
{title && <Text style={styles.title}>{title}</Text>}
|
|
46
|
-
<ScrollView
|
|
47
|
-
horizontal
|
|
48
|
-
showsHorizontalScrollIndicator={true}
|
|
49
|
-
>
|
|
50
|
-
<View style={[styles.chart, { height: chartHeight }]}>
|
|
51
|
-
{values.map((value, index) => {
|
|
52
|
-
const height = (value / maxValue) * chartHeight;
|
|
53
|
-
const label = labels[index] || `M${index + 1}`;
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<View key={index} style={styles.pointWrapper}>
|
|
57
|
-
<View style={[styles.point, {
|
|
58
|
-
height: Math.max(height, 2),
|
|
59
|
-
}]} />
|
|
60
|
-
<View style={styles.connector} />
|
|
61
|
-
<Text style={styles.pointLabel} numberOfLines={1}>{label}</Text>
|
|
62
|
-
<Text style={styles.valueText}>{formatNumber(value)}</Text>
|
|
63
|
-
</View>
|
|
64
|
-
);
|
|
65
|
-
})}
|
|
66
|
-
</View>
|
|
67
|
-
</ScrollView>
|
|
68
|
-
</View>
|
|
69
|
-
);
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const styles = StyleSheet.create({
|
|
73
|
-
container: {
|
|
74
|
-
backgroundColor: '#fff',
|
|
75
|
-
borderRadius: 12,
|
|
76
|
-
padding: 16,
|
|
77
|
-
marginBottom: 16,
|
|
78
|
-
borderWidth: 1,
|
|
79
|
-
borderColor: '#e8e8e8',
|
|
80
|
-
},
|
|
81
|
-
title: {
|
|
82
|
-
fontSize: 18,
|
|
83
|
-
fontWeight: '700',
|
|
84
|
-
color: '#000',
|
|
85
|
-
marginBottom: 20,
|
|
86
|
-
textAlign: 'center',
|
|
87
|
-
},
|
|
88
|
-
chart: {
|
|
89
|
-
flexDirection: 'row',
|
|
90
|
-
alignItems: 'flex-end',
|
|
91
|
-
paddingHorizontal: 8,
|
|
92
|
-
minWidth: 400,
|
|
93
|
-
},
|
|
94
|
-
pointWrapper: {
|
|
95
|
-
alignItems: 'center',
|
|
96
|
-
marginHorizontal: 12,
|
|
97
|
-
},
|
|
98
|
-
point: {
|
|
99
|
-
width: 30,
|
|
100
|
-
backgroundColor: '#2196F3',
|
|
101
|
-
borderRadius: 2,
|
|
102
|
-
},
|
|
103
|
-
connector: {
|
|
104
|
-
width: 2,
|
|
105
|
-
backgroundColor: '#2196F3',
|
|
106
|
-
opacity: 0.3,
|
|
107
|
-
marginVertical: 2,
|
|
108
|
-
flex: 1,
|
|
109
|
-
},
|
|
110
|
-
pointLabel: {
|
|
111
|
-
marginTop: 8,
|
|
112
|
-
fontSize: 12,
|
|
113
|
-
color: '#666',
|
|
114
|
-
textAlign: 'center',
|
|
115
|
-
width: 60,
|
|
116
|
-
},
|
|
117
|
-
valueText: {
|
|
118
|
-
fontSize: 11,
|
|
119
|
-
color: '#999',
|
|
120
|
-
marginTop: 4,
|
|
121
|
-
fontWeight: '500',
|
|
122
|
-
},
|
|
123
|
-
noDataContainer: {
|
|
124
|
-
padding: 40,
|
|
125
|
-
alignItems: 'center',
|
|
126
|
-
},
|
|
127
|
-
noDataText: {
|
|
128
|
-
fontSize: 16,
|
|
129
|
-
color: '#999',
|
|
130
|
-
textAlign: 'center',
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
export default LineChart;
|