22import { classNames } from ' @ember-decorators/component' ;
33import { computed } from ' @ember/object' ;
44import 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' ;
68import 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' )
917export 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' ,
0 commit comments