Skip to content

Commit

Permalink
Improve data visualizations
Browse files Browse the repository at this point in the history
  • Loading branch information
FlorianCassayre committed Aug 19, 2024
1 parent f13db93 commit 87668b5
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 53 deletions.
23 changes: 17 additions & 6 deletions scripts/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ const groupStatistics = (inputs: ResponseActivityRide[], groupBy: (date: string)
);
};

const groupDistribution = (inputs: ResponseActivityRide[], groupBy: (ride: ResponseActivityRide) => number[], step: number) => {
const values = inputs.flatMap(groupBy);
const record = _.counting(values, v => Math.round(v / step));
const buckets = _.keys(record).map(key => parseInt(key));
return _.list(_.min(buckets) ?? 0, _.max(buckets) ?? 0, i => [i * step, (record[i] ?? 0) / values.length]);
};

const targetStatisticsDaily = (inputs: ResponseActivityRide[]) =>
groupStatistics(inputs, date => timestampToIso(date));

Expand Down Expand Up @@ -104,12 +111,14 @@ const targetRecords = (inputs: ResponseActivityRide[]) => ({
totalOperationTime: _.sum(inputs, input => parseInt(input.operation_time)) + _.sum(MISSING_DATA, d => d.time * 60 * 60 * 1000),
});

const targetCadence = (inputs: ResponseActivityRide[]) => {
const values = inputs.flatMap(({ cadence }) => cadence.flatMap(array => array.map(v => v !== null ? v : -1)));
const cadencesRecord = _.counting(values, v => v);
const cadenceBuckets = _.keys(cadencesRecord).map(key => parseInt(key));
return _.list(_.min(cadenceBuckets) ?? 0, _.max(cadenceBuckets) ?? 0, i => [i, (cadencesRecord[i] ?? 0) / values.length]);
};
const targetCadence = (inputs: ResponseActivityRide[]) =>
groupDistribution(inputs, ({ cadence }) => cadence.flatMap(array => array.map(v => v !== null ? v : -1)), 1);

const targetSpeed = (inputs: ResponseActivityRide[]) =>
groupDistribution(inputs, ({ speed }) => speed.flatMap(array => array.map(v => v !== null ? v : -1)), 1);

const targetPower = (inputs: ResponseActivityRide[]) =>
groupDistribution(inputs, ({ power_output }) => power_output.flatMap(array => array.map(v => v !== null ? v : -1)), 5);

