Skip to content

Commit

Permalink
Locality filter component (#325)
Browse files Browse the repository at this point in the history
  • Loading branch information
macfarlandian committed Jan 28, 2021
1 parent f266832 commit f4dde8d
Show file tree
Hide file tree
Showing 15 changed files with 446 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const testMetadataMapping = {

function getTestMetric() {
return createMetricMapping({
localityLabelMapping: undefined,
metadataMapping: testMetadataMapping,
tenantId: testTenantId,
}).get(testMetricId) as HistoricalPopulationBreakdownMetric;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2021 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 { render, screen, fireEvent } from "@testing-library/react";
import React from "react";
import createMetricMapping from "../contentModels/createMetricMapping";
import type PopulationBreakdownByLocationMetric from "../contentModels/PopulationBreakdownByLocationMetric";
import contentFixture from "../contentModels/__fixtures__/tenant_content_exhaustive";
import { reactImmediately } from "../testUtils";
import LocalityFilterSelect from "./LocalityFilterSelect";

const testTenantId = "US_ND";
const testMetricId = "ProbationPopulationCurrent";
const testMetadataMapping = {
[testMetricId]: contentFixture.metrics[testMetricId],
};

// judicial districts present in backend fixture
const expectedLocalities = [
{ id: "ALL", label: "All Districts" },
{ id: "EAST_CENTRAL", label: "East Central" },
{ id: "NORTH_CENTRAL", label: "North Central" },
{ id: "NORTHEAST", label: "Northeast" },
{ id: "NORTHEAST_CENTRAL", label: "Northeast Central" },
{ id: "NORTHWEST", label: "Northwest" },
{ id: "SOUTH_CENTRAL", label: "South Central" },
{ id: "SOUTHEAST", label: "Southeast" },
{ id: "SOUTHWEST", label: "Southwest" },
{ id: "OTHER", label: "Other" },
];

function getTestMetric() {
return createMetricMapping({
localityLabelMapping: {
Probation: { label: "Judicial District", entries: expectedLocalities },
},
metadataMapping: testMetadataMapping,
tenantId: testTenantId,
}).get(testMetricId) as PopulationBreakdownByLocationMetric;
}

test("has expected options", () => {
const metric = getTestMetric();
render(<LocalityFilterSelect metric={metric} />);

const menuButton = screen.getByRole("button", {
name: "Judicial District All Districts",
});
fireEvent.click(menuButton);

const options = screen.getAllByRole("option");

expect(options.length).toBe(expectedLocalities.length);

options.forEach((option, index) =>
expect(option).toHaveTextContent(expectedLocalities[index].label)
);
});

test("changes demographic filter", () => {
const metric = getTestMetric();
render(<LocalityFilterSelect metric={metric} />);

const menuButton = screen.getByRole("button", {
name: "Judicial District All Districts",
});

expectedLocalities.forEach((expectedLocality) => {
// open the menu
fireEvent.click(menuButton);

const option = screen.getByRole("option", { name: expectedLocality.label });
fireEvent.click(option);

reactImmediately(() => {
expect(metric.localityId).toBe(expectedLocality.id);
expect(menuButton).toHaveTextContent(expectedLocality.label);
});
});

expect.hasAssertions();
});
53 changes: 53 additions & 0 deletions spotlight-client/src/LocalityFilterSelect/LocalityFilterSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2021 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 { action } from "mobx";
import { observer } from "mobx-react-lite";
import React from "react";
import type PopulationBreakdownByLocationMetric from "../contentModels/PopulationBreakdownByLocationMetric";
import type SentenceTypeByLocationMetric from "../contentModels/SentenceTypeByLocationMetric";
import type SupervisionSuccessRateDemographicsMetric from "../contentModels/SupervisionSuccessRateDemographicsMetric";
import type SupervisionSuccessRateMonthlyMetric from "../contentModels/SupervisionSuccessRateMonthlyMetric";
import { Dropdown } from "../UiLibrary";

type LocalityFilterSelectProps = {
metric:
| PopulationBreakdownByLocationMetric
| SentenceTypeByLocationMetric
| SupervisionSuccessRateMonthlyMetric
| SupervisionSuccessRateDemographicsMetric;
};

const LocalityFilterSelect: React.FC<LocalityFilterSelectProps> = ({
metric,
}) => {
const onChange = action("change locality filter", (newFilter: string) => {
// eslint-disable-next-line no-param-reassign
metric.localityId = newFilter;
});

return (
<Dropdown
label={metric.localityLabels.label}
onChange={onChange}
options={metric.localityLabels.entries}
selectedId={metric.localityId}
/>
);
};

export default observer(LocalityFilterSelect);
18 changes: 18 additions & 0 deletions spotlight-client/src/LocalityFilterSelect/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2021 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/>.
// =============================================================================

export { default } from "./LocalityFilterSelect";
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,34 @@ const content: TenantContent = {
],
},
},
localities: {
Sentencing: {
label: "sentencing locality",
entries: [
{
id: "sentencing a",
label: "sentencing A",
},
{
id: "sentencing b",
label: "sentencing B",
},
],
},
Parole: {
label: "parole locality",
entries: [
{
id: "parole a",
label: "parole A",
},
{
id: "parole b",
label: "parole B",
},
],
},
},
};

