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

[Agency Dashboard] DataVizStore implementation #186

Merged
merged 12 commits into from
Nov 30, 2022
Merged
27 changes: 23 additions & 4 deletions agency-dashboard/src/DashboardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import { ReactComponent as LeftArrowIcon } from "@justice-counts/common/assets/l
import { ReactComponent as ShareIcon } from "@justice-counts/common/assets/share-icon.svg";
import { DatapointsView } from "@justice-counts/common/components/DataViz/DatapointsView";
import { MetricInsights } from "@justice-counts/common/components/DataViz/MetricInsights";
import { transformDataForMetricInsights } from "@justice-counts/common/components/DataViz/utils";
import { COMMON_DESKTOP_WIDTH } from "@justice-counts/common/components/GlobalStyles";
import { DataVizTimeRangesMap } from "@justice-counts/common/types";
import { observer } from "mobx-react-lite";
import React, { useEffect, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
Expand Down Expand Up @@ -91,7 +93,16 @@ const DashboardView = () => {
const navigate = useNavigate();
const params = useParams();
const agencyId = Number(params.id);
const { datapointsStore } = useStore();
const { datapointsStore, dataVizStore } = useStore();

const {
timeRange,
disaggregationName,
countOrPercentageView,
setTimeRange,
setDisaggregationName,
setCountOrPercentageView,
} = dataVizStore;

const { search } = useLocation();
const query = new URLSearchParams(search);
Expand Down Expand Up @@ -151,16 +162,18 @@ const DashboardView = () => {
const metricName =
datapointsStore.metricKeyToDisplayName[metricKey] || metricKey;

const datapoints =
datapointsStore.datapointsByMetric[metricKey]?.aggregate || [];
const filteredAggregateData = transformDataForMetricInsights(
datapointsStore.datapointsByMetric[metricKey]?.aggregate || [],
DataVizTimeRangesMap[dataVizStore.timeRange]
);

return (
<Container key={metricKey}>
<HeaderBar />
<LeftPanel>
<BackButton onClick={() => navigate(`/agency/${agencyId}`)} />
<MetricTitle>{metricName}</MetricTitle>
<MetricInsights datapoints={datapoints} />
<MetricInsights datapoints={filteredAggregateData} />
<MetricOverviewContent>
Measures the number of individuals with at least one parole violation
during the reporting period.
Expand All @@ -181,6 +194,12 @@ const DashboardView = () => {
dimensionNamesByDisaggregation={
datapointsStore.dimensionNamesByMetricAndDisaggregation[metricKey]
}
timeRange={timeRange}
disaggregationName={disaggregationName}
countOrPercentageView={countOrPercentageView}
setTimeRange={setTimeRange}
setDisaggregationName={setDisaggregationName}
setCountOrPercentageView={setCountOrPercentageView}
metricNames={metricNames}
onMetricsSelect={(metric) =>
navigate(`/agency/${agencyId}/dashboard?metric=${metric}`)
Expand Down
5 changes: 5 additions & 0 deletions agency-dashboard/src/stores/RootStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import DataVizStore from "@justice-counts/common/stores/DataVizStore";

import DatapointsStore from "./DatapointsStore";

class RootStore {
datapointsStore: DatapointsStore;

dataVizStore: DataVizStore;

constructor() {
this.datapointsStore = new DatapointsStore();
this.dataVizStore = new DataVizStore();
}
}

Expand Down
5 changes: 2 additions & 3 deletions common/components/DataViz/DatapointsTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ const reportFrequencyBadgeColors: BadgeColorMapping = {
export const DatapointsTitle: React.FC<{
metricName: string;
metricFrequency?: string;
insights?: string;
}> = ({ metricName, metricFrequency, insights }) => {
}> = ({ metricName, metricFrequency }) => {
const [titleWidth, setTitleWidth] = useState<number>(0);
const titleRef = useRef<HTMLDivElement | null>(null);

Expand All @@ -44,7 +43,7 @@ export const DatapointsTitle: React.FC<{

return (
<MetricTitleWrapper>
<MetricTitle ref={titleRef} titleWidth={titleWidth} title={insights}>
<MetricTitle ref={titleRef} titleWidth={titleWidth}>
{metricName}
{metricFrequency && (
<Badge
Expand Down
148 changes: 70 additions & 78 deletions common/components/DataViz/DatapointsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,7 @@

import { ReactComponent as GridIcon } from "@justice-counts/common/assets/grid-icon.svg";
import BarChart from "@justice-counts/common/components/DataViz/BarChart";
import Legend from "@justice-counts/common/components/DataViz/Legend";
import {
DatapointsGroupedByAggregateAndDisaggregations,
DatapointsViewSetting,
DataVizAggregateName,
DataVizTimeRangesMap,
DimensionNamesByDisaggregation,
ReportFrequency,
} from "@justice-counts/common/types";
import { DropdownMenu, DropdownToggle } from "@recidiviz/design-system";
import React, { useEffect } from "react";

import { DatapointsTitle } from "./DatapointsTitle";
import { DatapointsTitle } from "@justice-counts/common/components/DataViz/DatapointsTitle";
import {
BottomMetricInsightsContainer,
DatapointsViewContainer,
Expand All @@ -47,17 +35,26 @@ import {
MobileSelectMetricsModalContainer,
SelectMetricsButtonContainer,
SelectMetricsButtonText,
} from "./DatapointsView.styles";
import { MetricInsights } from "./MetricInsights";
} from "@justice-counts/common/components/DataViz/DatapointsView.styles";
import Legend from "@justice-counts/common/components/DataViz/Legend";
import { MetricInsights } from "@justice-counts/common/components/DataViz/MetricInsights";
import {
filterByTimeRange,
filterNullDatapoints,
getAverageTotalValue,
getLatestDateFormatted,
getPercentChangeOverTime,
sortDatapointDimensions,
transformData,
} from "./utils";
transformDataForBarChart,
transformDataForMetricInsights,
} from "@justice-counts/common/components/DataViz/utils";
import {
DatapointsGroupedByAggregateAndDisaggregations,
DataVizAggregateName,
DataVizCountOrPercentageView,
DataVizTimeRangeDisplayName,
DataVizTimeRangesMap,
DimensionNamesByDisaggregation,
NoDisaggregationOption,
ReportFrequency,
} from "@justice-counts/common/types";
import { DropdownMenu, DropdownToggle } from "@recidiviz/design-system";
import React, { useEffect } from "react";

const noDisaggregationOption = "None";

Expand Down Expand Up @@ -92,8 +89,14 @@ const SelectMetricButtonDropdown: React.FC<{
);

export const DatapointsView: React.FC<{
datapointsGroupedByAggregateAndDisaggregations: DatapointsGroupedByAggregateAndDisaggregations;
dimensionNamesByDisaggregation: DimensionNamesByDisaggregation;
datapointsGroupedByAggregateAndDisaggregations?: DatapointsGroupedByAggregateAndDisaggregations;
dimensionNamesByDisaggregation?: DimensionNamesByDisaggregation;
timeRange: DataVizTimeRangeDisplayName;
disaggregationName: string;
countOrPercentageView: DataVizCountOrPercentageView;
setTimeRange: (timeRange: DataVizTimeRangeDisplayName) => void;
setDisaggregationName: (disaggregation: string) => void;
setCountOrPercentageView: (viewSetting: DataVizCountOrPercentageView) => void;
metricName?: string;
metricFrequency?: ReportFrequency;
metricNames?: string[];
Expand All @@ -103,63 +106,64 @@ export const DatapointsView: React.FC<{
}> = ({
datapointsGroupedByAggregateAndDisaggregations,
dimensionNamesByDisaggregation,
timeRange,
disaggregationName,
countOrPercentageView,
setTimeRange,
setDisaggregationName,
setCountOrPercentageView,
metricName,
metricFrequency,
metricNames,
onMetricsSelect,
showBottomMetricInsights = false,
resizeHeight = false,
}) => {
const [selectedTimeRange, setSelectedTimeRange] =
React.useState<string>("All");
const [selectedDisaggregation, setSelectedDisaggregation] =
React.useState<string>(noDisaggregationOption);
const [datapointsViewSetting, setDatapointsViewSetting] =
React.useState<DatapointsViewSetting>("Count");
const [mobileSelectMetricsVisible, setMobileSelectMetricsVisible] =
React.useState<boolean>(false);

const data =
(selectedDisaggregation !== noDisaggregationOption &&
const selectedData =
(disaggregationName !== NoDisaggregationOption &&
Object.values(
datapointsGroupedByAggregateAndDisaggregations.disaggregations[
selectedDisaggregation
datapointsGroupedByAggregateAndDisaggregations?.disaggregations[
disaggregationName
] || {}
)) ||
datapointsGroupedByAggregateAndDisaggregations?.aggregate ||
[];
const isAnnual = data[0]?.frequency === "ANNUAL";
const disaggregations = Object.keys(dimensionNamesByDisaggregation);
const isAnnual = selectedData[0]?.frequency === "ANNUAL";
const disaggregations = Object.keys(dimensionNamesByDisaggregation || {});
const disaggregationOptions = [...disaggregations];
disaggregationOptions.unshift(noDisaggregationOption);
const dimensionNames =
selectedDisaggregation !== noDisaggregationOption
? (dimensionNamesByDisaggregation[selectedDisaggregation] || [])
disaggregationName !== noDisaggregationOption
? (dimensionNamesByDisaggregation?.[disaggregationName] || [])
.slice() // Must use slice() before sorting a MobX observableArray
.sort(sortDatapointDimensions)
: [DataVizAggregateName];

const selectedTimeRangeValue = DataVizTimeRangesMap[selectedTimeRange];
const selectedTimeRangeValue = DataVizTimeRangesMap[timeRange];

useEffect(() => {
if (isAnnual && selectedTimeRangeValue === 6) {
setSelectedTimeRange("All");
setTimeRange("All");
}
if (!disaggregationOptions.includes(selectedDisaggregation)) {
setSelectedDisaggregation(noDisaggregationOption);
setDatapointsViewSetting("Count");
if (!disaggregationOptions.includes(disaggregationName)) {
setDisaggregationName(noDisaggregationOption);
setCountOrPercentageView("Count");
}
if (selectedDisaggregation === noDisaggregationOption) {
setDatapointsViewSetting("Count");
if (disaggregationName === noDisaggregationOption) {
setCountOrPercentageView("Count");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [datapointsGroupedByAggregateAndDisaggregations]);

useEffect(() => {
if (selectedDisaggregation === noDisaggregationOption) {
setDatapointsViewSetting("Count");
if (disaggregationName === noDisaggregationOption) {
setCountOrPercentageView("Count");
}
}, [selectedDisaggregation]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disaggregationName]);

/** Prevent body from scrolling when modal is open */
useEffect(() => {
Expand All @@ -174,22 +178,22 @@ export const DatapointsView: React.FC<{
const renderChartForMetric = () => {
return (
<BarChart
data={transformData(
data,
data={transformDataForBarChart(
selectedData,
selectedTimeRangeValue,
datapointsViewSetting
countOrPercentageView
)}
dimensionNames={dimensionNames}
percentageView={
!!selectedDisaggregation && datapointsViewSetting === "Percentage"
!!disaggregationName && countOrPercentageView === "Percentage"
}
resizeHeight={resizeHeight}
/>
);
};

const renderLegend = () => {
if (selectedDisaggregation !== noDisaggregationOption) {
if (disaggregationName !== noDisaggregationOption) {
return <Legend names={dimensionNames} />;
}
return <Legend />;
Expand All @@ -200,7 +204,7 @@ export const DatapointsView: React.FC<{
<DatapointsViewControlsContainer>
<DatapointsViewControlsDropdown
title="Date Range"
selectedValue={selectedTimeRange}
selectedValue={timeRange}
options={
isAnnual
? Object.keys(DataVizTimeRangesMap).filter(
Expand All @@ -209,49 +213,38 @@ export const DatapointsView: React.FC<{
: Object.keys(DataVizTimeRangesMap)
}
onSelect={(key) => {
setSelectedTimeRange(key);
setTimeRange(key as DataVizTimeRangeDisplayName);
}}
/>
{disaggregationOptions.length > 1 && (
<DatapointsViewControlsDropdown
title="Disaggregation"
selectedValue={selectedDisaggregation}
selectedValue={disaggregationName}
options={disaggregationOptions}
onSelect={(key) => {
setSelectedDisaggregation(key);
setDisaggregationName(key);
}}
/>
)}
{selectedDisaggregation !== noDisaggregationOption && (
{disaggregationName !== noDisaggregationOption && (
<DatapointsViewControlsDropdown
title="View"
selectedValue={datapointsViewSetting}
selectedValue={countOrPercentageView}
options={["Count", "Percentage"]}
onSelect={(key) => {
setDatapointsViewSetting(key as DatapointsViewSetting);
setCountOrPercentageView(key as DataVizCountOrPercentageView);
}}
/>
)}
</DatapointsViewControlsContainer>
);
};

// insights data
const dataSelectedInTimeRange = filterNullDatapoints(
filterByTimeRange(
datapointsGroupedByAggregateAndDisaggregations?.aggregate || [],
selectedTimeRangeValue
)
);
const percentChange = getPercentChangeOverTime(dataSelectedInTimeRange);
const avgValue = getAverageTotalValue(dataSelectedInTimeRange, isAnnual);
const mostRecentValue = getLatestDateFormatted(
dataSelectedInTimeRange,
isAnnual
const filteredAggregateData = transformDataForMetricInsights(
terryttsai marked this conversation as resolved.
Show resolved Hide resolved
datapointsGroupedByAggregateAndDisaggregations?.aggregate || [],
selectedTimeRangeValue
);

const chartViewInsightsInfo = `Year-to-Year: ${percentChange},\nAvg. Total Value: ${avgValue},\nMost Recent: ${mostRecentValue}`;

return (
<DatapointsViewContainer>
<DatapointsViewHeaderWrapper>
Expand All @@ -260,10 +253,9 @@ export const DatapointsView: React.FC<{
<DatapointsTitle
metricName={metricName}
metricFrequency={metricFrequency}
insights={chartViewInsightsInfo}
/>
{data.length > 0 && (
<MetricInsights datapoints={dataSelectedInTimeRange} />
{selectedData.length > 0 && (
<MetricInsights datapoints={filteredAggregateData} />
)}
</MetricHeaderWrapper>
)}
Expand All @@ -284,7 +276,7 @@ export const DatapointsView: React.FC<{
{renderLegend()}
{showBottomMetricInsights && (
<BottomMetricInsightsContainer>
<MetricInsights datapoints={dataSelectedInTimeRange} />
<MetricInsights datapoints={filteredAggregateData} />
</BottomMetricInsightsContainer>
)}
<MobileSelectMetricsButtonContainer>
Expand Down
Loading