const targetGears = (inputs: ResponseActivityRide[]) => {
const values = inputs
Expand Down Expand Up @@ -208,6 +217,8 @@ const compile = () => {
records: targetRecords(inputs),
cumulativeDistance: targetCumulativeDistance(inputs),
cadence: targetCadence(inputs),
speed: targetSpeed(inputs),
power: targetPower(inputs),
gears: targetGears(inputs),
};

Expand Down
10 changes: 7 additions & 3 deletions src/Visualizations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { DailyCalendar } from './viz/DailyCalendar';
import { DistanceTimeSeries } from './viz/DistanceTimeSeries';
import { MonthlyChart } from './viz/MonthlyChart';
import { CadenceDistribution } from './viz/CadenceDistribution';
import { GearDistribution } from './viz/GearDistribution';
import { GearUsage } from './viz/GearUsage';
import { SpeedGearDistribution } from './viz/SpeedGearDistribution';
import { Grid, GridItem } from '@chakra-ui/react';
import { SpeedDistribution } from './viz/SpeedDistribution';
import { PowerDistribution } from './viz/PowerDistribution';

export const Visualizations: React.FC = () => {
const largeProps = { colSpan: 2 }
Expand All @@ -27,10 +28,13 @@ export const Visualizations: React.FC = () => {
<MonthlyChart />
</GridItem>
<GridItem {...smallProps}>
<CadenceDistribution />
<SpeedDistribution />
</GridItem>
<GridItem {...smallProps}>
<GearDistribution />
<CadenceDistribution />
</GridItem>
<GridItem {...largeProps}>
<PowerDistribution />
</GridItem>
<GridItem {...smallProps}>
<GearUsage />
Expand Down
2 changes: 2 additions & 0 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export interface Data {
records: TargetRecords;
cumulativeDistance: TargetCumulative;
cadence: TargetBuckets;
speed: TargetBuckets;
power: TargetBuckets;
gears: TargetGears;
}

Expand Down
46 changes: 4 additions & 42 deletions src/viz/CadenceDistribution.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,11 @@
import React from 'react';
import { useDataQuery } from '../hooks/useDataQuery';
import { TargetBuckets } from '../types/types';
import { Box, Heading, useColorModeValue, useTheme } from '@chakra-ui/react';
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis } from 'recharts';
import { useColorModeValue, useTheme } from '@chakra-ui/react';
import { Distribution } from './Distribution';

interface CadenceDistributionContentProps {
data: TargetBuckets;
}

const CadenceDistributionContent: React.FC<CadenceDistributionContentProps> = ({ data }) => {
export const CadenceDistribution: React.FC = () => {
const theme = useTheme();
const strokeColor = useColorModeValue(theme.colors.yellow[500], theme.colors.yellow[300]);
const fillColor = useColorModeValue(theme.colors.yellow[200], theme.colors.yellow[100]);

const filtered = data
.filter(([bucket]) => bucket >= 0)
.map(([bucket, value]) => ([bucket, bucket > 0 ? value : 0] as const));
const total = filtered.length > 0 ? filtered.map(([_, value]) => value).reduce((a, b) => a + b) : 0;
const seriesData = filtered
.map(([name, value]) => ({ name, value: 100 * value / total }));
const xTicks = seriesData.map(({ name }) => name).filter(bucket => bucket % 10 === 0);

return (
<Box w="100%">
<Heading as="h1" fontSize={{ base: "2xl", md: "3xl" }} mb={3} textAlign="center">
Pedaling rate
</Heading>
<ResponsiveContainer width="100%" height={300}>
<AreaChart
data={seriesData}
margin={{ top: 5, right: 5, left: 5, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" ticks={xTicks} unit=" RPM" />
{/*<YAxis unit="%" />*/}
<Area type="monotone" dataKey="value" stroke={strokeColor} fill={fillColor} />
</AreaChart>
</ResponsiveContainer>
</Box>
);
};

export const CadenceDistribution: React.FC = () => {
const { data } = useDataQuery('cadence')
if (!data) return null;

return <CadenceDistributionContent data={data} />;
return <Distribution dataKey="cadence" heading="Cadence" unit="RPM" strokeColor={strokeColor} fillColor={fillColor} />;
};
53 changes: 53 additions & 0 deletions src/viz/Distribution.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';
import { useDataQuery } from '../hooks/useDataQuery';
import { Data, TargetBuckets } from '../types/types';
import { Box, Heading } from '@chakra-ui/react';
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, XAxis } from 'recharts';

interface DistributionContentProps extends Omit<DistributionProps, 'dataKey'> {
data: TargetBuckets;
}

const DistributionContent: React.FC<DistributionContentProps> = ({ data, heading, unit, strokeColor, fillColor }) => {
const filtered = data
.filter(([bucket]) => bucket >= 0)
.map(([bucket, value]) => ([bucket, bucket > 0 ? value : 0] as const));
const total = filtered.length > 0 ? filtered.map(([_, value]) => value).reduce((a, b) => a + b) : 0;
const seriesData = filtered
.map(([name, value]) => ({ name, value: 100 * value / total }));
const xTicks = seriesData.map(({ name }) => name).filter(bucket => bucket % 10 === 0);

return (
<Box w="100%">
<Heading as="h1" fontSize={{ base: "2xl", md: "3xl" }} mb={3} textAlign="center">
{heading}
</Heading>
<ResponsiveContainer width="100%" height={300}>
<AreaChart
data={seriesData}
margin={{ top: 5, right: 5, left: 5, bottom: 5 }}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" ticks={xTicks} unit={` ${unit}`} />
{/*<YAxis unit="%" />*/}
<Area type="monotone" dataKey="value" stroke={strokeColor} fill={fillColor} />
</AreaChart>
</ResponsiveContainer>
</Box>
);
};

interface DistributionProps {
dataKey: { [K in keyof Data]: Data[K] extends TargetBuckets ? K : never }[keyof Data];
heading: string;
unit: string;
strokeColor: string;
fillColor: string;
}

export const Distribution: React.FC<DistributionProps> = ({ dataKey, ...rest }) => {
const { data } = useDataQuery(dataKey)
if (!data) return null;

return <DistributionContent data={data} {...rest} />;
};
2 changes: 1 addition & 1 deletion src/viz/GearUsage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const GearUsageContent: React.FC<GearUsageContentProps> = ({ data }) => {
return (
<Box w="100%">
<Heading as="h1" fontSize={{ base: "2xl", md: "3xl" }} mb={3} textAlign="center">
Gears usage
Gears
</Heading>
<ResponsiveContainer width="100%" height={300}>
<BarChart
Expand Down
11 changes: 11 additions & 0 deletions src/viz/PowerDistribution.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { useColorModeValue, useTheme } from '@chakra-ui/react';
import { Distribution } from './Distribution';

export const PowerDistribution: React.FC = () => {
const theme = useTheme();
const strokeColor = useColorModeValue(theme.colors.yellow[500], theme.colors.yellow[300]);
const fillColor = useColorModeValue(theme.colors.yellow[200], theme.colors.yellow[100]);

return <Distribution dataKey="power" heading="Power" unit="W" strokeColor={strokeColor} fillColor={fillColor} />;
};
11 changes: 11 additions & 0 deletions src/viz/SpeedDistribution.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import { useColorModeValue, useTheme } from '@chakra-ui/react';
import { Distribution } from './Distribution';

export const SpeedDistribution: React.FC = () => {
const theme = useTheme();
const strokeColor = useColorModeValue(theme.colors.yellow[500], theme.colors.yellow[300]);
const fillColor = useColorModeValue(theme.colors.yellow[200], theme.colors.yellow[100]);

return <Distribution dataKey="speed" heading="Speed" unit="km/h" strokeColor={strokeColor} fillColor={fillColor} />;
};
2 changes: 1 addition & 1 deletion src/viz/SpeedGearDistribution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const SpeedGearDistributionContent: React.FC<SpeedGearDistributionContentProps>
return (
<Box w="100%">
<Heading as="h1" fontSize={{ base: "2xl", md: "3xl" }} mb={3} textAlign="center">
Speed per gear
Gears per speed
</Heading>
<ResponsiveContainer width="100%" height={300}>
<AreaChart
Expand Down

0 comments on commit 87668b5

Please sign in to comment.