Skip to content

Commit

Permalink
Merge 84fc7c9 into d7c99d2
Browse files Browse the repository at this point in the history
  • Loading branch information
macfarlandian committed Feb 23, 2021
2 parents d7c99d2 + 84fc7c9 commit 9dde31d
Show file tree
Hide file tree
Showing 24 changed files with 1,870 additions and 17 deletions.
6 changes: 6 additions & 0 deletions spotlight-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@types/d3-color": "^2.0.1",
"@types/d3-force": "^2.1.0",
"@types/d3-format": "^2.0.0",
"@types/d3-geo": "^2.0.0",
"@types/d3-interpolate": "^2.0.0",
"@types/d3-scale": "^3.2.2",
"@types/date-fns": "^2.6.0",
Expand All @@ -32,7 +33,9 @@
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-measure": "^2.0.6",
"@types/react-simple-maps": "^1.0.3",
"@types/styled-components": "^5.1.5",
"@types/topojson": "^3.2.2",
"@w11r/use-breakpoint": "^1.8.0",
"assert-never": "^1.2.1",
"change-case": "^4.1.2",
Expand All @@ -42,6 +45,7 @@
"d3-force": "^2.1.1",
"d3-force-limit": "^1.1.3",
"d3-format": "^2.0.0",
"d3-geo": "^2.0.1",
"d3-interpolate": "^2.0.1",
"d3-scale": "^3.2.3",
"date-fns": "^2.16.1",
Expand All @@ -64,11 +68,13 @@
"react-is": "^16.13.1",
"react-measure": "^2.5.2",
"react-scripts": "3.4.3",
"react-simple-maps": "^2.3.0",
"react-spring": "^8.0.27",
"react-stickyfill": "^0.2.5",
"semiotic": "^1.20.6",
"styled-components": "^5.2.1",
"styled-reset": "^4.3.3",
"topojson": "^3.0.2",
"typescript": "^4.0.0",
"utility-types": "^3.10.0",
"wait-for-localhost": "^3.3.0"
Expand Down
9 changes: 8 additions & 1 deletion spotlight-client/src/MetricVizMapper/MetricVizMapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ import VizSentenceTypeByLocation from "../VizSentenceTypeByLocation";
import VizRecidivismRateCumulative from "../VizRecidivismRateCumulative";
import SupervisionSuccessRateMetric from "../contentModels/SupervisionSuccessRateMetric";
import VizSupervisionSuccessRate from "../VizSupervisionSuccessRate";
import ProgramParticipationCurrentMetric from "../contentModels/ProgramParticipationCurrentMetric";
import VizProgramParticipationCurrent from "../VizProgramParticipationCurrent";

