@dhiraj0720/report1chart 1.0.6 → 2.1.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 +48 -9
- package/src/api/ApiService.jsx +180 -0
- package/src/api/ApiService.test.jsx +27 -0
- package/src/components/charts/BarChart.jsx +121 -0
- package/src/components/charts/ChartUtils.jsx +38 -0
- package/src/components/charts/LineChart.jsx +142 -0
- package/src/components/charts/PieChart.jsx +125 -0
- package/src/components/common/ErrorDisplay.jsx +51 -0
- package/src/components/common/LoadingSpinner.jsx +32 -0
- package/src/components/common/ReportCard.jsx +98 -0
- package/src/components/tables/CompactTable.jsx +269 -0
- package/src/components/tables/DefaultTable.jsx +225 -0
- package/src/components/tables/FreezeTable.jsx +264 -0
- package/src/components/tables/TableUtils.jsx +47 -0
- package/src/index.jsx +9 -1349
- package/src/screens/ReportDashboard.jsx +132 -0
- package/src/screens/ReportDetail.jsx +265 -0
- package/src/utils/Constants.jsx +38 -0
- package/src/utils/Formatters.jsx +24 -0
- package/src/utils/Validators.jsx +60 -0
package/package.json
CHANGED
|
@@ -1,15 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dhiraj0720/report1chart",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"main": "src/index.jsx",
|
|
5
|
-
"publishConfig": {
|
|
6
|
-
"access": "public"
|
|
7
|
-
},
|
|
8
5
|
"scripts": {
|
|
9
|
-
"
|
|
6
|
+
"build": "echo 'Build step would go here'",
|
|
7
|
+
"test": "jest",
|
|
8
|
+
"lint": "eslint src/",
|
|
9
|
+
"prepare": "npm run build"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.6.0",
|
|
13
|
+
"react": "^18.2.0",
|
|
14
|
+
"react-native": "^0.72.0"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"react": ">=16.8.0",
|
|
18
|
+
"react-native": ">=0.60.0"
|
|
10
19
|
},
|
|
11
|
-
"
|
|
12
|
-
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@babel/core": "^7.23.0",
|
|
22
|
+
"@babel/preset-env": "^7.22.0",
|
|
23
|
+
"@babel/preset-react": "^7.22.0",
|
|
24
|
+
"jest": "^29.7.0",
|
|
25
|
+
"eslint": "^8.53.0",
|
|
26
|
+
"eslint-plugin-react": "^7.33.2"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"reports",
|
|
30
|
+
"charts",
|
|
31
|
+
"tables",
|
|
32
|
+
"analytics",
|
|
33
|
+
"dashboard",
|
|
34
|
+
"react-native",
|
|
35
|
+
"api",
|
|
36
|
+
"data-visualization"
|
|
37
|
+
],
|
|
38
|
+
"author": "Dhiraj",
|
|
13
39
|
"license": "MIT",
|
|
14
|
-
"description": ""
|
|
15
|
-
|
|
40
|
+
"description": "A comprehensive report chart and table package for React Native with built-in API fetching, data transformation, and error handling",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "https://github.com/dhiraj0720/report1chart.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/dhiraj0720/report1chart/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/dhiraj0720/report1chart#readme",
|
|
49
|
+
"files": [
|
|
50
|
+
"src/",
|
|
51
|
+
"README.md",
|
|
52
|
+
"LICENSE"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
console.log('Fetching from npm package:', apiConfig.url);
|
|
17
|
+
|
|
18
|
+
const config = {
|
|
19
|
+
url: apiConfig.url,
|
|
20
|
+
method: apiConfig.method || 'GET',
|
|
21
|
+
timeout: apiConfig.timeout || 15000,
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'Accept': 'application/json',
|
|
25
|
+
...(apiConfig.token && {
|
|
26
|
+
'Authorization': `Bearer ${apiConfig.token}`
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const response = await this.api(config);
|
|
32
|
+
|
|
33
|
+
// Transform data based on endpoint
|
|
34
|
+
return this.transformData(response.data, apiConfig.url);
|
|
35
|
+
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error('NPM Package API Error:', error);
|
|
38
|
+
throw this.handleError(error, apiConfig.url);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
transformData(data, url) {
|
|
43
|
+
// Transform data based on endpoint patterns
|
|
44
|
+
if (url.includes('cumulative-operating-profit')) {
|
|
45
|
+
return this.transformPerformanceData(data);
|
|
46
|
+
} else if (url.includes('revenue-by-mode')) {
|
|
47
|
+
return this.transformRevenueByMode(data);
|
|
48
|
+
} else if (url.includes('shipment-by-direction')) {
|
|
49
|
+
return this.transformShipmentByDirection(data);
|
|
50
|
+
} else if (url.includes('revenue-trend')) {
|
|
51
|
+
return this.transformRevenueTrend(data);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Return as-is for unknown formats
|
|
55
|
+
return data;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
transformPerformanceData(data) {
|
|
59
|
+
// Transform performance report data to match table structure
|
|
60
|
+
if (!data || !data.data) return [];
|
|
61
|
+
|
|
62
|
+
return data.data.map(item => ({
|
|
63
|
+
name: item.name || item.activity || 'Unknown',
|
|
64
|
+
actual2024: item.actual2024 || item.lastYear || 0,
|
|
65
|
+
actual2025: item.actual2025 || item.currentYear || 0,
|
|
66
|
+
budget2025: item.budget2025 || item.budget || 0,
|
|
67
|
+
yoYChangePercent: this.calculatePercentageChange(
|
|
68
|
+
item.actual2024 || item.lastYear || 0,
|
|
69
|
+
item.actual2025 || item.currentYear || 0
|
|
70
|
+
),
|
|
71
|
+
budgetVariancePercent: this.calculatePercentageChange(
|
|
72
|
+
item.budget2025 || item.budget || 0,
|
|
73
|
+
item.actual2025 || item.currentYear || 0
|
|
74
|
+
),
|
|
75
|
+
grossMarginPercent: item.grossMargin || item.margin || 0,
|
|
76
|
+
isTotal: item.isTotal || item.name?.toLowerCase().includes('total')
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
transformRevenueByMode(data) {
|
|
81
|
+
// Transform revenue by mode data
|
|
82
|
+
if (Array.isArray(data)) {
|
|
83
|
+
return data.map(item => ({
|
|
84
|
+
mode: item.mode || item.transportMode || 'Unknown',
|
|
85
|
+
revenue: item.revenue || item.value || 0
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Handle different response formats
|
|
90
|
+
if (data.data && Array.isArray(data.data)) {
|
|
91
|
+
return data.data;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
transformShipmentByDirection(data) {
|
|
98
|
+
// Transform shipment by direction data
|
|
99
|
+
if (Array.isArray(data)) {
|
|
100
|
+
return data.map(item => ({
|
|
101
|
+
direction: item.direction || item.type || 'Unknown',
|
|
102
|
+
shipments: item.shipments || item.count || item.value || 0
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (data.data && Array.isArray(data.data)) {
|
|
107
|
+
return data.data;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
transformRevenueTrend(data) {
|
|
114
|
+
// Transform revenue trend data
|
|
115
|
+
if (data.labels && data.values) {
|
|
116
|
+
return {
|
|
117
|
+
labels: data.labels,
|
|
118
|
+
values: data.values
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return data;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
calculatePercentageChange(oldValue, newValue) {
|
|
126
|
+
if (!oldValue || oldValue === 0) return 0;
|
|
127
|
+
return ((newValue - oldValue) / Math.abs(oldValue)) * 100;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
handleError(error, url) {
|
|
131
|
+
let errorMessage = 'Failed to fetch report data';
|
|
132
|
+
|
|
133
|
+
if (error.response) {
|
|
134
|
+
// Server responded with error
|
|
135
|
+
errorMessage = `Server Error: ${error.response.status} - ${error.response.data?.message || 'Unknown error'}`;
|
|
136
|
+
} else if (error.request) {
|
|
137
|
+
// Request made but no response
|
|
138
|
+
errorMessage = 'Network Error: No response from server';
|
|
139
|
+
} else {
|
|
140
|
+
// Something else happened
|
|
141
|
+
errorMessage = error.message || 'Unknown error occurred';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fallback mock data for development
|
|
145
|
+
console.warn('Using fallback data for:', url);
|
|
146
|
+
return {
|
|
147
|
+
error: errorMessage,
|
|
148
|
+
fallbackData: this.getFallbackData(url)
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
getFallbackData(url) {
|
|
153
|
+
// Return mock data for development
|
|
154
|
+
if (url.includes('cumulative-operating-profit')) {
|
|
155
|
+
return [
|
|
156
|
+
{ name: "Ocean Transportation", actual2024: 45000000, actual2025: 52000000, budget2025: 50000000, grossMarginPercent: 25, isTotal: false },
|
|
157
|
+
{ name: "Air Freight", actual2024: 18000000, actual2025: 21000000, budget2025: 20000000, grossMarginPercent: 22, isTotal: false },
|
|
158
|
+
{ name: "Road Logistics", actual2024: 32000000, actual2025: 38000000, budget2025: 35000000, grossMarginPercent: 28, isTotal: false },
|
|
159
|
+
{ name: "Warehousing", actual2024: 12000000, actual2025: 15000000, budget2025: 14000000, grossMarginPercent: 35, isTotal: false },
|
|
160
|
+
{ name: "Customs Clearance", actual2024: 8000000, actual2025: 9500000, budget2025: 9000000, grossMarginPercent: 40, isTotal: false },
|
|
161
|
+
{ name: "TOTAL", actual2024: 115000000, actual2025: 135500000, budget2025: 128000000, grossMarginPercent: 28, isTotal: true }
|
|
162
|
+
];
|
|
163
|
+
} else if (url.includes('revenue-by-mode')) {
|
|
164
|
+
return [
|
|
165
|
+
{ mode: "Ocean", revenue: 101951456 },
|
|
166
|
+
{ mode: "Road", revenue: 89158437 },
|
|
167
|
+
{ mode: "Air", revenue: 40171032 }
|
|
168
|
+
];
|
|
169
|
+
} else if (url.includes('shipment-by-direction')) {
|
|
170
|
+
return [
|
|
171
|
+
{ direction: "Export", shipments: 568 },
|
|
172
|
+
{ direction: "Import", shipments: 546 }
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export default new ApiService();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import ApiService from './ApiService';
|
|
2
|
+
|
|
3
|
+
describe('ApiService', () => {
|
|
4
|
+
it('should transform performance data correctly', () => {
|
|
5
|
+
const mockData = {
|
|
6
|
+
data: [
|
|
7
|
+
{ name: 'Ocean', actual2024: 100, actual2025: 120, budget2025: 110, grossMargin: 25 }
|
|
8
|
+
]
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const result = ApiService.transformPerformanceData(mockData);
|
|
12
|
+
|
|
13
|
+
expect(result).toHaveLength(1);
|
|
14
|
+
expect(result[0].name).toBe('Ocean');
|
|
15
|
+
expect(result[0].yoYChangePercent).toBe(20);
|
|
16
|
+
expect(result[0].budgetVariancePercent).toBeCloseTo(9.09);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should handle API errors gracefully', async () => {
|
|
20
|
+
const apiConfig = {
|
|
21
|
+
url: 'http://invalid-url',
|
|
22
|
+
method: 'GET'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
await expect(ApiService.fetchReportData(apiConfig)).rejects.toThrow();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
ScrollView,
|
|
6
|
+
Dimensions,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
|
|
10
|
+
const BarChart = ({ data, title }) => {
|
|
11
|
+
const formatNumber = (num) => {
|
|
12
|
+
return num.toLocaleString();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const getColor = (index) => {
|
|
16
|
+
const colors = ['#4CAF50', '#2196F3', '#FF9800', '#E91E63', '#9C27B0'];
|
|
17
|
+
return colors[index % colors.length];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
21
|
+
return (
|
|
22
|
+
<View style={styles.noDataContainer}>
|
|
23
|
+
<Text style={styles.noDataText}>No data available</Text>
|
|
24
|
+
</View>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const values = data.map(d => d.revenue || d.value || d.shipments || 0);
|
|
29
|
+
const labels = data.map(d => d.mode || d.label || d.direction || '');
|
|
30
|
+
|
|
31
|
+
const maxValue = Math.max(...values, 1);
|
|
32
|
+
const chartHeight = 200;
|
|
33
|
+
const chartWidth = Dimensions.get('window').width - 64;
|
|
34
|
+
const barWidth = Math.min(40, (chartWidth / values.length) - 10);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<View style={styles.container}>
|
|
38
|
+
{title && <Text style={styles.title}>{title}</Text>}
|
|
39
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
40
|
+
<View style={[styles.chart, { height: chartHeight, width: chartWidth }]}>
|
|
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
|
+
width: barWidth,
|
|
50
|
+
backgroundColor: getColor(index)
|
|
51
|
+
}]} />
|
|
52
|
+
<Text style={styles.barLabel} numberOfLines={1}>{label}</Text>
|
|
53
|
+
<Text style={styles.valueText}>{formatNumber(value)}</Text>
|
|
54
|
+
</View>
|
|
55
|
+
);
|
|
56
|
+
})}
|
|
57
|
+
</View>
|
|
58
|
+
</ScrollView>
|
|
59
|
+
</View>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const styles = StyleSheet.create({
|
|
64
|
+
container: {
|
|
65
|
+
backgroundColor: '#fff',
|
|
66
|
+
borderRadius: 12,
|
|
67
|
+
padding: 16,
|
|
68
|
+
marginBottom: 16,
|
|
69
|
+
borderWidth: 1,
|
|
70
|
+
borderColor: '#e8e8e8',
|
|
71
|
+
shadowColor: '#000',
|
|
72
|
+
shadowOffset: { width: 0, height: 1 },
|
|
73
|
+
shadowOpacity: 0.05,
|
|
74
|
+
shadowRadius: 2,
|
|
75
|
+
elevation: 2,
|
|
76
|
+
},
|
|
77
|
+
title: {
|
|
78
|
+
fontSize: 18,
|
|
79
|
+
fontWeight: '700',
|
|
80
|
+
color: '#000',
|
|
81
|
+
marginBottom: 20,
|
|
82
|
+
textAlign: 'center',
|
|
83
|
+
},
|
|
84
|
+
chart: {
|
|
85
|
+
flexDirection: 'row',
|
|
86
|
+
alignItems: 'flex-end',
|
|
87
|
+
justifyContent: 'space-between',
|
|
88
|
+
paddingHorizontal: 8,
|
|
89
|
+
},
|
|
90
|
+
barWrapper: {
|
|
91
|
+
alignItems: 'center',
|
|
92
|
+
marginHorizontal: 4,
|
|
93
|
+
},
|
|
94
|
+
bar: {
|
|
95
|
+
borderRadius: 4,
|
|
96
|
+
},
|
|
97
|
+
barLabel: {
|
|
98
|
+
marginTop: 8,
|
|
99
|
+
fontSize: 11,
|
|
100
|
+
color: '#666',
|
|
101
|
+
textAlign: 'center',
|
|
102
|
+
width: 60,
|
|
103
|
+
},
|
|
104
|
+
valueText: {
|
|
105
|
+
fontSize: 10,
|
|
106
|
+
color: '#999',
|
|
107
|
+
marginTop: 4,
|
|
108
|
+
fontWeight: '500',
|
|
109
|
+
},
|
|
110
|
+
noDataContainer: {
|
|
111
|
+
padding: 40,
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
},
|
|
114
|
+
noDataText: {
|
|
115
|
+
fontSize: 16,
|
|
116
|
+
color: '#999',
|
|
117
|
+
textAlign: 'center',
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export default BarChart;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const formatCurrency = (value) => {
|
|
2
|
+
const num = Number(value);
|
|
3
|
+
if (isNaN(num)) return '0';
|
|
4
|
+
|
|
5
|
+
if (num >= 1000000) {
|
|
6
|
+
return `${(num / 1000000).toFixed(1)}M`;
|
|
7
|
+
} else if (num >= 1000) {
|
|
8
|
+
return `${(num / 1000).toFixed(1)}K`;
|
|
9
|
+
}
|
|
10
|
+
return num.toLocaleString();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const formatNumber = (num) => {
|
|
14
|
+
return num.toLocaleString();
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const getColor = (index) => {
|
|
18
|
+
const colors = ['#4CAF50', '#2196F3', '#FF9800', '#E91E63', '#9C27B0', '#00BCD4'];
|
|
19
|
+
return colors[index % colors.length];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const calculatePercentageChange = (oldValue, newValue) => {
|
|
23
|
+
if (!oldValue || oldValue === 0) return 0;
|
|
24
|
+
return ((newValue - oldValue) / Math.abs(oldValue)) * 100;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getChartDimensions = (dataLength) => {
|
|
28
|
+
const { width: screenWidth } = Dimensions.get('window');
|
|
29
|
+
const minWidth = screenWidth - 64;
|
|
30
|
+
const chartWidth = Math.max(minWidth, dataLength * 60);
|
|
31
|
+
const barWidth = Math.max(20, (chartWidth / dataLength) - 10);
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
chartHeight: 200,
|
|
35
|
+
chartWidth,
|
|
36
|
+
barWidth
|
|
37
|
+
};
|
|
38
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
ScrollView,
|
|
6
|
+
Dimensions,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
} from 'react-native';
|
|
9
|
+
|
|
10
|
+
const LineChart = ({ data, title }) => {
|
|
11
|
+
const formatNumber = (num) => {
|
|
12
|
+
return num.toLocaleString();
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (!data || (!Array.isArray(data) && !data.values)) {
|
|
16
|
+
return (
|
|
17
|
+
<View style={styles.noDataContainer}>
|
|
18
|
+
<Text style={styles.noDataText}>No data available</Text>
|
|
19
|
+
</View>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle different data formats
|
|
24
|
+
let values, labels;
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(data)) {
|
|
27
|
+
// If data is array of objects
|
|
28
|
+
values = data.map(d => d.value || d.revenue || d.shipments || 0);
|
|
29
|
+
labels = data.map(d => d.label || d.month || d.mode || d.direction || '');
|
|
30
|
+
} else {
|
|
31
|
+
// If data has separate values and labels
|
|
32
|
+
values = data.values || [];
|
|
33
|
+
labels = data.labels || [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (values.length === 0) {
|
|
37
|
+
return (
|
|
38
|
+
<View style={styles.noDataContainer}>
|
|
39
|
+
<Text style={styles.noDataText}>No data available</Text>
|
|
40
|
+
</View>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const maxValue = Math.max(...values, 1);
|
|
45
|
+
const chartHeight = 200;
|
|
46
|
+
const chartWidth = Math.max(Dimensions.get('window').width - 64, values.length * 60);
|
|
47
|
+
const barWidth = Math.max(20, (chartWidth / values.length) - 10);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View style={styles.container}>
|
|
51
|
+
{title && <Text style={styles.title}>{title}</Text>}
|
|
52
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
53
|
+
<View style={[styles.chart, { height: chartHeight, width: chartWidth }]}>
|
|
54
|
+
{values.map((value, index) => {
|
|
55
|
+
const height = (value / maxValue) * chartHeight;
|
|
56
|
+
const label = labels[index] || `M${index + 1}`;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<View key={index} style={styles.pointWrapper}>
|
|
60
|
+
<View style={[styles.point, {
|
|
61
|
+
height: Math.max(height, 2),
|
|
62
|
+
width: barWidth
|
|
63
|
+
}]} />
|
|
64
|
+
<View style={styles.connector} />
|
|
65
|
+
<Text style={styles.pointLabel} numberOfLines={1}>{label}</Text>
|
|
66
|
+
<Text style={styles.valueText}>{formatNumber(value)}</Text>
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
})}
|
|
70
|
+
</View>
|
|
71
|
+
</ScrollView>
|
|
72
|
+
</View>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const styles = StyleSheet.create({
|
|
77
|
+
container: {
|
|
78
|
+
backgroundColor: '#fff',
|
|
79
|
+
borderRadius: 12,
|
|
80
|
+
padding: 16,
|
|
81
|
+
marginBottom: 16,
|
|
82
|
+
borderWidth: 1,
|
|
83
|
+
borderColor: '#e8e8e8',
|
|
84
|
+
shadowColor: '#000',
|
|
85
|
+
shadowOffset: { width: 0, height: 1 },
|
|
86
|
+
shadowOpacity: 0.05,
|
|
87
|
+
shadowRadius: 2,
|
|
88
|
+
elevation: 2,
|
|
89
|
+
},
|
|
90
|
+
title: {
|
|
91
|
+
fontSize: 18,
|
|
92
|
+
fontWeight: '700',
|
|
93
|
+
color: '#000',
|
|
94
|
+
marginBottom: 20,
|
|
95
|
+
textAlign: 'center',
|
|
96
|
+
},
|
|
97
|
+
chart: {
|
|
98
|
+
flexDirection: 'row',
|
|
99
|
+
alignItems: 'flex-end',
|
|
100
|
+
justifyContent: 'space-between',
|
|
101
|
+
paddingHorizontal: 8,
|
|
102
|
+
},
|
|
103
|
+
pointWrapper: {
|
|
104
|
+
alignItems: 'center',
|
|
105
|
+
marginHorizontal: 4,
|
|
106
|
+
},
|
|
107
|
+
point: {
|
|
108
|
+
backgroundColor: '#2196F3',
|
|
109
|
+
borderRadius: 2,
|
|
110
|
+
},
|
|
111
|
+
connector: {
|
|
112
|
+
width: 2,
|
|
113
|
+
backgroundColor: '#2196F3',
|
|
114
|
+
opacity: 0.3,
|
|
115
|
+
marginVertical: 2,
|
|
116
|
+
flex: 1,
|
|
117
|
+
},
|
|
118
|
+
pointLabel: {
|
|
119
|
+
marginTop: 8,
|
|
120
|
+
fontSize: 11,
|
|
121
|
+
color: '#666',
|
|
122
|
+
textAlign: 'center',
|
|
123
|
+
width: 60,
|
|
124
|
+
},
|
|
125
|
+
valueText: {
|
|
126
|
+
fontSize: 10,
|
|
127
|
+
color: '#999',
|
|
128
|
+
marginTop: 4,
|
|
129
|
+
fontWeight: '500',
|
|
130
|
+
},
|
|
131
|
+
noDataContainer: {
|
|
132
|
+
padding: 40,
|
|
133
|
+
alignItems: 'center',
|
|
134
|
+
},
|
|
135
|
+
noDataText: {
|
|
136
|
+
fontSize: 16,
|
|
137
|
+
color: '#999',
|
|
138
|
+
textAlign: 'center',
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export default LineChart;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
View,
|
|
4
|
+
Text,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
} from 'react-native';
|
|
7
|
+
|
|
8
|
+
const PieChart = ({ data, title }) => {
|
|
9
|
+
const formatNumber = (num) => {
|
|
10
|
+
return num.toLocaleString();
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const getColor = (index) => {
|
|
14
|
+
const colors = ['#4CAF50', '#2196F3', '#FF9800', '#E91E63', '#9C27B0'];
|
|
15
|
+
return colors[index % colors.length];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
19
|
+
return (
|
|
20
|
+
<View style={styles.noDataContainer}>
|
|
21
|
+
<Text style={styles.noDataText}>No data available</Text>
|
|
22
|
+
</View>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const total = data.reduce((sum, item) => sum + (item.shipments || item.revenue || item.value || 0), 0);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<View style={styles.container}>
|
|
30
|
+
{title && <Text style={styles.title}>{title}</Text>}
|
|
31
|
+
<View style={styles.pieContainer}>
|
|
32
|
+
{data.map((item, index) => {
|
|
33
|
+
const value = item.shipments || item.revenue || item.value || 0;
|
|
34
|
+
const label = item.direction || item.mode || item.label || `Item ${index + 1}`;
|
|
35
|
+
const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : '0';
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<View key={index} style={styles.pieItem}>
|
|
39
|
+
<View style={[styles.pieColor, { backgroundColor: getColor(index) }]} />
|
|
40
|
+
<Text style={styles.pieLabel}>{label}:</Text>
|
|
41
|
+
<Text style={styles.pieValue}>
|
|
42
|
+
{formatNumber(value)} ({percentage}%)
|
|
43
|
+
</Text>
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</View>
|
|
48
|
+
{total > 0 && (
|
|
49
|
+
<Text style={styles.totalText}>Total: {formatNumber(total)}</Text>
|
|
50
|
+
)}
|
|
51
|
+
</View>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const styles = StyleSheet.create({
|
|
56
|
+
container: {
|
|
57
|
+
backgroundColor: '#fff',
|
|
58
|
+
borderRadius: 12,
|
|
59
|
+
padding: 16,
|
|
60
|
+
marginBottom: 16,
|
|
61
|
+
borderWidth: 1,
|
|
62
|
+
borderColor: '#e8e8e8',
|
|
63
|
+
shadowColor: '#000',
|
|
64
|
+
shadowOffset: { width: 0, height: 1 },
|
|
65
|
+
shadowOpacity: 0.05,
|
|
66
|
+
shadowRadius: 2,
|
|
67
|
+
elevation: 2,
|
|
68
|
+
},
|
|
69
|
+
title: {
|
|
70
|
+
fontSize: 18,
|
|
71
|
+
fontWeight: '700',
|
|
72
|
+
color: '#000',
|
|
73
|
+
marginBottom: 20,
|
|
74
|
+
textAlign: 'center',
|
|
75
|
+
},
|
|
76
|
+
pieContainer: {
|
|
77
|
+
padding: 10,
|
|
78
|
+
},
|
|
79
|
+
pieItem: {
|
|
80
|
+
flexDirection: 'row',
|
|
81
|
+
alignItems: 'center',
|
|
82
|
+
marginBottom: 12,
|
|
83
|
+
padding: 8,
|
|
84
|
+
backgroundColor: '#f9f9f9',
|
|
85
|
+
borderRadius: 8,
|
|
86
|
+
},
|
|
87
|
+
pieColor: {
|
|
88
|
+
width: 16,
|
|
89
|
+
height: 16,
|
|
90
|
+
borderRadius: 8,
|
|
91
|
+
marginRight: 12,
|
|
92
|
+
},
|
|
93
|
+
pieLabel: {
|
|
94
|
+
fontSize: 15,
|
|
95
|
+
fontWeight: '600',
|
|
96
|
+
color: '#000',
|
|
97
|
+
flex: 1,
|
|
98
|
+
},
|
|
99
|
+
pieValue: {
|
|
100
|
+
fontSize: 14,
|
|
101
|
+
color: '#666',
|
|
102
|
+
fontWeight: '500',
|
|
103
|
+
},
|
|
104
|
+
totalText: {
|
|
105
|
+
fontSize: 16,
|
|
106
|
+
fontWeight: '700',
|
|
107
|
+
color: '#4CAF50',
|
|
108
|
+
textAlign: 'center',
|
|
109
|
+
marginTop: 16,
|
|
110
|
+
paddingTop: 16,
|
|
111
|
+
borderTopWidth: 1,
|
|
112
|
+
borderTopColor: '#eee',
|
|
113
|
+
},
|
|
114
|
+
noDataContainer: {
|
|
115
|
+
padding: 40,
|
|
116
|
+
alignItems: 'center',
|
|
117
|
+
},
|
|
118
|
+
noDataText: {
|
|
119
|
+
fontSize: 16,
|
|
120
|
+
color: '#999',
|
|
121
|
+
textAlign: 'center',
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export default PieChart;
|