export default content;
2 changes: 2 additions & 0 deletions spotlight-client/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const ERROR_MESSAGES = {
auth0Configuration: "No Auth0 configuration found.",
unauthorized: "You do not have permission to view this content.",
noMetricData: "Unable to retrieve valid data for this metric.",
noLocalityLabels:
"Unable to create Metric because locality labels are missing",
};

export const NAV_BAR_HEIGHT = 80;
Expand Down
66 changes: 66 additions & 0 deletions spotlight-client/src/contentApi/sources/us_nd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@

import { TenantContent } from "../types";

// localities for both sentencing and probation
const judicialDistricts = [
{ id: "ALL", label: "All Districts" },
{ id: "EAST_CENTRAL", label: "East Central" },
{ id: "NORTH_CENTRAL", label: "North Central" },
{ id: "NORTHEAST", label: "Northeast" },
{ id: "NORTHEAST_CENTRAL", label: "Northeast Central" },
{ id: "NORTHWEST", label: "Northwest" },
{ id: "SOUTH_CENTRAL", label: "South Central" },
{ id: "SOUTHEAST", label: "Southeast" },
{ id: "SOUTHWEST", label: "Southwest" },
{ id: "OTHER", label: "Other" },
];

const content: TenantContent = {
name: "North Dakota",
description:
Expand Down Expand Up @@ -328,6 +342,58 @@ const content: TenantContent = {
],
},
},
localities: {
Sentencing: {
label: "Judicial District",
entries: judicialDistricts,
},
Prison: {
label: "Facility",
entries: [
{ id: "ALL", label: "All Facilities" },
{ id: "BTC", label: "Bismarck Transition Center" },
{ id: "DWCRC", label: "Dakota Women's Correctional" },
{ id: "FTPFAR", label: "Fargo-Female Transition Program" },
{ id: "MTPFAR", label: "Fargo-Male Transition Program" },
{ id: "GFC", label: "Grand Forks County Correctional" },
{ id: "JRCC", label: "James River Correctional Center" },
{ id: "LRRP", label: "Lake Region Residential Reentry Center" },
{ id: "FTPMND", label: "Mandan-Female Transition Program" },
{ id: "MTPMND", label: "Mandan-Male Transition Program" },
{ id: "MRCC", label: "Missouri River Correctional" },
{ id: "NDSP", label: "North Dakota State Penitentiary" },
{ id: "TRC", label: "Tompkins Rehabilitation And Corrections Center" },
],
},
Probation: {
label: "Judicial District",
entries: judicialDistricts,
},
Parole: {
label: "Office",
entries: [
{ label: "All Offices", id: "ALL" },
{ label: "Beulah", id: "16" },
{ label: "Bismarck", id: "1" },
{ label: "Bottineau", id: "14" },
{ label: "Central Office", id: "17" },
{ label: "Devils Lake", id: "6" },
{ label: "Dickinson", id: "11" },
{ label: "Fargo", id: "4" },
{ label: "Grafton", id: "12" },
{ label: "Grand Forks", id: "5" },
{ label: "Jamestown", id: "2" },
{ label: "Mandan", id: "13" },
{ label: "Minot", id: "3" },
{ label: "Oakes", id: "15" },
{ label: "Rolla", id: "8" },
{ label: "Wahpeton", id: "7" },
{ label: "Washburn", id: "9" },
{ label: "Watford City", id: "18" },
{ label: "Williston", id: "10" },
],
},
},
};

