Skip to content

Commit 088d1bf

Browse files
committed
Fix request time chart to exclude anomalies
1 parent d3c9d87 commit 088d1bf

2 files changed

Lines changed: 112 additions & 34 deletions

File tree

app/components/request-time-chart.gjs

Lines changed: 108 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22
import { classNames } from '@ember-decorators/component';
33
import { computed } from '@ember/object';
44
import Component from '@ember/component';
5-
import moment from 'moment-timezone';
5+
import { inject as service } from '@ember/service';
6+
import formatTimespan from 'prison-rideshare-ui/utils/format-timespan';
7+
import momentTimeZone from 'moment-timezone';
68
import HighCharts from 'ember-highcharts/components/high-charts';
79

10+
const MAX_RIDE_DURATION_HOURS = 24;
11+
12+
const dataLabelFormatter = function () {
13+
return this.point.value > 0 ? this.point.value : null;
14+
};
15+
816
@classNames('request-time-chart')
917
export default class RequestTimeChart extends Component {
1018
<template>
@@ -13,51 +21,78 @@ export default class RequestTimeChart extends Component {
1321
@chartOptions={{this.options}}
1422
@theme={{this.theme}}
1523
/>
24+
25+
{{#if this.excludedRides.length}}
26+
<details class='request-time-exclusions'>
27+
<summary>
28+
Rides excluded from visit times chart (duration over
29+
{{MAX_RIDE_DURATION_HOURS}}
30+
hours or invalid):
31+
{{this.excludedRides.length}}
32+
</summary>
33+
<ul>
34+
{{#each this.excludedRidesDisplay as |excluded|}}
35+
<li>
36+
{{excluded.timespan}}
37+
· Visitor:
38+
{{excluded.visitor}}
39+
· Driver:
40+
{{excluded.driver}}
41+
</li>
42+
{{/each}}
43+
</ul>
44+
</details>
45+
{{/if}}
1646
</template>
17-
@computed('rides.@each.start', 'grouping')
47+
48+
@service moment;
49+
50+
@computed('grouping', 'rides.@each.{end,passengers,start}')
1851
get data() {
1952
const grouping = this.grouping;
53+
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
54+
55+
const days = dayNames.map((name, day) => ({
56+
hours: new Array(24).fill(0),
57+
name,
58+
day,
59+
}));
2060

21-
const data = this.rides
22-
.reduce((days, ride) => {
23-
const start = ride.get('start'),
24-
end = ride.get('end');
25-
const startInTimeZone = moment.tz(start, 'America/Winnipeg');
26-
// const day = (startInTimeZone.day() - 1) % 7;
27-
const day = startInTimeZone.day() ? startInTimeZone.day() - 1 : 6;
61+
this.rides.forEach((ride) => {
62+
const start = ride.get('start');
63+
const end = ride.get('end');
64+
if (!start || !end) {
65+
return;
66+
}
2867

29-
const rideAddition = grouping === 'rides' ? 1 : ride.get('passengers');
68+
const startInTimeZone = momentTimeZone.tz(start, 'America/Winnipeg');
69+
const endInTimeZone = momentTimeZone.tz(end, 'America/Winnipeg');
3070

31-
if (!days[day]) {
32-
days[day] = {
33-
hours: new Array(24),
34-
name: startInTimeZone.format('ddd'),
35-
day,
36-
};
37-
}
71+
const durationHours = endInTimeZone.diff(startInTimeZone, 'hours', true);
3872

39-
let currentHour = startInTimeZone.startOf('hour');
73+
if (durationHours <= 0 || durationHours > MAX_RIDE_DURATION_HOURS) {
74+
return;
75+
}
4076

41-
while (currentHour.isBefore(end)) {
42-
const hour = currentHour.hour();
77+
const rideAddition = grouping === 'rides' ? 1 : ride.get('passengers');
4378

44-
if (!days[day].hours[hour]) {
45-
days[day].hours[hour] = 0;
46-
}
79+
let currentHour = startInTimeZone.clone().startOf('hour');
4780

48-
days[day].hours[hour] += rideAddition;
81+
while (currentHour.isBefore(endInTimeZone)) {
82+
const hour = currentHour.hour();
83+
const day = currentHour.day() ? currentHour.day() - 1 : 6;
4984

50-
currentHour = currentHour.add(1, 'hour');
51-
}
85+
days[day].hours[hour] += rideAddition;
86+
currentHour = currentHour.add(1, 'hour');
87+
}
88+
});
5289

53-
return days;
54-
}, new Array(7))
55-
.reduce((data, day, index) => {
56-
day.hours.forEach((hourCount, hour) =>
57-
data.push([hour, index, hourCount || 0]),
58-
);
59-
return data;
60-
}, []);
90+
const data = days.reduce((result, day, index) => {
91+
day.hours.forEach((hourCount, hour) =>
92+
result.push([hour, index, hourCount || 0]),
93+
);
94+
return result;
95+
}, []);
6196

6297
return [
6398
{
@@ -67,11 +102,50 @@ export default class RequestTimeChart extends Component {
67102
dataLabels: {
68103
enabled: true,
69104
color: '#000000',
105+
formatter: dataLabelFormatter,
70106
},
71107
},
72108
];
73109
}
74110

111+
@computed('rides.@each.{start,end}')
112+
get excludedRides() {
113+
return (this.rides || []).filter((ride) => {
114+
const start = ride.get('start');
115+
const end = ride.get('end');
116+
117+
if (!start || !end) {
118+
return true;
119+
}
120+
121+
const startInTimeZone = momentTimeZone.tz(start, 'America/Winnipeg');
122+
const endInTimeZone = momentTimeZone.tz(end, 'America/Winnipeg');
123+
const durationHours = endInTimeZone.diff(startInTimeZone, 'hours', true);
124+
125+
return durationHours <= 0 || durationHours > MAX_RIDE_DURATION_HOURS;
126+
});
127+
}
128+
129+
@computed('excludedRides.@each.{start,end,name,driver}')
130+
get excludedRidesDisplay() {
131+
return this.excludedRides.map((ride) => {
132+
const start = ride.get('start');
133+
const end = ride.get('end');
134+
const timespan =
135+
start && end
136+
? formatTimespan(this.moment, start, end)
137+
: start
138+
? 'Missing end'
139+
: 'Missing start';
140+
141+
return {
142+
timespan,
143+
visitor: ride.get('name') || 'Unknown',
144+
driver: ride.get('driver.name') || 'Unassigned',
145+
};
146+
});
147+
}
148+
75149
options = Object.freeze({
76150
chart: {
77151
type: 'heatmap',

app/styles/statistics.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,7 @@
5959
.request-time-chart svg rect {
6060
stroke-width: 0;
6161
}
62+
63+
.request-time-chart .request-time-exclusions {
64+
padding: 2rem;
65+
}

0 commit comments

Comments
 (0)