UNPKG

@naarni/design-system

Version:

Naarni React Native Design System for EV Fleet Apps

158 lines (152 loc) 6.48 kB
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>); };