Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Date range comparisons UI in the style of Google Analytics #605

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
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
3 changes: 3 additions & 0 deletions frontend/src/UIConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,6 @@ export const WEEKENDS = [

// Marey chart: how long of a dwell at a stop results in a second data point for exit.
export const DWELL_THRESHOLD_SECS = 120;

// Representation of a metric with no value (e.g., missing score).
export const NO_VALUE = '--';
68 changes: 59 additions & 9 deletions frontend/src/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,24 @@ export function fetchTripMetrics(params) {
}
}

fragment byDayFields on TripIntervalMetrics {
dates
startTime
endTime
tripTimes {
median
percentiles(percentiles:[10,90]) { percentile value }
}
waitTimes {
median
percentiles(percentiles:[90]) { percentile value }
}
departureScheduleAdherence {
onTimeCount
scheduledCount
}
}

fragment timeRangeFields on TripIntervalMetrics {
startTime endTime
waitTimes {
Expand All @@ -179,6 +197,12 @@ export function fetchTripMetrics(params) {
interval2: interval(dates:$dates2, startTime:$startTime2, endTime:$endTime2) {
...intervalFields
}
byDay(dates:$dates, startTime:$startTime, endTime:$endTime) {
...byDayFields
}
byDay2: byDay(dates:$dates2, startTime:$startTime2, endTime:$endTime2) {
...byDayFields
}
timeRanges(dates:$dates) {
...timeRangeFields
}
Expand Down Expand Up @@ -372,12 +396,12 @@ export function fetchRouteMetrics(params) {

export function fetchAgencyMetrics(params) {
const dates = computeDates(params.firstDateRange);
const dates2 = params.secondDateRange && computeDates(params.secondDateRange);

return function(dispatch, getState) {
const query = `query($agencyId:String!, $dates:[String!], $startTime:String, $endTime:String) {
agency(agencyId:$agencyId) {
agencyId
interval(dates:$dates, startTime:$startTime, endTime:$endTime) {
const query = `

fragment intervalFields on AgencyIntervalMetrics {
routes {
routeId
directions {
Expand All @@ -389,15 +413,41 @@ export function fetchAgencyMetrics(params) {
}
}
}

query($agencyId:String!, $dates:[String!], $startTime:String, $endTime:String,${
params.secondDateRange
? `$dates2:[String!], $startTime2:String, $endTime2:String,`
: ``
} ) {
agency(agencyId:$agencyId) {
agencyId
interval(dates:$dates, startTime:$startTime, endTime:$endTime) {
...intervalFields
}${
params.secondDateRange
? `interval2:interval(dates:$dates2, startTime:$startTime2, endTime:$endTime2) {
...intervalFields
}`
: ``
}
}
}`.replace(/\s+/g, ' ');

const variablesJson = JSON.stringify({
const variables = {
agencyId: Agencies[0].id,
dates,
startTime: params.startTime,
endTime: params.endTime,
});
startTime: params.firstDateRange.startTime,
endTime: params.firstDateRange.endTime,
};
if (params.secondDateRange) {
Object.assign(variables, {
dates2,
startTime2: params.secondDateRange.startTime,
endTime2: params.secondDateRange.endTime,
});
}

const variablesJson = JSON.stringify(variables);

if (getState().agencyMetrics.variablesJson !== variablesJson) {
dispatch({
Expand Down Expand Up @@ -493,7 +543,7 @@ export function handleGraphParams(params) {
const graphParams = getState().graphParams;

if (
oldParams.date !== graphParams.date ||
oldParams.firstDateRange.date !== graphParams.firstDateRange.date ||
oldParams.routeId !== graphParams.routeId ||
oldParams.agencyId !== graphParams.agencyId
) {
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/components/DateTimeLabel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, { Fragment } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import CircleIcon from '@material-ui/icons/FiberManualRecord';
import { generateDateLabels } from '../helpers/dateTime';

const useStyles = makeStyles(theme => ({
heading: {
fontSize: theme.typography.pxToRem(15),
},
secondaryHeading: {
fontSize: theme.typography.pxToRem(12),
color: theme.palette.text.secondary,
textAlign: 'left',
},
}));

/**
* Displays the current date and time selections as styled text.
*
* @param {any} props
*/
export default function DateTimeLabel(props) {
const { dateRangeParams, colorCode, style } = props;

const classes = useStyles();

const { dateLabel, smallLabel } = generateDateLabels(dateRangeParams);

return (
<Fragment>
<span style={style}>
<Typography className={classes.heading} display="inline">
{dateLabel}&nbsp;
</Typography>
<Typography className={classes.secondaryHeading} display="inline">
{smallLabel}
</Typography>
</span>
{colorCode ? (
<CircleIcon
style={{ fill: colorCode, verticalAlign: 'sub' }}
fontSize="small"
/>
) : null}
</Fragment>
);
}
41 changes: 2 additions & 39 deletions frontend/src/components/DateTimePanel.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, { useState, Fragment } from 'react';
import Moment from 'moment';
import { makeStyles } from '@material-ui/core/styles';
import Box from '@material-ui/core/Box';
import CircularProgress from '@material-ui/core/CircularProgress';
Expand All @@ -12,12 +11,11 @@ import ExpandLessIcon from '@material-ui/icons/ExpandLess';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline';
import InfoIcon from '@material-ui/icons/InfoOutlined';
import DateTimeLabel from './DateTimeLabel';
import DateTimePopover from './DateTimePopover';
import { TIME_RANGES, TIME_RANGE_ALL_DAY } from '../UIConstants';
import { typeForPage } from '../reducers/page';
import { fullQueryFromParams } from '../routesMap';
import { isLoadingRequest } from '../reducers/loadingReducer';
import { getDaysOfTheWeekLabel } from '../helpers/dateTime';

const useStyles = makeStyles(theme => ({
button: {
Expand Down Expand Up @@ -96,13 +94,6 @@ function DateTimePanel(props) {
});
}

/**
* convert yyyy/mm/dd to mm/dd/yyyy
*/
function convertDate(ymdString) {
return Moment(ymdString).format('MM/DD/YYYY');
}

const firstOpen = Boolean(anchorEl) && anchorEl.id === 'firstDateRange';
const secondOpen = Boolean(anchorEl) && anchorEl.id === 'secondDateRange';

Expand Down Expand Up @@ -176,27 +167,6 @@ function DateTimePanel(props) {

const dateRangeParams = graphParams[target];

// these are the read-only representations of the date and time range
let dateLabel = convertDate(dateRangeParams.date);
let smallLabel = '';

if (dateRangeParams.startDate !== dateRangeParams.date) {
dateLabel = `${convertDate(dateRangeParams.startDate)} - ${dateLabel}`;

// generate a days of the week label

smallLabel = `${getDaysOfTheWeekLabel(dateRangeParams.daysOfTheWeek)}, `;
}

// convert the state's current time range to a string or the sentinel value
const timeRange =
dateRangeParams.startTime && dateRangeParams.endTime
? `${dateRangeParams.startTime}-${dateRangeParams.endTime}`
: TIME_RANGE_ALL_DAY;

smallLabel += TIME_RANGES.find(range => range.value === timeRange)
.shortLabel;

return (
<Fragment>
<Button
Expand All @@ -206,14 +176,7 @@ function DateTimePanel(props) {
id={target}
>
<div className={classes.dateTime}>
<span>
<Typography className={classes.heading} display="inline">
{dateLabel}&nbsp;
</Typography>
<Typography className={classes.secondaryHeading} display="inline">
{smallLabel}
</Typography>
</span>
<DateTimeLabel dateRangeParams={dateRangeParams} />
{(target === 'firstDateRange' && firstOpen) ||
(target === 'secondDateRange' && secondOpen) ? (
<ExpandLessIcon />
Expand Down
11 changes: 7 additions & 4 deletions frontend/src/components/DateTimePopover.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import Moment from 'moment';
import { makeStyles } from '@material-ui/core/styles';
import Checkbox from '@material-ui/core/Checkbox';
Expand Down Expand Up @@ -71,17 +71,20 @@ function DateTimePopover(props) {
graphParams[targetRange] || initialGraphParams.firstDateRange,
);

function resetLocalDateRangeParams() {
// React wants this method to be memoized via useCallback, otherwise
// an exhaustive-deps warning is issued for the useEffect call below.

const resetLocalDateRangeParams = useCallback(() => {
setLocalDateRangeParams(
graphParams[targetRange] || initialGraphParams.firstDateRange,
);
}
}, [graphParams, targetRange]);

// Whenever targetRange changes, we need to resync our local state with Redux

useEffect(() => {
resetLocalDateRangeParams();
}, [targetRange, graphParams]);
}, [targetRange, graphParams, resetLocalDateRangeParams]);

const classes = useStyles();
const maxDate = Moment(Date.now()).format('YYYY-MM-DD');
Expand Down
32 changes: 14 additions & 18 deletions frontend/src/components/Info.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {
Crosshair,
} from 'react-vis';
import { AppBar, Box, Tab, Tabs, Typography } from '@material-ui/core';
import InfoByDay from './InfoByDay';
import InfoIntervalsOfDay from './InfoIntervalsOfDay';
import InfoTripSummary from './InfoTripSummary';
import { CHART_COLORS, REACT_VIS_CROSSHAIR_NO_LINE } from '../UIConstants';
Expand All @@ -30,7 +29,7 @@ function Info(props) {
const headways = tripMetrics ? tripMetrics.interval.headways : null;
const waitTimes = tripMetrics ? tripMetrics.interval.waitTimes : null;
const tripTimes = tripMetrics ? tripMetrics.interval.tripTimes : null;
const byDayData = tripMetrics ? tripMetrics.byDay : null;
// const byDayData = tripMetrics ? tripMetrics.byDay : null;

const headways2 =
tripMetrics && tripMetrics.interval2
Expand All @@ -44,14 +43,6 @@ function Info(props) {
tripMetrics && tripMetrics.interval2
? tripMetrics.interval2.tripTimes
: null;
/*
* By day data is not requested for the second date range.
*
* The second range can have the same data the first, as they are using the same GraphQL query API.
* It gets tricky for the "by day" tab, because how do you chart two date ranges by day when they
* could have different numbers of days in them? Does this chart only work when the ranges have
* the same number of days? Do we "scale" the time axis so both are the full width of the chart?
*/

const headwayData =
headways && headways.histogram
Expand Down Expand Up @@ -135,6 +126,10 @@ function Info(props) {

function handleTabChange(event, newValue) {
setTabValue(newValue);
// This is a workaround to trigger the react-vis resize listeners,
// because the hidden flexible width charts are all width zero, and
// stay width zero when unhidden.
window.dispatchEvent(new Event('resize'));
}

function a11yProps(index) {
Expand All @@ -145,11 +140,11 @@ function Info(props) {
}

const SUMMARY = 0;
const BY_DAY = 1;
const TIME_OF_DAY = 2;
const HEADWAYS = 3;
const WAITS = 4;
const TRIPS = 5;
// const BY_DAY = 1;
const TIME_OF_DAY = 1;
const HEADWAYS = 2;
const WAITS = 3;
const TRIPS = 4;

return (
<div>
Expand All @@ -167,7 +162,7 @@ function Info(props) {
label="Summary"
{...a11yProps(SUMMARY)}
/>
<Tab style={{ minWidth: 72 }} label="By Day" {...a11yProps(BY_DAY)} />
{/* <Tab style={{ minWidth: 72 }} label="By Day" {...a11yProps(BY_DAY)} /> */}
<Tab
style={{ minWidth: 72 }}
label="By Time of Day"
Expand Down Expand Up @@ -201,19 +196,20 @@ function Info(props) {
/>
</Box>

{/*
<Box p={2} hidden={tabValue !== BY_DAY}>
<Typography variant="h5" display="inline">
Performance by Day
</Typography>

<InfoByDay
byDayData={
byDayData /* consider switching to trip metrics here for consistency */
byDayData
}
graphParams={graphParams}
routes={routes}
/>
</Box>
</Box> */}

<Box p={2} hidden={tabValue !== TIME_OF_DAY}>
<Typography variant="h5" display="inline">
Expand Down
Loading