Skip to content

Commit

Permalink
compute user metrics on phone using e-mission-common
Browse files Browse the repository at this point in the history
In MetricsTab, userMetrics will be populated not by a call to 'fetchMetricsFromServer', but by MetricsSummaries.generate_summaries from e-mission-common, which will compute user metrics from timeline data (ie timelineMap and timelineLabelMap)

aggMetrics will still be the same as before, fetched from the server.

There are a couple differences between metrics computed on e-misison-server and metrics computed on e-mission-common. (see metricsTypes.ts where DayOfMetricData can now be either DayOfClientMetricData or DayOfServerMetricData)

To reconcile these differences, added helper functions `dateForDayOfMetricData`, `tsForDayOfMetricData`, and `valueForModeOnDay`
  • Loading branch information
JGreenlee committed Mar 25, 2024
1 parent 1a12b8b commit f165a3f
Show file tree
Hide file tree
Showing 7 changed files with 78 additions and 24 deletions.
10 changes: 7 additions & 3 deletions www/js/metrics/ActiveMinutesTableCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
formatDateRangeOfDays,
secondsToMinutes,
segmentDaysByWeeks,
valueForModeOnDay,
} from './metricsHelper';
import { useTranslation } from 'react-i18next';
import { ACTIVE_MODES } from './WeeklyActiveMinutesCard';
Expand All @@ -21,7 +22,10 @@ const ActiveMinutesTableCard = ({ userMetrics }: Props) => {
if (!userMetrics?.duration) return [];
const totals = {};
ACTIVE_MODES.forEach((mode) => {
const sum = userMetrics.duration.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0);
const sum = userMetrics.duration.reduce(
(acc, day) => acc + (valueForModeOnDay(day, mode) || 0),
0,
);
totals[mode] = secondsToMinutes(sum);
});
totals['period'] = formatDateRangeOfDays(userMetrics.duration);
Expand All @@ -35,7 +39,7 @@ const ActiveMinutesTableCard = ({ userMetrics }: Props) => {
.map((week) => {
const totals = {};
ACTIVE_MODES.forEach((mode) => {
const sum = week.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0);
const sum = week.reduce((acc, day) => acc + (valueForModeOnDay(day, mode) || 0), 0);
totals[mode] = secondsToMinutes(sum);
});
totals['period'] = formatDateRangeOfDays(week);
Expand All @@ -49,7 +53,7 @@ const ActiveMinutesTableCard = ({ userMetrics }: Props) => {
.map((day) => {
const totals = {};
ACTIVE_MODES.forEach((mode) => {
const sum = day[`label_${mode}`] || 0;
const sum = valueForModeOnDay(day, mode) || 0;
totals[mode] = secondsToMinutes(sum);
});
totals['period'] = formatDate(day);
Expand Down
3 changes: 2 additions & 1 deletion www/js/metrics/DailyActiveMinutesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next';
import { labelKeyToRichMode, labelOptions } from '../survey/multilabel/confirmHelper';
import LineChart from '../components/LineChart';
import { getBaseModeByText } from '../diary/diaryHelper';
import { valueForModeOnDay } from './metricsHelper';

const ACTIVE_MODES = ['walk', 'bike'] as const;
type ActiveMode = (typeof ACTIVE_MODES)[number];
Expand All @@ -21,7 +22,7 @@ const DailyActiveMinutesCard = ({ userMetrics }: Props) => {
const recentDays = userMetrics?.duration?.slice(-14);
recentDays?.forEach((day) => {
ACTIVE_MODES.forEach((mode) => {
const activeSeconds = day[`label_${mode}`];
const activeSeconds = valueForModeOnDay(day, mode);
if (activeSeconds) {
records.push({
label: labelKeyToRichMode(mode),
Expand Down
19 changes: 14 additions & 5 deletions www/js/metrics/MetricsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@ import { Card, Checkbox, Text, useTheme } from 'react-native-paper';
import colorLib from 'color';
import BarChart from '../components/BarChart';
import { DayOfMetricData } from './metricsTypes';
import { formatDateRangeOfDays, getLabelsForDay, getUniqueLabelsForDays } from './metricsHelper';
import {
formatDateRangeOfDays,
getLabelsForDay,
tsForDayOfMetricData,
getUniqueLabelsForDays,
valueForModeOnDay,
} from './metricsHelper';
import ToggleSwitch from '../components/ToggleSwitch';
import { cardStyles } from './MetricsTab';
import { labelKeyToRichMode, labelOptions } from '../survey/multilabel/confirmHelper';
import { getBaseModeByKey, getBaseModeByText } from '../diary/diaryHelper';
import { getBaseModeByKey, getBaseModeByText, modeColors } from '../diary/diaryHelper';
import { useTranslation } from 'react-i18next';

type Props = {
Expand Down Expand Up @@ -42,12 +48,12 @@ const MetricsCard = ({
metricDataDays.forEach((day) => {
const labels = getLabelsForDay(day);
labels.forEach((label) => {
const rawVal = day[`label_${label}`];
const rawVal = valueForModeOnDay(day, label);
if (rawVal) {
records.push({
label: labelKeyToRichMode(label),
x: unitFormatFn ? unitFormatFn(rawVal) : rawVal,
y: day.ts * 1000, // time (as milliseconds) will go on Y axis because it will be a horizontal chart
y: tsForDayOfMetricData(day) * 1000, // time (as milliseconds) will go on Y axis because it will be a horizontal chart
});
}
});
Expand Down Expand Up @@ -76,7 +82,10 @@ const MetricsCard = ({
// for each label, sum up cumulative values across all days
const vals = {};
uniqueLabels.forEach((label) => {
const sum = metricDataDays.reduce((acc, day) => acc + (day[`label_${label}`] || 0), 0);
const sum = metricDataDays.reduce(
(acc, day) => acc + (valueForModeOnDay(day, label) || 0),
0,
);
vals[label] = unitFormatFn ? unitFormatFn(sum) : sum;
});
return vals;
Expand Down
17 changes: 15 additions & 2 deletions www/js/metrics/MetricsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { ServerConnConfig } from '../types/appConfigTypes';
import DateSelect from '../diary/list/DateSelect';
import TimelineContext from '../TimelineContext';
import { isoDateRangeToTsRange } from '../diary/timelineHelper';
import { MetricsSummaries } from '../../../../e-mission-common/js';

export const METRIC_LIST = ['duration', 'mean_speed', 'count', 'distance'] as const;

Expand Down Expand Up @@ -54,17 +55,29 @@ const MetricsTab = () => {
const { t } = useTranslation();
const { getFormattedSpeed, speedSuffix, getFormattedDistance, distanceSuffix } =
useImperialConfig();
const { dateRange, setDateRange, refreshTimeline } = useContext(TimelineContext);
const { dateRange, setDateRange, timelineMap, timelineLabelMap, refreshTimeline } =
useContext(TimelineContext);

const [aggMetrics, setAggMetrics] = useState<MetricsData | undefined>(undefined);
const [userMetrics, setUserMetrics] = useState<MetricsData | undefined>(undefined);

// aggregate metrics are fetched from the server
useEffect(() => {
if (!appConfig?.server) return;
loadMetricsForPopulation('user', dateRange);
loadMetricsForPopulation('aggregate', dateRange);
}, [dateRange, appConfig?.server]);

// user metrics are computed on the phone from the timeline data
useEffect(() => {
if (!timelineMap) return;
const userMetrics = MetricsSummaries.generate_summaries(
METRIC_LIST,
[...timelineMap.values()],
timelineLabelMap,
) as MetricsData;
setUserMetrics(userMetrics);
}, [timelineMap]);

async function loadMetricsForPopulation(
population: 'user' | 'aggregate',
dateRange: [string, string],
Expand Down
9 changes: 6 additions & 3 deletions www/js/metrics/WeeklyActiveMinutesCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { View } from 'react-native';
import { Card, Text, useTheme } from 'react-native-paper';
import { MetricsData } from './metricsTypes';
import { cardMargin, cardStyles } from './MetricsTab';
import { formatDateRangeOfDays, segmentDaysByWeeks } from './metricsHelper';
import { formatDateRangeOfDays, segmentDaysByWeeks, valueForModeOnDay } from './metricsHelper';
import { useTranslation } from 'react-i18next';
import BarChart from '../components/BarChart';
import { labelKeyToRichMode, labelOptions } from '../survey/multilabel/confirmHelper';
Expand All @@ -22,13 +22,16 @@ const WeeklyActiveMinutesCard = ({ userMetrics }: Props) => {
const records: { x: string; y: number; label: string }[] = [];
const [recentWeek, prevWeek] = segmentDaysByWeeks(userMetrics?.duration, 2);
ACTIVE_MODES.forEach((mode) => {
const prevSum = prevWeek?.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0);
const prevSum = prevWeek?.reduce((acc, day) => acc + (valueForModeOnDay(day, mode) || 0), 0);
if (prevSum) {
// `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(lastWeekDistance)})`
const xLabel = `${t('main-metrics.prev-week')}\n(${formatDateRangeOfDays(prevWeek)})`;
records.push({ label: labelKeyToRichMode(mode), x: xLabel, y: prevSum / 60 });
}
const recentSum = recentWeek?.reduce((acc, day) => acc + (day[`label_${mode}`] || 0), 0);
const recentSum = recentWeek?.reduce(
(acc, day) => acc + (valueForModeOnDay(day, mode) || 0),
0,
);
if (recentSum) {
const xLabel = `${t('main-metrics.past-week')}\n(${formatDateRangeOfDays(recentWeek)})`;
records.push({ label: labelKeyToRichMode(mode), x: xLabel, y: recentSum / 60 });
Expand Down
34 changes: 25 additions & 9 deletions www/js/metrics/metricsHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) {
const uniqueLabels: string[] = [];
metricDataDays.forEach((e) => {
Object.keys(e).forEach((k) => {
if (k.startsWith('label_')) {
const label = k.substring(6); // remove 'label_' prefix leaving just the mode label
if (k.startsWith('label_') || k.startsWith('mode_')) {
let i = k.indexOf('_');
const label = k.substring(i + 1); // remove prefix leaving just the mode label
if (!uniqueLabels.includes(label)) uniqueLabels.push(label);
}
});
Expand All @@ -18,8 +19,9 @@ export function getUniqueLabelsForDays(metricDataDays: DayOfMetricData[]) {

export const getLabelsForDay = (metricDataDay: DayOfMetricData) =>
Object.keys(metricDataDay).reduce((acc, k) => {
if (k.startsWith('label_')) {
acc.push(k.substring(6)); // remove 'label_' prefix leaving just the mode label
if (k.startsWith('label_') || k.startsWith('mode_')) {
let i = k.indexOf('_');
acc.push(k.substring(i + 1)); // remove prefix leaving just the mode label
}
return acc;
}, [] as string[]);
Expand All @@ -39,14 +41,18 @@ export function segmentDaysByWeeks(days: DayOfMetricData[], nWeeks?: number) {
}

export function formatDate(day: DayOfMetricData) {
const dt = DateTime.fromISO(day.fmt_time, { zone: 'utc' });
const dt = DateTime.fromISO(dateForDayOfMetricData(day), { zone: 'utc' });
return dt.toLocaleString({ ...DateTime.DATE_SHORT, year: undefined });
}

export function formatDateRangeOfDays(days: DayOfMetricData[]) {
if (!days?.length) return '';
const firstDayDt = DateTime.fromISO(days[0].fmt_time, { zone: 'utc' });
const lastDayDt = DateTime.fromISO(days[days.length - 1].fmt_time, { zone: 'utc' });
const firstDayDt = DateTime.fromISO(dateForDayOfMetricData(days[0]), {
zone: 'utc',
});
const lastDayDt = DateTime.fromISO(dateForDayOfMetricData(days[days.length - 1]), {
zone: 'utc',
});
const firstDay = firstDayDt.toLocaleString({ ...DateTime.DATE_SHORT, year: undefined });
const lastDay = lastDayDt.toLocaleString({ ...DateTime.DATE_SHORT, year: undefined });
return `${firstDay} - ${lastDay}`;
Expand Down Expand Up @@ -115,8 +121,9 @@ export function parseDataFromMetrics(metrics, population) {
}
}
//this section handles user lables, assuming 'label_' prefix
if (field.startsWith('label_')) {
let actualMode = field.slice(6, field.length); //remove prefix
if (field.startsWith('label_') || field.startsWith('mode_')) {
let i = field.indexOf('_');
let actualMode = field.substring(i + 1); // remove prefix
logDebug('Mapped field ' + field + ' to mode ' + actualMode);
if (!(actualMode in mode_bins)) {
mode_bins[actualMode] = [];
Expand All @@ -137,6 +144,15 @@ export function parseDataFromMetrics(metrics, population) {
return Object.entries(mode_bins).map(([key, values]) => ({ key, values }));
}

export const dateForDayOfMetricData = (day: DayOfMetricData) =>
'date' in day ? day.date : day.fmt_time.substring(0, 10);

export const tsForDayOfMetricData = (day: DayOfMetricData) =>
DateTime.fromISO(dateForDayOfMetricData(day)).toSeconds();

export const valueForModeOnDay = (day: DayOfMetricData, key: string) =>
day[`mode_${key}`] || day[`label_${key}`];

export type MetricsSummary = { key: string; values: number };
export function generateSummaryFromData(modeMap, metric) {
logDebug(`Invoked getSummaryDataRaw on ${JSON.stringify(modeMap)} with ${metric}`);
Expand Down
10 changes: 9 additions & 1 deletion www/js/metrics/metricsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,22 @@ import { LocalDt } from '../types/serverData';
import { METRIC_LIST } from './MetricsTab';

type MetricName = (typeof METRIC_LIST)[number];

type LabelProps = { [k in `label_${string}`]?: number }; // label_<mode>, where <mode> could be anything
export type DayOfMetricData = LabelProps & {
export type DayOfServerMetricData = LabelProps & {
ts: number;
fmt_time: string;
nUsers: number;
local_dt: LocalDt;
};

type ModeProps = { [k in `mode_${string}`]?: number }; // mode_<mode>, where <mode> could be anything
export type DayOfClientMetricData = ModeProps & {
date: string; // yyyy-mm-dd
};

export type DayOfMetricData = DayOfClientMetricData | DayOfServerMetricData;

export type MetricsData = {
[key in MetricName]: DayOfMetricData[];
};

0 comments on commit f165a3f

Please sign in to comment.