Skip to content

Commit

Permalink
Refactor demographics utilities (#329)
Browse files Browse the repository at this point in the history
  • Loading branch information
macfarlandian committed Feb 1, 2021
1 parent c8aa07b commit ad6a674
Show file tree
Hide file tree
Showing 29 changed files with 389 additions and 344 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ test("changes demographic filter", () => {
fireEvent.click(raceOption);

reactImmediately(() => {
expect(metric.demographicView).toBe("race");
expect(metric.demographicView).toBe("raceOrEthnicity");
expect(menuButton).toHaveTextContent("Race");
});

Expand All @@ -86,7 +86,7 @@ test("changes demographic filter", () => {
const ageOption = screen.getByRole("option", { name: "Age" });
fireEvent.click(ageOption);
reactImmediately(() => {
expect(metric.demographicView).toBe("age");
expect(metric.demographicView).toBe("ageBucket");
expect(menuButton).toHaveTextContent("Age");
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { action } from "mobx";
import { observer } from "mobx-react-lite";
import React from "react";
import HistoricalPopulationBreakdownMetric from "../contentModels/HistoricalPopulationBreakdownMetric";
import { DemographicView, isDemographicView } from "../metricsApi";
import { DemographicView, isDemographicView } from "../demographics";
import { Dropdown } from "../UiLibrary";

type DemographicFilterOption = { id: DemographicView; label: string };
Expand All @@ -33,9 +33,9 @@ const DemographicFilterSelect: React.FC<DemographicFilterSelectProps> = ({
}) => {
const options: DemographicFilterOption[] = [
{ id: "total", label: "Total" },
{ id: "race", label: "Race" },
{ id: "raceOrEthnicity", label: "Race" },
{ id: "gender", label: "Gender" },
{ id: "age", label: "Age" },
{ id: "ageBucket", label: "Age" },
];

const onChange = action("change demographic filter", (newFilter: string) => {
Expand Down
1 change: 1 addition & 0 deletions spotlight-client/src/charts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
export { default as ProportionalBar } from "./ProportionalBar";
export { default as WindowedTimeSeries } from "./WindowedTimeSeries";
export * from "./WindowedTimeSeries";
export * from "./types";
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@
// =============================================================================

import { DataSeries } from "../charts/types";
import {
DemographicsByCategoryRecord,
recordIsTotalByDimension,
} from "../metricsApi";
import { recordIsTotalByDimension } from "../demographics";
import { DemographicsByCategoryRecord } from "../metricsApi";
import Metric from "./Metric";

export default class DemographicsByCategoryMetric extends Metric<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,7 @@
import { isEqual } from "date-fns";
import { advanceTo, clear } from "jest-date-mock";
import { runInAction } from "mobx";
import {
DEMOGRAPHIC_UNKNOWN,
DIMENSION_DATA_KEYS,
DIMENSION_MAPPINGS,
} from "../demographics";
import { DemographicViewList, getDemographicCategories } from "../demographics";
import {
fetchMetrics,
HistoricalPopulationBreakdownRecord,
Expand Down Expand Up @@ -102,23 +98,24 @@ const getMetric = async () => {
test("fills in missing data", async () => {
const metric = await getMetric();

DIMENSION_MAPPINGS.forEach((categoryLabels, demographicView) => {
DemographicViewList.forEach((demographicView) => {
if (demographicView === "nofilter") return;

runInAction(() => {
metric.demographicView = demographicView;
});

reactImmediately(() => {
const data = metric.dataSeries;
if (data) {
Array.from(categoryLabels.keys()).forEach((identifier, index) => {
if (identifier === DEMOGRAPHIC_UNKNOWN) return;
const categories = getDemographicCategories(demographicView);
categories.forEach(({ identifier }, index) => {
const series = data[index].coordinates;
expect(series.length).toBe(240);

const expectedRecordShape = { ...imputedRecordBase };
if (demographicView !== "total") {
const categoryKey = DIMENSION_DATA_KEYS[demographicView];
expectedRecordShape[categoryKey] = identifier;
expectedRecordShape[demographicView] = identifier;
}
series.forEach((record) => {
// separate imputed records from existing records
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,18 @@
import { ascending } from "d3-array";
import { eachMonthOfInterval, format, startOfMonth, subMonths } from "date-fns";
import { makeObservable, observable, runInAction } from "mobx";
import { DataSeries } from "../charts/types";
import { DataSeries } from "../charts";
import {
DEMOGRAPHIC_UNKNOWN,
DIMENSION_DATA_KEYS,
DIMENSION_MAPPINGS,
DemographicViewList,
recordIsTotalByDimension,
getDemographicCategories,
RaceIdentifier,
GenderIdentifier,
AgeIdentifier,
} from "../demographics";
import {
AgeIdentifier,
DemographicFields,
GenderIdentifier,
HistoricalPopulationBreakdownRecord,
RaceIdentifier,
recordIsTotalByDimension,
} from "../metricsApi";
import { colors } from "../UiLibrary";
import Metric, { BaseMetricConstructorOptions } from "./Metric";
Expand Down Expand Up @@ -123,45 +122,44 @@ export default class HistoricalPopulationBreakdownMetric extends Metric<
const missingRecords: HistoricalPopulationBreakdownRecord[] = [];

// isolate each data series and impute any missing records
DIMENSION_MAPPINGS.forEach((categoryLabels, demographicView) => {
DemographicViewList.forEach((demographicView) => {
if (demographicView === "nofilter") return;

const recordsForDemographicView = transformedData.filter(
recordIsTotalByDimension(demographicView)
);
Array.from(categoryLabels.keys())
// don't need to include unknown in this data;
// they are minimal to nonexistent in historical data and make the legend confusing
.filter((identifier) => identifier !== DEMOGRAPHIC_UNKNOWN)
.forEach((identifier) => {
let recordsForCategory;
if (demographicView !== "total") {
const categoryKey = DIMENSION_DATA_KEYS[demographicView];
recordsForCategory = recordsForDemographicView.filter(
(record) => record[categoryKey] === identifier
);
} else {
recordsForCategory = recordsForDemographicView;
}
missingRecords.push(
...getMissingMonthsForSeries({
records: recordsForCategory,
includeCurrentMonth,
demographicFields: {
raceOrEthnicity:
demographicView === "race"
? (identifier as RaceIdentifier)
: "ALL",
gender:
demographicView === "gender"
? (identifier as GenderIdentifier)
: "ALL",
ageBucket:
demographicView === "age"
? (identifier as AgeIdentifier)
: "ALL",
},
})

const categories = getDemographicCategories(demographicView);
categories.forEach(({ identifier }) => {
let recordsForCategory;
if (demographicView !== "total") {
recordsForCategory = recordsForDemographicView.filter(
(record) => record[demographicView] === identifier
);
});
} else {
recordsForCategory = recordsForDemographicView;
}
missingRecords.push(
...getMissingMonthsForSeries({
records: recordsForCategory,
includeCurrentMonth,
demographicFields: {
raceOrEthnicity:
demographicView === "raceOrEthnicity"
? (identifier as RaceIdentifier)
: "ALL",
gender:
demographicView === "gender"
? (identifier as GenderIdentifier)
: "ALL",
ageBucket:
demographicView === "ageBucket"
? (identifier as AgeIdentifier)
: "ALL",
},
})
);
});
});

transformedData.push(...missingRecords);
Expand All @@ -185,28 +183,15 @@ export default class HistoricalPopulationBreakdownMetric extends Metric<
const { records, demographicView } = this;
if (!records || demographicView === "nofilter") return null;

const labelsForDimension = DIMENSION_MAPPINGS.get(demographicView);
// this should never happen, it's really just a type safety measure.
// if it does, something has gone catastrophically wrong
if (!labelsForDimension)
throw new Error("Unsupported demographic view. Unable to provide data.");

return (
Array.from(labelsForDimension)
// don't need to include unknown in this data;
// they are minimal to nonexistent in historical data and make the legend confusing
.filter(([identifier]) => identifier !== DEMOGRAPHIC_UNKNOWN)
.map(([identifier, label], index) => ({
label,
color: colors.dataViz[index],
coordinates:
demographicView === "total"
? records
: records.filter(
(record) =>
record[DIMENSION_DATA_KEYS[demographicView]] === identifier
),
}))
);
const categories = getDemographicCategories(demographicView);

return categories.map(({ identifier, label }, index) => ({
label,
color: colors.dataViz[index],
coordinates:
demographicView === "total"
? records
: records.filter((record) => record[demographicView] === identifier),
}));
}
}
4 changes: 2 additions & 2 deletions spotlight-client/src/contentModels/Metric.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,13 +181,13 @@ test("demographic filter", async () => {
reactImmediately(() => expect(metric.records).toMatchSnapshot());

runInAction(() => {
metric.demographicView = "race";
metric.demographicView = "raceOrEthnicity";
});

reactImmediately(() => expect(metric.records).toMatchSnapshot());

runInAction(() => {
metric.demographicView = "age";
metric.demographicView = "ageBucket";
});

reactImmediately(() => expect(metric.records).toMatchSnapshot());
Expand Down
2 changes: 1 addition & 1 deletion spotlight-client/src/contentModels/Metric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ import {
import { DataSeries } from "../charts/types";
import { ERROR_MESSAGES } from "../constants";
import { LocalityLabels, TenantId } from "../contentApi/types";
import { DemographicView } from "../demographics";
import {
fetchMetrics,
RawMetricData,
DemographicFields,
DemographicView,
LocalityFields,
} from "../metricsApi";
import { MetricRecord, CollectionMap } from "./types";
Expand Down
3 changes: 2 additions & 1 deletion spotlight-client/src/contentModels/RecidivismRateMetric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
// =============================================================================

import { DataSeries } from "../charts/types";
import { RecidivismRateRecord, recordIsTotalByDimension } from "../metricsApi";
import { recordIsTotalByDimension } from "../demographics";
import { RecidivismRateRecord } from "../metricsApi";
import Metric from "./Metric";

export default class RecidivismRateMetric extends Metric<RecidivismRateRecord> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
// =============================================================================

import { DataSeries } from "../charts/types";
import { recordIsTotalByDimension } from "../demographics";
import {
recordIsTotalByDimension,
recordMatchesLocality,
SentenceTypeByLocationRecord,
} from "../metricsApi";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
// =============================================================================

import { DataSeries } from "../charts/types";
import { recordIsTotalByDimension } from "../demographics";
import {
recordIsTotalByDimension,
recordMatchesLocality,
SupervisionSuccessRateDemographicsRecord,
} from "../metricsApi";
Expand Down
2 changes: 1 addition & 1 deletion spotlight-client/src/contentModels/createMetricMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ import RecidivismRateMetric from "./RecidivismRateMetric";
import SentenceTypeByLocationMetric from "./SentenceTypeByLocationMetric";
import SupervisionSuccessRateDemographicsMetric from "./SupervisionSuccessRateDemographicsMetric";
import SupervisionSuccessRateMonthlyMetric from "./SupervisionSuccessRateMonthlyMetric";
import { NOFILTER_KEY, TOTAL_KEY } from "../metricsApi/utils";
import { ERROR_MESSAGES } from "../constants";
import { NOFILTER_KEY, TOTAL_KEY } from "../demographics";

type MetricMappingFactoryOptions = {
localityLabelMapping: TenantContent["localities"];
Expand Down
81 changes: 0 additions & 81 deletions spotlight-client/src/demographics.ts

This file was deleted.

Loading

0 comments on commit ad6a674

Please sign in to comment.