Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions common/components/charts/Legend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@

interface LegendProps {
showUnderRatio?: boolean;
usingTmBenchmark?: boolean;
}

interface LegendLongTermProps {
isTrendlineVisible: boolean;
onToggleTrendline: () => void;
}

export const LegendSingleDay: React.FC<LegendProps> = ({ showUnderRatio }) => {
export const LegendSingleDay: React.FC<LegendProps> = ({ showUnderRatio, usingTmBenchmark }) => {
const { line } = useDelimitatedRoute();
return (
<Disclosure>
Expand All @@ -31,7 +32,7 @@
'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 showUnderRatio={showUnderRatio} />
<LegendSingle showUnderRatio={showUnderRatio} usingTmBenchmark={usingTmBenchmark} />
</div>

<div
Expand All @@ -54,14 +55,17 @@
);
};

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

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

View workflow job for this annotation

GitHub Actions / frontend (24, 3.12)

'showUnderRatio' is assigned a value but never used
usingTmBenchmark = false,
}) => {
return (
<>
<div className="col-span-2 flex flex-row items-baseline gap-2 pb-1 italic lg:pb-0">
<p>
Compare to{' '}
<span className="top-[1px] inline-block h-2.5 w-2.5 items-center border-t-2 border-[#bbb] bg-[#ddd] shadow-sm"></span>{' '}
MBTA benchmark:
{usingTmBenchmark ? 'TransitMatters' : 'MBTA'} benchmark:
</p>
</div>
<p>
Expand Down
156 changes: 96 additions & 60 deletions common/components/charts/SingleDayLineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,69 +20,70 @@
import { FIVE_MINUTES } from '../../constants/time';
import { LegendSingleDay } from './Legend';
import { ChartDiv } from './ChartDiv';
import { ChartBorder } from './ChartBorder';

Check warning on line 23 in common/components/charts/SingleDayLineChart.tsx

View workflow job for this annotation

GitHub Actions / frontend (24, 3.12)

Maximum number of dependencies (20) exceeded

const pointColors = (
data: DataPoint[],
metric_field: string,
benchmark_field?: string,
effectiveBenchmark: (number | null)[],
showUnderRatio?: boolean
) => {
return data.map((point: DataPoint) => {
if (benchmark_field) {
const benchmarkValue = point[benchmark_field];
// Check for null, undefined, NaN, or other invalid values
if (
benchmarkValue === null ||
benchmarkValue === undefined ||
typeof benchmarkValue !== 'number' ||
!Number.isFinite(benchmarkValue)
) {
return CHART_COLORS.GREY;
}
const ratio = point[metric_field] / benchmarkValue;
if (!Number.isFinite(ratio)) {
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;
} else if (ratio <= 1.5) {
return CHART_COLORS.YELLOW;
} else if (ratio <= 2.0) {
return CHART_COLORS.RED;
} else if (ratio > 2.0) {
return CHART_COLORS.PURPLE;
}
return data.map((point: DataPoint, idx: number) => {
const benchmarkValue = effectiveBenchmark[idx];
if (
benchmarkValue === null ||
benchmarkValue === undefined ||
typeof benchmarkValue !== 'number' ||
!Number.isFinite(benchmarkValue)
) {
return CHART_COLORS.GREY;
}

return CHART_COLORS.GREY; //whatever
const ratio = point[metric_field] / benchmarkValue;
if (!Number.isFinite(ratio)) {
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;
} else if (ratio <= 1.5) {
return CHART_COLORS.YELLOW;
} else if (ratio <= 2.0) {
return CHART_COLORS.RED;
} else if (ratio > 2.0) {
return CHART_COLORS.PURPLE;
}
return CHART_COLORS.GREY;
});
};

const departureFromNormalString = (metric: number, benchmark: number, showUnderRatio?: boolean) => {
const departureFromNormalString = (
metric: number,
benchmark: number,
showUnderRatio?: boolean,
referenceWord: 'schedule' | 'benchmark' = 'schedule'
) => {
// Handle invalid benchmark values
if (!benchmark || typeof benchmark !== 'number' || !Number.isFinite(benchmark)) {
return '';
}
const ratio = metric / benchmark;
if (showUnderRatio && ratio <= 0.5) {
return '50%+ under schedule';
return `50%+ under ${referenceWord}`;
} else if (showUnderRatio && ratio <= 0.75) {
return '25%+ under schedule';
return `25%+ under ${referenceWord}`;
} else if (!isFinite(ratio) || ratio <= 1.25) {
return '';
} else if (ratio <= 1.5) {
return '25%+ over schedule';
return `25%+ over ${referenceWord}`;
} else if (ratio <= 2.0) {
return '50%+ over schedule';
return `50%+ over ${referenceWord}`;
} else if (ratio > 2.0) {
return '100%+ over schedule';
return `100%+ over ${referenceWord}`;
}
return '';
};
Expand All @@ -94,6 +95,7 @@
metricField,
pointField,
benchmarkField,
tmBenchmarkField,
fname,
includeBothStopsForLocation = false,
location,
Expand All @@ -108,21 +110,38 @@
const isMobile = !useBreakpoint('md');
const labels = useMemo(() => data.map((item) => item[pointField]), [data, pointField]);

// Format benchmark data if it exists.
const benchmarkData = data.map((datapoint) => {
// Per-point MBTA scheduled benchmark.
const mbtaBenchmarkData = data.map((datapoint) => {
const value = benchmarkField && datapoint[benchmarkField];
// Handle NaN, null, undefined, and other falsy values
if (!value || typeof value !== 'number' || !Number.isFinite(value)) {
return null;
}
return value;
});
const displayBenchmarkData = benchmarkData.some((datapoint) => datapoint !== null);

// Per-point TM historical benchmark (already capped at the MBTA value by the
// backend generator, so we never go *above* the MBTA number).
const tmBenchmarkData = data.map((datapoint) => {
const raw = tmBenchmarkField && datapoint[tmBenchmarkField];
if (!raw || typeof raw !== 'number' || !Number.isFinite(raw)) {
return null;
}
return raw;
});

// Effective benchmark = TM when available, else MBTA. This is what the
// chart band *and* the dot colors are compared against. When TM is absent
// (bus, CR, missing pair) we transparently fall back to the MBTA value.
const effectiveBenchmarkData = data.map(
(_, idx) => tmBenchmarkData[idx] ?? mbtaBenchmarkData[idx]
);
const displayBenchmarkData = effectiveBenchmarkData.some((d) => d !== null);
const usingTmBenchmark = tmBenchmarkData.some((d) => d !== null);

const multiplier = units === 'Minutes' ? 1 / 60 : 1;
const benchmarkDataFormatted = benchmarkData
.map((datapoint) => (datapoint ? (datapoint * multiplier).toFixed(2) : null))
.filter((datapoint) => datapoint !== null);
const effectiveBenchmarkFormatted = effectiveBenchmarkData
.map((d) => (d !== null ? (d * multiplier).toFixed(2) : null))
.filter((d) => d !== null);

const convertedData = data.map((datapoint) =>
((datapoint[metricField] as number) * multiplier).toFixed(2)
Expand All @@ -146,24 +165,27 @@
pointBackgroundColor: pointColors(
data,
metricField,
benchmarkField,
effectiveBenchmarkData,
showUnderRatio
),
pointHoverRadius: 3,
pointHoverBackgroundColor: pointColors(
data,
metricField,
benchmarkField,
effectiveBenchmarkData,
showUnderRatio
),
pointRadius: 3,
pointHitRadius: 10,
data: convertedData,
},
{
label: `Benchmark MBTA`,
// Dataset label switches based on whether any point on this
// chart uses the TM value. The tooltip overrides this per
// point to get the exact name right for each hover.
label: usingTmBenchmark ? `TransitMatters Benchmark` : `MBTA Benchmark`,
backgroundColor: '#a0a0a030',
data: benchmarkDataFormatted,
data: effectiveBenchmarkFormatted,
pointRadius: 0,
pointHoverRadius: 3,
fill: true,
Expand All @@ -182,13 +204,22 @@
position: 'nearest',
callbacks: {
label: (tooltipItem) => {
if (
!tooltipItem.parsed.y ||
(tooltipItem.parsed.y === 0 && tooltipItem.dataset.label === 'Benchmark MBTA')
) {
const datasetLabel = tooltipItem.dataset.label ?? '';
const isBenchmark =
datasetLabel === 'MBTA Benchmark' ||
datasetLabel === 'TransitMatters Benchmark';
if (!tooltipItem.parsed.y || (tooltipItem.parsed.y === 0 && isBenchmark)) {
return '';
}
return `${tooltipItem.dataset.label}: ${
// Per-point label: a chart in "TM mode" can still have
// individual points that fell back to MBTA; name each
// point's benchmark source honestly.
const displayLabel = isBenchmark
? tmBenchmarkData[tooltipItem.dataIndex] != null
? 'TransitMatters Benchmark'
: 'MBTA Benchmark'
: datasetLabel;
return `${displayLabel}: ${
units === 'Minutes'
? getFormattedTimeString(tooltipItem.parsed.y, 'minutes')
: `${tooltipItem.parsed.y} ${units}`
Expand All @@ -197,11 +228,16 @@
afterBody: (tooltipItems) => {
const result: string[] = [];

// Add departure from normal information
const benchmarkItem = tooltipItems.find(
(t) =>
t.dataset.label === 'MBTA Benchmark' ||
t.dataset.label === 'TransitMatters Benchmark'
);
const departureInfo = departureFromNormalString(
tooltipItems[0].parsed.y ?? 0,
tooltipItems[1]?.parsed.y ?? 0,
showUnderRatio
benchmarkItem?.parsed.y ?? 0,
showUnderRatio,
usingTmBenchmark ? 'benchmark' : 'schedule'
);
if (departureInfo) {
result.push(departureInfo);
Expand Down Expand Up @@ -314,7 +350,7 @@
{alerts && <AlertsDisclaimer alerts={alerts} />}
<div className="flex flex-row items-end gap-4">
{showLegend && benchmarkField ? (
<LegendSingleDay showUnderRatio={showUnderRatio} />
<LegendSingleDay showUnderRatio={showUnderRatio} usingTmBenchmark={usingTmBenchmark} />
) : (
<div className="w-full" />
)}
Expand Down
3 changes: 3 additions & 0 deletions common/types/charts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface SingleDayDataPoint {
dwell_time_sec?: number;
benchmark_travel_time_sec?: number;
benchmark_headway_time_sec?: number | null;
tm_benchmark_travel_time_sec?: number | null;
threshold_flag_1?: string;
speed_mph?: number;
benchmark_speed_mph?: number;
Expand Down Expand Up @@ -73,6 +74,7 @@ export enum BenchmarkFieldKeys {
benchmarkTravelTimeSec = 'benchmark_travel_time_sec',
benchmarkHeadwayTimeSec = 'benchmark_headway_time_sec',
benchmarkSpeedMph = 'benchmark_speed_mph',
tmBenchmarkTravelTimeSec = 'tm_benchmark_travel_time_sec',
}

export type PointField = PointFieldKeys;
Expand Down Expand Up @@ -113,6 +115,7 @@ export interface SingleDayLineProps extends LineProps {
metricField: MetricField;
date: string | undefined;
benchmarkField?: BenchmarkField;
tmBenchmarkField?: BenchmarkField;
units: 'Minutes' | 'MPH';
}

Expand Down
1 change: 1 addition & 0 deletions common/types/dataPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface TravelTimePoint extends DataPoint {
arr_dt: string;
travel_time_sec: number;
benchmark_travel_time_sec: number;
tm_benchmark_travel_time_sec?: number | null;
threshold_flag_1?: string;
threshold_flag_2?: string;
threshold_flag_3?: string;
Expand Down
1 change: 1 addition & 0 deletions modules/traveltimes/charts/TravelTimesSingleChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export const TravelTimesSingleChart: React.FC<TravelTimesSingleChartProps> = ({
metricField={MetricFieldKeys.travelTimeSec}
pointField={PointFieldKeys.depDt}
benchmarkField={BenchmarkFieldKeys.benchmarkTravelTimeSec}
tmBenchmarkField={BenchmarkFieldKeys.tmBenchmarkTravelTimeSec}
includeBothStopsForLocation={true}
units="Minutes"
location={getLocationDetails(fromStation, toStation)}
Expand Down
6 changes: 5 additions & 1 deletion server/chalicelib/s3_historical.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

import itertools
import math
from chalicelib import date_utils
from chalicelib import date_utils, tm_benchmarks


def pairwise(iterable):
Expand Down Expand Up @@ -234,6 +234,9 @@ def travel_times(stops_a: list, stops_b: list, start_date: date, end_date: date)

# not every vehicle will have vehicle_consist
vehicle_consist = departure.get("vehicle_consist")
tm_benchmark = tm_benchmarks.get_travel_time_benchmark(
departure["route_id"], departure["stop_id"], arrival["stop_id"]
)
travel_times.append(
{
"route_id": departure["route_id"],
Expand All @@ -242,6 +245,7 @@ def travel_times(stops_a: list, stops_b: list, start_date: date, end_date: date)
"arr_dt": date_utils.return_formatted_date(arr_dt),
"travel_time_sec": travel_time_sec,
"benchmark_travel_time_sec": benchmark,
"tm_benchmark_travel_time_sec": tm_benchmark,
"vehicle_consist": vehicle_consist,
"vehicle_label": departure["vehicle_label"],
}
Expand Down
Loading
Loading