feat: single chart with normalized curves, values below with delta
This commit is contained in:
parent
3e4e7d6266
commit
4dd2ddaa58
1 changed files with 98 additions and 67 deletions
|
|
@ -11,50 +11,11 @@ const COLORS = {
|
|||
muscleMass: '#2196F3',
|
||||
}
|
||||
|
||||
function SingleChart({
|
||||
title,
|
||||
data,
|
||||
labels,
|
||||
color,
|
||||
unit,
|
||||
}: {
|
||||
title: string
|
||||
data: number[]
|
||||
labels: string[]
|
||||
color: string
|
||||
unit: string
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.chartBlock}>
|
||||
<Text style={styles.chartTitle}>{title}</Text>
|
||||
<LineChart
|
||||
data={{ labels, datasets: [{ data, color: () => color, strokeWidth: 2 }] }}
|
||||
width={Dimensions.get('window').width - 32}
|
||||
height={180}
|
||||
yAxisSuffix={unit}
|
||||
chartConfig={{
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundGradientFrom: '#ffffff',
|
||||
backgroundGradientTo: '#ffffff',
|
||||
decimalPlaces: 1,
|
||||
color: (opacity = 1) => `rgba(${hexToRgb(color)}, ${opacity})`,
|
||||
labelColor: () => '#666',
|
||||
propsForDots: { r: '3' },
|
||||
}}
|
||||
bezier
|
||||
style={styles.chart}
|
||||
withInnerLines={false}
|
||||
fromZero={false}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): string {
|
||||
const r = parseInt(hex.slice(1, 3), 16)
|
||||
const g = parseInt(hex.slice(3, 5), 16)
|
||||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
return `${r}, ${g}, ${b}`
|
||||
function normalize(values: number[]): number[] {
|
||||
const min = Math.min(...values)
|
||||
const max = Math.max(...values)
|
||||
if (max === min) return values.map(() => 50)
|
||||
return values.map(v => ((v - min) / (max - min)) * 100)
|
||||
}
|
||||
|
||||
export default function WeightChart({ measurements }: Props) {
|
||||
|
|
@ -70,6 +31,8 @@ export default function WeightChart({ measurements }: Props) {
|
|||
cutoff.setDate(cutoff.getDate() - 30)
|
||||
const filtered = sorted.filter(m => new Date(m.measuredAt) >= cutoff)
|
||||
const dataPoints = filtered.length >= 2 ? filtered : sorted
|
||||
const latest = dataPoints[dataPoints.length - 1]
|
||||
const previous = dataPoints.length > 1 ? dataPoints[dataPoints.length - 2] : null
|
||||
|
||||
const labels = dataPoints.map(m => {
|
||||
const d = new Date(m.measuredAt)
|
||||
|
|
@ -79,41 +42,109 @@ export default function WeightChart({ measurements }: Props) {
|
|||
const labelStep = labels.length > 8 ? Math.ceil(labels.length / 8) : 1
|
||||
const sparseLabels = labels.map((l, i) => (i % labelStep === 0 ? l : ''))
|
||||
|
||||
const weightData = dataPoints.map(m => m.weight)
|
||||
const fatData = dataPoints.map(m => m.fatMass)
|
||||
const muscleData = dataPoints.map(m => m.muscleMass)
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Voortgang (30 dagen)</Text>
|
||||
<LineChart
|
||||
data={{
|
||||
labels: sparseLabels,
|
||||
datasets: [
|
||||
{ data: normalize(weightData), color: () => COLORS.weight, strokeWidth: 2 },
|
||||
{ data: normalize(fatData), color: () => COLORS.fatMass, strokeWidth: 2 },
|
||||
{ data: normalize(muscleData), color: () => COLORS.muscleMass, strokeWidth: 2 },
|
||||
],
|
||||
}}
|
||||
width={Dimensions.get('window').width - 32}
|
||||
height={220}
|
||||
chartConfig={{
|
||||
backgroundColor: '#ffffff',
|
||||
backgroundGradientFrom: '#ffffff',
|
||||
backgroundGradientTo: '#ffffff',
|
||||
decimalPlaces: 0,
|
||||
color: (opacity = 1) => `rgba(0, 0, 0, ${opacity})`,
|
||||
labelColor: () => 'transparent',
|
||||
propsForDots: { r: '3' },
|
||||
propsForLabels: { fontSize: 9 },
|
||||
}}
|
||||
bezier
|
||||
style={styles.chart}
|
||||
withInnerLines={false}
|
||||
fromZero={false}
|
||||
/>
|
||||
|
||||
<SingleChart
|
||||
title="Gewicht"
|
||||
data={dataPoints.map(m => m.weight)}
|
||||
labels={sparseLabels}
|
||||
<View style={styles.legend}>
|
||||
<MetricInfo
|
||||
color={COLORS.weight}
|
||||
label="Gewicht"
|
||||
value={latest.weight}
|
||||
prev={previous?.weight}
|
||||
unit="kg"
|
||||
/>
|
||||
|
||||
<SingleChart
|
||||
title="Vetmassa"
|
||||
data={dataPoints.map(m => m.fatMass)}
|
||||
labels={sparseLabels}
|
||||
<MetricInfo
|
||||
color={COLORS.fatMass}
|
||||
label="Vetmassa"
|
||||
value={latest.fatMass}
|
||||
prev={previous?.fatMass}
|
||||
unit="kg"
|
||||
/>
|
||||
|
||||
<SingleChart
|
||||
title="Spiermassa"
|
||||
data={dataPoints.map(m => m.muscleMass)}
|
||||
labels={sparseLabels}
|
||||
<MetricInfo
|
||||
color={COLORS.muscleMass}
|
||||
label="Spiermassa"
|
||||
value={latest.muscleMass}
|
||||
prev={previous?.muscleMass}
|
||||
unit="kg"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricInfo({
|
||||
color,
|
||||
label,
|
||||
value,
|
||||
prev,
|
||||
unit,
|
||||
}: {
|
||||
color: string
|
||||
label: string
|
||||
value: number
|
||||
prev?: number
|
||||
unit: string
|
||||
}) {
|
||||
const diff = prev !== undefined ? value - prev : 0
|
||||
const sign = diff > 0 ? '+' : ''
|
||||
return (
|
||||
<View style={styles.metricItem}>
|
||||
<View style={styles.metricRow}>
|
||||
<View style={[styles.dot, { backgroundColor: color }]} />
|
||||
<Text style={styles.metricLabel}>{label}</Text>
|
||||
</View>
|
||||
<Text style={styles.metricValue}>
|
||||
{value.toFixed(1)} {unit}
|
||||
{prev !== undefined && (
|
||||
<Text style={styles.metricDiff}>
|
||||
{' '}{sign}{diff.toFixed(1)}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { marginHorizontal: 16, marginBottom: 16 },
|
||||
title: { fontSize: 18, fontWeight: '700', marginBottom: 8 },
|
||||
chartBlock: { marginBottom: 12 },
|
||||
chartTitle: { fontSize: 13, fontWeight: '600', color: '#555', marginBottom: 4, marginLeft: 4 },
|
||||
chart: { borderRadius: 12 },
|
||||
legend: { flexDirection: 'row', justifyContent: 'space-around', marginTop: 8, gap: 8 },
|
||||
metricItem: { alignItems: 'center', flex: 1 },
|
||||
metricRow: { flexDirection: 'row', alignItems: 'center', gap: 4, marginBottom: 2 },
|
||||
dot: { width: 8, height: 8, borderRadius: 4 },
|
||||
metricLabel: { fontSize: 11, color: '#888' },
|
||||
metricValue: { fontSize: 13, fontWeight: '600', color: '#333' },
|
||||
metricDiff: { fontSize: 11, color: '#999' },
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue