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] Create agency data store to fetch published data #189

Merged
merged 9 commits into from
Dec 2, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions agency-dashboard/src/AgencyOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ const AgencyOverview = () => {
const navigate = useNavigate();
const params = useParams();
const agencyId = Number(params.id);
const { datapointsStore } = useStore();
const { agencyDataStore, datapointsStore } = useStore();

const fetchDatapoints = async () => {
try {
await datapointsStore.getDatapoints(agencyId);
await agencyDataStore.fetchAgencyData(agencyId);
} catch (error) {
showToast("Error fetching data.", false, "red", 4000);
}
Expand Down Expand Up @@ -61,7 +62,7 @@ const AgencyOverview = () => {
navigate(`/agency/${agencyId}/dashboard?metric=${metricKey}`);
}}
>
{datapointsStore.metricKeyToDisplayName[metricKey] || metricKey}
{agencyDataStore.metricKeyToDisplayName[metricKey] || metricKey}
</MetricCategory>
)
)}
Expand Down
19 changes: 12 additions & 7 deletions agency-dashboard/src/DashboardView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const DashboardView = () => {
const navigate = useNavigate();
const params = useParams();
const agencyId = Number(params.id);
const { datapointsStore, dataVizStore } = useStore();
const { agencyDataStore, datapointsStore, dataVizStore } = useStore();

const {
timeRange,
Expand All @@ -108,6 +108,7 @@ const DashboardView = () => {
const query = new URLSearchParams(search);
const metricKey = query.get("metric");
useEffect(() => {
agencyDataStore.fetchAgencyData(agencyId);
datapointsStore.getDatapoints(agencyId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand Down Expand Up @@ -155,12 +156,12 @@ const DashboardView = () => {
return <>Loading...</>;
}

const metricNames = Object.keys(
datapointsStore.dimensionNamesByMetricAndDisaggregation
const metricNames = agencyDataStore.metrics.map(
(metric) => metric.display_name
);

const metricName =
datapointsStore.metricKeyToDisplayName[metricKey] || metricKey;
agencyDataStore.metricKeyToDisplayName[metricKey] || metricKey;

const filteredAggregateData = transformDataForMetricInsights(
datapointsStore.datapointsByMetric[metricKey]?.aggregate || [],
Expand Down Expand Up @@ -201,9 +202,13 @@ const DashboardView = () => {
setDisaggregationName={setDisaggregationName}
setCountOrPercentageView={setCountOrPercentageView}
metricNames={metricNames}
onMetricsSelect={(metric) =>
navigate(`/agency/${agencyId}/dashboard?metric=${metric}`)
}
onMetricsSelect={(selectedMetricName) => {
const mKey =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe selectedMetricKey?

agencyDataStore.metricDisplayNameToKey[selectedMetricName];
if (mKey) {
navigate(`/agency/${agencyId}/dashboard?metric=${mKey}`);
}
}}
showBottomMetricInsights={!isDesktopWidth}
resizeHeight={isDesktopWidth}
/>
Expand Down
85 changes: 85 additions & 0 deletions agency-dashboard/src/stores/AgencyDataStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2022 Recidiviz, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import { Metric } from "@justice-counts/common/types";
import { makeAutoObservable, runInAction } from "mobx";

import { request } from "../utils/networking";

class AgencyDataStore {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm calling it AgencyDataStore because in the future this will include not just an agency's published metric datapoints and metric information but also agency metadata such as name, description etc

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I love that! Good thinking

metrics: Metric[];

loading: boolean;

constructor() {
makeAutoObservable(this);
this.metrics = [];
this.loading = true;
}

get metricKeyToDisplayName(): { [metricKey: string]: string | null } {
const mapping: { [metricKey: string]: string | null } = {};
this.metrics.forEach((metric) => {
mapping[metric.key] = metric.display_name;
});
return mapping;
}

get metricDisplayNameToKey(): { [displayName: string]: string } {
const mapping: { [displayName: string]: string } = {};
this.metrics.forEach((metric) => {
mapping[metric.display_name] = metric.key;
});
return mapping;
}

async fetchAgencyData(agencyId: number): Promise<void | Error> {
try {
const response = (await request({
path: `/api/agencies/${agencyId}/published_data`,
method: "GET",
})) as Response;
if (response.status === 200) {
const result = await response.json();
runInAction(() => {
this.metrics = result;
});
} else {
const error = await response.json();
throw new Error(error.description);
}
runInAction(() => {
this.loading = false;
});
} catch (error) {
runInAction(() => {
this.loading = false;
});
throw error;
}
}

resetState() {
// reset the state
runInAction(() => {
this.metrics = [];
this.loading = true;
});
}
}

export default AgencyDataStore;
85 changes: 0 additions & 85 deletions agency-dashboard/src/stores/DatapointsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,101 +16,16 @@
// =============================================================================

import BaseDatapointsStore from "@justice-counts/common/stores/BaseDatapointsStore";
import {
DatapointsByMetric,
DataVizAggregateName,
DimensionNamesByMetricAndDisaggregation,
RawDatapoint,
} from "@justice-counts/common/types";
import { isPositiveNumber } from "@justice-counts/common/utils";
import { makeObservable, override, runInAction } from "mobx";

import { request } from "../utils/networking";

class DatapointsStore extends BaseDatapointsStore {
rawDatapoints: RawDatapoint[];

dimensionNamesByMetricAndDisaggregation: DimensionNamesByMetricAndDisaggregation;

loading: boolean;

constructor() {
super();
makeObservable(this, {
getDatapoints: override,
});
this.rawDatapoints = [];
this.dimensionNamesByMetricAndDisaggregation = {};
this.loading = true;
}

get metricKeyToDisplayName(): { [metricKey: string]: string | null } {
const mapping: { [metricKey: string]: string | null } = {};
this.rawDatapoints.forEach((dp) => {
mapping[dp.metric_definition_key] = dp.metric_display_name;
});
return mapping;
}

/**
* Transforms raw data from the server into Datapoints keyed by metric,
* grouped by aggregate values and disaggregations.
* Aggregate is an array of objects each containing start_date, end_date, and the aggregate value.
* Disaggregations are keyed by disaggregation name and each value is an object
* with the key being the start_date and the value being an object
* containing start_date, end_date and key value pairs for each dimension and their values.
* See the DatapointsByMetric type for details.
*/
get datapointsByMetric(): DatapointsByMetric {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already inherited from BaseDatapointsStore, wonder how this got here

return this.rawDatapoints.reduce((res: DatapointsByMetric, dp) => {
if (!res[dp.metric_definition_key]) {
res[dp.metric_definition_key] = {
aggregate: [],
disaggregations: {},
};
}

const sanitizedValue =
dp.value !== null && isPositiveNumber(dp.value)
? Number(dp.value)
: null;

if (
dp.disaggregation_display_name === null ||
dp.dimension_display_name === null
) {
res[dp.metric_definition_key].aggregate.push({
[DataVizAggregateName]: sanitizedValue,
start_date: dp.start_date,
end_date: dp.end_date,
frequency: dp.frequency,
dataVizMissingData: 0,
});
} else {
if (
!res[dp.metric_definition_key].disaggregations[
dp.disaggregation_display_name
]
) {
res[dp.metric_definition_key].disaggregations[
dp.disaggregation_display_name
] = {};
}
res[dp.metric_definition_key].disaggregations[
dp.disaggregation_display_name
][dp.start_date] = {
...res[dp.metric_definition_key].disaggregations[
dp.disaggregation_display_name
][dp.start_date],
start_date: dp.start_date,
end_date: dp.end_date,
[dp.dimension_display_name]: sanitizedValue,
frequency: dp.frequency,
dataVizMissingData: 0,
};
}
return res;
}, {});
}

async getDatapoints(agencyId: number): Promise<void | Error> {
Expand Down
4 changes: 4 additions & 0 deletions agency-dashboard/src/stores/RootStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,18 @@

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

import AgencyDataStore from "./AgencyDataStore";
import DatapointsStore from "./DatapointsStore";

class RootStore {
agencyDataStore: AgencyDataStore;

datapointsStore: DatapointsStore;

dataVizStore: DataVizStore;

constructor() {
this.agencyDataStore = new AgencyDataStore();
this.datapointsStore = new DatapointsStore();
this.dataVizStore = new DataVizStore();
}
Expand Down
9 changes: 0 additions & 9 deletions common/stores/BaseDatapointsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ abstract class DatapointsStore {
rawDatapoints: observable,
dimensionNamesByMetricAndDisaggregation: observable,
loading: observable,
metricKeyToDisplayName: computed,
datapointsByMetric: computed,
getDatapoints: action,
resetState: action,
Expand All @@ -53,14 +52,6 @@ abstract class DatapointsStore {
this.loading = true;
}

get metricKeyToDisplayName(): { [metricKey: string]: string | null } {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use agency data store's instead

const mapping: { [metricKey: string]: string | null } = {};
this.rawDatapoints.forEach((dp) => {
mapping[dp.metric_definition_key] = dp.metric_display_name;
});
return mapping;
}

/**
* Transforms raw data from the server into Datapoints keyed by metric,
* grouped by aggregate values and disaggregations.
Expand Down
6 changes: 5 additions & 1 deletion common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export type MetricConfigurationSettings = {
export interface Metric {
key: string;
system: AgencySystems;
custom_frequency?: ReportFrequency;
datapoints?: RawDatapoint[];
display_name: string;
description: string;
reporting_note: string;
Expand All @@ -105,8 +107,8 @@ export interface Metric {
disaggregations: MetricDisaggregations[];
enabled?: boolean;
settings?: MetricConfigurationSettings[];
starting_month?: number;
frequency?: ReportFrequency;
custom_frequency?: ReportFrequency;
}

export interface MetricDefinition {
Expand All @@ -131,6 +133,7 @@ export interface MetricDisaggregations {
required: boolean;
helper_text: string | null | undefined;
enabled?: boolean;
should_sum_to_total: boolean;
}

export interface MetricDisaggregationDimensions {
Expand All @@ -143,6 +146,7 @@ export interface MetricDisaggregationDimensions {
display_name?: string;
race?: string;
ethnicity?: string;
datapoints?: RawDatapoint[];
}

export interface CreateReportFormValuesType extends Record<string, unknown> {
Expand Down
1 change: 1 addition & 0 deletions publisher/src/components/Reports/DataEntryForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ describe("test data entry form", () => {
],
required: false,
helper_text: "Break down the metric by NIBRS offense types.",
should_sum_to_total: false,
},
],
},
Expand Down
13 changes: 0 additions & 13 deletions publisher/src/stores/DatapointsStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@
// =============================================================================

import BaseDatapointsStore from "@justice-counts/common/stores/BaseDatapointsStore";
import {
DimensionNamesByMetricAndDisaggregation,
RawDatapoint,
} from "@justice-counts/common/types";
import {
IReactionDisposer,
makeObservable,
Expand All @@ -37,12 +33,6 @@ class DatapointsStore extends BaseDatapointsStore {

api: API;

rawDatapoints: RawDatapoint[];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why is it safe to get rid of these fields now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're already inherited from the BaseDatapointsStore! I'm not sure how those got in there if either I missed them or they got added back in


dimensionNamesByMetricAndDisaggregation: DimensionNamesByMetricAndDisaggregation;

loading: boolean;

disposers: IReactionDisposer[] = [];

constructor(userStore: UserStore, api: API) {
Expand All @@ -57,9 +47,6 @@ class DatapointsStore extends BaseDatapointsStore {

this.api = api;
this.userStore = userStore;
this.rawDatapoints = [];
this.dimensionNamesByMetricAndDisaggregation = {};
this.loading = true;

this.disposers.push(
reaction(
Expand Down
1 change: 1 addition & 0 deletions publisher/src/stores/FormStore.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ beforeEach(() => {
],
required: false,
helper_text: "Break down the metric by NIBRS offense types.",
should_sum_to_total: false,
},
],
},
Expand Down
1 change: 0 additions & 1 deletion publisher/src/stores/MetricConfigStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,6 @@ class MetricConfigStore {
}

const metrics: Metric[] = await response.json();

return metrics;
};

Expand Down