@naarni/design-system
Version:
Naarni React Native Design System for EV Fleet Apps
158 lines (152 loc) • 6.48 kB
JavaScript
import React, { useMemo } from 'react';
import { Canvas, Path, Line, Skia, Group, } from '@shopify/react-native-skia';
import { View, Text } from 'react-native';
import { useDeviceTheme } from '../../theme/deviceTheme';
import { styles } from './styles';
export const LineChart = ({ data = [], width = 300, height = 200, showXAxis = true, showYAxis = true, isLabelOnXaxis = true, isLabelOnYaxis = true, color, showGrid = true, showTooltip = true, tooltipFormatter, }) => {
const { colors } = useDeviceTheme();
// Validate inputs
if (!data || !Array.isArray(data) || data.length === 0) {
return (<View style={[styles.container, { width, height, justifyContent: 'center', alignItems: 'center' }]}>
<Text style={{ color: colors.text.secondary }}>No data available</Text>
</View>);
}
if (width <= 0 || height <= 0) {
return <View style={[styles.container, { width, height }]}/>;
}
// Filter valid data
const validData = data.filter(val => typeof val === 'number' && !isNaN(val));
if (validData.length === 0) {
return (<View style={[styles.container, { width, height, justifyContent: 'center', alignItems: 'center' }]}>
<Text style={{ color: colors.text.secondary }}>No valid data</Text>
</View>);
}
// Simple chart dimensions
const padding = 40;
const chartWidth = Math.max(0, width - padding * 2);
const chartHeight = Math.max(0, height - padding * 2);
// Calculate points
const points = useMemo(() => {
const pts = [];
const minValue = Math.min(...validData);
const maxValue = Math.max(...validData);
const valueRange = maxValue - minValue;
validData.forEach((value, index) => {
let x;
let y;
if (validData.length === 1) {
x = padding;
y = height - padding - chartHeight / 2;
}
else {
x = (index / (validData.length - 1)) * chartWidth + padding;
y = valueRange === 0
? height - padding - chartHeight / 2
: height - padding - ((value - minValue) / valueRange) * chartHeight;
}
pts.push({ x, y });
});
return pts;
}, [validData, width, height, chartWidth, chartHeight, padding]);
// Create path
const path = useMemo(() => {
const p = Skia.Path.Make();
if (points.length > 0) {
const firstPoint = points[0];
p.moveTo(firstPoint.x, firstPoint.y);
for (let i = 1; i < points.length; i++) {
const point = points[i];
p.lineTo(point.x, point.y);
}
}
return p;
}, [points]);
// Generate simple labels
const yAxisLabels = useMemo(() => {
if (!isLabelOnYaxis)
return [];
const minValue = Math.min(...validData);
const maxValue = Math.max(...validData);
const valueRange = maxValue - minValue;
if (valueRange === 0) {
return [{ value: minValue, y: height - padding - chartHeight / 2 }];
}
const labels = [];
const count = 3; // Fewer labels to avoid overlap
for (let i = 0; i < count; i++) {
const value = minValue + (valueRange * i) / (count - 1);
const y = height - padding - ((value - minValue) / valueRange) * chartHeight;
labels.push({ value, y });
}
return labels;
}, [validData, isLabelOnYaxis, height, padding, chartHeight]);
const xAxisLabels = useMemo(() => {
if (!isLabelOnXaxis)
return [];
const labels = [];
const maxLabels = Math.min(validData.length, 5); // Limit to 5 labels
const step = Math.max(1, Math.ceil(validData.length / maxLabels));
for (let i = 0; i < validData.length; i += step) {
const x = validData.length === 1
? padding
: (i / (validData.length - 1)) * chartWidth + padding;
labels.push({
value: validData[i],
x,
label: `Item ${i + 1}`,
});
}
return labels;
}, [validData, isLabelOnXaxis, chartWidth, padding]);
const formatLabel = (value) => {
if (isNaN(value))
return '0';
if (value >= 1000) {
return `${(value / 1000).toFixed(1)}k`;
}
return value.toFixed(1);
};
return (<View style={[styles.container, { width, height }]}>
{/* Y-Axis Labels */}
{yAxisLabels.map((label, index) => (<Text key={`y-label-${index}`} style={[
styles.yAxisLabel,
{
position: 'absolute',
left: 5,
top: label.y - 8,
color: colors.text.secondary,
fontSize: 10,
},
]}>
{formatLabel(label.value)}
</Text>))}
{/* X-Axis Labels */}
{xAxisLabels.map((label, index) => (<Text key={`x-label-${index}`} style={[
styles.xAxisLabel,
{
position: 'absolute',
left: label.x - 20,
top: height - padding + 5,
color: colors.text.secondary,
fontSize: 10,
},
]}>
{label.label}
</Text>))}
<Canvas style={styles.canvas}>
{/* X-Axis */}
{showXAxis && (<Line p1={{ x: padding, y: height - padding }} p2={{ x: width - padding, y: height - padding }} color={colors.surface.border} strokeWidth={1}/>)}
{/* Y-Axis */}
{showYAxis && (<Line p1={{ x: padding, y: padding }} p2={{ x: padding, y: height - padding }} color={colors.surface.border} strokeWidth={1}/>)}
{/* Line Chart */}
<Group>
<Path path={path} strokeWidth={2.5} color={color || colors.primary.main} style="stroke" strokeJoin="round" strokeCap="round"/>
</Group>
{/* Data Points */}
{points.map((point, index) => (<Group key={`point-${index}`}>
<Path path={Skia.Path.Make().addCircle(point.x, point.y, 4)} color={color || colors.primary.main} style="fill"/>
<Path path={Skia.Path.Make().addCircle(point.x, point.y, 4)} color={colors.background.default} style="stroke" strokeWidth={1}/>
</Group>))}
</Canvas>
</View>);
};