Skip to content

Commit

Permalink
Add weekly distance chart
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianCassayre committed Feb 23, 2025
1 parent 25523ab commit 78e6f0e
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 14 deletions.
30 changes: 30 additions & 0 deletions scripts/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,35 @@ const targetStatisticsDaily = (inputs: ResponseActivityRide[]) =>
const targetStatisticsMonthly = (inputs: ResponseActivityRide[]) =>
groupStatistics(inputs, date => timestampToIso(date).split('-').slice(0, 2).join('-'));

const WEEKDAYS = [
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
'Sunday',
];

const targetStatisticsWeekly = (inputs: ResponseActivityRide[]) => {
const daily = groupStatistics(inputs, date => timestampToIso(date));
const dailyDistances: { count: number, distance: number }[][] = _.list(0, WEEKDAYS.length - 1, () => []);
Object.entries(daily).forEach(([isoDate, values]) => dailyDistances[(WEEKDAYS.length + new Date(isoDate).getDay() - 1) % WEEKDAYS.length].push(values));
const totalWeeks = _.max(dailyDistances.map(array => array.length)) || 0;
return Object.fromEntries(WEEKDAYS.map((label, i) => {
const countAndDistancesBase = dailyDistances[i];
// Correct for empty days
const countAndDistances = countAndDistancesBase.concat(_.list(0, totalWeeks - countAndDistancesBase.length - 1, () => ({ count: 0, distance: 0 })));
const count = computeMeanStdDev(countAndDistances.map(v => v.count)), distance = computeMeanStdDev(countAndDistances.map(v => v.distance));
return [label, {
meanCount: count.mean,
stdDevCount: count.stdDev,
meanDistance: distance.mean,
stdDevDistance: distance.stdDev,
}];
}));
};