type MetricVizMapperProps = {
metric: Metric<MetricRecord>;
Expand Down Expand Up @@ -62,7 +64,12 @@ const MetricVizMapper: React.FC<MetricVizMapperProps> = ({ metric }) => {
if (metric instanceof SupervisionSuccessRateMetric) {
return <VizSupervisionSuccessRate metric={metric} />;
}
return <h3>Placeholder for {metric.name}</h3>;
if (metric instanceof ProgramParticipationCurrentMetric) {
return <VizProgramParticipationCurrent metric={metric} />;
}

// there are no other metric types, so this should only be reached when developing new ones
throw new Error("unknown metric type");
};

export default MetricVizMapper;
3 changes: 3 additions & 0 deletions spotlight-client/src/UiLibrary/colors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ export default {
dataVizNamed: dataVizColorMap,
footerBackground: pineDark,
link: pineAccent2,
mapFill: "#E9ECEC",
mapFillHover: "#D8E3E3",
mapStroke: white,
rule: gray,
ruleHover: "#AFC1C3",
text: pine,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// 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 { screen, within } from "@testing-library/react";
import { runInAction } from "mobx";
import React from "react";
import ProgramParticipationCurrentMetric from "../contentModels/ProgramParticipationCurrentMetric";
import DataStore from "../DataStore";
import { reactImmediately, renderWithStore } from "../testUtils";
import VizProgramParticipationCurrent from "./VizProgramParticipationCurrent";

jest.mock("../MeasureWidth/MeasureWidth");

let metric: ProgramParticipationCurrentMetric;

beforeEach(() => {
runInAction(() => {
DataStore.tenantStore.currentTenantId = "US_ND";
});
reactImmediately(() => {
const metricToTest = DataStore.tenant?.metrics.get(
"ParoleProgrammingCurrent"
);
// it will be
if (metricToTest instanceof ProgramParticipationCurrentMetric) {
metric = metricToTest;
}
});
});

afterEach(() => {
runInAction(() => {
DataStore.tenantStore.currentTenantId = undefined;
});
});

test("loading", () => {
renderWithStore(<VizProgramParticipationCurrent metric={metric} />);
expect(screen.getByText(/loading/i)).toBeVisible();
});

test("renders a map", async () => {
renderWithStore(<VizProgramParticipationCurrent metric={metric} />);

const map = await screen.findByRole("figure", { name: "Region map chart" });
expect(map).toBeVisible();

expect(
within(map).getByRole("img", { name: "Region 1 value 7" })
).toBeVisible();
expect(
within(map).getByRole("img", { name: "Region 2 value 47" })
).toBeVisible();
expect(
within(map).getByRole("img", { name: "Region 3 value 50" })
).toBeVisible();
expect(
within(map).getByRole("img", { name: "Region 4 value 25" })
).toBeVisible();
expect(
within(map).getByRole("img", { name: "Region 5 value 51" })
).toBeVisible();
expect(
within(map).getByRole("img", { name: "Region 6 value 21" })
).toBeVisible();
expect(
within(map).getByRole("img", { name: "Region 7 value 106" })
).toBeVisible();
expect(
within(map).getByRole("img", { name: "Region 8 value 17" })
).toBeVisible();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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 { observer } from "mobx-react-lite";
import React from "react";
import styled from "styled-components/macro";
import { TopologicalMap } from "../charts";
import ProgramParticipationCurrentMetric from "../contentModels/ProgramParticipationCurrentMetric";
import NoMetricData from "../NoMetricData";

const MapWrapper = styled.figure``;

type VizProgramParticipationCurrentProps = {
metric: ProgramParticipationCurrentMetric;
};

const VizProgramParticipationCurrent: React.FC<VizProgramParticipationCurrentProps> = ({
metric,
}) => {
const { dataMapping } = metric;

if (dataMapping) {
return (
<MapWrapper aria-label={`${metric.localityLabels.label} map chart`}>
<TopologicalMap
aspectRatio={metric.mapData.aspectRatio}
localityData={dataMapping}
topology={metric.mapData.topology}
/>
</MapWrapper>
);
}

return <NoMetricData metric={metric} />;
};

export default observer(VizProgramParticipationCurrent);
18 changes: 18 additions & 0 deletions spotlight-client/src/VizProgramParticipationCurrent/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 "./VizProgramParticipationCurrent";
55 changes: 55 additions & 0 deletions spotlight-client/src/charts/TopologicalMap/RatioContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// 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 React from "react";
import styled from "styled-components/macro";

const RatioContainerOuter = styled.div`
position: relative;
height: 0;
`;

const RatioContainerInner = styled.div({
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
});

/**
* Implements a version of the Aspect Ratio Box technique described here:
* https://github.com/zcreativelabs/react-simple-maps/issues/37#issuecomment-349435145
* but with explicit width (our flex layout prefers it or elements may collapse).
* This is needed to size the map SVG properly in IE 11 and some mobile devices.
*/
const RatioContainer: React.FC<{ width: number; aspectRatio: number }> = ({
aspectRatio,
children,
width,
}) => {
return (
<RatioContainerOuter
// this calculation requires the inverse aspect ratio; the ratio of height to width
style={{ paddingBottom: `calc(${1 / aspectRatio} * 100%)`, width }}
>
<RatioContainerInner>{children}</RatioContainerInner>
</RatioContainerOuter>
);
};

export default RatioContainer;
112 changes: 112 additions & 0 deletions spotlight-client/src/charts/TopologicalMap/Region.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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 { geoCentroid } from "d3-geo";
import { rem } from "polished";
import React, { useState } from "react";
import { Geography, Marker, GeographyProps } from "react-simple-maps";
import { Spring } from "react-spring/renderprops.cjs";
import styled from "styled-components/macro";
import { ValuesType } from "utility-types";
import { LocalityDataMapping } from "../../contentModels/types";
import { colors } from "../../UiLibrary";

const RegionGroup = styled.g`
&:focus {
outline: none;
}
`;

const RegionGeography = styled(Geography)`
&:focus {
outline: none;
}
`;

const RegionMarker = styled(Marker)``;

const RegionLabel = styled.text`
font-size: ${rem(18)};
font-weight: 600;
letter-spacing: -0.015em;
text-anchor: middle;
`;

const Region = ({
data,
geography,
}: {
data: ValuesType<LocalityDataMapping>;
geography: GeographyProps["geography"];
}): React.ReactElement => {
const centroid = geoCentroid(geography);
const { label, value } = data;
const [hoverRegion, setHoverRegion] = useState(false);

const setHover = () => {
setHoverRegion(true);
};

const clearHover = () => {
setHoverRegion(false);
};

return (
<RegionGroup
onBlur={clearHover}
onMouseOut={clearHover}
onFocus={setHover}
onMouseOver={setHover}
tabIndex={0}
role="img"
aria-label={`${label} value ${value}`}
>
{/*
using spring renderprops instead of hook because react-simple-maps
components are not compatible with the `animated` wrapper
*/}
<Spring
from={{
fill: colors.mapFill,
textFill: colors.text,
}}
to={{
fill: hoverRegion ? colors.mapFillHover : colors.mapFill,
textFill: hoverRegion ? colors.accent : colors.text,
}}
>
{(props) => (
<>
<RegionGeography
key={`region_${geography.id}`}
geography={geography}
fill={props.fill}
stroke={colors.mapStroke}
strokeWidth={1.5}
tabIndex={-1}
/>
<RegionMarker key={`marker_${geography.id}`} coordinates={centroid}>
<RegionLabel fill={props.textFill}>{value}</RegionLabel>
</RegionMarker>
</>
)}
</Spring>
</RegionGroup>
);
};

export default Region;
Loading

0 comments on commit 9dde31d

Please sign in to comment.