Skip to content

Commit

Permalink
Add demographic breakdown to recidivism chart (#238)
Browse files Browse the repository at this point in the history
  • Loading branch information
macfarlandian committed Oct 30, 2020
1 parent bd79aa8 commit 739a0aa
Show file tree
Hide file tree
Showing 13 changed files with 1,119 additions and 98 deletions.
7 changes: 6 additions & 1 deletion public-dashboard-client/src/controls/CohortSelect.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ import {
const SELECT_ALL_ID = "ALL";

const DropdownWrapper = styled(DropdownWrapperBase)`
/*
increasing the z index so that following menu buttons
don't cover this up when they are stacked
*/
z-index: ${(props) => props.theme.zIndex.control + 1};
${ControlValue} {
border: 0;
cursor: pointer;
Expand Down Expand Up @@ -226,7 +232,6 @@ function NativeSelect({ buttonContents, options, selected, setSelected }) {
setSelected(
options.filter((opt) => currentlySelectedIds.includes(opt.id))
);
// toggleSelected(options.find((opt) => opt.id === event.target.value));
}}
value={selected.map((opt) => opt.id)}
>
Expand Down
26 changes: 24 additions & 2 deletions public-dashboard-client/src/controls/DimensionControl.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 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 React from "react";
import PropTypes from "prop-types";
import Dropdown from "./Dropdown";
import { DIMENSIONS_LIST } from "../constants";

export default function DimensionControl({ onChange }) {
export default function DimensionControl({ onChange, ...passThruProps }) {
return (
<Dropdown label="View" onChange={onChange} options={DIMENSIONS_LIST} />
<Dropdown
label="View"
onChange={onChange}
options={DIMENSIONS_LIST}
{...passThruProps}
/>
);
}

Expand Down
23 changes: 22 additions & 1 deletion public-dashboard-client/src/controls/Dropdown.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 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 { useId } from "@reach/auto-id";
import useBreakpoint from "@w11r/use-breakpoint";
import classNames from "classnames";
Expand Down Expand Up @@ -43,6 +60,7 @@ const DropdownMenu = styled(DropdownMenuBase)`
// in the absence of that it will be uncontrolled and expose the ID of
// its selected option via a listener
export default function Dropdown({
disabled,
highlighted,
label,
onChange,
Expand Down Expand Up @@ -78,14 +96,15 @@ export default function Dropdown({
// selecting something other than the default (first) option
// causes a highlight
highlighted || currentOptionId !== options[0].id,
"Dropdown--disabled": disabled,
})}
>
<ControlContainer>
<ControlLabel id={labelId}>{label}</ControlLabel>

{!renderNativeSelect && (
<Menu>
<MenuButton aria-labelledby={labelId}>
<MenuButton aria-labelledby={labelId} disabled={disabled}>
<ControlValue>{selectedOption.label}</ControlValue>
</MenuButton>
<MenuPopover>
Expand Down Expand Up @@ -131,6 +150,7 @@ export default function Dropdown({
}

Dropdown.propTypes = {
disabled: PropTypes.bool,
highlighted: PropTypes.bool,
label: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
Expand All @@ -139,6 +159,7 @@ Dropdown.propTypes = {
};

Dropdown.defaultProps = {
disabled: false,
highlighted: false,
selectedId: undefined,
};
16 changes: 16 additions & 0 deletions public-dashboard-client/src/controls/Dropdown.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,19 @@ test("passes selections to callback", () => {

expect(mockOnChange.mock.calls[1][0]).toBe(testOptions[2].id);
});

test("can be disabled", () => {
const { getByRole, queryByRole } = render(
<Dropdown
label={testLabel}
options={testOptions}
onChange={mockOnChange}
disabled
/>
);
const menuButton = getByRole("button", { name: testLabel });
expect(menuButton).toBeDisabled();

userEvent.click(menuButton);
expect(queryByRole("menuitem")).toBeNull();
});
18 changes: 18 additions & 0 deletions public-dashboard-client/src/controls/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 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 as DimensionControl } from "./DimensionControl";
export { default as MonthControl } from "./MonthControl";
export { default as LocationControl } from "./LocationControl";
export { default as Dropdown } from "./Dropdown";
export { default as CohortSelect } from "./CohortSelect";
export { DropdownOptionType } from "./shared";
22 changes: 22 additions & 0 deletions public-dashboard-client/src/controls/shared.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 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 PropTypes from "prop-types";
import styled, { css } from "styled-components";

Expand Down Expand Up @@ -41,6 +58,11 @@ export const DropdownWrapper = styled(ControlContainer)`
color: ${(props) => props.theme.colors.bodyLight};
}
}
&.Dropdown--disabled {
opacity: 0.5;
pointer-events: none;
}
`;

export const DropdownMenu = styled.div`
Expand Down
19 changes: 18 additions & 1 deletion public-dashboard-client/src/detail-page/DetailPage.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 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 { ErrorBoundary } from "@sentry/react";
import useBreakpoint, { mediaQuery } from "@w11r/use-breakpoint";
import { ascending } from "d3-array";
Expand Down Expand Up @@ -154,6 +171,7 @@ function DetailSection({
: undefined
}
>
{otherControls}
{showMonthControl && monthList && (
<MonthControl months={monthList} onChange={setMonth} />
)}
Expand All @@ -177,7 +195,6 @@ function DetailSection({
{showDimensionControl && (
<DimensionControl onChange={setDimension} />
)}
{otherControls}
</DetailSectionControls>
)}
</Sticky>
Expand Down
37 changes: 29 additions & 8 deletions public-dashboard-client/src/page-sentencing/PageSentencing.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ import Loading from "../loading";
import VizRecidivismRates from "../viz-recidivism-rates";
import VizSentencePopulation from "../viz-sentence-population";
import VizSentenceTypes from "../viz-sentence-types";
import { PATHS, ALL_PAGES, SECTION_TITLES } from "../constants";
import CohortSelect from "../controls/CohortSelect";
import { PATHS, ALL_PAGES, SECTION_TITLES, DIMENSION_KEYS } from "../constants";
import { CohortSelect, DimensionControl } from "../controls";
import { assignOrderedDatavizColor } from "../utils";

function getCohortOptions(data) {
Expand All @@ -38,6 +38,7 @@ function getCohortOptions(data) {

export default function PageSentencing() {
const { apiData, isLoading } = useChartData("us_nd/sentencing");

// lifted state for the recidivism section
const cohortOptions = useMemo(
() =>
Expand All @@ -46,13 +47,25 @@ export default function PageSentencing() {
: getCohortOptions(apiData.recidivism_rates_by_cohort_by_year),
[apiData.recidivism_rates_by_cohort_by_year, isLoading]
);
const [selectedCohorts, setSelectedCohorts] = useState();
const [selectedCohorts, setSelectedCohorts] = useState([]);
const [highlightedCohort, setHighlightedCohort] = useState();
const [recidivismDimension, setRecidivismDimension] = useState(
DIMENSION_KEYS.total
);

if (isLoading) {
return <Loading />;
}

const singleCohortSelected = selectedCohorts.length === 1;

// doing this inside the render loop rather than in an effect
// to prevent an intermediate state from flashing on the chart;
// the current value check avoids an infinite render loop
if (!singleCohortSelected && recidivismDimension !== DIMENSION_KEYS.total) {
setRecidivismDimension(DIMENSION_KEYS.total);
}

const TITLE = ALL_PAGES.get(PATHS.sentencing);
const DESCRIPTION = (
<>
Expand Down Expand Up @@ -115,14 +128,22 @@ export default function PageSentencing() {
</>
),
otherControls: (
<CohortSelect
options={cohortOptions}
onChange={setSelectedCohorts}
onHighlight={setHighlightedCohort}
/>
<>
<CohortSelect
options={cohortOptions}
onChange={setSelectedCohorts}
onHighlight={setHighlightedCohort}
/>
<DimensionControl
disabled={!singleCohortSelected}
onChange={setRecidivismDimension}
selectedId={recidivismDimension}
/>
</>
),
VizComponent: VizRecidivismRates,
vizData: {
dimension: recidivismDimension,
highlightedCohort,
recidivismRates: apiData.recidivism_rates_by_cohort_by_year,
selectedCohorts,
Expand Down
49 changes: 48 additions & 1 deletion public-dashboard-client/src/testUtils.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,36 @@
// Recidiviz - a data platform for criminal justice reform
// Copyright (C) 2020 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 fs from "fs";
import path from "path";
import React from "react";
import { render } from "@testing-library/react";
import { ThemeProvider } from "styled-components";
import { THEME } from "./theme";
import { InfoPanelProvider } from "./info-panel";

// eslint-disable-next-line react/prop-types
const GlobalWrapper = ({ children }) => {
// include globally expected Context providers or other required wrappers here
return <ThemeProvider theme={THEME}>{children}</ThemeProvider>;
return (
<ThemeProvider theme={THEME}>
<InfoPanelProvider>{children}</InfoPanelProvider>
</ThemeProvider>
);
};

const customRender = (ui, options) =>
Expand All @@ -20,3 +44,26 @@ export { customRender as render };

// provide original render method as a fallback if needed
export { render as renderUnwrapped };

// retrieve a data fixture from spotlight-api
export function getDataFixture(filename) {
const filePath = path.resolve(
__dirname,
`../../spotlight-api/core/demo_data/${filename}`
);
const stringContents = fs.readFileSync(filePath).toString();

// copypasta from spotlight-api/core/metricsApi for transforming JSONLines
if (!stringContents || stringContents.length === 0) {
return null;
}
const jsonObject = [];
const splitStrings = stringContents.split("\n");
splitStrings.forEach((line) => {
if (line) {
jsonObject.push(JSON.parse(line));
}
});

return jsonObject;
}

0 comments on commit 739a0aa

Please sign in to comment.