const targetCumulativeDistance = (inputs: ResponseActivityRide[]) => {
const daily = Object.entries(targetStatisticsDaily(inputs));
const array: [string, number][] = [];
Expand Down Expand Up @@ -268,6 +297,7 @@ const compile = () => {
const targets = {
statisticsDaily: targetStatisticsDaily(inputs),
statisticsMonthly: targetStatisticsMonthly(inputs),
statisticsWeekly: targetStatisticsWeekly(inputs),
records: targetRecords(inputs),
cumulativeDistance: targetCumulativeDistance(inputs),
cadence: targetCadence(inputs),
Expand Down
6 changes: 5 additions & 1 deletion src/Visualizations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Grid, GridItem } from '@chakra-ui/react';
import { SpeedDistribution } from './viz/SpeedDistribution';
import { PowerDistribution } from './viz/PowerDistribution';
import { SpeedAccelerationChart } from './viz/SpeedAccelerationChart';
import { WeeklyChart } from './viz/WeeklyChart';

export const Visualizations: React.FC = () => {
const largeProps = { colSpan: 2 }
Expand All @@ -22,12 +23,15 @@ export const Visualizations: React.FC = () => {
<GridItem {...largeProps}>
<DailyCalendar />
</GridItem>
<GridItem {...smallProps}>
<GridItem {...largeProps}>
<DistanceTimeSeries />
</GridItem>
<GridItem {...smallProps}>
<MonthlyChart />
</GridItem>
<GridItem {...smallProps}>
<WeeklyChart />
</GridItem>
<GridItem {...smallProps}>
<SpeedDistribution />
</GridItem>
Expand Down
10 changes: 9 additions & 1 deletion src/types/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface Data {
statisticsDaily: TargetStatistics;
statisticsMonthly: TargetStatistics;
statisticsMonthly: Record<string, TargetStatistics>;
statisticsWeekly: Record<string, TargetStatisticsAggregated>;
records: TargetRecords;
cumulativeDistance: TargetCumulative;
cadence: TargetBuckets;
Expand All @@ -15,6 +16,13 @@ export interface TargetStatistics {
distance: number;
}

export interface TargetStatisticsAggregated {
meanCount: number,
stdDevCount: number,
meanDistance: number,
stdDevDistance: number,
}

export interface TargetRecords {
totalDistance: number;
maxSpeed: number;
Expand Down
5 changes: 5 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Percentile to Z-score mapping for some well-known values
export const Z_SCORES = {
50: 0.674,
95: 1.96,
} as const;
15 changes: 10 additions & 5 deletions src/viz/MonthlyChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,39 @@ import { useDataQuery } from '../hooks/useDataQuery';
import React from 'react';
import { TargetStatistics } from '../types/types';
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from 'recharts';
import { Box, Heading, useColorModeValue, useTheme } from '@chakra-ui/react';
import { Box, Heading, Text, useColorModeValue, useTheme } from '@chakra-ui/react';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

interface MonthlyChartContentProps {
data: TargetStatistics;
data: Record<string, TargetStatistics>;
}

const MonthlyChartContent: React.FC<MonthlyChartContentProps> = ({ data }) => {
const theme = useTheme();
const color = useColorModeValue(theme.colors.blue[500], theme.colors.blue[200]);

const barData = Object.entries(data).map(([key, obj]) => ({ name: key, ...obj, distance: obj.distance / 1000 })).reverse().slice(0, 12).reverse();
const lastMonths = 12;

const barData = Object.entries(data).map(([key, obj]) => ({ name: key, ...obj, distance: obj.distance / 1000 })).reverse().slice(0, lastMonths).reverse();

return (
<Box w="100%">
<Heading as="h1" fontSize={{ base: "2xl", md: "3xl" }} mb={3} textAlign="center">
<Heading as="h1" fontSize={{ base: "2xl", md: "3xl" }} textAlign="center">
Monthly distance
</Heading>
<Text fontSize="xs" textAlign="center" mb={3}>
Last {lastMonths} months
</Text>
<ResponsiveContainer width="100%" height={300}>
<BarChart
data={barData}
margin={{ top: 5, right: 5, left: 5, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis unit="km" />
<YAxis unit=" km" />
<Bar dataKey="distance" fill={color} />
</BarChart>
</ResponsiveContainer>
Expand Down
9 changes: 2 additions & 7 deletions src/viz/SpeedAccelerationChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,7 @@ import {
XAxis,
YAxis,
} from 'recharts';

// Percentile to Z-score mapping for some well-known values
const Z_SCORES = {
50: 0.674,
95: 1.96,
} as const;
import { Z_SCORES } from '../utils';

interface SpeedAccelerationChartContentProps {
data: TargetAccelerations;
Expand All @@ -40,7 +35,7 @@ const SpeedAccelerationChartContent: React.FC<SpeedAccelerationChartContentProps
Acceleration
</Heading>
<Text fontSize="xs" textAlign="center" mb={3}>
With a {percentile}% confidence interval
{percentile}% confidence interval
</Text>
<ResponsiveContainer width="100%" height={300}>
<ComposedChart
Expand Down
56 changes: 56 additions & 0 deletions src/viz/WeeklyChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useDataQuery } from '../hooks/useDataQuery';
import React from 'react';
import { TargetStatisticsAggregated } from '../types/types';
import { Bar, BarChart, CartesianGrid, ErrorBar, ResponsiveContainer, XAxis, YAxis } from 'recharts';
import { Box, Heading, Text, useColorModeValue, useTheme } from '@chakra-ui/react';
import { Z_SCORES } from '../utils';

interface WeeklyChartContentProps {
data: Record<string, TargetStatisticsAggregated>;
}

const WeeklyChartContent: React.FC<WeeklyChartContentProps> = ({ data }) => {
const theme = useTheme();
const color = useColorModeValue(theme.colors.blue[500], theme.colors.blue[200]);
const errorColor = useColorModeValue(theme.colors.blue[700], theme.colors.blue[50]);

const percentile = 50 as const;
const zScore = Z_SCORES[percentile];

const barData = Object.entries(data).map(([key, obj]) => ({ weekday: key.substring(0, 1), ...obj, meanDistance: obj.meanDistance / 1000, confidenceDistance: zScore * obj.stdDevDistance / 1000 }));

return (
<Box w="100%">
<Heading as="h1" fontSize={{ base: "2xl", md: "3xl" }} textAlign="center">
Weekly distance
</Heading>
<Text fontSize="xs" textAlign="center" mb={3}>
{percentile}% confidence interval
</Text>
<ResponsiveContainer width="100%" height={300}>
<BarChart
data={barData}
margin={{ top: 5, right: 5, left: 5, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="weekday" />
<YAxis unit=" km" domain={[0, 'auto']} allowDataOverflow />
<Bar dataKey="meanDistance" fill={color}>
<ErrorBar dataKey="confidenceDistance" width={4} strokeWidth={2} stroke={errorColor} />
</Bar>
</BarChart>
</ResponsiveContainer>
</Box>
);
};

export const WeeklyChart = () => {
const { data } = useDataQuery('statisticsWeekly');

if (!data) {
return null;
}
return (
<WeeklyChartContent data={data} />
)
};

0 comments on commit 78e6f0e

Please sign in to comment.