Skip to content

Commit

Permalink
Highlight unusually low headways in chart (#972)
Browse files Browse the repository at this point in the history
  • Loading branch information
devinmatte authored Apr 16, 2024
1 parent b326076 commit 05d72aa
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 31 deletions.
16 changes: 10 additions & 6 deletions common/components/charts/Legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Disclosure } from '@headlessui/react';
import React from 'react';

export const LegendSingleDay: React.FC = () => {
interface LegendProps {
showUnderRatio?: boolean;
}

export const LegendSingleDay: React.FC<LegendProps> = ({ showUnderRatio = false }) => {
return (
<Disclosure>
{({ open }) => (
Expand All @@ -19,15 +23,15 @@ export const LegendSingleDay: React.FC = () => {
'grid w-full grid-cols-2 items-baseline p-1 px-4 text-left text-xs lg:flex lg:flex-row lg:gap-4'
}
>
<LegendSingle />
<LegendSingle showUnderRatio={showUnderRatio} />
</Disclosure.Panel>
</div>
)}
</Disclosure>
);
};

const LegendSingle: React.FC = () => {
const LegendSingle: React.FC<LegendProps> = ({ showUnderRatio = false }) => {

Check warning on line 34 in common/components/charts/Legend.tsx

View workflow job for this annotation

GitHub Actions / frontend (20, 3.12)

'showUnderRatio' is assigned a value but never used
return (
<>
<div className="col-span-2 flex flex-row items-baseline gap-2 pb-1 italic lg:pb-0">
Expand All @@ -43,19 +47,19 @@ const LegendSingle: React.FC = () => {
</p>
<p>
<span className="mr-1 inline-block h-2.5 w-2.5 rounded-full border border-[#D9D31E] bg-[#f5ed00]"></span>
{'25%+ above'}
{'25%+ off'}
</p>
<p>
<span
className={`mr-1 inline-block h-2.5 w-2.5 rounded-full border border-[#A1384A] bg-[#c33149]`}
></span>{' '}
{'50%+ above'}
{'50%+ off'}
</p>
<p>
<span
className={`mr-1 inline-block h-2.5 w-2.5 rounded-full border border-[#925396] bg-[#bb5cc1]`}
></span>{' '}
{'100%+ above'}
{'100%+ off'}
</p>
</>
);
Expand Down
56 changes: 44 additions & 12 deletions common/components/charts/SingleDayLineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,46 @@ import { LegendSingleDay } from './Legend';
import { ChartDiv } from './ChartDiv';
import { ChartBorder } from './ChartBorder';

const pointColors = (data: DataPoint[], metric_field: string, benchmark_field?: string) => {
const pointColors = (
data: DataPoint[],
metric_field: string,
benchmark_field?: string,
showUnderRatio?: boolean
) => {
return data.map((point: DataPoint) => {
if (benchmark_field) {
const ratio = point[metric_field] / point[benchmark_field];
if (point[benchmark_field] === null) {
return CHART_COLORS.GREY; //grey
return CHART_COLORS.GREY;
} else if (ratio <= 0.05 && showUnderRatio) {
// Not actually 100% off, but we want to show it as an extreme
return CHART_COLORS.PURPLE;
} else if (ratio <= 0.5 && showUnderRatio) {
return CHART_COLORS.RED;
} else if (ratio <= 0.75 && showUnderRatio) {
return CHART_COLORS.YELLOW;
} else if (ratio <= 1.25) {
return CHART_COLORS.GREEN; //green
return CHART_COLORS.GREEN;
} else if (ratio <= 1.5) {
return CHART_COLORS.YELLOW; //yellow
return CHART_COLORS.YELLOW;
} else if (ratio <= 2.0) {
return CHART_COLORS.RED; //red
return CHART_COLORS.RED;
} else if (ratio > 2.0) {
return CHART_COLORS.PURPLE; //purple
return CHART_COLORS.PURPLE;
}
}

return CHART_COLORS.GREY; //whatever
});
};

const departureFromNormalString = (metric: number, benchmark: number) => {
const departureFromNormalString = (metric: number, benchmark: number, showUnderRatio?: boolean) => {
const ratio = metric / benchmark;
if (!isFinite(ratio) || ratio <= 1.25) {
if (showUnderRatio && ratio <= 0.5) {
return '50%+ under schedule';
} else if (showUnderRatio && ratio <= 0.75) {
return '25%+ under schedule';
} else if (!isFinite(ratio) || ratio <= 1.25) {
return '';
} else if (ratio <= 1.5) {
return '25%+ over schedule';
Expand All @@ -67,6 +83,7 @@ export const SingleDayLineChart: React.FC<SingleDayLineProps> = ({
location,
units,
showLegend = true,
showUnderRatio = false,
}) => {
const ref = useRef();
const alerts = useAlertStore((store) => store.alerts)?.filter((alert) => alert.applied);
Expand Down Expand Up @@ -104,9 +121,19 @@ export const SingleDayLineChart: React.FC<SingleDayLineProps> = ({
label: `Actual`,
fill: false,
borderColor: '#a0a0a030',
pointBackgroundColor: pointColors(data, metricField, benchmarkField),
pointBackgroundColor: pointColors(
data,
metricField,
benchmarkField,
showUnderRatio
),
pointHoverRadius: 3,
pointHoverBackgroundColor: pointColors(data, metricField, benchmarkField),
pointHoverBackgroundColor: pointColors(
data,
metricField,
benchmarkField,
showUnderRatio
),
pointRadius: 3,
pointHitRadius: 10,
data: convertedData,
Expand Down Expand Up @@ -148,7 +175,8 @@ export const SingleDayLineChart: React.FC<SingleDayLineProps> = ({
afterBody: (tooltipItems) => {
return departureFromNormalString(
tooltipItems[0].parsed.y,
tooltipItems[1]?.parsed.y
tooltipItems[1]?.parsed.y,
showUnderRatio
);
},
},
Expand Down Expand Up @@ -223,7 +251,11 @@ export const SingleDayLineChart: React.FC<SingleDayLineProps> = ({
<div className="flex flex-col">
{alerts && <AlertsDisclaimer alerts={alerts} />}
<div className="flex flex-row items-end gap-4 ">
{showLegend && benchmarkField ? <LegendSingleDay /> : <div className="w-full" />}
{showLegend && benchmarkField ? (
<LegendSingleDay showUnderRatio={showUnderRatio} />
) : (
<div className="w-full" />
)}
{date && (
<DownloadButton
data={data}
Expand Down
2 changes: 1 addition & 1 deletion common/components/general/DataPair.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const DataPair: React.FC<DataPairProps> = ({ children, last }) => {
<div
className={classNames(
last ? 'border-0' : 'border-r',
'flex h-full min-w-[9.5rem] flex-col gap-1 px-2 md:min-w-[12rem]'
'flex h-full min-w-[9.5rem] flex-col gap-2 px-2 md:min-w-[12rem]'
)}
>
{children}
Expand Down
2 changes: 1 addition & 1 deletion common/components/widgets/MiniWidgetCreator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { DataPair } from '../general/DataPair';
import { SmallDelta } from './internal/SmallDelta';
import { SmallData } from './internal/SmallData';

interface MiniWidgetObject {
export interface MiniWidgetObject {
type: 'delta' | 'data' | string;
widgetValue: WidgetValueInterface;
text: string;
Expand Down
1 change: 1 addition & 0 deletions common/constants/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const COLORS = {
// Colors for charts
export const CHART_COLORS = {
GREY: '#1c1c1c',
BLUE: '#0096FF',
GREEN: '#64b96a',
YELLOW: '#f5ed00',
RED: '#c33149',
Expand Down
4 changes: 2 additions & 2 deletions common/types/basicWidgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export class SZWidgetValue extends BaseWidgetValue implements WidgetValueInterfa
getFormattedValue(isLarge?: boolean) {
if (typeof this.value === 'undefined') return '...';
return (
<p className="">
<p className="text-sm">
<WidgetText isLarge={isLarge} text={`${Math.abs(this.value).toString()}`} />{' '}
<UnitText isLarge={isLarge} text={this.getUnits()} />
</p>
Expand All @@ -131,7 +131,7 @@ export class PercentageWidgetValue extends BaseWidgetValue implements WidgetValu
getFormattedValue(isLarge?: boolean) {
if (this.value === undefined) return '...';
return (
<p>
<p className="text-sm">
<WidgetText isLarge={isLarge} text={`${Math.round(100 * this.value).toString()}`} />{' '}
<UnitText isLarge={isLarge} text={this.getUnits()} />
</p>
Expand Down
2 changes: 2 additions & 0 deletions common/types/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export interface LineProps {
includeBothStopsForLocation?: boolean;
fname: DataName;
showLegend?: boolean;
/** Show ratios under 1.00 differently in chart */
showUnderRatio?: boolean;
}

export interface AggregateLineProps extends LineProps {
Expand Down
6 changes: 3 additions & 3 deletions common/utils/time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ export const getFormattedTimeValue = (value: number, isLarge?: boolean) => {
switch (true) {
case absValue < 100:
return (
<p>
<p className="text-sm">
<WidgetText isLarge={isLarge} text={absValue.toFixed(0)} />
<UnitText isLarge={isLarge} text={'s'} />
</p>
);
case absValue < 3600:
return (
<p>
<p className="text-sm">
<WidgetText isLarge={isLarge} text={duration.format('m')} />
<UnitText isLarge={isLarge} text={'m'} />{' '}
<WidgetText isLarge={isLarge} text={duration.format('s').padStart(2, '0')} />
Expand All @@ -41,7 +41,7 @@ export const getFormattedTimeValue = (value: number, isLarge?: boolean) => {
);
default:
return (
<p>
<p className="text-sm">
<WidgetText isLarge={isLarge} text={duration.format('H')} />
<UnitText isLarge={isLarge} text={'h'} />{' '}
<WidgetText isLarge={isLarge} text={duration.format('m').padStart(2, '0')} />
Expand Down
62 changes: 56 additions & 6 deletions common/utils/widgets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { MPHWidgetValue, TimeWidgetValue } from '../types/basicWidgets';
import type { AggregateDataPoint, SingleDayDataPoint } from '../types/charts';
import type { MiniWidgetObject } from '../components/widgets/MiniWidgetCreator';
import { MPHWidgetValue, PercentageWidgetValue, TimeWidgetValue } from '../types/basicWidgets';
import {
BenchmarkFieldKeys,
type AggregateDataPoint,
type SingleDayDataPoint,
MetricFieldKeys,
} from '../types/charts';
import type { DataPoint } from '../types/dataPoints';

const getAverage = (data: (number | undefined)[]) => {
const { length } = data;
Expand All @@ -17,6 +24,30 @@ const getAverage = (data: (number | undefined)[]) => {
}
};

const getBunchedPercentage = (data: SingleDayDataPoint[]) => {
const bunchedPoints = data.map((point: DataPoint) => {
const ratio =
point[MetricFieldKeys.headwayTimeSec] / point[BenchmarkFieldKeys.benchmarkHeadwayTimeSec];
if (ratio <= 0.5) {
return 1;
}
return 0;
});
return bunchedPoints.reduce((a, b) => a + b, 0) / data.length;
};

const getOnTimePercentage = (data: SingleDayDataPoint[]) => {
const onTimePoints = data.map((point: DataPoint) => {
const ratio =
point[MetricFieldKeys.headwayTimeSec] / point[BenchmarkFieldKeys.benchmarkHeadwayTimeSec];
if (ratio > 0.75 && ratio < 1.25) {
return 1;
}
return 0;
});
return onTimePoints.reduce((a, b) => a + b, 0) / data.length;
};

const getPeaks = (data: (number | undefined)[]) => {
data.sort((a, b) => {
if (b !== undefined && a !== undefined) return a - b;
Expand Down Expand Up @@ -68,7 +99,9 @@ const getSingleDayPointsOfInterest = (
const _data = getSingleDayNumberArray(data, type);
const { max, min, median, p10, p90 } = getPeaks(_data);
const average = getAverage(_data);
return { max, min, median, average, p10, p90 };
const onTimePercentage = getOnTimePercentage(data);
const bunchedPercentage = getBunchedPercentage(data);
return { max, min, median, average, p10, p90, onTimePercentage, bunchedPercentage };
};

function getWidget(type: string, value: number | undefined) {
Expand All @@ -82,14 +115,31 @@ function getWidget(type: string, value: number | undefined) {
export const getSingleDayWidgets = (
data: SingleDayDataPoint[],
type: 'traveltimes' | 'dwells' | 'headways' | 'speeds'
) => {
const { max, min, median, average, p10, p90 } = getSingleDayPointsOfInterest(data, type);
return [
): MiniWidgetObject[] => {
const { max, min, median, average, p10, p90, onTimePercentage, bunchedPercentage } =
getSingleDayPointsOfInterest(data, type);

const widgets: MiniWidgetObject[] = [
{ text: 'Avg', widgetValue: getWidget(type, average), type: 'data' },
{ text: 'Median', widgetValue: getWidget(type, median), type: 'data' },
{ text: '10%', widgetValue: getWidget(type, p10), type: 'data' },
{ text: '90%', widgetValue: getWidget(type, p90), type: 'data' },
{ text: 'Min', widgetValue: getWidget(type, min), type: 'data' },
{ text: 'Max', widgetValue: getWidget(type, max), type: 'data' },
];

if (type === 'headways') {
widgets.push({
text: 'On Time Trips',
widgetValue: new PercentageWidgetValue(onTimePercentage),
type: 'data',
});
widgets.push({
text: 'Bunched Trips',
widgetValue: new PercentageWidgetValue(bunchedPercentage),
type: 'data',
});
}

return widgets;
};
1 change: 1 addition & 0 deletions modules/headways/charts/HeadwaysSingleChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const HeadwaysSingleChart: React.FC<HeadwaysChartProps> = ({
fname={'headways'}
units={'Minutes'}
showLegend={showLegend && anyHeadwayBenchmarks}
showUnderRatio={true}
/>
);
}, [linePath, headways, date, fromStation, toStation, showLegend, anyHeadwayBenchmarks]);
Expand Down

0 comments on commit 05d72aa

Please sign in to comment.