export default content;
10 changes: 10 additions & 0 deletions spotlight-client/src/contentApi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export type NamedEntity = {
description: string;
};

export type LocalityLabels = {
label: string;
entries: { id: string; label: string }[];
};

// ============================
// Tenant types

Expand All @@ -44,6 +49,11 @@ export type TenantContent = NamedEntity & {
systemNarratives: {
[key in SystemNarrativeTypeId]?: SystemNarrativeContent;
};
// this is optional because it is possible (though unlikely)
// to not have any metrics that actually need it
localities?: {
[key in SystemNarrativeTypeId]?: LocalityLabels;
};
};

// ============================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ const getMetric = async () => {
tenantId: "US_ND",
defaultDemographicView: "total",
defaultLocalityId: undefined,
localityLabels: undefined,
dataTransformer: mockTransformer,
sourceFileName: mockSourceFileName,
});
Expand Down
4 changes: 3 additions & 1 deletion spotlight-client/src/contentModels/Metric.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ import { reactImmediately } from "../testUtils";
import createMetricMapping from "./createMetricMapping";

const testTenantId = "US_ND";
const testMetadataMapping = retrieveContent({ tenantId: testTenantId }).metrics;
const allTestContent = retrieveContent({ tenantId: testTenantId });
const testMetadataMapping = allTestContent.metrics;

const getTestMapping = () =>
createMetricMapping({
localityLabelMapping: allTestContent.localities,
metadataMapping: testMetadataMapping,
tenantId: testTenantId,
});
Expand Down
12 changes: 11 additions & 1 deletion spotlight-client/src/contentModels/Metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from "mobx";
import { DataSeries } from "../charts/types";
import { ERROR_MESSAGES } from "../constants";
import { TenantId } from "../contentApi/types";
import { LocalityLabels, TenantId } from "../contentApi/types";
import {
fetchMetrics,
RawMetricData,
Expand All @@ -45,6 +45,9 @@ export type BaseMetricConstructorOptions<RecordFormat extends MetricRecord> = {
? DemographicView
: undefined;
defaultLocalityId: RecordFormat extends LocalityFields ? string : undefined;
localityLabels: RecordFormat extends LocalityFields
? LocalityLabels
: undefined;
};

/**
Expand Down Expand Up @@ -87,6 +90,10 @@ export default abstract class Metric<RecordFormat extends MetricRecord> {
// filter properties
localityId: RecordFormat extends LocalityFields ? string : undefined;

localityLabels: RecordFormat extends LocalityFields
? LocalityLabels
: undefined;

demographicView: RecordFormat extends DemographicFields
? DemographicView
: undefined;
Expand All @@ -100,10 +107,12 @@ export default abstract class Metric<RecordFormat extends MetricRecord> {
dataTransformer,
defaultDemographicView,
defaultLocalityId,
localityLabels,
}: BaseMetricConstructorOptions<RecordFormat>) {
makeObservable(this, {
allRecords: observable.ref,
demographicView: observable,
localityId: observable,
error: observable,
populateAllRecords: action,
isLoading: observable,
Expand All @@ -123,6 +132,7 @@ export default abstract class Metric<RecordFormat extends MetricRecord> {

// initialize filters
this.localityId = defaultLocalityId;
this.localityLabels = localityLabels;
this.demographicView = defaultDemographicView;
}

Expand Down

0 comments on commit f4dde8d

Please sign in to comment.