From 41a5bf6de6b504d327d6d77cff5235103788f165 Mon Sep 17 00:00:00 2001 From: nasaownsky Date: Thu, 9 Jun 2022 16:47:43 +0300 Subject: [PATCH] Base implementation of Riders narrative --- spotlight-client/src/DataStore/TenantStore.ts | 6 ++ .../OtherNarrativeLinks.tsx | 1 + .../OtherNarrativesPageContainer.tsx} | 12 ++- .../RidersNarrativePage.tsx | 93 +++++++++++++++++++ .../index.ts | 2 +- .../src/PageNarrative/PageNarrative.tsx | 4 +- .../src/SiteNavigation/SiteNavigation.tsx | 6 ++ .../src/contentApi/sources/us_id.ts | 49 ++++++++++ spotlight-client/src/contentApi/types.ts | 46 +++++++-- spotlight-client/src/contentModels/Metric.ts | 6 +- .../src/contentModels/RidersNarrative.ts | 77 +++++++++++++++ .../src/contentModels/SystemNarrative.ts | 12 ++- spotlight-client/src/contentModels/Tenant.ts | 26 +++++- .../__fixtures__/tenant_content_exhaustive.ts | 46 +++++++++ .../src/contentModels/createMetricMapping.ts | 83 ++++++++++++++++- spotlight-client/src/contentModels/types.ts | 11 ++- 16 files changed, 452 insertions(+), 28 deletions(-) rename spotlight-client/src/{RacialDisparitiesNarrativePage/RacialDisparitiesNarrativePageContainer.tsx => OtherNarrativesPageContainer/OtherNarrativesPageContainer.tsx} (72%) create mode 100644 spotlight-client/src/OtherNarrativesPageContainer/RidersNarrativePage.tsx rename spotlight-client/src/{RacialDisparitiesNarrativePage => OtherNarrativesPageContainer}/index.ts (92%) create mode 100644 spotlight-client/src/contentModels/RidersNarrative.ts diff --git a/spotlight-client/src/DataStore/TenantStore.ts b/spotlight-client/src/DataStore/TenantStore.ts index 50792181..d4e53745 100644 --- a/spotlight-client/src/DataStore/TenantStore.ts +++ b/spotlight-client/src/DataStore/TenantStore.ts @@ -24,6 +24,7 @@ import { TenantId, } from "../contentApi/types"; import RacialDisparitiesNarrative from "../contentModels/RacialDisparitiesNarrative"; +import RidersNarrative from "../contentModels/RidersNarrative"; import type SystemNarrative from "../contentModels/SystemNarrative"; import Tenant, { createTenant } from "../contentModels/Tenant"; import type RootStore from "./RootStore"; @@ -104,6 +105,7 @@ export default class TenantStore { get currentNarrative(): | SystemNarrative | RacialDisparitiesNarrative + | RidersNarrative | undefined { const { currentNarrativeTypeId, currentTenant } = this; if (!currentNarrativeTypeId || !currentTenant) return undefined; @@ -112,6 +114,10 @@ export default class TenantStore { return currentTenant.systemNarratives[currentNarrativeTypeId]; } + if (currentNarrativeTypeId === "Riders") { + return currentTenant.ridersNarrative; + } + return currentTenant.racialDisparitiesNarrative; } diff --git a/spotlight-client/src/OtherNarrativeLinks/OtherNarrativeLinks.tsx b/spotlight-client/src/OtherNarrativeLinks/OtherNarrativeLinks.tsx index 3023a984..ca4d6eec 100644 --- a/spotlight-client/src/OtherNarrativeLinks/OtherNarrativeLinks.tsx +++ b/spotlight-client/src/OtherNarrativeLinks/OtherNarrativeLinks.tsx @@ -129,6 +129,7 @@ const OtherNarrativeLinks = (): React.ReactElement | null => { const narrativesToDisplay = [ ...Object.values(tenant.systemNarratives), tenant.racialDisparitiesNarrative, + tenant.ridersNarrative, ].filter((narrative): narrative is Narrative => { if (narrative === undefined) return false; return narrative.id !== currentNarrativeTypeId; diff --git a/spotlight-client/src/RacialDisparitiesNarrativePage/RacialDisparitiesNarrativePageContainer.tsx b/spotlight-client/src/OtherNarrativesPageContainer/OtherNarrativesPageContainer.tsx similarity index 72% rename from spotlight-client/src/RacialDisparitiesNarrativePage/RacialDisparitiesNarrativePageContainer.tsx rename to spotlight-client/src/OtherNarrativesPageContainer/OtherNarrativesPageContainer.tsx index f1dc746b..f9d67302 100644 --- a/spotlight-client/src/RacialDisparitiesNarrativePage/RacialDisparitiesNarrativePageContainer.tsx +++ b/spotlight-client/src/OtherNarrativesPageContainer/OtherNarrativesPageContainer.tsx @@ -19,9 +19,11 @@ import { observer } from "mobx-react-lite"; import React from "react"; import RacialDisparitiesNarrative from "../contentModels/RacialDisparitiesNarrative"; import { useDataStore } from "../StoreProvider"; -import RacialDisparitiesNarrativePage from "./RacialDisparitiesNarrativePage"; +import RacialDisparitiesNarrativePage from "../RacialDisparitiesNarrativePage/RacialDisparitiesNarrativePage"; +import RidersNarrative from "../contentModels/RidersNarrative"; +import RidersNarrativePage from "./RidersNarrativePage"; -const RacialDisparitiesNarrativePageContainer: React.FC = () => { +const OtherNarrativesPageContainer: React.FC = () => { const { narrative } = useDataStore(); if (narrative instanceof RacialDisparitiesNarrative) { @@ -29,7 +31,11 @@ const RacialDisparitiesNarrativePageContainer: React.FC = () => { return ; } + if (narrative instanceof RidersNarrative && narrative.id === "Riders") { + return ; + } + return null; }; -export default observer(RacialDisparitiesNarrativePageContainer); +export default observer(OtherNarrativesPageContainer); diff --git a/spotlight-client/src/OtherNarrativesPageContainer/RidersNarrativePage.tsx b/spotlight-client/src/OtherNarrativesPageContainer/RidersNarrativePage.tsx new file mode 100644 index 00000000..2e7be145 --- /dev/null +++ b/spotlight-client/src/OtherNarrativesPageContainer/RidersNarrativePage.tsx @@ -0,0 +1,93 @@ +// 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 . +// ============================================================================= + +import HTMLReactParser from "html-react-parser"; +import React from "react"; +import { + NarrativeIntroContainer, + NarrativeIntroCopy, + NarrativeSectionBody, + NarrativeSectionTitle, + NarrativeTitle, +} from "../UiLibrary"; +import { NarrativeLayout, StickySection } from "../NarrativeLayout"; +import MetricVizMapper from "../MetricVizMapper"; +import RidersNarrative from "../contentModels/RidersNarrative"; + +const RidersNarrativePage: React.FC<{ + narrative: RidersNarrative; +}> = ({ narrative }) => { + return ( + + {narrative.title} + + {HTMLReactParser(narrative.introduction)} + + + ), + }, + ...narrative.sections.map((section) => { + let contents; + + // all sections except the conclusion should look like this + if (section.type === "text") { + contents = ( + {section.title} + } + rightContents={ + + {HTMLReactParser(section.body)} + + } + /> + ); + } else { + contents = ( + + + {section.title} + + + {HTMLReactParser(section.body)} + + + } + rightContents={} + /> + ); + } + + return { + title: section.title, + contents, + }; + }), + ]} + /> + ); +}; + +export default RidersNarrativePage; diff --git a/spotlight-client/src/RacialDisparitiesNarrativePage/index.ts b/spotlight-client/src/OtherNarrativesPageContainer/index.ts similarity index 92% rename from spotlight-client/src/RacialDisparitiesNarrativePage/index.ts rename to spotlight-client/src/OtherNarrativesPageContainer/index.ts index 27d8bedd..732e89db 100644 --- a/spotlight-client/src/RacialDisparitiesNarrativePage/index.ts +++ b/spotlight-client/src/OtherNarrativesPageContainer/index.ts @@ -15,4 +15,4 @@ // along with this program. If not, see . // ============================================================================= -export { default } from "./RacialDisparitiesNarrativePageContainer"; +export { default } from "./OtherNarrativesPageContainer"; diff --git a/spotlight-client/src/PageNarrative/PageNarrative.tsx b/spotlight-client/src/PageNarrative/PageNarrative.tsx index 85f042e9..d403effa 100644 --- a/spotlight-client/src/PageNarrative/PageNarrative.tsx +++ b/spotlight-client/src/PageNarrative/PageNarrative.tsx @@ -20,7 +20,7 @@ import useBreakpoint from "@w11r/use-breakpoint"; import React from "react"; import { isSystemNarrativeTypeId, NarrativeTypeId } from "../contentApi/types"; import NarrativeFooter from "../NarrativeFooter"; -import RacialDisparitiesNarrativePage from "../RacialDisparitiesNarrativePage"; +import OtherNarrativesPageContainer from "../OtherNarrativesPageContainer"; import { NarrativesSlug } from "../routerUtils/types"; import SystemNarrativePage from "../SystemNarrativePage"; import withRouteSync from "../withRouteSync"; @@ -41,7 +41,7 @@ const PageNarrative: React.FC = ({ narrativeTypeId }) => { {isSystemNarrativeTypeId(narrativeTypeId) ? ( ) : ( - + )} {showFooter && } diff --git a/spotlight-client/src/SiteNavigation/SiteNavigation.tsx b/spotlight-client/src/SiteNavigation/SiteNavigation.tsx index 372522c9..dd368933 100644 --- a/spotlight-client/src/SiteNavigation/SiteNavigation.tsx +++ b/spotlight-client/src/SiteNavigation/SiteNavigation.tsx @@ -72,6 +72,12 @@ const SiteNavigation: React.FC = ({ openShareModal }) => { label: tenant.racialDisparitiesNarrative.title, }); } + if (tenant.ridersNarrative) { + narrativeOptions.push({ + id: "Riders", + label: tenant.ridersNarrative.title, + }); + } } return ( diff --git a/spotlight-client/src/contentApi/sources/us_id.ts b/spotlight-client/src/contentApi/sources/us_id.ts index 7f484e4d..c05b4e95 100644 --- a/spotlight-client/src/contentApi/sources/us_id.ts +++ b/spotlight-client/src/contentApi/sources/us_id.ts @@ -281,6 +281,22 @@ const content: TenantContent = { probation revocation admission are included in the probation page.

${demographicsBoilerplate} ${paroleBoilerplate}`, }, + RidersPopulationHistorical: { + name: "Population Over Time", + methodology: `tbd`, + }, + RidersPopulationCurrent: { + name: "Current population of Rider", + methodology: `tbd`, + }, + RidersOriginalCharge: { + name: "Original Charge", + methodology: `tbd`, + }, + RidersReincarcerationRate: { + name: "Reincarceration Rate", + methodology: `tbd`, + }, }, systemNarratives: { Sentencing: { @@ -788,6 +804,39 @@ const content: TenantContent = { }, }, }, + ridersNarrative: { + title: "Rider Program", + introduction: `Retained jurisdiction or "riders" are individuals whom the court has retained jurisdiction over sentenced to a period of incarceration in an IDOC facility. + The IDOC assesses riders to determine their needs and places them in the appropriate facilities to receive intensive programming and education. Upon completion of a rider, the court determines whether to place the resident on probation or sentence them to term.`, + sections: [ + { + title: "How prevalent are “riders” in the IDOC population?", + body: `The Rider program was first introduced in 2005 as an alternative to a longer prison sentence. Since then, the Rider population has risen to the point that 16% of IDOC’s institutional population is comprised of people on retained jurisdiction.`, + metricTypeId: "RidersPopulationHistorical", + }, + { + title: "Who is on a Rider in Idaho?", + body: + "Some insights about the demographic composition of riders as compared to termers in Idaho.", + metricTypeId: "RidersPopulationCurrent", + }, + { + title: "Why are people placed on Riders?", + body: `In general, riders are considered a last resort” before full incarceration. However, most people placed on riders have not committed a serious or violent offense; XX% of riders in the past three years have been for drug use or possession.`, + metricTypeId: "RidersOriginalCharge", + }, + { + title: "What is the reincarceration rate for riders?", + body: `Despite high programming completion rates during rider sentences, those individuals tend to return to prison at higher rates compared to similar individuals who were simply sent to probation. XX% of riders return to prison within three years as compared to YY% of those on probation in a similar time period.`, + metricTypeId: "RidersReincarcerationRate", + }, + { + title: "Where do we go from here? ", + body: `

Instead of sending low-level offenders to Riders, IDOC believes that sending the same people to treatment programs outside of prison (as conditions of probation or otherwise) would result in better social outcomes and more money saved that would otherwise be spent on incarceration. Money saved can be reinvested into community-based treatment programs to prevent people from entering the system in the first place.

`, + type: "text", + }, + ], + }, }; export default content; diff --git a/spotlight-client/src/contentApi/types.ts b/spotlight-client/src/contentApi/types.ts index f11ccfcf..18070755 100644 --- a/spotlight-client/src/contentApi/types.ts +++ b/spotlight-client/src/contentApi/types.ts @@ -72,7 +72,8 @@ export type TenantContent = { "PrisonAdmissionReasonsCurrent" >]?: MetricContent & { fieldMapping?: CategoryFieldMapping[] }; } & - { [key in MetricTypeId]?: MetricContent }; + { [key in MetricTypeId]?: MetricContent } & + { [key in RidersMetricTypeId]?: MetricContent }; systemNarratives: { [key in SystemNarrativeTypeId]?: SystemNarrativeContent; }; @@ -88,6 +89,7 @@ export type TenantContent = { ProgramRegions: MapData; }; racialDisparitiesNarrative?: RacialDisparitiesNarrativeContent; + ridersNarrative?: RidersNarrativeContent; // if categories are enumerated for any of the keys here, they will be the only ones used; // otherwise categories default to including all values in the associated unions demographicCategories?: DemographicCategoryFilter; @@ -119,9 +121,20 @@ export const MetricTypeIdList = [ "ParoleRevocationsAggregate", "ParoleProgrammingCurrent", ] as const; +export const RidersMetricTypeIdList = [ + "RidersPopulationHistorical", + "RidersPopulationCurrent", + "RidersOriginalCharge", + "RidersReincarcerationRate", +] as const; export type MetricTypeId = typeof MetricTypeIdList[number]; -export function isMetricTypeId(x: string): x is MetricTypeId { - return MetricTypeIdList.includes(x as MetricTypeId); +export type RidersMetricTypeId = typeof RidersMetricTypeIdList[number]; + +export type AllMetricsTypeId = MetricTypeId | RidersMetricTypeId; +export function isMetricTypeId(x: string): x is AllMetricsTypeId { + return [...MetricTypeIdList, ...RidersMetricTypeIdList].includes( + x as AllMetricsTypeId + ); } type MetricContent = { name: string; methodology: string }; @@ -144,9 +157,14 @@ export function isSystemNarrativeTypeId(x: string): x is SystemNarrativeTypeId { return SystemNarrativeTypeIdList.includes(x as SystemNarrativeTypeId); } -export type NarrativeTypeId = SystemNarrativeTypeId | "RacialDisparities"; +export type NarrativeTypeId = + | SystemNarrativeTypeId + | "RacialDisparities" + | "Riders"; export function isNarrativeTypeId(x: string): x is NarrativeTypeId { - return isSystemNarrativeTypeId(x) || x === "RacialDisparities"; + return ( + isSystemNarrativeTypeId(x) || x === "RacialDisparities" || x === "Riders" + ); } type NarrativeSection = { @@ -158,14 +176,26 @@ type SystemNarrativeSection = NarrativeSection & { metricTypeId: MetricTypeId; }; -export type SystemNarrativeContent = { +type NarrativeContent = { title: string; - previewTitle?: string; - preview: MetricTypeId; introduction: string; +}; + +export type SystemNarrativeContent = NarrativeContent & { + previewTitle?: string; + preview?: MetricTypeId; sections: SystemNarrativeSection[]; }; +type RidersNarrativeSection = NarrativeSection & { + metricTypeId?: RidersMetricTypeId; + type?: "text" | "metric"; +}; + +export type RidersNarrativeContent = NarrativeContent & { + sections: RidersNarrativeSection[]; +}; + export type RacialDisparitiesChartLabels = { totalPopulation: string; totalSentenced: string; diff --git a/spotlight-client/src/contentModels/Metric.ts b/spotlight-client/src/contentModels/Metric.ts index 344a81d5..5eafbbe0 100644 --- a/spotlight-client/src/contentModels/Metric.ts +++ b/spotlight-client/src/contentModels/Metric.ts @@ -25,9 +25,9 @@ import { when, } from "mobx"; import { + AllMetricsTypeId, DemographicCategoryFilter, LocalityLabels, - MetricTypeId, TenantId, } from "../contentApi/types"; import RootStore from "../DataStore/RootStore"; @@ -73,7 +73,7 @@ function formatUnknownCounts(unknowns: UnknownCounts) { } export type BaseMetricConstructorOptions = { - id: MetricTypeId; + id: AllMetricsTypeId; name: string; methodology: string; tenantId: TenantId; @@ -105,7 +105,7 @@ export type BaseMetricConstructorOptions = { export default abstract class Metric implements Hydratable { // metadata properties - readonly id: MetricTypeId; + readonly id: AllMetricsTypeId; readonly methodology: string; diff --git a/spotlight-client/src/contentModels/RidersNarrative.ts b/spotlight-client/src/contentModels/RidersNarrative.ts new file mode 100644 index 00000000..19459049 --- /dev/null +++ b/spotlight-client/src/contentModels/RidersNarrative.ts @@ -0,0 +1,77 @@ +// 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 . +// ============================================================================= + +import { RidersNarrativeContent } from "../contentApi/types"; +import Metric from "./Metric"; +import { MetricMapping, MetricRecord } from "./types"; + +export type RidersNarrativeSection = { + title: string; + body: string; + type?: "text" | "metric"; + metric?: Metric; +}; + +type ConstructorArgs = { + title: string; + type?: string; + introduction: string; + sections: RidersNarrativeSection[]; +}; + +export default class RidersNarrative { + readonly id = "Riders"; + + readonly title: string; + + readonly introduction: string; + + readonly sections: RidersNarrativeSection[]; + + constructor({ title, introduction, sections }: ConstructorArgs) { + this.title = title; + this.introduction = introduction; + this.sections = sections; + } +} + +export function createRidersNarrative({ + content, + allMetrics, +}: { + content: RidersNarrativeContent; + allMetrics: MetricMapping; +}): RidersNarrative { + const sections: RidersNarrativeSection[] = []; + // building sections in a type-safe way: make sure the related metric + // actually exists or else the section is omitted + content.sections.forEach(({ title, body, type, metricTypeId }) => { + const metric = metricTypeId && allMetrics.get(metricTypeId); + if (metric instanceof Metric) { + sections.push({ title, body, metric }); + } + if (!metric) { + sections.push({ title, body, type }); + } + }); + + return new RidersNarrative({ + title: content.title, + introduction: content.introduction, + sections, + }); +} diff --git a/spotlight-client/src/contentModels/SystemNarrative.ts b/spotlight-client/src/contentModels/SystemNarrative.ts index 04f9d2c8..f5328d7a 100644 --- a/spotlight-client/src/contentModels/SystemNarrative.ts +++ b/spotlight-client/src/contentModels/SystemNarrative.ts @@ -23,6 +23,8 @@ import { import Metric from "./Metric"; import { MetricMapping, MetricRecord } from "./types"; +type NarrativeTypeId = SystemNarrativeTypeId; + export type SystemNarrativeSection = { title: string; body: string; @@ -30,22 +32,22 @@ export type SystemNarrativeSection = { }; type ConstructorArgs = { - id: SystemNarrativeTypeId; + id: NarrativeTypeId; title: string; previewTitle?: string; - preview: MetricTypeId; + preview?: MetricTypeId; introduction: string; sections: SystemNarrativeSection[]; }; export default class SystemNarrative { - readonly id: SystemNarrativeTypeId; + readonly id: NarrativeTypeId; readonly title: string; readonly previewTitle?: string; - readonly preview: MetricTypeId; + readonly preview?: MetricTypeId; readonly introduction: string; @@ -73,7 +75,7 @@ export function createSystemNarrative({ content, allMetrics, }: { - id: SystemNarrativeTypeId; + id: NarrativeTypeId; content: SystemNarrativeContent; allMetrics: MetricMapping; }): SystemNarrative { diff --git a/spotlight-client/src/contentModels/Tenant.ts b/spotlight-client/src/contentModels/Tenant.ts index dcf9c523..1aba7816 100644 --- a/spotlight-client/src/contentModels/Tenant.ts +++ b/spotlight-client/src/contentModels/Tenant.ts @@ -20,10 +20,11 @@ import { SystemNarrativeTypeIdList, TenantId } from "../contentApi/types"; import RootStore from "../DataStore/RootStore"; import createMetricMapping from "./createMetricMapping"; import RacialDisparitiesNarrative from "./RacialDisparitiesNarrative"; +import RidersNarrative, { createRidersNarrative } from "./RidersNarrative"; import { createSystemNarrative } from "./SystemNarrative"; import { MetricMapping, SystemNarrativeMapping } from "./types"; -type InitOptions = { +export type InitOptions = { id: TenantId; name: string; docName: string; @@ -36,6 +37,7 @@ type InitOptions = { metrics: MetricMapping; systemNarratives: SystemNarrativeMapping; racialDisparitiesNarrative?: RacialDisparitiesNarrative; + ridersNarrative?: RidersNarrative; }; /** @@ -64,12 +66,14 @@ export default class Tenant { readonly smallDataDisclaimer: string; - readonly metrics: InitOptions["metrics"]; + readonly metrics: MetricMapping; readonly systemNarratives: SystemNarrativeMapping; readonly racialDisparitiesNarrative?: RacialDisparitiesNarrative; + readonly ridersNarrative?: RidersNarrative; + constructor({ id, name, @@ -83,6 +87,7 @@ export default class Tenant { metrics, systemNarratives, racialDisparitiesNarrative, + ridersNarrative, }: InitOptions) { this.id = id; this.name = name; @@ -96,6 +101,7 @@ export default class Tenant { this.metrics = metrics; this.systemNarratives = systemNarratives; this.racialDisparitiesNarrative = racialDisparitiesNarrative; + this.ridersNarrative = ridersNarrative; } } @@ -139,6 +145,18 @@ function getSystemNarrativesForTenant({ return narrativeMapping; } +function getRidersNarrativeForTenant({ + allTenantContent, + metrics: allMetrics, +}: MetricRelatedModelOptions) { + const content = allTenantContent.ridersNarrative; + if (content) { + return createRidersNarrative({ content, allMetrics }); + } + + return undefined; +} + /** * Factory function for creating an instance of the `Tenant` specified by `tenantId`. */ @@ -175,5 +193,9 @@ export function createTenant({ metrics, }), racialDisparitiesNarrative, + ridersNarrative: getRidersNarrativeForTenant({ + allTenantContent, + metrics, + }), }); } diff --git a/spotlight-client/src/contentModels/__fixtures__/tenant_content_exhaustive.ts b/spotlight-client/src/contentModels/__fixtures__/tenant_content_exhaustive.ts index 121f263d..47178d03 100644 --- a/spotlight-client/src/contentModels/__fixtures__/tenant_content_exhaustive.ts +++ b/spotlight-client/src/contentModels/__fixtures__/tenant_content_exhaustive.ts @@ -153,6 +153,22 @@ const content: ExhaustiveTenantContent = { name: "test ParoleProgrammingCurrent name", methodology: "test ParoleProgrammingCurrent methodology", }, + RidersPopulationHistorical: { + name: "test RidersPopulationHistorical name", + methodology: "test RidersPopulationHistorical methodology", + }, + RidersPopulationCurrent: { + name: "test RidersPopulationCurrent name", + methodology: "test RidersPopulationCurrent methodology", + }, + RidersOriginalCharge: { + name: "test RidersOriginalCharge name", + methodology: "test RidersOriginalCharge methodology", + }, + RidersReincarcerationRate: { + name: "test RidersReincarcerationRate name", + methodology: "test RidersReincarcerationRate methodology", + }, }, systemNarratives: { Prison: { @@ -377,6 +393,36 @@ const content: ExhaustiveTenantContent = { }, }, }, + ridersNarrative: { + title: "test rider narrative", + introduction: `introduction copy`, + sections: [ + { + title: "test RidersPopulationHistorical title", + body: "test RidersPopulationHistorical body", + type: "metric", + metricTypeId: "RidersPopulationHistorical", + }, + { + title: "test RidersPopulationCurrent title", + body: "test RidersPopulationCurrent body", + type: "metric", + metricTypeId: "RidersPopulationCurrent", + }, + { + title: "test RidersOriginalCharge title", + body: "test RidersOriginalCharge body", + type: "metric", + metricTypeId: "RidersOriginalCharge", + }, + { + title: "test RidersReincarcerationRate title", + body: "test RidersReincarcerationRate body", + type: "metric", + metricTypeId: "RidersReincarcerationRate", + }, + ], + }, }; export default content; diff --git a/spotlight-client/src/contentModels/createMetricMapping.ts b/spotlight-client/src/contentModels/createMetricMapping.ts index 1121c80c..daafeb79 100644 --- a/spotlight-client/src/contentModels/createMetricMapping.ts +++ b/spotlight-client/src/contentModels/createMetricMapping.ts @@ -16,7 +16,12 @@ // ============================================================================= import { assertNever } from "assert-never"; -import { MetricTypeIdList, TenantContent, TenantId } from "../contentApi/types"; +import { + MetricTypeIdList, + RidersMetricTypeIdList, + TenantContent, + TenantId, +} from "../contentApi/types"; import { parolePopulationCurrent, parolePopulationHistorical, @@ -83,7 +88,7 @@ export default function createMetricMapping({ // to maintain type safety we iterate through all of the known metrics; // iterating through the metadata object's keys widens the type to `string`, // which prevents us from guaranteeing exhaustiveness at the type level - MetricTypeIdList.forEach((metricType) => { + [...MetricTypeIdList, ...RidersMetricTypeIdList].forEach((metricType) => { // not all metrics are required; metadata object is the source of truth // for which metrics to include const metadata = metadataMapping[metricType]; @@ -530,6 +535,80 @@ export default function createMetricMapping({ }) ); break; + case "RidersPopulationHistorical": + metricMapping.set( + metricType, + new HistoricalPopulationBreakdownMetric({ + ...metadata, + id: metricType, + tenantId, + defaultDemographicView: "total", + defaultLocalityId: undefined, + localityLabels: undefined, + dataTransformer: prisonPopulationHistorical, + sourceFileName: "incarceration_population_by_month_by_demographics", + rootStore, + }) + ); + break; + case "RidersPopulationCurrent": + // should be bar chart pair + metricMapping.set( + metricType, + new HistoricalPopulationBreakdownMetric({ + ...metadata, + id: metricType, + tenantId, + defaultDemographicView: "total", + defaultLocalityId: undefined, + localityLabels: undefined, + dataTransformer: prisonPopulationHistorical, + sourceFileName: "incarceration_population_by_month_by_demographics", + rootStore, + }) + ); + break; + case "RidersOriginalCharge": + metricMapping.set( + metricType, + new DemographicsByCategoryMetric({ + ...metadata, + demographicFilter, + id: metricType, + tenantId, + defaultDemographicView: "total", + defaultLocalityId: undefined, + localityLabels: undefined, + dataTransformer: (rawRecords: RawMetricData) => { + let fieldMapping; + + if ("fieldMapping" in metadata) { + fieldMapping = metadata.fieldMapping; + } + return prisonAdmissionReasons(rawRecords, fieldMapping); + }, + sourceFileName: "incarceration_population_by_admission_reason", + rootStore, + }) + ); + break; + case "RidersReincarcerationRate": + metricMapping.set( + metricType, + new RecidivismRateMetric({ + ...metadata, + demographicFilter, + id: metricType, + tenantId, + defaultDemographicView: "total", + defaultLocalityId: undefined, + localityLabels: undefined, + dataTransformer: recidivismRateAllFollowup, + sourceFileName: "recidivism_rates_by_cohort_by_year", + rootStore, + }) + ); + break; default: assertNever(metricType); diff --git a/spotlight-client/src/contentModels/types.ts b/spotlight-client/src/contentModels/types.ts index d407ffdf..d07d5187 100644 --- a/spotlight-client/src/contentModels/types.ts +++ b/spotlight-client/src/contentModels/types.ts @@ -15,7 +15,11 @@ // along with this program. If not, see . // ============================================================================= -import { MetricTypeId, SystemNarrativeTypeId } from "../contentApi/types"; +import { + MetricTypeId, + RidersMetricTypeId, + SystemNarrativeTypeId, +} from "../contentApi/types"; import { DemographicFieldKey, DemographicsByCategoryRecord, @@ -54,7 +58,10 @@ export type MetricRecord = | SentenceTypeByLocationRecord | SupervisionSuccessRateMonthlyRecord; -export type MetricMapping = Map>; +export type MetricMapping = Map< + MetricTypeId | RidersMetricTypeId, + Metric +>; export type DemographicCategoryRecords = { label: string;