Skip to content

Commit

Permalink
Make long lists of demographic unknowns collapsible (#448)
Browse files Browse the repository at this point in the history
  • Loading branch information
macfarlandian committed Jun 7, 2021
1 parent 87f2781 commit bbf45bd
Show file tree
Hide file tree
Showing 9 changed files with 405 additions and 221 deletions.
52 changes: 52 additions & 0 deletions spotlight-client/src/UiLibrary/AutoHeightTransition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 Measure from "react-measure";
import { animated, useSpring } from "react-spring/web.cjs";

type VerticallyExpandableProps = {
initialHeight?: number;
};

/**
* A container that transitions smoothly to the height of its children
* as they change size.
*/
export const AutoHeightTransition: React.FC<VerticallyExpandableProps> = ({
children,
initialHeight = 0,
}) => {
const [containerStyles, setContainerStyles] = useSpring(() => ({
from: { height: initialHeight },
height: initialHeight,
config: { friction: 40, tension: 220, clamp: true },
}));

return (
<animated.div style={containerStyles}>
<Measure
bounds
onResize={({ bounds }) => {
if (bounds) setContainerStyles({ height: bounds.height });
}}
>
{({ measureRef }) => <div ref={measureRef}>{children}</div>}
</Measure>
</animated.div>
);
};
1 change: 1 addition & 0 deletions spotlight-client/src/UiLibrary/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// =============================================================================

export { default as animation } from "./animation";
export * from "./AutoHeightTransition";
export { default as breakpoints } from "./breakpoints";
export { default as Check } from "./Check";
export * from "./Checkbox";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@
import { observer } from "mobx-react-lite";
import { rem } from "polished";
import React from "react";
import Measure from "react-measure";
import { animated, useSpring, useTransition } from "react-spring/web.cjs";
import { animated, useTransition } from "react-spring/web.cjs";
import styled from "styled-components/macro";
import { BubbleChart, ProportionalBar } from "../charts";
import { useHighlightedItem } from "../charts/utils";
import DemographicsByCategoryMetric from "../contentModels/DemographicsByCategoryMetric";
import DemographicFilterSelect from "../DemographicFilterSelect";
import MetricVizControls from "../MetricVizControls";
import { animation, zIndex } from "../UiLibrary";
import { animation, AutoHeightTransition, zIndex } from "../UiLibrary";
import VizNotes from "../VizNotes";
import withMetricHydrator from "../withMetricHydrator";

Expand Down Expand Up @@ -56,12 +55,6 @@ const VizDemographicsByCategory: React.FC<VizDemographicsByCategoryProps> = ({

const { demographicView, dataSeries, unknowns } = metric;

const [chartContainerStyles, setChartContainerStyles] = useSpring(() => ({
from: { height: bubbleChartHeight },
height: bubbleChartHeight,
config: { friction: 40, tension: 280, clamp: true },
}));

const chartTransitions = useTransition(
{ demographicView, dataSeries },
(item) => item.demographicView,
Expand All @@ -75,65 +68,55 @@ const VizDemographicsByCategory: React.FC<VizDemographicsByCategoryProps> = ({

if (dataSeries) {
return (
<Measure
bounds
onResize={({ bounds }) => {
if (bounds) setChartContainerStyles({ height: bounds.height });
}}
>
{({ measureRef }) => (
<>
<MetricVizControls
filters={[<DemographicFilterSelect metric={metric} />]}
metric={metric}
/>
<animated.div style={chartContainerStyles}>
{chartTransitions.map(({ item, key, props }) => (
<ChartWrapper key={key} ref={measureRef}>
<animated.div style={props}>
{
// for type safety we have to check this again
// but it should always be defined if we've gotten this far
item.dataSeries &&
(item.demographicView === "total" ? (
<BubbleChart
height={bubbleChartHeight}
data={item.dataSeries[0].records}
/>
) : (
item.dataSeries.map(
({ label, records }, index, categories) => (
<CategoryBarWrapper
key={label}
style={{
// prevents subsequent charts from covering up the tooltip for this one
zIndex:
zIndex.base + categories.length - index,
}}
>
<ProportionalBar
data={records}
height={
barChartsHeight / categories.length -
barChartsGutter
}
title={label}
showLegend={index === categories.length - 1}
{...{ highlighted, setHighlighted }}
/>
</CategoryBarWrapper>
)
)
))
}
</animated.div>
</ChartWrapper>
))}
</animated.div>
<VizNotes smallData unknowns={unknowns} />
</>
)}
</Measure>
<>
<MetricVizControls
filters={[<DemographicFilterSelect metric={metric} />]}
metric={metric}
/>
<AutoHeightTransition initialHeight={bubbleChartHeight}>
{chartTransitions.map(({ item, key, props }) => (
<ChartWrapper key={key}>
<animated.div style={props}>
{
// for type safety we have to check this again
// but it should always be defined if we've gotten this far
item.dataSeries &&
(item.demographicView === "total" ? (
<BubbleChart
height={bubbleChartHeight}
data={item.dataSeries[0].records}
/>
) : (
item.dataSeries.map(
({ label, records }, index, categories) => (
<CategoryBarWrapper
key={label}
style={{
// prevents subsequent charts from covering up the tooltip for this one
zIndex: zIndex.base + categories.length - index,
}}
>
<ProportionalBar
data={records}
height={
barChartsHeight / categories.length -
barChartsGutter
}
title={label}
showLegend={index === categories.length - 1}
{...{ highlighted, setHighlighted }}
/>
</CategoryBarWrapper>
)
)
))
}
</animated.div>
</ChartWrapper>
))}
</AutoHeightTransition>
<VizNotes smallData unknowns={unknowns} />
</>
);
}

Expand Down
151 changes: 151 additions & 0 deletions spotlight-client/src/VizNotes/UnknownsNote.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
// 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 { fireEvent, render, screen } from "@testing-library/react";
import React from "react";
import { UnknownsNote } from "./UnknownsNote";

test("format single unknowns", () => {
render(
<UnknownsNote unknowns={{ gender: 1, ageBucket: 4, raceOrEthnicity: 0 }} />
);

expect(screen.getByText("age group (4), gender (1).", { exact: false }));
});

test("format unknowns by date", () => {
render(
<UnknownsNote
unknowns={[
{
date: new Date(2021, 0),
unknowns: { gender: 1, ageBucket: 0, raceOrEthnicity: 1 },
},
{
date: new Date(2021, 1),
unknowns: { gender: 2, ageBucket: 0, raceOrEthnicity: 0 },
},
]}
/>
);

expect(
screen.getByText(
"gender (1), race or ethnicity (1) for Jan 1 2021; gender (2) for Feb 1 2021.",
{ exact: false }
)
);
});

test("format unknowns by cohort", () => {
render(
<UnknownsNote
unknowns={[
{
cohort: 2012,
unknowns: { gender: 1, ageBucket: 2, raceOrEthnicity: 3 },
},
]}
/>
);

expect(
screen.getByText(
"age group (2), gender (1), race or ethnicity (3) for the 2012 cohort.",
{ exact: false }
)
);
});

test("truncate long lists", () => {
render(
<UnknownsNote
unknowns={[
{
date: new Date(2021, 0),
unknowns: { gender: 1, ageBucket: 0, raceOrEthnicity: 1 },
},
{
date: new Date(2021, 1),
unknowns: { gender: 2, ageBucket: 0, raceOrEthnicity: 0 },
},
{
date: new Date(2021, 2),
unknowns: { gender: 2, ageBucket: 0, raceOrEthnicity: 0 },
},
{
date: new Date(2021, 3),
unknowns: { gender: 2, ageBucket: 0, raceOrEthnicity: 0 },
},
{
date: new Date(2021, 4),
unknowns: { gender: 2, ageBucket: 0, raceOrEthnicity: 0 },
},
]}
/>
);

const noteText = screen.getByText("for Jan 1 2021;", { exact: false });
expect(noteText).not.toHaveTextContent("for Apr 1 2021");
expect(noteText).not.toHaveTextContent("for May 1 2021");
expect(noteText).toHaveTextContent("+ 2 more");
});

test("expand and collapse overflowing text", () => {
render(
<UnknownsNote
unknowns={[
{
date: new Date(2021, 0),
unknowns: { gender: 1, ageBucket: 0, raceOrEthnicity: 1 },
},
{
date: new Date(2021, 1),
unknowns: { gender: 2, ageBucket: 0, raceOrEthnicity: 0 },
},
{
date: new Date(2021, 2),
unknowns: { gender: 2, ageBucket: 0, raceOrEthnicity: 0 },
},
{
date: new Date(2021, 3),
unknowns: { gender: 2, ageBucket: 0, raceOrEthnicity: 0 },
},
{
date: new Date(2021, 4),
unknowns: { gender: 2, ageBucket: 0, raceOrEthnicity: 0 },
},
]}
/>
);

const collapseButton = screen.getByRole("button", { name: "+ 2 more" });

expect(collapseButton).toBeVisible();

fireEvent.click(collapseButton);

const noteText = screen.getByText("for Apr 1 2021;", { exact: false });
expect(noteText).toHaveTextContent("for May 1 2021");

expect(collapseButton).toHaveTextContent("(Hide extended list)");

fireEvent.click(collapseButton);
expect(noteText).not.toHaveTextContent("for Apr 1 2021");
expect(noteText).not.toHaveTextContent("for May 1 2021");
expect(collapseButton).toHaveTextContent("+ 2 more");
});
Loading

0 comments on commit bbf45bd

Please sign in to comment.