-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add by-hour trips histogram to Service details page (#931)
* done??? * remove unused no-unused-modules overrides * lint
- Loading branch information
Showing
33 changed files
with
435 additions
and
87 deletions.
There are no files selected for viewing
29 changes: 29 additions & 0 deletions
29
common/components/charts/ByHourHistogram/ByHourHistogram.stories.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import React from 'react'; | ||
|
||
import { COLORS } from '../../../constants/colors'; | ||
import { ByHourHistogram } from './ByHourHistogram'; | ||
|
||
export default { | ||
title: 'ByHourHistogram', | ||
component: ByHourHistogram, | ||
}; | ||
|
||
const simpleData = [ | ||
{ | ||
label: '2023', | ||
data: [0, 0, 0, 0, 5, 6, 7, 8, 7, 6, 5, 5, 4, 4, 5, 6, 7, 8, 9, 10, 10, 9, 8, 7], | ||
style: { opacity: 0.5 }, | ||
}, | ||
{ | ||
label: '2024', | ||
data: [0, 0, 0, 0, 6, 7, 8, 9, 9, 7, 6, 5, 4, 5, 6, 6, 8, 9, 9, 11, 12, 10, 8, 8], | ||
}, | ||
]; | ||
|
||
export const Default = () => ( | ||
<ByHourHistogram | ||
data={simpleData} | ||
valueAxis={{ title: 'Items', tooltipItemLabel: 'trips per direction' }} | ||
style={{ color: COLORS.mbta.red }} | ||
/> | ||
); |
121 changes: 121 additions & 0 deletions
121
common/components/charts/ByHourHistogram/ByHourHistogram.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
import React, { useMemo } from 'react'; | ||
import { Bar as BarChart } from 'react-chartjs-2'; | ||
import { | ||
Chart as ChartJS, | ||
CategoryScale, | ||
LinearScale, | ||
BarElement, | ||
Tooltip, | ||
Legend, | ||
} from 'chart.js'; | ||
import Color from 'color'; | ||
|
||
import { useBreakpoint } from '../../../hooks/useBreakpoint'; | ||
import type { ByHourDataset, DisplayStyle, ValueAxis as ValueAxis } from './types'; | ||
import { resolveStyle } from './styles'; | ||
|
||
ChartJS.register(CategoryScale, LinearScale, BarElement, Tooltip, Legend); | ||
|
||
interface Props { | ||
style?: Partial<DisplayStyle>; | ||
data: ByHourDataset[]; | ||
valueAxis: ValueAxis; | ||
} | ||
|
||
const allTimeLabels = ['AM', 'PM'] | ||
.map((label) => Array.from({ length: 12 }, (_, i) => `${i === 0 ? 12 : i} ${label}`)) | ||
.flat(); | ||
|
||
const rotate = <T extends string | number>(arr: T[]): T[] => [...arr.slice(1), arr[0]]; | ||
|
||
const stripZeroHoursAndRotateMidnightToEnd = ( | ||
datasets: ByHourDataset[] | ||
): { data: ByHourDataset[]; timeLabels: string[] } => { | ||
let stripIndex = 0; | ||
const rotatedDatasets = datasets.map((dataset) => ({ ...dataset, data: rotate(dataset.data) })); | ||
for (let i = 0; i < rotatedDatasets[0].data.length; i++) { | ||
if (rotatedDatasets.every((dataset) => dataset.data[i] === 0)) { | ||
stripIndex++; | ||
} else { | ||
break; | ||
} | ||
} | ||
const timeLabels = rotate(allTimeLabels).slice(stripIndex); | ||
const strippedDatasets = rotatedDatasets.map((dataset) => { | ||
return { | ||
...dataset, | ||
data: dataset.data.slice(stripIndex), | ||
}; | ||
}); | ||
return { data: strippedDatasets, timeLabels }; | ||
}; | ||
|
||
export const ByHourHistogram: React.FC<Props> = (props) => { | ||
const { data: dataWithZeros, valueAxis, style: baseStyle = null } = props; | ||
const { data, timeLabels } = useMemo( | ||
() => stripZeroHoursAndRotateMidnightToEnd(dataWithZeros), | ||
[dataWithZeros] | ||
); | ||
const isMobile = !useBreakpoint('md'); | ||
|
||
const chartData = useMemo(() => { | ||
return { | ||
labels: timeLabels, | ||
datasets: data.map((dataset) => { | ||
const style = resolveStyle([baseStyle, dataset.style ?? null]); | ||
return { | ||
tooltip: {}, | ||
label: dataset.label, | ||
data: dataset.data, | ||
borderColor: style.color, | ||
borderWidth: style.borderWidth, | ||
backgroundColor: Color(style.color) | ||
.alpha(style.opacity ?? 0) | ||
.toString(), | ||
}; | ||
}), | ||
}; | ||
}, [data, baseStyle, timeLabels]); | ||
|
||
const chartOptions = useMemo(() => { | ||
return { | ||
scales: { | ||
x: { | ||
grid: { | ||
display: false, | ||
}, | ||
}, | ||
y: { | ||
title: { | ||
display: true, | ||
text: valueAxis.title, | ||
}, | ||
beginAtZero: true, | ||
grid: { | ||
display: false, | ||
}, | ||
}, | ||
}, | ||
plugins: { | ||
legend: { | ||
display: true, | ||
position: 'bottom' as const, | ||
}, | ||
tooltip: { | ||
mode: 'index' as const, | ||
callbacks: { | ||
label: (context) => { | ||
const { datasetIndex, dataIndex } = context; | ||
const dataset = data[datasetIndex]; | ||
const value = dataset.data[dataIndex]; | ||
const { label } = dataset; | ||
return `${label}: ${value} ${valueAxis.tooltipItemLabel ?? ''}`.trim(); | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
}, [valueAxis.title, valueAxis.tooltipItemLabel, data]); | ||
|
||
return <BarChart data={chartData} options={chartOptions} height={isMobile ? 50 : 70} />; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { ByHourHistogram } from './ByHourHistogram'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import type { DisplayStyle } from './types'; | ||
|
||
export const defaultStyle: DisplayStyle = { | ||
color: 'black', | ||
borderWidth: 0, | ||
opacity: 1, | ||
}; | ||
|
||
export const resolveStyle = (styles: (null | Partial<DisplayStyle>)[]): DisplayStyle => { | ||
return styles.reduce((a, b) => ({ ...a, ...b }), defaultStyle) as DisplayStyle; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export type DisplayStyle = { | ||
color: string; | ||
opacity?: number; | ||
borderWidth: number; | ||
}; | ||
|
||
type ByHourData = number[]; | ||
|
||
export type ByHourDataset = { | ||
label: string; | ||
data: ByHourData; | ||
style?: Partial<DisplayStyle>; | ||
}; | ||
|
||
export type ValueAxis = { | ||
title: string; | ||
tooltipItemLabel?: string; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,4 +17,4 @@ | |
|
||
.inner { | ||
overflow-x: scroll; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,69 @@ | ||
import classNames from 'classnames'; | ||
import React from 'react'; | ||
import { BusDataNotice } from '../notices/BusDataNotice'; | ||
import React, { useState } from 'react'; | ||
import type { UseQueryResult } from '@tanstack/react-query'; | ||
import { ErrorBoundary } from 'react-error-boundary'; | ||
|
||
interface WidgetPageProps { | ||
children?: React.ReactNode; | ||
import { ChartPlaceHolder } from '../graphics/ChartPlaceHolder'; | ||
import { WidgetDiv } from './WidgetDiv'; | ||
import { WidgetTitle } from './WidgetTitle'; | ||
|
||
type ReadyDependency = unknown | UseQueryResult; | ||
type ReadyState = 'ready' | 'waiting' | 'error'; | ||
|
||
interface Props { | ||
ready?: ReadyDependency | ReadyDependency[]; | ||
title: React.ReactNode; | ||
subtitle?: React.ReactNode; | ||
children: React.ReactNode; | ||
} | ||
|
||
export const WidgetPage: React.FC<WidgetPageProps> = ({ children }) => { | ||
const isUseQueryResult = (obj: unknown): obj is UseQueryResult => { | ||
return !!obj && typeof obj === 'object' && 'isLoading' in obj && 'isError' in obj; | ||
}; | ||
|
||
const getReadyState = (ready: ReadyDependency | ReadyDependency[]) => { | ||
const readyArray = Array.isArray(ready) ? ready : [ready]; | ||
const eachState: ReadyState[] = readyArray.map((entry) => | ||
isUseQueryResult(entry) | ||
? entry.isError | ||
? 'error' | ||
: entry.data | ||
? 'ready' | ||
: 'waiting' | ||
: entry | ||
? 'ready' | ||
: 'waiting' | ||
); | ||
if (eachState.some((state) => state === 'error')) { | ||
return 'error'; | ||
} | ||
if (eachState.some((state) => state === 'waiting')) { | ||
return 'waiting'; | ||
} | ||
return 'ready'; | ||
}; | ||
|
||
export const Widget: React.FC<Props> = (props) => { | ||
const { title, subtitle, children, ready: readyDependencies } = props; | ||
const [hasError, setHasError] = useState<boolean>(); | ||
|
||
const readyState = hasError | ||
? 'error' | ||
: readyDependencies | ||
? getReadyState(readyDependencies) | ||
: 'ready'; | ||
|
||
return ( | ||
<div className={classNames('flex w-full flex-1 flex-col items-center gap-y-2 lg:p-0')}> | ||
{children} | ||
<BusDataNotice /> | ||
</div> | ||
<WidgetDiv> | ||
<WidgetTitle title={title} subtitle={subtitle} /> | ||
{readyState === 'ready' ? ( | ||
<ErrorBoundary onError={() => setHasError(true)} fallbackRender={() => null}> | ||
{children} | ||
</ErrorBoundary> | ||
) : ( | ||
<div className="relative flex h-full"> | ||
<ChartPlaceHolder readyState={readyState} /> | ||
</div> | ||
)} | ||
</WidgetDiv> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import classNames from 'classnames'; | ||
import React from 'react'; | ||
import { BusDataNotice } from '../notices/BusDataNotice'; | ||
|
||
interface WidgetPageProps { | ||
children?: React.ReactNode; | ||
} | ||
|
||
export const WidgetPage: React.FC<WidgetPageProps> = ({ children }) => { | ||
return ( | ||
<div className={classNames('flex w-full flex-1 flex-col items-center gap-y-2 lg:p-0')}> | ||
{children} | ||
<BusDataNotice /> | ||
</div> | ||
); | ||
}; |
16 changes: 8 additions & 8 deletions
16
modules/dashboard/WidgetTitle.tsx → common/components/widgets/WidgetTitle.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { Widget } from './Widget'; | ||
export { WidgetDiv } from './WidgetDiv'; | ||
export { WidgetTitle } from './WidgetTitle'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.