Skip to content

Commit

Permalink
Merge 99ad18d into 187c4ec
Browse files Browse the repository at this point in the history
  • Loading branch information
macfarlandian committed Mar 12, 2021
2 parents 187c4ec + 99ad18d commit a5383b6
Show file tree
Hide file tree
Showing 27 changed files with 1,001 additions and 307 deletions.
10 changes: 4 additions & 6 deletions spotlight-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@
"@types/d3-scale": "^3.2.2",
"@types/date-fns": "^2.6.0",
"@types/downloadjs": "^1.4.2",
"@types/lodash.isempty": "^4.4.6",
"@types/lodash.mapvalues": "^4.6.6",
"@types/lodash.xor": "^4.5.6",
"@types/flat": "^5.0.1",
"@types/lodash": "^4.14.168",
"@types/qs": "^6.9.5",
"@types/reach__router": "^1.3.6",
"@types/react": "^17.0.0",
Expand All @@ -57,12 +56,11 @@
"date-fns": "^2.16.1",
"downloadjs": "^1.4.7",
"downshift": "^6.1.0",
"flat": "^5.0.2",
"html-react-parser": "^1.1.1",
"intersection-observer": "^0.12.0",
"jszip": "^3.6.0",
"lodash.isempty": "^4.4.0",
"lodash.mapvalues": "^4.6.0",
"lodash.xor": "^4.5.0",
"lodash": "^4.17.21",
"mobx": "^6.0.4",
"mobx-react-lite": "^3.0.1",
"mobx-utils": "^6.0.1",
Expand Down
92 changes: 7 additions & 85 deletions spotlight-client/src/MetricVizControls/MetricVizControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,64 +15,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
// =============================================================================

import HTMLReactParser from "html-react-parser";
import { rem } from "polished";
import React, { useState } from "react";
import styled from "styled-components/macro";
import downloadPath from "../assets/cloud-download.svg";
import machineLearningPath from "../assets/machine-learning.svg";
import React from "react";
import Metric from "../contentModels/Metric";
import { MetricRecord } from "../contentModels/types";
import { colors, CopyBlock, Modal, ModalHeading, zIndex } from "../UiLibrary";

const Wrapper = styled.div`
background: ${colors.background};
display: flex;
flex-wrap: wrap;
justify-content: space-between;
z-index: ${zIndex.control};
`;

const ControlsGroup = styled.div`
display: flex;
flex-wrap: wrap;
margin-bottom: ${rem(16)};
padding-bottom: ${rem(16)};
`;

const FilterWrapper = styled.div`
margin: 0 ${rem(16)};
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
`;

const Button = styled.button`
background: none;
border: none;
color: ${colors.link};
cursor: pointer;
font-size: ${rem(14)};
font-weight: 500;
padding: none;
`;

const ButtonIcon = styled.img`
height: ${rem(16)};
/* icon needs minor adjustment to align with text */
margin-bottom: ${rem(-2)};
width: ${rem(16)};
`;

const MethodologyCopy = styled(CopyBlock)`
line-height: 1.7;
margin-top: ${rem(16)};
`;
import VizControls from "../VizControls";

type MetricVizControlsProps = {
filters: React.ReactElement[];
Expand All @@ -83,36 +29,12 @@ const MetricVizControls = ({
filters,
metric,
}: MetricVizControlsProps): React.ReactElement => {
const [showMethodology, setShowMethodology] = useState(false);

return (
<Wrapper>
<ControlsGroup>
{filters.map((filter, index) => (
// there's nothing else to use as a key, but these should be pretty static
// so there isn't any real performance concern
// eslint-disable-next-line react/no-array-index-key
<FilterWrapper key={index}>{filter}</FilterWrapper>
))}
</ControlsGroup>
<ControlsGroup>
<Button onClick={() => metric.download()}>
<ButtonIcon src={downloadPath} /> Download Data
</Button>
<Button onClick={() => setShowMethodology(true)}>
<ButtonIcon src={machineLearningPath} /> Methodology
</Button>
<Modal
isOpen={showMethodology}
onRequestClose={() => setShowMethodology(false)}
>
<ModalHeading>Methodology</ModalHeading>
<MethodologyCopy>
{HTMLReactParser(metric.methodology)}
</MethodologyCopy>
</Modal>
</ControlsGroup>
</Wrapper>
<VizControls
filters={filters}
download={() => metric.download()}
methodology={metric.methodology}
/>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// 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 useBreakpoint from "@w11r/use-breakpoint";
import { rem } from "polished";
import React, { useState } from "react";
import styled from "styled-components/macro";
import { ItemToHighlight, ProportionalBar } from "../charts";
import { DemographicCategoryRecords } from "../contentModels/types";
import VizControls, { VizControlsProps } from "../VizControls";

const CHART_HEIGHT = 165;

const CHART_HEIGHT_MOBILE = 100;

const Wrapper = styled.div`
padding: ${rem(48)} 0;
`;

const Spacer = styled.div`
height: ${rem(24)};
`;

type BarChartPairProps = {
data: DemographicCategoryRecords[];
download: () => void;
filters: VizControlsProps["filters"];
methodology: string;
};

export default function BarChartPair({
data,
download,
filters,
methodology,
}: BarChartPairProps): React.ReactElement {
const [highlightedCategory, setHighlightedCategory] = useState<
ItemToHighlight | undefined
>();

const chartHeight = useBreakpoint(CHART_HEIGHT, [
"mobile-",
CHART_HEIGHT_MOBILE,
]);

return (
<Wrapper>
<VizControls
filters={filters}
methodology={methodology}
download={download}
/>
<ProportionalBar
data={data[0].records}
title={data[0].label}
height={chartHeight}
highlighted={highlightedCategory}
showLegend={false}
/>
<Spacer />
<ProportionalBar
data={data[1].records}
title={data[1].label}
height={chartHeight}
highlighted={highlightedCategory}
setHighlighted={setHighlightedCategory}
/>
</Wrapper>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 RacialDisparitiesNarrative from "../contentModels/RacialDisparitiesNarrative";
import contentFixture from "../contentModels/__fixtures__/tenant_content_exhaustive";
import { reactImmediately } from "../testUtils";
import RaceOrEthnicityFilterSelect from "./RaceOrEthnicityFilterSelect";

const expectedOptions = [
{ id: "AMERICAN_INDIAN_ALASKAN_NATIVE", label: "Native American" },
{ id: "BLACK", label: "Black" },
{ id: "HISPANIC", label: "Hispanic" },
{ id: "WHITE", label: "White" },
{ id: "OTHER", label: "Other" },
];

let narrative: RacialDisparitiesNarrative;

beforeEach(() => {
narrative = RacialDisparitiesNarrative.build({
tenantId: "US_ND",
content: contentFixture.racialDisparitiesNarrative,
});

render(<RaceOrEthnicityFilterSelect narrative={narrative} />);
});

test("has expected options", () => {
const menuButton = screen.getByRole("button", {
name: "Race or Ethnicity Black",
});
fireEvent.click(menuButton);

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

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

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

test("changes demographic filter", () => {
const menuButton = screen.getByRole("button", {
name: "Race or Ethnicity Black",
});

expectedOptions.forEach((expectedOption) => {
// open the menu
fireEvent.click(menuButton);

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

reactImmediately(() => {
expect(narrative.selectedCategory).toBe(expectedOption.id);
expect(menuButton).toHaveTextContent(expectedOption.label);
});
});

expect.hasAssertions();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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 RacialDisparitiesNarrative from "../contentModels/RacialDisparitiesNarrative";
import {
getDemographicCategories,
getDemographicViewLabel,
RaceIdentifier,
} from "../demographics";
import { Dropdown } from "../UiLibrary";

const DROPDOWN_OPTIONS = getDemographicCategories(
"raceOrEthnicity"
).map(({ identifier, label }) => ({ label, id: identifier }));

type RaceOrEthnicityFilterSelectProps = {
narrative: RacialDisparitiesNarrative;
};

const RaceOrEthnicityFilterSelect: React.FC<RaceOrEthnicityFilterSelectProps> = ({
narrative,
}) => {
const onChange = action(
"change race/ethnicity filter",
(newFilter: string) => {
// safe assertion because this is the id type in our options array
// eslint-disable-next-line no-param-reassign
narrative.selectedCategory = newFilter as RaceIdentifier;
}
);

return (
<Dropdown
label={getDemographicViewLabel("raceOrEthnicity")}
onChange={onChange}
options={DROPDOWN_OPTIONS}
selectedId={narrative.selectedCategory}
/>
);
};

export default observer(RaceOrEthnicityFilterSelect);
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import mockContentFixture from "../contentModels/__fixtures__/tenant_content_exh
import { renderNavigableApp } from "../testUtils";

jest.mock("../contentApi/sources/us_nd", () => mockContentFixture);
jest.mock("../MeasureWidth/MeasureWidth");

const narrativeContent = mockContentFixture.racialDisparitiesNarrative;

Expand Down Expand Up @@ -75,7 +76,7 @@ test("renders dynamic text", async () => {
screen.getByText(
(content, element) =>
normalizeContents(element.textContent || "") ===
"supervision body 33% 47% 16% 19% 25% 27% 34% 35%"
"supervision body supervision 33% 47% 16% 19% 25% 27% 34% 35%"
)
).toBeVisible();

Expand All @@ -95,3 +96,10 @@ test("renders dynamic text", async () => {
)
).toBeVisible();
});

test("renders charts", async () => {
const charts = await screen.findAllByRole("group", {
name: /\d+ bars in a bar chart/,
});
expect(charts.length).toBe(12);
});
Loading

0 comments on commit a5383b6

Please sign in to comment.