diff --git a/agency-dashboard/package.json b/agency-dashboard/package.json
index 571874aeb..e86c41c5a 100644
--- a/agency-dashboard/package.json
+++ b/agency-dashboard/package.json
@@ -18,7 +18,7 @@
"web-vitals": "^2.1.4"
},
"scripts": {
- "start": "react-app-rewired start",
+ "dev": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject",
diff --git a/publisher/src/components/DataViz/BarChart.tsx b/common/components/DataViz/BarChart.tsx
similarity index 99%
rename from publisher/src/components/DataViz/BarChart.tsx
rename to common/components/DataViz/BarChart.tsx
index 23bbcca3a..f4de31689 100644
--- a/publisher/src/components/DataViz/BarChart.tsx
+++ b/common/components/DataViz/BarChart.tsx
@@ -27,7 +27,7 @@ import {
} from "recharts";
import styled from "styled-components/macro";
-import { Datapoint } from "../../shared/types";
+import { Datapoint } from "../../types";
import { rem } from "../../utils";
import { palette } from "../GlobalStyles";
import Tooltip from "./Tooltip";
diff --git a/publisher/src/components/DataViz/Legend.tsx b/common/components/DataViz/Legend.tsx
similarity index 100%
rename from publisher/src/components/DataViz/Legend.tsx
rename to common/components/DataViz/Legend.tsx
diff --git a/publisher/src/components/DataViz/Tooltip.tsx b/common/components/DataViz/Tooltip.tsx
similarity index 98%
rename from publisher/src/components/DataViz/Tooltip.tsx
rename to common/components/DataViz/Tooltip.tsx
index 08a8d4b31..dd68ed829 100644
--- a/publisher/src/components/DataViz/Tooltip.tsx
+++ b/common/components/DataViz/Tooltip.tsx
@@ -19,7 +19,7 @@ import React from "react";
import { TooltipProps as RechartsTooltipProps } from "recharts";
import styled from "styled-components/macro";
-import { Datapoint } from "../../shared/types";
+import { Datapoint } from "../../types";
import { formatNumberInput } from "../../utils";
import { palette, typography } from "../GlobalStyles";
import { LegendColor } from "./Legend";
diff --git a/common/components/DataViz/utils.test.ts b/common/components/DataViz/utils.test.ts
new file mode 100644
index 000000000..cabc85cdb
--- /dev/null
+++ b/common/components/DataViz/utils.test.ts
@@ -0,0 +1,2066 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 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 { Datapoint } from "../../types";
+import {
+ fillTimeGapsBetweenDatapoints,
+ filterByTimeRange,
+ filterNullDatapoints,
+ incrementMonth,
+ incrementYear,
+ transformData,
+ transformToRelativePerchanges,
+} from "./utils";
+
+const testDatapoints: Datapoint[] = [
+ {
+ start_date: "Wed, 01 Jan 2020 00:00:00 GMT",
+ end_date: "Sat, 01 Feb 2020 00:00:00 GMT",
+ Pretrial: 50515,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 45427,
+ "Transfer or Hold": 31375,
+ Unknown: 29749,
+ },
+ {
+ start_date: "Sat, 01 Feb 2020 00:00:00 GMT",
+ end_date: "Sun, 01 Mar 2020 00:00:00 GMT",
+ Pretrial: 54758,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 65594,
+ "Transfer or Hold": 89614,
+ Unknown: 73426,
+ },
+ {
+ start_date: "Sun, 01 Mar 2020 00:00:00 GMT",
+ end_date: "Wed, 01 Apr 2020 00:00:00 GMT",
+ Pretrial: 52304,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 94209,
+ "Transfer or Hold": 82736,
+ Unknown: 62748,
+ },
+ {
+ start_date: "Wed, 01 Apr 2020 00:00:00 GMT",
+ end_date: "Fri, 01 May 2020 00:00:00 GMT",
+ Pretrial: 23335,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 90737,
+ "Transfer or Hold": 57573,
+ Unknown: 93184,
+ },
+ {
+ start_date: "Fri, 01 May 2020 00:00:00 GMT",
+ end_date: "Mon, 01 Jun 2020 00:00:00 GMT",
+ Pretrial: 39489,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 27098,
+ "Transfer or Hold": 41196,
+ Unknown: 28077,
+ },
+ {
+ start_date: "Mon, 01 Jun 2020 00:00:00 GMT",
+ end_date: "Wed, 01 Jul 2020 00:00:00 GMT",
+ Pretrial: 66362,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 44360,
+ "Transfer or Hold": 61195,
+ Unknown: 31909,
+ },
+ {
+ start_date: "Wed, 01 Jul 2020 00:00:00 GMT",
+ end_date: "Sat, 01 Aug 2020 00:00:00 GMT",
+ Pretrial: 69380,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 53465,
+ "Transfer or Hold": 94375,
+ Unknown: 13442,
+ },
+ {
+ start_date: "Sat, 01 Aug 2020 00:00:00 GMT",
+ end_date: "Tue, 01 Sep 2020 00:00:00 GMT",
+ Pretrial: 30380,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 82698,
+ "Transfer or Hold": 41340,
+ Unknown: 40324,
+ },
+ {
+ start_date: "Tue, 01 Sep 2020 00:00:00 GMT",
+ end_date: "Thu, 01 Oct 2020 00:00:00 GMT",
+ Pretrial: 30338,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 74647,
+ "Transfer or Hold": 85087,
+ Unknown: 28198,
+ },
+ {
+ start_date: "Thu, 01 Oct 2020 00:00:00 GMT",
+ end_date: "Sun, 01 Nov 2020 00:00:00 GMT",
+ Pretrial: 35571,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 23313,
+ "Transfer or Hold": 41785,
+ Unknown: 63556,
+ },
+ {
+ start_date: "Sun, 01 Nov 2020 00:00:00 GMT",
+ end_date: "Tue, 01 Dec 2020 00:00:00 GMT",
+ Pretrial: 65779,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 53664,
+ "Transfer or Hold": 65962,
+ Unknown: 59783,
+ },
+ {
+ start_date: "Tue, 01 Dec 2020 00:00:00 GMT",
+ end_date: "Fri, 01 Jan 2021 00:00:00 GMT",
+ Pretrial: 46192,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 49529,
+ "Transfer or Hold": 78956,
+ Unknown: 12664,
+ },
+ {
+ start_date: "Fri, 01 Jan 2021 00:00:00 GMT",
+ end_date: "Mon, 01 Feb 2021 00:00:00 GMT",
+ Pretrial: 34311,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 24082,
+ "Transfer or Hold": 70165,
+ Unknown: 13003,
+ },
+ {
+ start_date: "Mon, 01 Feb 2021 00:00:00 GMT",
+ end_date: "Mon, 01 Mar 2021 00:00:00 GMT",
+ Pretrial: 94307,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 67799,
+ "Transfer or Hold": 80348,
+ Unknown: 24894,
+ },
+ {
+ start_date: "Mon, 01 Mar 2021 00:00:00 GMT",
+ end_date: "Thu, 01 Apr 2021 00:00:00 GMT",
+ Pretrial: 55116,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 65802,
+ "Transfer or Hold": 46493,
+ Unknown: 47052,
+ },
+ {
+ start_date: "Thu, 01 Apr 2021 00:00:00 GMT",
+ end_date: "Sat, 01 May 2021 00:00:00 GMT",
+ Pretrial: 60342,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 27224,
+ "Transfer or Hold": 14822,
+ Unknown: 35285,
+ },
+ {
+ start_date: "Sat, 01 May 2021 00:00:00 GMT",
+ end_date: "Tue, 01 Jun 2021 00:00:00 GMT",
+ Pretrial: 76072,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 40997,
+ "Transfer or Hold": 84448,
+ Unknown: 86808,
+ },
+ {
+ start_date: "Tue, 01 Jun 2021 00:00:00 GMT",
+ end_date: "Thu, 01 Jul 2021 00:00:00 GMT",
+ Pretrial: 55707,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 21701,
+ "Transfer or Hold": 97274,
+ Unknown: 70005,
+ },
+ {
+ start_date: "Thu, 01 Jul 2021 00:00:00 GMT",
+ end_date: "Sun, 01 Aug 2021 00:00:00 GMT",
+ "Transfer or Hold": 92055,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 64106,
+ Pretrial: 19694,
+ Unknown: 51952,
+ },
+ {
+ start_date: "Sun, 01 Aug 2021 00:00:00 GMT",
+ end_date: "Wed, 01 Sep 2021 00:00:00 GMT",
+ Pretrial: 19163,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 95457,
+ "Transfer or Hold": 50399,
+ Unknown: 37598,
+ },
+ {
+ start_date: "Wed, 01 Sep 2021 00:00:00 GMT",
+ end_date: "Fri, 01 Oct 2021 00:00:00 GMT",
+ Pretrial: 56132,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 49555,
+ "Transfer or Hold": 67307,
+ Unknown: 16254,
+ },
+ {
+ start_date: "Fri, 01 Oct 2021 00:00:00 GMT",
+ end_date: "Mon, 01 Nov 2021 00:00:00 GMT",
+ Pretrial: 74036,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 53503,
+ "Transfer or Hold": 29868,
+ Unknown: 94671,
+ },
+ {
+ start_date: "Mon, 01 Nov 2021 00:00:00 GMT",
+ end_date: "Wed, 01 Dec 2021 00:00:00 GMT",
+ Sentenced: 34289,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Pretrial: 30696,
+ "Transfer or Hold": 56361,
+ Unknown: 18829,
+ },
+ {
+ start_date: "Wed, 01 Dec 2021 00:00:00 GMT",
+ end_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ Pretrial: 21968,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 47320,
+ "Transfer or Hold": 43536,
+ Unknown: 71164,
+ },
+ {
+ start_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Feb 2022 00:00:00 GMT",
+ Pretrial: 38210,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 70537,
+ "Transfer or Hold": 87658,
+ Unknown: 84018,
+ },
+ {
+ start_date: "Tue, 01 Feb 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Mar 2022 00:00:00 GMT",
+ Pretrial: 37196,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 34013,
+ "Transfer or Hold": 35342,
+ Unknown: 16376,
+ },
+ {
+ start_date: "Tue, 01 Mar 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ Pretrial: 13935,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 28989,
+ "Transfer or Hold": 56841,
+ Unknown: 52659,
+ },
+ {
+ start_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ end_date: "Sun, 01 May 2022 00:00:00 GMT",
+ Pretrial: 54440,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Unknown: 55161,
+ "Transfer or Hold": 17782,
+ Sentenced: 90906,
+ },
+ {
+ start_date: "Sun, 01 May 2022 00:00:00 GMT",
+ end_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ Pretrial: 49829,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 85046,
+ "Transfer or Hold": 60630,
+ Unknown: 10732,
+ },
+ {
+ start_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ Pretrial: 15321,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 68658,
+ "Transfer or Hold": 73603,
+ Unknown: 72871,
+ },
+ {
+ start_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ end_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ Pretrial: 61998,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 29566,
+ "Transfer or Hold": 98614,
+ Unknown: 63892,
+ },
+ {
+ start_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ Pretrial: 49052,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 19779,
+ "Transfer or Hold": 38057,
+ Unknown: 49554,
+ },
+ {
+ start_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ end_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ Sentenced: 52422,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Pretrial: 34244,
+ "Transfer or Hold": 62878,
+ Unknown: 48777,
+ },
+ {
+ start_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ Pretrial: 67196,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 54738,
+ "Transfer or Hold": 95846,
+ Unknown: 94767,
+ },
+ {
+ start_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ Pretrial: 76254,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 63070,
+ "Transfer or Hold": 25465,
+ Unknown: 81989,
+ },
+ {
+ start_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
+ Unknown: 29537,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ "Transfer or Hold": 52722,
+ Sentenced: 84901,
+ Pretrial: 34694,
+ },
+];
+
+const testDatapoints2: Datapoint[] = [
+ {
+ start_date: "Tue, 01 Mar 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 97164,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 36671,
+ Black: 26028,
+ "External / Unknown": 47948,
+ Hispanic: 57558,
+ "Native Hawaiian / Pacific Islander": 90632,
+ Other: 16338,
+ White: 22298,
+ },
+ {
+ start_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ end_date: "Sun, 01 May 2022 00:00:00 GMT",
+ Hispanic: 66829,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ "External / Unknown": 97373,
+ Black: 20261,
+ Asian: 63835,
+ "American Indian / Alaskan Native": 31596,
+ "Native Hawaiian / Pacific Islander": 82033,
+ Other: 29044,
+ White: 96511,
+ },
+ {
+ start_date: "Sun, 01 May 2022 00:00:00 GMT",
+ end_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 79637,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 14139,
+ Black: 85121,
+ "External / Unknown": 38446,
+ Hispanic: 31772,
+ "Native Hawaiian / Pacific Islander": 88002,
+ Other: 33259,
+ White: 70561,
+ },
+ {
+ start_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 45039,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 65632,
+ Black: 39540,
+ "External / Unknown": 16119,
+ Hispanic: 14102,
+ "Native Hawaiian / Pacific Islander": 52909,
+ Other: 60103,
+ White: 73688,
+ },
+ {
+ start_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ end_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 80150,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 74203,
+ Black: 23688,
+ "External / Unknown": 44627,
+ Hispanic: 65335,
+ "Native Hawaiian / Pacific Islander": 56843,
+ Other: 58110,
+ White: 62313,
+ },
+ {
+ start_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 21221,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 36873,
+ Black: 30958,
+ "External / Unknown": 40857,
+ Hispanic: 85505,
+ "Native Hawaiian / Pacific Islander": 66954,
+ Other: 93569,
+ White: 52647,
+ },
+ {
+ start_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ end_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ Other: 69493,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ White: 37434,
+ "Native Hawaiian / Pacific Islander": 87941,
+ "American Indian / Alaskan Native": 55887,
+ Asian: 69104,
+ Black: 73150,
+ "External / Unknown": 64346,
+ Hispanic: 33261,
+ },
+ {
+ start_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 69650,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 24615,
+ Black: 55423,
+ "External / Unknown": 98968,
+ Hispanic: 68576,
+ "Native Hawaiian / Pacific Islander": 94690,
+ Other: 64054,
+ White: 81617,
+ },
+ {
+ start_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 17038,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 44387,
+ Black: 51777,
+ "External / Unknown": 54533,
+ Hispanic: 16689,
+ "Native Hawaiian / Pacific Islander": 69182,
+ Other: 99842,
+ White: 29578,
+ },
+ {
+ start_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
+ "Native Hawaiian / Pacific Islander": 89112,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Other: 37341,
+ White: 59971,
+ "American Indian / Alaskan Native": 18236,
+ Asian: 60388,
+ Black: 35380,
+ "External / Unknown": 36941,
+ Hispanic: 90445,
+ },
+];
+
+const testDatapoints2Percentages: Datapoint[] = [
+ {
+ start_date: "Tue, 01 Mar 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 0.24621107498790026,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 0.09292337008440668,
+ Black: 0.06595428203640308,
+ "External / Unknown": 0.12149899781318022,
+ Hispanic: 0.145850490450718,
+ "Native Hawaiian / Pacific Islander": 0.22965915512230226,
+ Other: 0.04140007145807413,
+ White: 0.05650255804701536,
+ },
+ {
+ start_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ end_date: "Sun, 01 May 2022 00:00:00 GMT",
+ Hispanic: 0.13709018999675884,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ "External / Unknown": 0.1997468624482545,
+ Black: 0.04156256025863519,
+ Asian: 0.1309484247623502,
+ "American Indian / Alaskan Native": 0.06481470085049294,
+ "Native Hawaiian / Pacific Islander": 0.16827903389253346,
+ Other: 0.059579635760910146,
+ White: 0.1979785920300647,
+ },
+ {
+ start_date: "Sun, 01 May 2022 00:00:00 GMT",
+ end_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 0.18060856766386127,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 0.03206580531912722,
+ Black: 0.19304571854936192,
+ "External / Unknown": 0.08719159426403318,
+ Hispanic: 0.07205564513751397,
+ "Native Hawaiian / Pacific Islander": 0.19957953176984466,
+ Other: 0.07542800898994641,
+ White: 0.16002512830631133,
+ },
+ {
+ start_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 0.1226779468964841,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 0.1787694889031738,
+ Black: 0.10769968294782258,
+ "External / Unknown": 0.04390518941416166,
+ Hispanic: 0.0384112526284824,
+ "Native Hawaiian / Pacific Islander": 0.1441143784796749,
+ Other: 0.16370951047579618,
+ White: 0.2007125502544044,
+ },
+ {
+ start_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ end_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 0.17226593647975688,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 0.1594840834012152,
+ Black: 0.0509124828862443,
+ "External / Unknown": 0.09591655579890343,
+ Hispanic: 0.14042414173306195,
+ "Native Hawaiian / Pacific Islander": 0.12217233471389669,
+ Other: 0.12489549056567276,
+ White: 0.1339289744212488,
+ },
+ {
+ start_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 0.04951421424971534,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 0.08603447632202789,
+ Black: 0.0722332144923749,
+ "External / Unknown": 0.09533020364735968,
+ Hispanic: 0.1995058144961081,
+ "Native Hawaiian / Pacific Islander": 0.15622141750508653,
+ Other: 0.21832126257629778,
+ White: 0.12283939671102981,
+ },
+ {
+ start_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ end_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ Other: 0.14164438175681185,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ White: 0.07629999836939684,
+ "Native Hawaiian / Pacific Islander": 0.17924609062892363,
+ "American Indian / Alaskan Native": 0.11391189851125931,
+ Asian: 0.14085150097020888,
+ Black: 0.14909827645245977,
+ "External / Unknown": 0.13115348867546106,
+ Hispanic: 0.06779436463547867,
+ },
+ {
+ start_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 0.12491189810489013,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 0.04414510225200101,
+ Black: 0.09939687191195011,
+ "External / Unknown": 0.1774914677910232,
+ Hispanic: 0.12298576201638112,
+ "Native Hawaiian / Pacific Islander": 0.16981920504740913,
+ Other: 0.11487590410926966,
+ White: 0.14637378876707563,
+ },
+ {
+ start_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ "American Indian / Alaskan Native": 0.044482619978800396,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Asian: 0.11588508351913447,
+ Black: 0.13517881292653763,
+ "External / Unknown": 0.14237414692475184,
+ Hispanic: 0.04357145467931681,
+ "Native Hawaiian / Pacific Islander": 0.18061959240364883,
+ Other: 0.2606663777393702,
+ White: 0.07722191182843985,
+ },
+ {
+ start_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
+ "Native Hawaiian / Pacific Islander": 0.20829612869144068,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Other: 0.08728325861238763,
+ White: 0.14018007825830853,
+ "American Indian / Alaskan Native": 0.04262600101913448,
+ Asian: 0.14115480091815602,
+ Black: 0.08269949090025104,
+ "External / Unknown": 0.0863482728475459,
+ Hispanic: 0.21141196875277574,
+ },
+];
+
+const testDatapoints3: Datapoint[] = [
+ {
+ Total: 28293,
+ start_date: "Tue, 01 Mar 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 56673,
+ start_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ end_date: "Sun, 01 May 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 59646,
+ start_date: "Sun, 01 May 2022 00:00:00 GMT",
+ end_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 95570,
+ start_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 23877,
+ start_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ end_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 42551,
+ start_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 77484,
+ start_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ end_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: null,
+ start_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 12312,
+ start_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 0,
+ start_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+];
+
+const testDatapoints3WithoutNullDatapoints: Datapoint[] = [
+ {
+ Total: 28293,
+ start_date: "Tue, 01 Mar 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 56673,
+ start_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ end_date: "Sun, 01 May 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 59646,
+ start_date: "Sun, 01 May 2022 00:00:00 GMT",
+ end_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 95570,
+ start_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 23877,
+ start_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ end_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 42551,
+ start_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 77484,
+ start_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ end_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 12312,
+ start_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 0,
+ start_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ },
+];
+
+const testDatapoints4: Datapoint[] = [
+ {
+ Total: 52342,
+ start_date: "Thu, 01 Jan 2015 00:00:00 GMT",
+ end_date: "Fri, 01 Jan 2016 00:00:00 GMT",
+ frequency: "ANNUAL",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 41241,
+ start_date: "Tue, 01 Jan 2019 00:00:00 GMT",
+ end_date: "Wed, 01 Jan 2020 00:00:00 GMT",
+ frequency: "ANNUAL",
+ dataVizMissingData: 0,
+ },
+ {
+ Total: 74435,
+ start_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
+ frequency: "ANNUAL",
+ dataVizMissingData: 0,
+ },
+];
+
+const testDatapoints4WithGapDatapoints: Datapoint[] = [
+ {
+ Total: 52342,
+ start_date: "Thu, 01 Jan 2015 00:00:00 GMT",
+ end_date: "Fri, 01 Jan 2016 00:00:00 GMT",
+ frequency: "ANNUAL",
+ dataVizMissingData: 0,
+ },
+ {
+ start_date: "Fri, 01 Jan 2016 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2017 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ start_date: "Sun, 01 Jan 2017 00:00:00 GMT",
+ end_date: "Mon, 01 Jan 2018 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ start_date: "Mon, 01 Jan 2018 00:00:00 GMT",
+ end_date: "Tue, 01 Jan 2019 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ Total: 41241,
+ start_date: "Tue, 01 Jan 2019 00:00:00 GMT",
+ end_date: "Wed, 01 Jan 2020 00:00:00 GMT",
+ frequency: "ANNUAL",
+ dataVizMissingData: 0,
+ },
+ {
+ start_date: "Wed, 01 Jan 2020 00:00:00 GMT",
+ end_date: "Fri, 01 Jan 2021 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ start_date: "Fri, 01 Jan 2021 00:00:00 GMT",
+ end_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ Total: 74435,
+ start_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
+ frequency: "ANNUAL",
+ dataVizMissingData: 0,
+ },
+];
+
+const testDatapoints4WithGapDatapoints2: Datapoint[] = [
+ {
+ start_date: "Tue, 01 Jan 2013 00:00:00 GMT",
+ end_date: "Wed, 01 Jan 2014 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ start_date: "Wed, 01 Jan 2014 00:00:00 GMT",
+ end_date: "Thu, 01 Jan 2015 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ Total: 52342,
+ start_date: "Thu, 01 Jan 2015 00:00:00 GMT",
+ end_date: "Fri, 01 Jan 2016 00:00:00 GMT",
+ frequency: "ANNUAL",
+ dataVizMissingData: 0,
+ },
+ {
+ start_date: "Fri, 01 Jan 2016 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2017 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ start_date: "Sun, 01 Jan 2017 00:00:00 GMT",
+ end_date: "Mon, 01 Jan 2018 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ start_date: "Mon, 01 Jan 2018 00:00:00 GMT",
+ end_date: "Tue, 01 Jan 2019 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ Total: 41241,
+ start_date: "Tue, 01 Jan 2019 00:00:00 GMT",
+ end_date: "Wed, 01 Jan 2020 00:00:00 GMT",
+ frequency: "ANNUAL",
+ dataVizMissingData: 0,
+ },
+ {
+ start_date: "Wed, 01 Jan 2020 00:00:00 GMT",
+ end_date: "Fri, 01 Jan 2021 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ start_date: "Fri, 01 Jan 2021 00:00:00 GMT",
+ end_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ dataVizMissingData: 24811.666666666668,
+ frequency: "ANNUAL",
+ Total: 0,
+ },
+ {
+ Total: 74435,
+ start_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
+ frequency: "ANNUAL",
+ dataVizMissingData: 0,
+ },
+];
+
+const testDatapoints5: Datapoint[] = [
+ {
+ start_date: "Thu, 01 Aug 2019 00:00:00 GMT",
+ end_date: "Sun, 01 Sep 2019 00:00:00 GMT",
+ Pretrial: 41,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 32,
+ "Transfer or Hold": 53,
+ Unknown: 12,
+ },
+ {
+ start_date: "Wed, 01 Jan 2020 00:00:00 GMT",
+ end_date: "Sat, 01 Feb 2020 00:00:00 GMT",
+ Pretrial: 36541,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 79028,
+ Unknown: 65749,
+ "Transfer or Hold": 48334,
+ },
+ {
+ start_date: "Sat, 01 Feb 2020 00:00:00 GMT",
+ end_date: "Sun, 01 Mar 2020 00:00:00 GMT",
+ Pretrial: 24112,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 82706,
+ "Transfer or Hold": 83057,
+ Unknown: 17415,
+ },
+ {
+ start_date: "Sun, 01 Mar 2020 00:00:00 GMT",
+ end_date: "Wed, 01 Apr 2020 00:00:00 GMT",
+ Pretrial: 23767,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 90978,
+ "Transfer or Hold": 82590,
+ Unknown: 35303,
+ },
+ {
+ start_date: "Wed, 01 Apr 2020 00:00:00 GMT",
+ end_date: "Fri, 01 May 2020 00:00:00 GMT",
+ Pretrial: 78458,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 46769,
+ "Transfer or Hold": 64874,
+ Unknown: 38205,
+ },
+ {
+ start_date: "Fri, 01 May 2020 00:00:00 GMT",
+ end_date: "Mon, 01 Jun 2020 00:00:00 GMT",
+ Pretrial: 22677,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 61323,
+ "Transfer or Hold": 72938,
+ Unknown: 29553,
+ },
+ {
+ start_date: "Mon, 01 Jun 2020 00:00:00 GMT",
+ end_date: "Wed, 01 Jul 2020 00:00:00 GMT",
+ Pretrial: 88997,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 38048,
+ "Transfer or Hold": 84709,
+ Unknown: 62951,
+ },
+ {
+ start_date: "Wed, 01 Jul 2020 00:00:00 GMT",
+ end_date: "Sat, 01 Aug 2020 00:00:00 GMT",
+ Pretrial: 16324,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 16996,
+ "Transfer or Hold": 63347,
+ Unknown: 91685,
+ },
+ {
+ start_date: "Sat, 01 Aug 2020 00:00:00 GMT",
+ end_date: "Tue, 01 Sep 2020 00:00:00 GMT",
+ Pretrial: 46650,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 23200,
+ "Transfer or Hold": 24195,
+ Unknown: 33681,
+ },
+ {
+ start_date: "Tue, 01 Sep 2020 00:00:00 GMT",
+ end_date: "Thu, 01 Oct 2020 00:00:00 GMT",
+ Pretrial: 82010,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 14841,
+ "Transfer or Hold": 82407,
+ Unknown: 97055,
+ },
+ {
+ start_date: "Thu, 01 Oct 2020 00:00:00 GMT",
+ end_date: "Sun, 01 Nov 2020 00:00:00 GMT",
+ Sentenced: 37221,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ "Transfer or Hold": 22512,
+ Unknown: 49751,
+ Pretrial: 51938,
+ },
+ {
+ start_date: "Sun, 01 Nov 2020 00:00:00 GMT",
+ end_date: "Tue, 01 Dec 2020 00:00:00 GMT",
+ Pretrial: 13252,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 89151,
+ "Transfer or Hold": 31343,
+ Unknown: 17920,
+ },
+ {
+ start_date: "Tue, 01 Dec 2020 00:00:00 GMT",
+ end_date: "Fri, 01 Jan 2021 00:00:00 GMT",
+ Pretrial: 42300,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 27629,
+ "Transfer or Hold": 11736,
+ Unknown: 13635,
+ },
+ {
+ start_date: "Fri, 01 Jan 2021 00:00:00 GMT",
+ end_date: "Mon, 01 Feb 2021 00:00:00 GMT",
+ Pretrial: 98369,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 46574,
+ "Transfer or Hold": 21353,
+ Unknown: 88769,
+ },
+ {
+ start_date: "Mon, 01 Feb 2021 00:00:00 GMT",
+ end_date: "Mon, 01 Mar 2021 00:00:00 GMT",
+ Pretrial: 78759,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 53386,
+ "Transfer or Hold": 59075,
+ Unknown: 27134,
+ },
+ {
+ start_date: "Mon, 01 Mar 2021 00:00:00 GMT",
+ end_date: "Thu, 01 Apr 2021 00:00:00 GMT",
+ Pretrial: 60053,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 34197,
+ "Transfer or Hold": 63531,
+ Unknown: 83579,
+ },
+ {
+ start_date: "Thu, 01 Apr 2021 00:00:00 GMT",
+ end_date: "Sat, 01 May 2021 00:00:00 GMT",
+ Pretrial: 44759,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 17229,
+ "Transfer or Hold": 77299,
+ Unknown: 58031,
+ },
+ {
+ start_date: "Sat, 01 May 2021 00:00:00 GMT",
+ end_date: "Tue, 01 Jun 2021 00:00:00 GMT",
+ Pretrial: 61558,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 59243,
+ "Transfer or Hold": 46498,
+ Unknown: 53175,
+ },
+ {
+ start_date: "Tue, 01 Jun 2021 00:00:00 GMT",
+ end_date: "Thu, 01 Jul 2021 00:00:00 GMT",
+ Pretrial: 73726,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 60098,
+ "Transfer or Hold": 24792,
+ Unknown: 90376,
+ },
+ {
+ start_date: "Thu, 01 Jul 2021 00:00:00 GMT",
+ end_date: "Sun, 01 Aug 2021 00:00:00 GMT",
+ Pretrial: 31102,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Unknown: 78794,
+ "Transfer or Hold": 10572,
+ Sentenced: 30228,
+ },
+ {
+ start_date: "Sun, 01 Aug 2021 00:00:00 GMT",
+ end_date: "Wed, 01 Sep 2021 00:00:00 GMT",
+ Pretrial: 99665,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 82644,
+ "Transfer or Hold": 58060,
+ Unknown: 72892,
+ },
+ {
+ start_date: "Wed, 01 Sep 2021 00:00:00 GMT",
+ end_date: "Fri, 01 Oct 2021 00:00:00 GMT",
+ Pretrial: 46416,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 33763,
+ "Transfer or Hold": 10680,
+ Unknown: 94129,
+ },
+ {
+ start_date: "Fri, 01 Oct 2021 00:00:00 GMT",
+ end_date: "Mon, 01 Nov 2021 00:00:00 GMT",
+ Pretrial: 80719,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 96265,
+ "Transfer or Hold": 12347,
+ Unknown: 18056,
+ },
+ {
+ start_date: "Mon, 01 Nov 2021 00:00:00 GMT",
+ end_date: "Wed, 01 Dec 2021 00:00:00 GMT",
+ Pretrial: 83549,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 77473,
+ "Transfer or Hold": 49179,
+ Unknown: 78634,
+ },
+ {
+ start_date: "Wed, 01 Dec 2021 00:00:00 GMT",
+ end_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ Sentenced: 52931,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ "Transfer or Hold": 24687,
+ Unknown: 92620,
+ Pretrial: 40598,
+ },
+ {
+ start_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Feb 2022 00:00:00 GMT",
+ Pretrial: 75641,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 59666,
+ "Transfer or Hold": 17668,
+ Unknown: 92062,
+ },
+ {
+ start_date: "Tue, 01 Feb 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Mar 2022 00:00:00 GMT",
+ Pretrial: 97743,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 34823,
+ "Transfer or Hold": 37207,
+ Unknown: 18960,
+ },
+ {
+ start_date: "Tue, 01 Mar 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ Pretrial: 80843,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 92536,
+ "Transfer or Hold": 56184,
+ Unknown: 51573,
+ },
+ {
+ start_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ end_date: "Sun, 01 May 2022 00:00:00 GMT",
+ Pretrial: 10958,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 48931,
+ "Transfer or Hold": 46672,
+ Unknown: 98907,
+ },
+ {
+ start_date: "Sun, 01 May 2022 00:00:00 GMT",
+ end_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ Pretrial: 63710,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 26703,
+ "Transfer or Hold": 72473,
+ Unknown: 85367,
+ },
+ {
+ start_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ Pretrial: 32303,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 85419,
+ "Transfer or Hold": 98064,
+ Unknown: 95473,
+ },
+ {
+ start_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ end_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ Unknown: 90809,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ "Transfer or Hold": 18233,
+ Sentenced: 38446,
+ Pretrial: 60273,
+ },
+ {
+ start_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ Pretrial: 51711,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 25203,
+ "Transfer or Hold": 12208,
+ Unknown: 37137,
+ },
+ {
+ start_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ end_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ Pretrial: 92358,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 99972,
+ "Transfer or Hold": 54671,
+ Unknown: 49974,
+ },
+ {
+ start_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ Pretrial: null,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: null,
+ "Transfer or Hold": null,
+ Unknown: null,
+ },
+ {
+ start_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ Unknown: 4000,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Pretrial: null,
+ Sentenced: null,
+ "Transfer or Hold": 1,
+ },
+ {
+ start_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
+ Pretrial: null,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: null,
+ "Transfer or Hold": 0,
+ Unknown: null,
+ },
+];
+
+const testDatapoints5Transformed: Datapoint[] = [
+ {
+ start_date: "Fri, 01 Sep 2017 00:00:00 GMT",
+ end_date: "Sun, 01 Oct 2017 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Sun, 01 Oct 2017 00:00:00 GMT",
+ end_date: "Wed, 01 Nov 2017 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Wed, 01 Nov 2017 00:00:00 GMT",
+ end_date: "Fri, 01 Dec 2017 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Fri, 01 Dec 2017 00:00:00 GMT",
+ end_date: "Mon, 01 Jan 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Mon, 01 Jan 2018 00:00:00 GMT",
+ end_date: "Thu, 01 Feb 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Thu, 01 Feb 2018 00:00:00 GMT",
+ end_date: "Thu, 01 Mar 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Thu, 01 Mar 2018 00:00:00 GMT",
+ end_date: "Sun, 01 Apr 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Sun, 01 Apr 2018 00:00:00 GMT",
+ end_date: "Tue, 01 May 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Tue, 01 May 2018 00:00:00 GMT",
+ end_date: "Fri, 01 Jun 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Fri, 01 Jun 2018 00:00:00 GMT",
+ end_date: "Sun, 01 Jul 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Sun, 01 Jul 2018 00:00:00 GMT",
+ end_date: "Wed, 01 Aug 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Wed, 01 Aug 2018 00:00:00 GMT",
+ end_date: "Sat, 01 Sep 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Sat, 01 Sep 2018 00:00:00 GMT",
+ end_date: "Mon, 01 Oct 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Mon, 01 Oct 2018 00:00:00 GMT",
+ end_date: "Thu, 01 Nov 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Thu, 01 Nov 2018 00:00:00 GMT",
+ end_date: "Sat, 01 Dec 2018 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Sat, 01 Dec 2018 00:00:00 GMT",
+ end_date: "Tue, 01 Jan 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Tue, 01 Jan 2019 00:00:00 GMT",
+ end_date: "Fri, 01 Feb 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Fri, 01 Feb 2019 00:00:00 GMT",
+ end_date: "Fri, 01 Mar 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Fri, 01 Mar 2019 00:00:00 GMT",
+ end_date: "Mon, 01 Apr 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Mon, 01 Apr 2019 00:00:00 GMT",
+ end_date: "Wed, 01 May 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Wed, 01 May 2019 00:00:00 GMT",
+ end_date: "Sat, 01 Jun 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Sat, 01 Jun 2019 00:00:00 GMT",
+ end_date: "Mon, 01 Jul 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Mon, 01 Jul 2019 00:00:00 GMT",
+ end_date: "Thu, 01 Aug 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Thu, 01 Aug 2019 00:00:00 GMT",
+ end_date: "Sun, 01 Sep 2019 00:00:00 GMT",
+ Pretrial: 0.2971014492753623,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.2318840579710145,
+ "Transfer or Hold": 0.38405797101449274,
+ Unknown: 0.08695652173913043,
+ },
+ {
+ start_date: "Sun, 01 Sep 2019 00:00:00 GMT",
+ end_date: "Tue, 01 Oct 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Tue, 01 Oct 2019 00:00:00 GMT",
+ end_date: "Fri, 01 Nov 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Fri, 01 Nov 2019 00:00:00 GMT",
+ end_date: "Sun, 01 Dec 2019 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Sun, 01 Dec 2019 00:00:00 GMT",
+ end_date: "Wed, 01 Jan 2020 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Wed, 01 Jan 2020 00:00:00 GMT",
+ end_date: "Sat, 01 Feb 2020 00:00:00 GMT",
+ Pretrial: 0.15911466044275688,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.3441206695347744,
+ Unknown: 0.2862983993172278,
+ "Transfer or Hold": 0.21046627070524096,
+ },
+ {
+ start_date: "Sat, 01 Feb 2020 00:00:00 GMT",
+ end_date: "Sun, 01 Mar 2020 00:00:00 GMT",
+ Pretrial: 0.11632013121713541,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.3989869265280525,
+ "Transfer or Hold": 0.4006802064740219,
+ Unknown: 0.0840127357807902,
+ },
+ {
+ start_date: "Sun, 01 Mar 2020 00:00:00 GMT",
+ end_date: "Wed, 01 Apr 2020 00:00:00 GMT",
+ Pretrial: 0.10216301721988669,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.39107110618213703,
+ "Transfer or Hold": 0.355015087818843,
+ Unknown: 0.15175078877913326,
+ },
+ {
+ start_date: "Wed, 01 Apr 2020 00:00:00 GMT",
+ end_date: "Fri, 01 May 2020 00:00:00 GMT",
+ Pretrial: 0.343652816833548,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.204852259686561,
+ "Transfer or Hold": 0.28415372351142765,
+ Unknown: 0.1673411999684634,
+ },
+ {
+ start_date: "Fri, 01 May 2020 00:00:00 GMT",
+ end_date: "Mon, 01 Jun 2020 00:00:00 GMT",
+ Pretrial: 0.12159836131502325,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.32882551973017465,
+ "Transfer or Hold": 0.39110734566279337,
+ Unknown: 0.15846877329200873,
+ },
+ {
+ start_date: "Mon, 01 Jun 2020 00:00:00 GMT",
+ end_date: "Wed, 01 Jul 2020 00:00:00 GMT",
+ Pretrial: 0.32397298920660345,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.13850494166469485,
+ "Transfer or Hold": 0.3083635172275714,
+ Unknown: 0.2291585519011303,
+ },
+ {
+ start_date: "Wed, 01 Jul 2020 00:00:00 GMT",
+ end_date: "Sat, 01 Aug 2020 00:00:00 GMT",
+ Pretrial: 0.0866675161399932,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.0902353041114509,
+ "Transfer or Hold": 0.33632241760108733,
+ Unknown: 0.48677476214746856,
+ },
+ {
+ start_date: "Sat, 01 Aug 2020 00:00:00 GMT",
+ end_date: "Tue, 01 Sep 2020 00:00:00 GMT",
+ Pretrial: 0.36523495607785417,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.1816388206003476,
+ "Transfer or Hold": 0.1894289338114401,
+ Unknown: 0.2636972895103581,
+ },
+ {
+ start_date: "Tue, 01 Sep 2020 00:00:00 GMT",
+ end_date: "Thu, 01 Oct 2020 00:00:00 GMT",
+ Pretrial: 0.296801091515781,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.05371082793788204,
+ "Transfer or Hold": 0.29823786792514284,
+ Unknown: 0.3512502126211941,
+ },
+ {
+ start_date: "Thu, 01 Oct 2020 00:00:00 GMT",
+ end_date: "Sun, 01 Nov 2020 00:00:00 GMT",
+ Sentenced: 0.23058195289365763,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ "Transfer or Hold": 0.1394605444115424,
+ Unknown: 0.30820458177943527,
+ Pretrial: 0.32175292091536467,
+ },
+ {
+ start_date: "Sun, 01 Nov 2020 00:00:00 GMT",
+ end_date: "Tue, 01 Dec 2020 00:00:00 GMT",
+ Pretrial: 0.08737620824706922,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.5878113749950549,
+ "Transfer or Hold": 0.2066580512441813,
+ Unknown: 0.11815436551369457,
+ },
+ {
+ start_date: "Tue, 01 Dec 2020 00:00:00 GMT",
+ end_date: "Fri, 01 Jan 2021 00:00:00 GMT",
+ Pretrial: 0.4438614900314795,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.28991605456453307,
+ "Transfer or Hold": 0.1231479538300105,
+ Unknown: 0.14307450157397691,
+ },
+ {
+ start_date: "Fri, 01 Jan 2021 00:00:00 GMT",
+ end_date: "Mon, 01 Feb 2021 00:00:00 GMT",
+ Pretrial: 0.38566247819183347,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.18259659302530729,
+ "Transfer or Hold": 0.08371591555093799,
+ Unknown: 0.34802501323192125,
+ },
+ {
+ start_date: "Mon, 01 Feb 2021 00:00:00 GMT",
+ end_date: "Mon, 01 Mar 2021 00:00:00 GMT",
+ Pretrial: 0.3606941022376508,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.24449288769612648,
+ "Transfer or Hold": 0.2705469100634749,
+ Unknown: 0.12426610000274783,
+ },
+ {
+ start_date: "Mon, 01 Mar 2021 00:00:00 GMT",
+ end_date: "Thu, 01 Apr 2021 00:00:00 GMT",
+ Pretrial: 0.2488109048723898,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.14168462048392444,
+ "Transfer or Hold": 0.26322091481604243,
+ Unknown: 0.34628355982764336,
+ },
+ {
+ start_date: "Thu, 01 Apr 2021 00:00:00 GMT",
+ end_date: "Sat, 01 May 2021 00:00:00 GMT",
+ Pretrial: 0.22683688259560708,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.08731590630353034,
+ "Transfer or Hold": 0.39174834531061536,
+ Unknown: 0.2940988657902472,
+ },
+ {
+ start_date: "Sat, 01 May 2021 00:00:00 GMT",
+ end_date: "Tue, 01 Jun 2021 00:00:00 GMT",
+ Pretrial: 0.2792075256039261,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.2687074212832352,
+ "Transfer or Hold": 0.21090015149178587,
+ Unknown: 0.24118490162105283,
+ },
+ {
+ start_date: "Tue, 01 Jun 2021 00:00:00 GMT",
+ end_date: "Thu, 01 Jul 2021 00:00:00 GMT",
+ Pretrial: 0.29609786659812365,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.24136518442359595,
+ "Transfer or Hold": 0.0995694640791672,
+ Unknown: 0.36296748489911324,
+ },
+ {
+ start_date: "Thu, 01 Jul 2021 00:00:00 GMT",
+ end_date: "Sun, 01 Aug 2021 00:00:00 GMT",
+ Pretrial: 0.2063890216064129,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Unknown: 0.5228672293889686,
+ "Transfer or Hold": 0.07015448319796146,
+ Sentenced: 0.20058926580665712,
+ },
+ {
+ start_date: "Sun, 01 Aug 2021 00:00:00 GMT",
+ end_date: "Wed, 01 Sep 2021 00:00:00 GMT",
+ Pretrial: 0.3181532332463984,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.26381834955516326,
+ "Transfer or Hold": 0.18534065842859468,
+ Unknown: 0.23268775876984368,
+ },
+ {
+ start_date: "Wed, 01 Sep 2021 00:00:00 GMT",
+ end_date: "Fri, 01 Oct 2021 00:00:00 GMT",
+ Pretrial: 0.25091357277228793,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.1825145414837719,
+ "Transfer or Hold": 0.05773347460375808,
+ Unknown: 0.508838411140182,
+ },
+ {
+ start_date: "Fri, 01 Oct 2021 00:00:00 GMT",
+ end_date: "Mon, 01 Nov 2021 00:00:00 GMT",
+ Pretrial: 0.3892191892452275,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.4641804934735543,
+ "Transfer or Hold": 0.059536036492162,
+ Unknown: 0.0870642807890562,
+ },
+ {
+ start_date: "Mon, 01 Nov 2021 00:00:00 GMT",
+ end_date: "Wed, 01 Dec 2021 00:00:00 GMT",
+ Pretrial: 0.2892620354181453,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.2682258036595288,
+ "Transfer or Hold": 0.17026676129970397,
+ Unknown: 0.2722453996226219,
+ },
+ {
+ start_date: "Wed, 01 Dec 2021 00:00:00 GMT",
+ end_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ Sentenced: 0.2510529511089188,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ "Transfer or Hold": 0.11709100912557628,
+ Unknown: 0.43929879147773626,
+ Pretrial: 0.19255724828776868,
+ },
+ {
+ start_date: "Sat, 01 Jan 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Feb 2022 00:00:00 GMT",
+ Pretrial: 0.30869215669470323,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.24349792072217666,
+ "Transfer or Hold": 0.0721033966298967,
+ Unknown: 0.3757065259532234,
+ },
+ {
+ start_date: "Tue, 01 Feb 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Mar 2022 00:00:00 GMT",
+ Pretrial: 0.5178903530384193,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.1845093332909454,
+ "Transfer or Hold": 0.19714093454774734,
+ Unknown: 0.1004593791228879,
+ },
+ {
+ start_date: "Tue, 01 Mar 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ Pretrial: 0.28755833475613224,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.32915030447897103,
+ "Transfer or Hold": 0.19984633771555405,
+ Unknown: 0.18344502304934265,
+ },
+ {
+ start_date: "Fri, 01 Apr 2022 00:00:00 GMT",
+ end_date: "Sun, 01 May 2022 00:00:00 GMT",
+ Pretrial: 0.05333190569821091,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.23814413923335995,
+ "Transfer or Hold": 0.22714972647808904,
+ Unknown: 0.4813742285903401,
+ },
+ {
+ start_date: "Sun, 01 May 2022 00:00:00 GMT",
+ end_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ Pretrial: 0.25663335387689173,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.10756365481988132,
+ "Transfer or Hold": 0.2919320209624858,
+ Unknown: 0.3438709703407411,
+ },
+ {
+ start_date: "Wed, 01 Jun 2022 00:00:00 GMT",
+ end_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ Pretrial: 0.10378173803809689,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.27443061887367115,
+ "Transfer or Hold": 0.3150559501893921,
+ Unknown: 0.3067316928988399,
+ },
+ {
+ start_date: "Fri, 01 Jul 2022 00:00:00 GMT",
+ end_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ Unknown: 0.437083957046799,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ "Transfer or Hold": 0.08775949287883673,
+ Sentenced: 0.1850491670717796,
+ Pretrial: 0.2901073830025847,
+ },
+ {
+ start_date: "Mon, 01 Aug 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ Pretrial: 0.4095628826459896,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.19961349289951608,
+ "Transfer or Hold": 0.09669013694073293,
+ Unknown: 0.2941334875137614,
+ },
+ {
+ start_date: "Thu, 01 Sep 2022 00:00:00 GMT",
+ end_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ Pretrial: 0.3109958750736594,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: 0.3366343968347504,
+ "Transfer or Hold": 0.18409293711591884,
+ Unknown: 0.16827679097567136,
+ },
+ {
+ start_date: "Sat, 01 Oct 2022 00:00:00 GMT",
+ end_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ dataVizMissingData: 0.3333333333333333,
+ frequency: "MONTHLY",
+ Pretrial: 0,
+ Sentenced: 0,
+ "Transfer or Hold": 0,
+ Unknown: 0,
+ },
+ {
+ start_date: "Tue, 01 Nov 2022 00:00:00 GMT",
+ end_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ Unknown: 0.9997500624843789,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Pretrial: null,
+ Sentenced: null,
+ "Transfer or Hold": 0.00024993751562109475,
+ },
+ {
+ start_date: "Thu, 01 Dec 2022 00:00:00 GMT",
+ end_date: "Sun, 01 Jan 2023 00:00:00 GMT",
+ Pretrial: null,
+ frequency: "MONTHLY",
+ dataVizMissingData: 0,
+ Sentenced: null,
+ "Transfer or Hold": 0,
+ Unknown: null,
+ },
+];
+
+beforeAll(() => {
+ jest.useFakeTimers("modern");
+ jest.setSystemTime(new Date(2022, 7, 23));
+});
+
+afterAll(() => {
+ jest.useRealTimers();
+});
+
+describe("incrementMonth", () => {
+ test("incrementMonth increments month correctly", () => {
+ const testDate = new Date("Tue, 01 Mar 2022 00:00:00 GMT");
+ const testDate2 = new Date("Thu, 01 Dec 2022 00:00:00 GMT");
+ expect(incrementMonth(testDate).toUTCString()).toBe(
+ "Fri, 01 Apr 2022 00:00:00 GMT"
+ );
+ expect(incrementMonth(testDate2).toUTCString()).toBe(
+ "Sun, 01 Jan 2023 00:00:00 GMT"
+ );
+ });
+});
+
+describe("incrementYear", () => {
+ test("incrementYear increments year correctly", () => {
+ const testDate = new Date("Tue, 01 Mar 2022 00:00:00 GMT");
+ const testDate2 = new Date("Thu, 01 Dec 2022 00:00:00 GMT");
+ expect(incrementYear(testDate).toUTCString()).toBe(
+ "Wed, 01 Mar 2023 00:00:00 GMT"
+ );
+ expect(incrementYear(testDate2).toUTCString()).toBe(
+ "Fri, 01 Dec 2023 00:00:00 GMT"
+ );
+ });
+});
+
+describe("filterByTimeRange", () => {
+ test("filterByTimeRange filters through different months", () => {
+ expect(filterByTimeRange(testDatapoints, 6).length).toBe(10);
+ expect(filterByTimeRange(testDatapoints, 12).length).toBe(16);
+ });
+});
+
+describe("transformToRelativePerchanges", () => {
+ test("transformToRelativePerchanges transforms datapoints correctly", () => {
+ expect(transformToRelativePerchanges(testDatapoints2)).toStrictEqual(
+ testDatapoints2Percentages
+ );
+ });
+});
+
+describe("filterNullDatapoints", () => {
+ test("filterNullDatapoints filters datapoints with all null dimensions only", () => {
+ expect(filterNullDatapoints(testDatapoints3)).toStrictEqual(
+ testDatapoints3WithoutNullDatapoints
+ );
+ });
+});
+
+describe("fillTimeGapsBetweenDatapoints", () => {
+ test("fillTimeGapsBetweenDatapoints adds datapoints between data", () => {
+ expect(fillTimeGapsBetweenDatapoints(testDatapoints4, 0)).toStrictEqual(
+ testDatapoints4WithGapDatapoints
+ );
+ });
+ test("fillTimeGapsBetweenDatapoints adds datapoints between data plus additional earlier month padding", () => {
+ expect(fillTimeGapsBetweenDatapoints(testDatapoints4, 120)).toStrictEqual(
+ testDatapoints4WithGapDatapoints2
+ );
+ });
+});
+
+describe("transformData", () => {
+ test("putting it all together", () => {
+ expect(transformData(testDatapoints5, 60, "Percentage")).toStrictEqual(
+ testDatapoints5Transformed
+ );
+ });
+});
diff --git a/common/components/DataViz/utils.ts b/common/components/DataViz/utils.ts
new file mode 100644
index 000000000..0211a7978
--- /dev/null
+++ b/common/components/DataViz/utils.ts
@@ -0,0 +1,340 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 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 { mapValues, pickBy } from "lodash";
+
+import {
+ Datapoint,
+ DatapointsViewSetting,
+ DataVizAggregateName,
+ DataVizTimeRange,
+} from "../../types";
+import { formatNumberInput } from "../../utils";
+
+export const thirtyOneDaysInSeconds = 2678400000;
+export const threeHundredSixtySixDaysInSeconds = 31622400000;
+
+export const nextMonthMap = new Map([
+ ["Jan", "Feb"],
+ ["Feb", "Mar"],
+ ["Mar", "Apr"],
+ ["Apr", "May"],
+ ["May", "Jun"],
+ ["Jun", "Jul"],
+ ["Jul", "Aug"],
+ ["Aug", "Sep"],
+ ["Sep", "Oct"],
+ ["Oct", "Nov"],
+ ["Nov", "Dec"],
+ ["Dec", "Jan"],
+]);
+
+const abbreviatedMonths = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+];
+
+export const splitUtcString = (utcString: string) => {
+ // the utc string can be split like this:
+ // const [dayOfWeek, day, month, year, time, timezone] = splitUtcString(str);
+ return utcString.split(" ");
+};
+
+export const getDatapointDimensions = (datapoint: Datapoint) =>
+ // gets the datapoint object minus the non-dimension keys "start_date", "end_date", "frequency", "dataVizMissingData"
+ pickBy(
+ datapoint,
+ (val, key) =>
+ key !== "start_date" &&
+ key !== "end_date" &&
+ key !== "frequency" &&
+ key !== "dataVizMissingData"
+ );
+
+export const sortDatapointDimensions = (dimA: string, dimB: string) => {
+ // sort alphabetically, except put "Other" and "Unknown" at the end.
+ if (dimA === "Other" && dimB === "Unknown") {
+ return -1;
+ }
+ if (dimB === "Other" && dimA === "Unknown") {
+ return 1;
+ }
+ if (dimA === "Other" || dimA === "Unknown") {
+ return 1;
+ }
+ if (dimB === "Other" || dimB === "Unknown") {
+ return -1;
+ }
+ return dimA.localeCompare(dimB);
+};
+
+export const getSumOfDimensionValues = (datapoint: Datapoint) => {
+ let sumOfDimensions = 0;
+ const dimensions = getDatapointDimensions(datapoint);
+ Object.values(dimensions).forEach((value) => {
+ sumOfDimensions += value as number;
+ });
+ return sumOfDimensions;
+};
+
+// write my own month incrementer since Date.setMonth doesn't keep the date the same...
+export const incrementMonth = (date: Date) => {
+ const [, day, month, year, time, timezone] = splitUtcString(
+ date.toUTCString()
+ );
+ return new Date(
+ `${day} ${nextMonthMap.get(month)} ${
+ month === "Dec" ? Number(year) + 1 : year
+ } ${time} ${timezone}`
+ );
+};
+
+export const incrementYear = (date: Date) => {
+ const clonedDate = new Date(date.getTime());
+ clonedDate.setFullYear(clonedDate.getFullYear() + 1);
+ return clonedDate;
+};
+
+// returns a new Date set to the GMT time zone
+// for comparing with Datapoint time strings which are also set to 00:00:00 GMT.
+export const createGMTDate = (
+ day: number,
+ monthIndex: number,
+ year: number
+) => {
+ return new Date(
+ `${day} ${abbreviatedMonths[monthIndex]} ${year} 00:00:00 GMT`
+ );
+};
+
+export const getHighestTotalValue = (data: Datapoint[]) => {
+ let highestValue = 0;
+ data.forEach((datapoint) => {
+ const sumOfDimensions = getSumOfDimensionValues(datapoint);
+ if (sumOfDimensions > highestValue) {
+ highestValue = sumOfDimensions;
+ }
+ });
+ return highestValue;
+};
+
+// functions to transform and filter an array of Datapoints to display in a chart
+
+export const filterByTimeRange = (
+ data: Datapoint[],
+ monthsAgo: DataVizTimeRange
+) => {
+ if (monthsAgo === 0) {
+ return data;
+ }
+ const earliestDate = new Date();
+ earliestDate.setMonth(earliestDate.getMonth() - monthsAgo);
+ earliestDate.setHours(
+ earliestDate.getHours() - earliestDate.getTimezoneOffset() / 60
+ ); // account for timezone offset since datapoint dates are in UTC+0 time.
+ return data.filter((dp) => {
+ return new Date(dp.start_date) >= earliestDate;
+ });
+};
+
+export const transformToRelativePerchanges = (data: Datapoint[]) => {
+ return data.map((datapoint) => {
+ const dimensions = getDatapointDimensions(datapoint);
+ const sumOfDimensions = getSumOfDimensionValues(datapoint);
+ const dimensionsPercentage = mapValues(dimensions, (val, key) => {
+ if (typeof val === "number" && val !== 0) {
+ return val / sumOfDimensions;
+ }
+ return val;
+ });
+ return {
+ ...datapoint,
+ ...dimensionsPercentage,
+ };
+ });
+};
+
+export const filterNullDatapoints = (data: Datapoint[]) => {
+ return data.filter((datapoint) => {
+ const dimensions = getDatapointDimensions(datapoint);
+ let hasReportedValues = false;
+ Object.values(dimensions).every((dimValue) => {
+ if (dimValue !== null) {
+ hasReportedValues = true;
+ return false;
+ }
+ return true;
+ });
+ return hasReportedValues;
+ });
+};
+
+/**
+ * A gap datapoint represents a time range with no reported data
+ * and is formatted by setting all dimension values to 0
+ * and setting the value of "dataVizMissingData" to ~1/3 the height of the bar on the chart.
+ *
+ * This method generates gap datapoints between datapoints up to a certain number of months ago.
+ */
+export const fillTimeGapsBetweenDatapoints = (
+ data: Datapoint[],
+ monthsAgo: number
+) => {
+ if (data.length === 0) {
+ return data;
+ }
+
+ const isAnnual = data[0].frequency === "ANNUAL";
+ const increment = isAnnual ? incrementYear : incrementMonth;
+ const defaultBarValue = getHighestTotalValue(data) / 3;
+ const dataWithGapDatapoints = [...data];
+ // create the map of dimensions with zero values
+ const dimensionsMap = mapValues(getDatapointDimensions(data[0]), (_) => 0);
+
+ // loop through all the datapoints
+ let totalOffset = 0; // whenever we insert a gap datapoint into `dataWithGapDatapoints`, increment the totalOffset
+ let lastDate = new Date();
+ if (isAnnual) {
+ lastDate.setFullYear(lastDate.getFullYear() - monthsAgo / 12);
+ } else {
+ lastDate.setMonth(lastDate.getMonth() - monthsAgo);
+ }
+ lastDate = createGMTDate(
+ 1,
+ isAnnual ? 0 : lastDate.getMonth(),
+ lastDate.getFullYear()
+ );
+ for (let i = 0; i < data.length; i += 1) {
+ const currentDate = new Date(data[i].start_date);
+ const timeInterval =
+ data[0].frequency === "MONTHLY"
+ ? thirtyOneDaysInSeconds
+ : threeHundredSixtySixDaysInSeconds;
+ // this while loop can insert multiple gap datapoints between datapoints
+ // so must increment this offset to maintain correct insert order
+ let offset = 0;
+ while (currentDate.getTime() - lastDate.getTime() > timeInterval) {
+ lastDate = increment(lastDate);
+ dataWithGapDatapoints.splice(i + offset + totalOffset, 0, {
+ start_date: lastDate.toUTCString(),
+ end_date: increment(lastDate).toUTCString(),
+ dataVizMissingData: defaultBarValue,
+ frequency: data[0].frequency,
+ ...dimensionsMap,
+ });
+ offset += 1;
+ }
+ totalOffset += offset;
+ lastDate = currentDate;
+ }
+
+ return dataWithGapDatapoints;
+};
+
+export const transformData = (
+ d: Datapoint[],
+ monthsAgo: DataVizTimeRange,
+ datapointsViewSetting: DatapointsViewSetting
+) => {
+ let transformedData = [...d];
+
+ if (transformedData.length === 0) {
+ return transformedData;
+ }
+
+ // filter by time range
+ transformedData = filterByTimeRange(transformedData, monthsAgo);
+
+ transformedData = filterNullDatapoints(transformedData);
+
+ // format data into percentages for percentage view
+ if (datapointsViewSetting === "Percentage") {
+ transformedData = transformToRelativePerchanges(transformedData);
+ }
+
+ return fillTimeGapsBetweenDatapoints(transformedData, monthsAgo);
+};
+
+// get insights from data
+
+export const getPercentChangeOverTime = (data: Datapoint[]) => {
+ if (data.length > 0) {
+ const start = data[0][DataVizAggregateName] as number | undefined;
+ const end = data[data.length - 1][DataVizAggregateName] as
+ | number
+ | undefined;
+ if (start !== undefined && end !== undefined) {
+ const formattedPercentChange = formatNumberInput(
+ Math.round(((end - start) / start) * 100).toString()
+ );
+ if (formattedPercentChange) {
+ return `${formattedPercentChange}%`;
+ }
+ }
+ }
+ return "N/A";
+};
+
+export const getAverageTotalValue = (data: Datapoint[], isAnnual: boolean) => {
+ if (data.length > 0) {
+ let totalValueFound = false;
+ const avgTotalValue =
+ data.reduce((res, dp) => {
+ if (dp[DataVizAggregateName] !== undefined) {
+ totalValueFound = true;
+ return res + (dp[DataVizAggregateName] as number);
+ }
+ return res;
+ }, 0) / data.length;
+ if (totalValueFound && avgTotalValue !== undefined) {
+ const formattedAvgTotalValue = formatNumberInput(
+ Math.round(avgTotalValue).toString()
+ );
+ if (formattedAvgTotalValue !== undefined) {
+ return `${formattedAvgTotalValue}/${isAnnual ? "yr" : "mo"}`;
+ }
+ }
+ }
+ return "N/A";
+};
+
+export const getLatestDateFormatted = (
+ data: Datapoint[],
+ isAnnual: boolean
+) => {
+ const mostRecentDate = data[data.length - 1]?.start_date;
+ if (mostRecentDate) {
+ const [, , month, year] = splitUtcString(mostRecentDate);
+ return `${!isAnnual ? `${month} ` : ""}${year}`;
+ }
+ return "N/A";
+};
+
+export const formatDateShort = (dateStr: string) => {
+ const [, , month, year] = splitUtcString(dateStr);
+ return `${abbreviatedMonths.indexOf(month) + 1}/${year}`;
+};
diff --git a/publisher/src/components/GlobalStyles/Palette.ts b/common/components/GlobalStyles/Palette.ts
similarity index 100%
rename from publisher/src/components/GlobalStyles/Palette.ts
rename to common/components/GlobalStyles/Palette.ts
diff --git a/publisher/src/components/GlobalStyles/Typography.ts b/common/components/GlobalStyles/Typography.ts
similarity index 100%
rename from publisher/src/components/GlobalStyles/Typography.ts
rename to common/components/GlobalStyles/Typography.ts
diff --git a/publisher/src/components/GlobalStyles/constants.ts b/common/components/GlobalStyles/constants.ts
similarity index 100%
rename from publisher/src/components/GlobalStyles/constants.ts
rename to common/components/GlobalStyles/constants.ts
diff --git a/publisher/src/components/GlobalStyles/index.ts b/common/components/GlobalStyles/index.ts
similarity index 100%
rename from publisher/src/components/GlobalStyles/index.ts
rename to common/components/GlobalStyles/index.ts
diff --git a/publisher/src/shared/types.ts b/common/types.ts
similarity index 100%
rename from publisher/src/shared/types.ts
rename to common/types.ts
diff --git a/common/utils/conversionUtils.ts b/common/utils/conversionUtils.ts
new file mode 100644
index 000000000..73ec81d43
--- /dev/null
+++ b/common/utils/conversionUtils.ts
@@ -0,0 +1,29 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 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 .
+// =============================================================================
+
+/**
+ * Converts pixel to rem based on a root `font-size` of 16px.
+ *
+ * @param px - pixel value as "24px"
+ * @param root (optional) - change conversion from a default root `font-size` of 16px
+ * @returns rem value as string
+ */
+
+export const rem = (px: string, root?: number) => {
+ const pxAsNumber = Number(px.replace("px", ""));
+ return `${pxAsNumber / (root || 16)}rem`;
+};
diff --git a/common/utils/dateUtils.test.ts b/common/utils/dateUtils.test.ts
new file mode 100644
index 000000000..f5b962be8
--- /dev/null
+++ b/common/utils/dateUtils.test.ts
@@ -0,0 +1,134 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 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 {
+ printDateRangeFromMonthYear,
+ printElapsedDaysMonthsYearsSinceDate,
+} from "./dateUtils";
+
+describe("printDateRangeFromMonthYear", () => {
+ test("monthly", () => {
+ const result1 = printDateRangeFromMonthYear(1, 2022);
+ const result2 = printDateRangeFromMonthYear(2, 2022);
+ const result3 = printDateRangeFromMonthYear(3, 2022);
+ const result4 = printDateRangeFromMonthYear(4, 2022);
+ const result5 = printDateRangeFromMonthYear(5, 2022);
+ const result6 = printDateRangeFromMonthYear(6, 2022);
+ const result7 = printDateRangeFromMonthYear(7, 2022);
+ const result8 = printDateRangeFromMonthYear(8, 2022);
+ const result9 = printDateRangeFromMonthYear(9, 2022);
+ const result10 = printDateRangeFromMonthYear(10, 2022);
+ const result11 = printDateRangeFromMonthYear(11, 2022);
+ const result12 = printDateRangeFromMonthYear(12, 2022);
+ const result13 = printDateRangeFromMonthYear(1, 2020);
+ const result14 = printDateRangeFromMonthYear(12, 2020);
+ expect(result1).toEqual("January 1, 2022 - January 31, 2022");
+ expect(result2).toEqual("February 1, 2022 - February 28, 2022");
+ expect(result3).toEqual("March 1, 2022 - March 31, 2022");
+ expect(result4).toEqual("April 1, 2022 - April 30, 2022");
+ expect(result5).toEqual("May 1, 2022 - May 31, 2022");
+ expect(result6).toEqual("June 1, 2022 - June 30, 2022");
+ expect(result7).toEqual("July 1, 2022 - July 31, 2022");
+ expect(result8).toEqual("August 1, 2022 - August 31, 2022");
+ expect(result9).toEqual("September 1, 2022 - September 30, 2022");
+ expect(result10).toEqual("October 1, 2022 - October 31, 2022");
+ expect(result11).toEqual("November 1, 2022 - November 30, 2022");
+ expect(result12).toEqual("December 1, 2022 - December 31, 2022");
+ expect(result13).toEqual("January 1, 2020 - January 31, 2020");
+ expect(result14).toEqual("December 1, 2020 - December 31, 2020");
+ });
+
+ test("annual", () => {
+ const result1 = printDateRangeFromMonthYear(1, 2022, "ANNUAL");
+ const result2 = printDateRangeFromMonthYear(7, 2022, "ANNUAL");
+ expect(result1).toEqual("January 1, 2022 - December 31, 2022");
+ expect(result2).toEqual("July 1, 2022 - June 30, 2023");
+ });
+});
+
+describe("printElapsedDaysMonthsYearsSinceDate", () => {
+ const dayAsMilliseconds = 86400000;
+ const zeroDaysLapsed = new Date(
+ Date.now() - dayAsMilliseconds * 0
+ ).toString();
+ const oneDayLapsed = new Date(Date.now() - dayAsMilliseconds * 1).toString();
+ const twoDaysLapsed = new Date(Date.now() - dayAsMilliseconds * 2).toString();
+ const fifteenDaysLapsed = new Date(
+ Date.now() - dayAsMilliseconds * 15
+ ).toString();
+ const fourtyDaysLapsed = new Date(
+ Date.now() - dayAsMilliseconds * 40
+ ).toString();
+ const sixtyDaysLapsed = new Date(
+ Date.now() - dayAsMilliseconds * 60
+ ).toString();
+ const hundredDaysLapsed = new Date(
+ Date.now() - dayAsMilliseconds * 100
+ ).toString();
+ const fiveHundredDaysLapsed = new Date(
+ Date.now() - dayAsMilliseconds * 500
+ ).toString();
+ const nineHundredDaysLapsed = new Date(
+ Date.now() - dayAsMilliseconds * 900
+ ).toString();
+
+ test("0 days ago prints today", () => {
+ const zeroDaysLapsedText =
+ printElapsedDaysMonthsYearsSinceDate(zeroDaysLapsed);
+ const nonZeroDaysLapsedText =
+ printElapsedDaysMonthsYearsSinceDate(fifteenDaysLapsed);
+ expect(zeroDaysLapsedText).toEqual("today");
+ expect(nonZeroDaysLapsedText).not.toEqual("today");
+ });
+
+ test("1 day ago prints yesterday", () => {
+ const oneDayLapsedText = printElapsedDaysMonthsYearsSinceDate(oneDayLapsed);
+ expect(oneDayLapsedText).toEqual("yesterday");
+ });
+
+ test("less than 31 days ago prints number of days lapsed", () => {
+ const twoDaysLapsedText =
+ printElapsedDaysMonthsYearsSinceDate(twoDaysLapsed);
+ const fifteenDaysLapsedText =
+ printElapsedDaysMonthsYearsSinceDate(fifteenDaysLapsed);
+ expect(twoDaysLapsedText).toEqual("2 days ago");
+ expect(fifteenDaysLapsedText).toEqual("15 days ago");
+ });
+
+ test("more than 30 days prints number of months lapsed", () => {
+ const fourtyDaysLapsedText =
+ printElapsedDaysMonthsYearsSinceDate(fourtyDaysLapsed);
+ const sixtyDaysLapsedText =
+ printElapsedDaysMonthsYearsSinceDate(sixtyDaysLapsed);
+ const hundredDaysLapsedText =
+ printElapsedDaysMonthsYearsSinceDate(hundredDaysLapsed);
+ expect(fourtyDaysLapsedText).toEqual("a month ago");
+ expect(sixtyDaysLapsedText).toEqual("2 months ago");
+ expect(hundredDaysLapsedText).toEqual("3 months ago");
+ });
+
+ test("more than 365 days prints number of years lapsed", () => {
+ const fiveHundredDaysLapsedText = printElapsedDaysMonthsYearsSinceDate(
+ fiveHundredDaysLapsed
+ );
+ const nineHundredDaysLapsedText = printElapsedDaysMonthsYearsSinceDate(
+ nineHundredDaysLapsed
+ );
+ expect(fiveHundredDaysLapsedText).toEqual("a year ago");
+ expect(nineHundredDaysLapsedText).toEqual("2 years ago");
+ });
+});
diff --git a/common/utils/dateUtils.ts b/common/utils/dateUtils.ts
new file mode 100644
index 000000000..546a9c2cf
--- /dev/null
+++ b/common/utils/dateUtils.ts
@@ -0,0 +1,133 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 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 { ReportFrequency } from "../types";
+
+export const monthsByName = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+];
+
+/**
+ * @returns the month and year as a string
+ * @example "March 2022"
+ */
+export const printDateAsMonthYear = (month: number, year: number): string => {
+ return new Intl.DateTimeFormat("en-US", {
+ month: "long",
+ year: "numeric",
+ }).format(Date.UTC(year, month, -15));
+};
+
+/**
+ * @returns either "Annual Report [YEAR]" or "[MONTH] [YEAR]" as a string depending on frequency
+ * @example "Annual Report 2022" or "March 2022"
+ */
+export const printReportTitle = (
+ month: number,
+ year: number,
+ frequency: ReportFrequency
+): string => {
+ if (frequency === "ANNUAL") {
+ return `Annual Report ${year}`;
+ }
+
+ return printDateAsMonthYear(month, year);
+};
+
+/**
+ * @returns elapsed number of days since a provided date as a string
+ * @example 'today', 'yesterday', '2 days ago', '3 months ago', '5 years ago'
+ */
+export const printElapsedDaysMonthsYearsSinceDate = (date: string): string => {
+ const now = +new Date(Date.now());
+ const stringDateToNumber = +new Date(date);
+ const daysLapsed = Math.floor(
+ (now - stringDateToNumber) / (1000 * 60 * 60 * 24)
+ );
+
+ if (daysLapsed === 0) {
+ return `today`;
+ }
+
+ if (daysLapsed === 1) {
+ return `yesterday`;
+ }
+
+ if (daysLapsed < 31) {
+ return `${daysLapsed !== 1 ? daysLapsed : "a"} day${
+ daysLapsed !== 1 ? "s" : ""
+ } ago`;
+ }
+
+ if (daysLapsed > 30 && daysLapsed < 365) {
+ const monthsLapsed = Math.floor(daysLapsed / 30);
+ return `${monthsLapsed !== 1 ? monthsLapsed : "a"} month${
+ monthsLapsed !== 1 ? "s" : ""
+ } ago`;
+ }
+
+ if (daysLapsed >= 365) {
+ const yearsLapsed = Math.floor(daysLapsed / 365);
+ return `${yearsLapsed !== 1 ? yearsLapsed : "a"} year${
+ yearsLapsed !== 1 ? "s" : ""
+ } ago`;
+ }
+
+ return "";
+};
+
+/**
+ * Prints a human-readable date range of the provided month based on month and year
+ * @returns date range of the month as a string
+ * @example printDateRangeFromMonthYear(12, 2022) returns 'December 1, 2022 - December 31, 2022'
+ */
+export const printDateRangeFromMonthYear = (
+ month: number,
+ year: number,
+ frequency: ReportFrequency = "MONTHLY"
+): string => {
+ /**
+ * Note: backend sends true month number, whereas JavaScript's Date API deals with zero-indexed month numbers
+ * The below method of calculating the last day (number) of a given month relies on getting the 0th day of the following month.
+ * Simply providing the true month number value (from `month` param) does the + 1 (following month) calculation for us.
+ */
+
+ if (frequency === "MONTHLY") {
+ const lastDayOfMonth = new Date(year, month, 0)?.getDate();
+ const currentMonth = monthsByName[month - 1];
+ return `${currentMonth} 1, ${year} - ${currentMonth} ${lastDayOfMonth}, ${year}`;
+ }
+
+ const currentMonth = monthsByName[month - 1];
+ const prevMonthNumber = month === 1 ? 12 : month - 1;
+ const prevMonth = monthsByName[prevMonthNumber - 1];
+ const lastDayOfPrevMonth = new Date(year, prevMonthNumber, 0)?.getDate();
+ return `${currentMonth} 1, ${year} - ${prevMonth} ${lastDayOfPrevMonth}, ${
+ month === 1 ? year : year + 1
+ }`;
+};
diff --git a/common/utils/helperUtils.test.ts b/common/utils/helperUtils.test.ts
new file mode 100644
index 000000000..c07454fa6
--- /dev/null
+++ b/common/utils/helperUtils.test.ts
@@ -0,0 +1,127 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 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 {
+ formatNumberInput,
+ isPositiveNumber,
+ normalizeToString,
+ sanitizeInputValue,
+} from "./helperUtils";
+
+describe("sanitizeInputValue", () => {
+ test("return previous value if input value is undefined", () => {
+ const undefinedInput = sanitizeInputValue(undefined, 2);
+ const definedInput = sanitizeInputValue("1", 2);
+
+ expect(undefinedInput).toBe(2);
+ expect(definedInput).toBe(1);
+ });
+
+ test("return null if empty string input", () => {
+ const emptyStringInput = sanitizeInputValue("", 2);
+ const nonEmptyStringInput = sanitizeInputValue("text", 2);
+
+ expect(emptyStringInput).toBeNull();
+ expect(nonEmptyStringInput).not.toBeNull();
+ });
+
+ test("return the number zero for string 0 and 0.00 with decimals", () => {
+ const zeroString = sanitizeInputValue("0", null);
+ const zeroDecimal = sanitizeInputValue("0.00", null);
+
+ expect(zeroString).toBe(0);
+ expect(zeroDecimal).toBe(0);
+ });
+
+ test("return value converted to number if convertible", () => {
+ const numberString = sanitizeInputValue("123", null);
+ const numberStringWithDecimals = sanitizeInputValue("123.2341", null);
+ const numberStringWithDecimalsAfterZero = sanitizeInputValue(
+ "0.12341",
+ null
+ );
+
+ expect(numberString).toBe(123);
+ expect(numberStringWithDecimals).toBe(123.2341);
+ expect(numberStringWithDecimalsAfterZero).toBe(0.12341);
+ });
+
+ test("return value as string if not convertible to number", () => {
+ const nonNumber = sanitizeInputValue("0.123abc", null);
+ expect(typeof nonNumber).toBe("string");
+ });
+});
+
+describe("normalizeToString", () => {
+ test("return string version of value", () => {
+ const undefinedInput = normalizeToString(undefined);
+ const nullInput = normalizeToString(null);
+ const booleanInput = normalizeToString(false);
+ const numberInput = normalizeToString(22);
+ const stringInput = normalizeToString("Hello");
+
+ expect(undefinedInput).toBe("");
+ expect(nullInput).toBe("");
+ expect(booleanInput).toBe("false");
+ expect(numberInput).toBe("22");
+ expect(stringInput).toBe("Hello");
+ });
+});
+
+describe("formatNumberInput", () => {
+ test("return formatted number with commas and decimals", () => {
+ const inputWithCommasSpaces = formatNumberInput(
+ " 1231223,23,23,3,3.123123123 11 "
+ );
+ const inputWithSeriesOfNumbers = formatNumberInput("123122323233312");
+
+ expect(inputWithCommasSpaces).toBe("1,231,223,232,333.12312312311");
+ expect(inputWithSeriesOfNumbers).toBe("123,122,323,233,312");
+ });
+
+ test("return formatted number on first decimal instance", () => {
+ const inputWithDecimalAtEnd = formatNumberInput("2,32,3,23,2.");
+ expect(inputWithDecimalAtEnd).toBe("2,323,232.");
+ });
+
+ test("return input value if not valid", () => {
+ const invalidInput = formatNumberInput("12xyz!");
+ expect(invalidInput).toBe("12xyz!");
+ });
+});
+
+describe("isPositiveNumber", () => {
+ test("valid positive numbers return true", () => {
+ expect(isPositiveNumber("1")).toBe(true);
+ expect(isPositiveNumber("12")).toBe(true);
+ expect(isPositiveNumber("13")).toBe(true);
+ expect(isPositiveNumber("3.4")).toBe(true);
+ expect(isPositiveNumber("0")).toBe(true);
+ });
+ test("negative numbers return false", () => {
+ expect(isPositiveNumber("-1")).toBe(false);
+ expect(isPositiveNumber("-5")).toBe(false);
+ expect(isPositiveNumber("-5.6")).toBe(false);
+ });
+ test("invalid numbers return false", () => {
+ expect(isPositiveNumber("-1 ")).toBe(false);
+ expect(isPositiveNumber("0.0.0")).toBe(false);
+ expect(isPositiveNumber("five")).toBe(false);
+ expect(isPositiveNumber(" ")).toBe(false);
+ expect(isPositiveNumber("")).toBe(false);
+ });
+});
diff --git a/common/utils/helperUtils.ts b/common/utils/helperUtils.ts
new file mode 100644
index 000000000..65f373ec7
--- /dev/null
+++ b/common/utils/helperUtils.ts
@@ -0,0 +1,225 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 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 { debounce, memoize } from "lodash";
+
+import { MetricContext } from "../types";
+
+export const isPositiveNumber = (value: string | number) => {
+ if (typeof value === "string") {
+ return (value.trim() !== "" && Number(value) === 0) || Number(value) > 0;
+ }
+ return value >= 0;
+};
+
+/**
+ * Separate multiple people on a list by comma - no comma for the last person on the list
+ * @example ['Editor 1', 'Editor 2', 'Editor 3'] would print: `Editor 1, Editor 2, Editor 3`
+ */
+export const printCommaSeparatedList = (list: string[]): string => {
+ const string = list.map((item, i) =>
+ i < list.length - 1 ? `${item}, ` : `${item}`
+ );
+ return string.join(" ");
+};
+
+/**
+ * Take a string, trim and remove all spacing, and lowercase it.
+ * @example normalizeString("All Reports ") will be "allreports"
+ */
+export const normalizeString = (string: string): string => {
+ return string.split(" ").join("").toLowerCase().trim();
+};
+
+/**
+ * Take a string, replace _ with ' ' space.
+ * @example "NOT_STARTED" becomes "NOT STARTED"
+ */
+export const removeSnakeCase = (string: string): string => {
+ return string.split("_").join(" ");
+};
+
+/**
+ * Concatenate two string keys by an `_` underscore (default) or a specified separator string
+ * @returns a single concatenated string
+ * @examples
+ * combineTwoKeyNames("KEY1", "KEY2") will return "KEY1_KEY2"
+ * combineTwoKeyNames("KEY1", "KEY2", "-") will return "KEY1-KEY2"
+ */
+export const combineTwoKeyNames = (
+ key1: string,
+ key2: string,
+ separator?: string
+) => {
+ return `${key1}${separator || "_"}${key2}`;
+};
+
+/**
+ * Remove commas, spaces and trim string
+ *
+ * @returns a trimmed string free from spaces and commas
+ * @example " 1,000,00 0 " becomes "1000000"
+ */
+
+export const removeCommaSpaceAndTrim = (string: string) => {
+ return string?.replaceAll(",", "").replaceAll(" ", "").trim();
+};
+
+/**
+ * Formats string version of numbers into string format with thousands separator
+ *
+ * @returns a string representation of a number with commas
+ * @example " 1231223,23,23,3,3.123123123 11 " " becomes "1,231,223,232,333.12312312311"
+ */
+
+export const formatNumberInput = (
+ value: string | undefined
+): string | undefined => {
+ if (value === undefined) {
+ return undefined;
+ }
+
+ const maxNumber = 999_999_999_999_999; // 1 quadrillion
+ const cleanValue = removeCommaSpaceAndTrim(value);
+ const splitValues = cleanValue.split(".");
+
+ if (Number(cleanValue) > maxNumber) {
+ return Number(cleanValue.slice(0, 15)).toLocaleString();
+ }
+
+ if (splitValues && splitValues.length === 2) {
+ if (cleanValue[cleanValue.length - 1] === ".") {
+ return Number(splitValues[0]) !== 0 && Number(splitValues[0])
+ ? `${Number(splitValues[0]).toLocaleString()}.`
+ : value;
+ }
+
+ if (cleanValue.includes(".")) {
+ const [wholeNumber, decimal] = cleanValue.split(".");
+ return Number(wholeNumber)
+ ? `${Number(wholeNumber).toLocaleString()}.${decimal}`
+ : value;
+ }
+ }
+ return Number(cleanValue) ? Number(cleanValue).toLocaleString() : value;
+};
+
+/**
+ * Sanitize by formatting and converting string input to appropriate value for backend.
+ *
+ * @param value input value
+ * @param previousValue previously saved value retrieved from the backend
+ * @returns
+ * * `previousValue` from the backend if `value` is undefined
+ * * `null` for empty string
+ * * number `0` for true zeros ("0", "0.000", etc.)
+ * * `value` converted to number
+ * * `value` itself (if it is not a number) or if the type is "TEXT"
+ */
+
+export const sanitizeInputValue = (
+ value: string | undefined,
+ previousValue: string | number | boolean | null | undefined,
+ type?: MetricContext["type"]
+): string | number | boolean | null | undefined => {
+ if (value === undefined) {
+ return previousValue;
+ }
+ const cleanValue = removeCommaSpaceAndTrim(value);
+ if (cleanValue === "") {
+ return null;
+ }
+ if (type === "TEXT") {
+ return value;
+ }
+ if (Number(cleanValue) === 0) {
+ return 0;
+ }
+ return Number(cleanValue) || value;
+};
+
+/**
+ * Converts string | number | boolean | null | undefined into string equivalents that conforms to a text input
+ *
+ * @returns a string, "" empty string to represent null and undefined, stringified version of number and boolean
+ */
+export const normalizeToString = (
+ value: string | number | boolean | null | undefined
+): string => {
+ const stringValue = value?.toString();
+ return !stringValue ? "" : stringValue;
+};
+
+/**
+ * Group a list of objects based on property value
+ * @param arr list of objects
+ * @param key name of the property on which to perform the grouping
+ * @returns dictionary of property value to list of objects with that value
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const groupBy = (arr: T[], key: (i: T) => K) => {
+ const result = {} as Record;
+ arr.forEach((item) => {
+ if (!result[key(item)]) {
+ result[key(item)] = [];
+ }
+ result[key(item)].push(item);
+ });
+ return result;
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export interface MemoizeDebouncedFunction any> {
+ (...args: Parameters): void;
+ flush: (...args: Parameters) => void;
+}
+
+/**
+ * This method should be used instead of the standard `debounce` if we want to
+ * debounce *only* if the arguments to the function are the same.
+ * For instance, consider a function `click(param: str)`. With standard debounce,
+ * calling `click('foo')` and `click('bar')` in quick succession will only result
+ * in the execution of `click('bar')`. However, using memoized debounce, both
+ * functions will execute, because their parameters are different.
+ * Taken from https://github.com/lodash/lodash/issues/2403#issuecomment-816137402
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export function memoizeDebounce any>(
+ func: F,
+ wait = 0,
+ options: _.DebounceSettings = {},
+ resolver?: (...args: Parameters) => unknown
+): MemoizeDebouncedFunction {
+ const debounceMemo = memoize<(...args: Parameters) => _.DebouncedFunc>(
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ (..._args: Parameters) => debounce(func, wait, options),
+ resolver
+ );
+
+ function wrappedFunction(
+ this: MemoizeDebouncedFunction,
+ ...args: Parameters
+ ): ReturnType | undefined {
+ return debounceMemo(...args)(...args);
+ }
+
+ wrappedFunction.flush = (...args: Parameters): void => {
+ debounceMemo(...args).flush();
+ };
+
+ return wrappedFunction as unknown as MemoizeDebouncedFunction;
+}
diff --git a/common/utils/index copy.ts b/common/utils/index copy.ts
new file mode 100644
index 000000000..3d8962b73
--- /dev/null
+++ b/common/utils/index copy.ts
@@ -0,0 +1,20 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 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 .
+// =============================================================================
+
+export * from "./conversionUtils";
+export * from "./dateUtils";
+export * from "./helperUtils";
diff --git a/common/utils/index.ts b/common/utils/index.ts
new file mode 100644
index 000000000..3d8962b73
--- /dev/null
+++ b/common/utils/index.ts
@@ -0,0 +1,20 @@
+// Recidiviz - a data platform for criminal justice reform
+// Copyright (C) 2022 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 .
+// =============================================================================
+
+export * from "./conversionUtils";
+export * from "./dateUtils";
+export * from "./helperUtils";
diff --git a/publisher/package.json b/publisher/package.json
index 0aeac5d84..f6bf34732 100644
--- a/publisher/package.json
+++ b/publisher/package.json
@@ -31,7 +31,7 @@
"web-vitals": "^2.1.0"
},
"scripts": {
- "start": "react-app-rewired start",
+ "dev": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject",
diff --git a/publisher/src/analytics.ts b/publisher/src/analytics.ts
index 765a17233..e3210a234 100644
--- a/publisher/src/analytics.ts
+++ b/publisher/src/analytics.ts
@@ -15,7 +15,7 @@
// along with this program. If not, see .
// =============================================================================
-import { UpdatedMetricsValues, UserAgency } from "./shared/types";
+import { UpdatedMetricsValues, UserAgency } from "@justice-counts/common/types";
const TEST_SENDING_ANALYTICS = false; // used for testing sending analytics in development
const LOG_ANALYTICS = false; // used for logging analytics being sent
diff --git a/publisher/src/components/Auth/VerificationPage.tsx b/publisher/src/components/Auth/VerificationPage.tsx
index 335b30136..04a67ca86 100644
--- a/publisher/src/components/Auth/VerificationPage.tsx
+++ b/publisher/src/components/Auth/VerificationPage.tsx
@@ -15,12 +15,15 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import React from "react";
import styled from "styled-components/macro";
import { useStore } from "../../stores/StoreProvider";
import logo from "../assets/jc-logo-green-vector.png";
-import { palette, typography } from "../GlobalStyles";
export const PageContainer = styled.div`
display: flex;
diff --git a/publisher/src/components/Badge/Badge.tsx b/publisher/src/components/Badge/Badge.tsx
index 0ecc41b1c..3f05c4541 100644
--- a/publisher/src/components/Badge/Badge.tsx
+++ b/publisher/src/components/Badge/Badge.tsx
@@ -14,10 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
// =============================================================================
+import { palette } from "@justice-counts/common/components/GlobalStyles";
import React from "react";
import styled from "styled-components/macro";
-import { palette } from "../GlobalStyles";
import { MiniLoader } from "../Loading/MiniLoader";
export type BadgeColors = "RED" | "GREEN" | "ORANGE" | "GREY";
diff --git a/publisher/src/components/DataUpload/DataUpload.styles.tsx b/publisher/src/components/DataUpload/DataUpload.styles.tsx
index 9a5b44aec..7560f1afd 100644
--- a/publisher/src/components/DataUpload/DataUpload.styles.tsx
+++ b/publisher/src/components/DataUpload/DataUpload.styles.tsx
@@ -15,11 +15,15 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ HEADER_BAR_HEIGHT,
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import styled from "styled-components/macro";
import { rem } from "../../utils";
import { OpacityGradient } from "../Forms";
-import { HEADER_BAR_HEIGHT, palette, typography } from "../GlobalStyles";
import {
Cell,
LabelCell,
diff --git a/publisher/src/components/DataUpload/DataUpload.tsx b/publisher/src/components/DataUpload/DataUpload.tsx
index 7af310f04..f33d25d8c 100644
--- a/publisher/src/components/DataUpload/DataUpload.tsx
+++ b/publisher/src/components/DataUpload/DataUpload.tsx
@@ -15,11 +15,11 @@
// along with this program. If not, see .
// =============================================================================
+import { AgencySystems } from "@justice-counts/common/types";
import { observer } from "mobx-react-lite";
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
-import { AgencySystems } from "../../shared/types";
import { useStore } from "../../stores";
import logoImg from "../assets/jc-logo-vector.png";
import { Logo, LogoContainer } from "../Header";
diff --git a/publisher/src/components/DataUpload/SystemSelection.tsx b/publisher/src/components/DataUpload/SystemSelection.tsx
index 8d50869e3..9431d8bdf 100644
--- a/publisher/src/components/DataUpload/SystemSelection.tsx
+++ b/publisher/src/components/DataUpload/SystemSelection.tsx
@@ -15,9 +15,9 @@
// along with this program. If not, see .
// =============================================================================
+import { AgencySystems } from "@justice-counts/common/types";
import React from "react";
-import { AgencySystems } from "../../shared/types";
import { removeSnakeCase } from "../../utils";
import { ReactComponent as CheckIcon } from "../assets/check-icon.svg";
import {
diff --git a/publisher/src/components/DataUpload/UploadFile.tsx b/publisher/src/components/DataUpload/UploadFile.tsx
index ba2c67a54..d43d6d3ca 100644
--- a/publisher/src/components/DataUpload/UploadFile.tsx
+++ b/publisher/src/components/DataUpload/UploadFile.tsx
@@ -15,9 +15,9 @@
// along with this program. If not, see .
// =============================================================================
+import { AgencySystems } from "@justice-counts/common/types";
import React, { Fragment, useEffect, useRef, useState } from "react";
-import { AgencySystems } from "../../shared/types";
import { removeSnakeCase } from "../../utils";
import { ReactComponent as FileIcon } from "../assets/file-icon.svg";
import { showToast } from "../Toast";
diff --git a/publisher/src/components/DataUpload/UploadedFiles.tsx b/publisher/src/components/DataUpload/UploadedFiles.tsx
index bdec22ee6..131185c04 100644
--- a/publisher/src/components/DataUpload/UploadedFiles.tsx
+++ b/publisher/src/components/DataUpload/UploadedFiles.tsx
@@ -15,11 +15,11 @@
// along with this program. If not, see .
// =============================================================================
+import { Permission } from "@justice-counts/common/types";
import { when } from "mobx";
import { observer } from "mobx-react-lite";
import React, { useEffect, useState } from "react";
-import { Permission } from "../../shared/types";
import { useStore } from "../../stores";
import { removeSnakeCase } from "../../utils";
import downloadIcon from "../assets/download-icon.png";
diff --git a/publisher/src/components/DataUpload/types.ts b/publisher/src/components/DataUpload/types.ts
index f4b44b668..4eb532665 100644
--- a/publisher/src/components/DataUpload/types.ts
+++ b/publisher/src/components/DataUpload/types.ts
@@ -15,7 +15,7 @@
// along with this program. If not, see .
// =============================================================================
-import { RawDatapoint } from "../../shared/types";
+import { RawDatapoint } from "@justice-counts/common/types";
export interface DataUploadResponseBody {
metrics: UploadedMetric[];
diff --git a/publisher/src/components/DataViz/DatapointsView.styles.tsx b/publisher/src/components/DataViz/DatapointsView.styles.tsx
index 8c3fd8d9c..8c601c259 100644
--- a/publisher/src/components/DataViz/DatapointsView.styles.tsx
+++ b/publisher/src/components/DataViz/DatapointsView.styles.tsx
@@ -15,6 +15,10 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import {
Dropdown,
DropdownMenu,
@@ -23,7 +27,6 @@ import {
import React from "react";
import styled from "styled-components/macro";
-import { palette, typography } from "../GlobalStyles";
import { ExtendedDropdownMenuItem } from "../Menu/Menu.styles";
export const DatapointsViewContainer = styled.div`
diff --git a/publisher/src/components/DataViz/DatapointsView.tsx b/publisher/src/components/DataViz/DatapointsView.tsx
index d878a7456..f8a7a55c9 100644
--- a/publisher/src/components/DataViz/DatapointsView.tsx
+++ b/publisher/src/components/DataViz/DatapointsView.tsx
@@ -15,17 +15,18 @@
// along with this program. If not, see .
// =============================================================================
-import { observer } from "mobx-react-lite";
-import React, { useEffect } from "react";
-
+import BarChart from "@justice-counts/common/components/DataViz/BarChart";
+import Legend from "@justice-counts/common/components/DataViz/Legend";
import {
DatapointsGroupedByAggregateAndDisaggregations,
DatapointsViewSetting,
DataVizAggregateName,
DataVizTimeRangesMap,
-} from "../../shared/types";
+} from "@justice-counts/common/types";
+import { observer } from "mobx-react-lite";
+import React, { useEffect } from "react";
+
import { useStore } from "../../stores";
-import BarChart from "./BarChart";
import {
DatapointsViewContainer,
DatapointsViewControlsContainer,
@@ -33,7 +34,6 @@ import {
MetricInsight,
MetricInsightsRow,
} from "./DatapointsView.styles";
-import Legend from "./Legend";
import {
filterByTimeRange,
filterNullDatapoints,
diff --git a/publisher/src/components/DataViz/utils.test.ts b/publisher/src/components/DataViz/utils.test.ts
index 38481aa17..ceb0f8758 100644
--- a/publisher/src/components/DataViz/utils.test.ts
+++ b/publisher/src/components/DataViz/utils.test.ts
@@ -15,7 +15,8 @@
// along with this program. If not, see .
// =============================================================================
-import { Datapoint } from "../../shared/types";
+import { Datapoint } from "@justice-counts/common/types";
+
import {
fillTimeGapsBetweenDatapoints,
filterByTimeRange,
diff --git a/publisher/src/components/DataViz/utils.ts b/publisher/src/components/DataViz/utils.ts
index 5999b1a1d..142c4a994 100644
--- a/publisher/src/components/DataViz/utils.ts
+++ b/publisher/src/components/DataViz/utils.ts
@@ -15,14 +15,14 @@
// along with this program. If not, see .
// =============================================================================
-import { mapValues, pickBy } from "lodash";
-
import {
Datapoint,
DatapointsViewSetting,
DataVizAggregateName,
DataVizTimeRange,
-} from "../../shared/types";
+} from "@justice-counts/common/types";
+import { mapValues, pickBy } from "lodash";
+
import { formatNumberInput } from "../../utils";
export const thirtyOneDaysInSeconds = 2678400000;
diff --git a/publisher/src/components/Forms/BinaryRadioButton.tsx b/publisher/src/components/Forms/BinaryRadioButton.tsx
index d61ad243b..2a6625446 100644
--- a/publisher/src/components/Forms/BinaryRadioButton.tsx
+++ b/publisher/src/components/Forms/BinaryRadioButton.tsx
@@ -15,11 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import React, { InputHTMLAttributes } from "react";
import styled from "styled-components/macro";
-import { palette, typography } from "../GlobalStyles";
-
export const BinaryRadioGroupContainer = styled.div`
display: flex;
flex-direction: column;
diff --git a/publisher/src/components/Forms/Dropdown.tsx b/publisher/src/components/Forms/Dropdown.tsx
index 5e9442e62..53ad54745 100644
--- a/publisher/src/components/Forms/Dropdown.tsx
+++ b/publisher/src/components/Forms/Dropdown.tsx
@@ -15,11 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import React, { SelectHTMLAttributes } from "react";
import styled from "styled-components/macro";
-import { palette, typography } from "../GlobalStyles";
-
const DropdownContainer = styled.div`
position: relative;
width: 100%;
diff --git a/publisher/src/components/Forms/Form.styles.tsx b/publisher/src/components/Forms/Form.styles.tsx
index 97c7c74e9..a50cd36ba 100644
--- a/publisher/src/components/Forms/Form.styles.tsx
+++ b/publisher/src/components/Forms/Form.styles.tsx
@@ -15,9 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ HEADER_BAR_HEIGHT,
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import styled from "styled-components/macro";
-import { HEADER_BAR_HEIGHT, palette, typography } from "../GlobalStyles";
import {
DATA_ENTRY_WIDTH,
ONE_PANEL_MAX_WIDTH,
diff --git a/publisher/src/components/Forms/NotReportedIcon.tsx b/publisher/src/components/Forms/NotReportedIcon.tsx
index a16284ef2..fa357ed42 100644
--- a/publisher/src/components/Forms/NotReportedIcon.tsx
+++ b/publisher/src/components/Forms/NotReportedIcon.tsx
@@ -15,12 +15,15 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components/macro";
import notReportedIcon from "../assets/not-reported-icon.png";
-import { palette, typography } from "../GlobalStyles";
import { TWO_PANEL_MAX_WIDTH } from "../Reports/ReportDataEntry.styles";
export const NotReportedIconWrapper = styled.div<{
diff --git a/publisher/src/components/Forms/TabbedDisaggregations.tsx b/publisher/src/components/Forms/TabbedDisaggregations.tsx
index 619941bee..e3253e015 100644
--- a/publisher/src/components/Forms/TabbedDisaggregations.tsx
+++ b/publisher/src/components/Forms/TabbedDisaggregations.tsx
@@ -15,9 +15,9 @@
// along with this program. If not, see .
// =============================================================================
+import { Metric as MetricType } from "@justice-counts/common/types";
import React, { useEffect, useState } from "react";
-import { Metric as MetricType } from "../../shared/types";
import { useStore } from "../../stores";
import successIcon from "../assets/status-check-icon.png";
import errorIcon from "../assets/status-error-icon.png";
diff --git a/publisher/src/components/Forms/TextInput.tsx b/publisher/src/components/Forms/TextInput.tsx
index c70dc4870..8fe7c6046 100644
--- a/publisher/src/components/Forms/TextInput.tsx
+++ b/publisher/src/components/Forms/TextInput.tsx
@@ -15,15 +15,18 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
+import { FormError } from "@justice-counts/common/types";
import React, { InputHTMLAttributes, useState } from "react";
import styled from "styled-components/macro";
-import { FormError } from "../../shared/types";
import { rem } from "../../utils";
import infoRedIcon from "../assets/info-red-icon.png";
import statusCheckIcon from "../assets/status-check-icon.png";
import statusErrorIcon from "../assets/status-error-icon.png";
-import { palette, typography } from "../GlobalStyles";
import { NotReportedIcon } from ".";
export const InputWrapper = styled.div`
diff --git a/publisher/src/components/Header/Header.styles.tsx b/publisher/src/components/Header/Header.styles.tsx
index 1cb71ad09..37cd776be 100644
--- a/publisher/src/components/Header/Header.styles.tsx
+++ b/publisher/src/components/Header/Header.styles.tsx
@@ -15,11 +15,12 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ HEADER_BAR_HEIGHT,
+ palette,
+} from "@justice-counts/common/components/GlobalStyles";
import styled from "styled-components/macro";
-import { HEADER_BAR_HEIGHT } from "../GlobalStyles";
-import { palette } from "../GlobalStyles/Palette";
-
export const HeaderBar = styled.header`
width: 100%;
height: ${HEADER_BAR_HEIGHT}px;
diff --git a/publisher/src/components/Menu/Menu.styles.tsx b/publisher/src/components/Menu/Menu.styles.tsx
index 7c5dd829a..c4bbd0503 100644
--- a/publisher/src/components/Menu/Menu.styles.tsx
+++ b/publisher/src/components/Menu/Menu.styles.tsx
@@ -14,6 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
// =============================================================================
+import {
+ HEADER_BAR_HEIGHT,
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import {
DropdownMenu,
DropdownMenuItem,
@@ -21,7 +26,6 @@ import {
} from "@recidiviz/design-system";
import styled from "styled-components/macro";
-import { HEADER_BAR_HEIGHT, palette, typography } from "../GlobalStyles";
import { ONE_PANEL_MAX_WIDTH } from "../Reports/ReportDataEntry.styles";
export const MenuContainer = styled.nav`
diff --git a/publisher/src/components/Menu/Menu.tsx b/publisher/src/components/Menu/Menu.tsx
index a4ea0ee74..b0f40a933 100644
--- a/publisher/src/components/Menu/Menu.tsx
+++ b/publisher/src/components/Menu/Menu.tsx
@@ -14,12 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
// =============================================================================
+import { Permission } from "@justice-counts/common/types";
import { Dropdown } from "@recidiviz/design-system";
import { observer } from "mobx-react-lite";
import React, { useEffect, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
-import { Permission } from "../../shared/types";
import { useStore } from "../../stores";
import { Button } from "../DataUpload";
import {
diff --git a/publisher/src/components/MetricsView/MetricsView.styles.tsx b/publisher/src/components/MetricsView/MetricsView.styles.tsx
index dc4144466..da9286c6e 100644
--- a/publisher/src/components/MetricsView/MetricsView.styles.tsx
+++ b/publisher/src/components/MetricsView/MetricsView.styles.tsx
@@ -15,10 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import styled from "styled-components/macro";
import { BinaryRadioGroupWrapper } from "../Forms";
-import { palette, typography } from "../GlobalStyles";
export const MetricsViewContainer = styled.div`
width: 100%;
diff --git a/publisher/src/components/MetricsView/MetricsView.tsx b/publisher/src/components/MetricsView/MetricsView.tsx
index 0faad657a..a2419b3fd 100644
--- a/publisher/src/components/MetricsView/MetricsView.tsx
+++ b/publisher/src/components/MetricsView/MetricsView.tsx
@@ -15,12 +15,16 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ AgencySystems,
+ FormError,
+ ReportFrequency,
+} from "@justice-counts/common/types";
import { debounce as _debounce } from "lodash";
import { reaction, when } from "mobx";
import { observer } from "mobx-react-lite";
import React, { useEffect, useRef, useState } from "react";
-import { AgencySystems, FormError, ReportFrequency } from "../../shared/types";
import { useStore } from "../../stores";
import {
isPositiveNumber,
diff --git a/publisher/src/components/Modal/Modal.tsx b/publisher/src/components/Modal/Modal.tsx
index 6cbda3820..14f0bb817 100644
--- a/publisher/src/components/Modal/Modal.tsx
+++ b/publisher/src/components/Modal/Modal.tsx
@@ -15,11 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ HEADER_BAR_HEIGHT,
+ palette,
+} from "@justice-counts/common/components/GlobalStyles";
import React, { useEffect, useState } from "react";
import styled, { css, keyframes } from "styled-components/macro";
-import { HEADER_BAR_HEIGHT, palette } from "../GlobalStyles";
-
const ModalContainer = styled.div`
width: 100vw;
height: 100vh;
diff --git a/publisher/src/components/Onboarding/Onboarding.tsx b/publisher/src/components/Onboarding/Onboarding.tsx
index 2fa141f18..48179b0bd 100644
--- a/publisher/src/components/Onboarding/Onboarding.tsx
+++ b/publisher/src/components/Onboarding/Onboarding.tsx
@@ -15,12 +15,15 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import React, { useEffect, useRef, useState } from "react";
import styled, { keyframes } from "styled-components/macro";
import { useStore } from "../../stores";
import logo from "../assets/jc-logo-vector-onboarding.png";
-import { palette, typography } from "../GlobalStyles";
import {
DATA_ENTRY_WIDTH,
ONE_PANEL_MAX_WIDTH,
diff --git a/publisher/src/components/Onboarding/OnboardingDataEntrySummary.tsx b/publisher/src/components/Onboarding/OnboardingDataEntrySummary.tsx
index 1523d44c6..d56a30fae 100644
--- a/publisher/src/components/Onboarding/OnboardingDataEntrySummary.tsx
+++ b/publisher/src/components/Onboarding/OnboardingDataEntrySummary.tsx
@@ -15,12 +15,15 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import React, { useEffect, useState } from "react";
import styled from "styled-components/macro";
import closeIcon from "../assets/dark-close-icon.png";
import { ReactComponent as Logo } from "../assets/jc-logo-vector.svg";
-import { palette, typography } from "../GlobalStyles";
import { ONE_PANEL_MAX_WIDTH } from "../Reports/ReportDataEntry.styles";
import { OnboardingBackdropContainer, OnboardingContainer } from "./Onboarding";
diff --git a/publisher/src/components/Reports/CreateReport.tsx b/publisher/src/components/Reports/CreateReport.tsx
index e6c51d5fb..e5bb40b8f 100644
--- a/publisher/src/components/Reports/CreateReport.tsx
+++ b/publisher/src/components/Reports/CreateReport.tsx
@@ -15,12 +15,19 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
+import {
+ CreateReportFormValuesType,
+ ReportOverview,
+} from "@justice-counts/common/types";
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components/macro";
import { trackReportCreated } from "../../analytics";
-import { CreateReportFormValuesType, ReportOverview } from "../../shared/types";
import { useStore } from "../../stores";
import { monthsByName, printDateRangeFromMonthYear } from "../../utils";
import {
@@ -38,7 +45,6 @@ import {
TitleWrapper,
} from "../Forms";
import { Dropdown } from "../Forms/Dropdown";
-import { palette, typography } from "../GlobalStyles";
import { showToast } from "../Toast";
import {
PublishButton,
diff --git a/publisher/src/components/Reports/DataEntryForm.tsx b/publisher/src/components/Reports/DataEntryForm.tsx
index cdca086a8..9343825b0 100644
--- a/publisher/src/components/Reports/DataEntryForm.tsx
+++ b/publisher/src/components/Reports/DataEntryForm.tsx
@@ -15,6 +15,11 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ HEADER_BAR_HEIGHT,
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import { reaction, runInAction } from "mobx";
import { observer } from "mobx-react-lite";
import React, { Fragment, useEffect, useRef, useState } from "react";
@@ -50,7 +55,6 @@ import {
TabbedDisaggregations,
Title,
} from "../Forms";
-import { HEADER_BAR_HEIGHT, palette, typography } from "../GlobalStyles";
import { Onboarding, OnboardingDataEntrySummary } from "../Onboarding";
import { showToast } from "../Toast";
import {
diff --git a/publisher/src/components/Reports/DataEntryFormComponents.tsx b/publisher/src/components/Reports/DataEntryFormComponents.tsx
index c5fe4d754..566940606 100644
--- a/publisher/src/components/Reports/DataEntryFormComponents.tsx
+++ b/publisher/src/components/Reports/DataEntryFormComponents.tsx
@@ -15,15 +15,15 @@
// along with this program. If not, see .
// =============================================================================
-import { observer } from "mobx-react-lite";
-import React from "react";
-
import {
Metric,
MetricContext,
MetricDisaggregationDimensions,
MetricDisaggregations,
-} from "../../shared/types";
+} from "@justice-counts/common/types";
+import { observer } from "mobx-react-lite";
+import React from "react";
+
import { useStore } from "../../stores";
import { formatNumberInput } from "../../utils";
import { BinaryRadioButton, TextInput } from "../Forms";
diff --git a/publisher/src/components/Reports/HelperText.tsx b/publisher/src/components/Reports/HelperText.tsx
index 8bb372b1e..aa4b11b87 100644
--- a/publisher/src/components/Reports/HelperText.tsx
+++ b/publisher/src/components/Reports/HelperText.tsx
@@ -15,11 +15,14 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import React from "react";
import styled from "styled-components/macro";
import { useStore } from "../../stores";
-import { palette, typography } from "../GlobalStyles";
import {
BREAKPOINT_HEIGHT,
TWO_PANEL_MAX_WIDTH,
diff --git a/publisher/src/components/Reports/PublishConfirmation.tsx b/publisher/src/components/Reports/PublishConfirmation.tsx
index de26b031b..c31a95327 100644
--- a/publisher/src/components/Reports/PublishConfirmation.tsx
+++ b/publisher/src/components/Reports/PublishConfirmation.tsx
@@ -15,23 +15,23 @@
// along with this program. If not, see .
// =============================================================================
+import { palette } from "@justice-counts/common/components/GlobalStyles";
+import {
+ MetricContextWithErrors,
+ MetricDisaggregationDimensionsWithErrors,
+ MetricDisaggregationsWithErrors,
+ MetricWithErrors,
+} from "@justice-counts/common/types";
import { observer } from "mobx-react-lite";
import React, { Fragment, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components/macro";
import { trackReportPublished } from "../../analytics";
-import {
- MetricContextWithErrors,
- MetricDisaggregationDimensionsWithErrors,
- MetricDisaggregationsWithErrors,
- MetricWithErrors,
-} from "../../shared/types";
import { useStore } from "../../stores";
import { printReportTitle, rem } from "../../utils";
import errorIcon from "../assets/status-error-icon.png";
import { Button } from "../Forms";
-import { palette } from "../GlobalStyles";
import { showToast } from "../Toast";
import { PublishButton } from "./ReportDataEntry.styles";
diff --git a/publisher/src/components/Reports/ReportDataEntry.styles.tsx b/publisher/src/components/Reports/ReportDataEntry.styles.tsx
index b150e7734..d9440a8ad 100644
--- a/publisher/src/components/Reports/ReportDataEntry.styles.tsx
+++ b/publisher/src/components/Reports/ReportDataEntry.styles.tsx
@@ -15,11 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import React from "react";
import styled from "styled-components/macro";
-import { palette, typography } from "../GlobalStyles";
-
export const SIDE_PANEL_WIDTH = 360;
export const DATA_ENTRY_WIDTH = 644;
export const SIDE_PANEL_HORIZONTAL_PADDING = 24;
diff --git a/publisher/src/components/Reports/ReportDataEntry.tsx b/publisher/src/components/Reports/ReportDataEntry.tsx
index 207559d06..b01081395 100644
--- a/publisher/src/components/Reports/ReportDataEntry.tsx
+++ b/publisher/src/components/Reports/ReportDataEntry.tsx
@@ -15,13 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import { Report } from "@justice-counts/common/types";
import { when } from "mobx";
import { observer } from "mobx-react-lite";
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { trackReportUnpublished } from "../../analytics";
-import { Report } from "../../shared/types";
import { useStore } from "../../stores";
import { printReportTitle } from "../../utils";
import { PageWrapper } from "../Forms";
diff --git a/publisher/src/components/Reports/ReportSummaryPanel.tsx b/publisher/src/components/Reports/ReportSummaryPanel.tsx
index 9c2b9066b..4b4d49498 100644
--- a/publisher/src/components/Reports/ReportSummaryPanel.tsx
+++ b/publisher/src/components/Reports/ReportSummaryPanel.tsx
@@ -15,12 +15,16 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
+import { Metric } from "@justice-counts/common/types";
import { observer } from "mobx-react-lite";
import React from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components/macro";
-import { Metric } from "../../shared/types";
import { useStore } from "../../stores";
import {
printCommaSeparatedList,
@@ -35,7 +39,6 @@ import {
PreTitle,
Title,
} from "../Forms";
-import { palette, typography } from "../GlobalStyles";
import HelperText from "./HelperText";
import {
BREAKPOINT_HEIGHT,
diff --git a/publisher/src/components/Reports/Reports.styles.tsx b/publisher/src/components/Reports/Reports.styles.tsx
index 73c1ff2d1..e2987c2ce 100644
--- a/publisher/src/components/Reports/Reports.styles.tsx
+++ b/publisher/src/components/Reports/Reports.styles.tsx
@@ -15,10 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ HEADER_BAR_HEIGHT,
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import styled from "styled-components/macro";
-import { HEADER_BAR_HEIGHT, palette, typography } from "../GlobalStyles";
-
const COLLAPSED_INNER_COLUMNS_WIDTH = 846;
export const PageHeader = styled.div`
diff --git a/publisher/src/components/ReviewMetrics/ReviewMetrics.styles.tsx b/publisher/src/components/ReviewMetrics/ReviewMetrics.styles.tsx
index 99e5cda8e..08dcea26e 100644
--- a/publisher/src/components/ReviewMetrics/ReviewMetrics.styles.tsx
+++ b/publisher/src/components/ReviewMetrics/ReviewMetrics.styles.tsx
@@ -15,10 +15,14 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ HEADER_BAR_HEIGHT,
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
import styled from "styled-components/macro";
import { DataUploadContainer } from "../DataUpload";
-import { HEADER_BAR_HEIGHT, palette, typography } from "../GlobalStyles";
export const MAIN_PANEL_MAX_WIDTH = 864;
diff --git a/publisher/src/components/ReviewMetrics/ReviewMetrics.tsx b/publisher/src/components/ReviewMetrics/ReviewMetrics.tsx
index 5208b1faa..ff007226e 100644
--- a/publisher/src/components/ReviewMetrics/ReviewMetrics.tsx
+++ b/publisher/src/components/ReviewMetrics/ReviewMetrics.tsx
@@ -15,11 +15,14 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ DataVizAggregateName,
+ RawDatapoint,
+} from "@justice-counts/common/types";
import { observer } from "mobx-react-lite";
import React, { useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
-import { DataVizAggregateName, RawDatapoint } from "../../shared/types";
import logoImg from "../assets/jc-logo-vector.png";
import {
Button,
diff --git a/publisher/src/components/Toast/Toast.ts b/publisher/src/components/Toast/Toast.ts
index 8143141fb..15fae52b5 100644
--- a/publisher/src/components/Toast/Toast.ts
+++ b/publisher/src/components/Toast/Toast.ts
@@ -15,8 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ HEADER_BAR_HEIGHT,
+ palette,
+ typography,
+} from "@justice-counts/common/components/GlobalStyles";
+
import checkIconWhite from "../assets/status-check-white-icon.png";
-import { HEADER_BAR_HEIGHT, palette, typography } from "../GlobalStyles";
type ToastColor = "blue" | "red" | "grey";
diff --git a/publisher/src/index.tsx b/publisher/src/index.tsx
index 55146997f..8b0d0c59f 100644
--- a/publisher/src/index.tsx
+++ b/publisher/src/index.tsx
@@ -15,6 +15,7 @@
// along with this program. If not, see .
// =============================================================================
+import { palette } from "@justice-counts/common/components/GlobalStyles";
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
@@ -22,7 +23,6 @@ import { createGlobalStyle } from "styled-components/macro";
import App from "./App";
import AuthWall from "./components/Auth";
-import { palette } from "./components/GlobalStyles";
import { StoreProvider } from "./stores";
// load analytics
diff --git a/publisher/src/mocks/PreviewDataObject.tsx b/publisher/src/mocks/PreviewDataObject.tsx
index ab2aa1355..9bfde3f4a 100644
--- a/publisher/src/mocks/PreviewDataObject.tsx
+++ b/publisher/src/mocks/PreviewDataObject.tsx
@@ -15,11 +15,10 @@
// along with this program. If not, see .
// =============================================================================
+import { palette } from "@justice-counts/common/components/GlobalStyles";
import React, { useState } from "react";
import styled from "styled-components/macro";
-import { palette } from "../components/GlobalStyles";
-
const PreviewButton = styled.button<{ open?: boolean }>`
height: 80px;
width: 80px;
diff --git a/publisher/src/pages/AccountSettings.tsx b/publisher/src/pages/AccountSettings.tsx
index 202d58038..6525e5183 100644
--- a/publisher/src/pages/AccountSettings.tsx
+++ b/publisher/src/pages/AccountSettings.tsx
@@ -15,6 +15,7 @@
// along with this program. If not, see .
// =============================================================================
+import { typography } from "@justice-counts/common/components/GlobalStyles";
import { debounce as _debounce } from "lodash";
import React, { useRef } from "react";
import styled from "styled-components/macro";
@@ -25,7 +26,6 @@ import {
UploadedFilesWrapper,
} from "../components/DataUpload";
import { TextInput, Title, TitleWrapper } from "../components/Forms";
-import { typography } from "../components/GlobalStyles";
import { useStore } from "../stores";
const SettingsContainer = styled.div`
diff --git a/publisher/src/pages/Reports.tsx b/publisher/src/pages/Reports.tsx
index 96f19d703..c31b640ff 100644
--- a/publisher/src/pages/Reports.tsx
+++ b/publisher/src/pages/Reports.tsx
@@ -15,6 +15,7 @@
// along with this program. If not, see .
// =============================================================================
+import { Permission, ReportOverview } from "@justice-counts/common/types";
import { reaction, when } from "mobx";
import { observer } from "mobx-react-lite";
import React, { Fragment, useEffect, useState } from "react";
@@ -45,7 +46,6 @@ import {
TabbedOptions,
Table,
} from "../components/Reports";
-import { Permission, ReportOverview } from "../shared/types";
import { useStore } from "../stores";
import {
normalizeString,
diff --git a/publisher/src/stores/DatapointsStore.ts b/publisher/src/stores/DatapointsStore.ts
index ca526e06d..ea0b4dc9f 100644
--- a/publisher/src/stores/DatapointsStore.ts
+++ b/publisher/src/stores/DatapointsStore.ts
@@ -15,6 +15,12 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ DatapointsByMetric,
+ DataVizAggregateName,
+ DimensionNamesByMetricAndDisaggregation,
+ RawDatapoint,
+} from "@justice-counts/common/types";
import {
IReactionDisposer,
makeAutoObservable,
@@ -22,12 +28,6 @@ import {
runInAction,
} from "mobx";
-import {
- DatapointsByMetric,
- DataVizAggregateName,
- DimensionNamesByMetricAndDisaggregation,
- RawDatapoint,
-} from "../shared/types";
import { isPositiveNumber } from "../utils";
import API from "./API";
import UserStore from "./UserStore";
diff --git a/publisher/src/stores/FormStore.ts b/publisher/src/stores/FormStore.ts
index 8a4303c95..c6160e9f1 100644
--- a/publisher/src/stores/FormStore.ts
+++ b/publisher/src/stores/FormStore.ts
@@ -15,8 +15,6 @@
// along with this program. If not, see .
// =============================================================================
-import { makeAutoObservable } from "mobx";
-
import {
FormError,
FormStoreContextValues,
@@ -24,7 +22,9 @@ import {
FormStoreMetricValues,
Metric,
UpdatedMetricsValues,
-} from "../shared/types";
+} from "@justice-counts/common/types";
+import { makeAutoObservable } from "mobx";
+
import {
isPositiveNumber,
normalizeToString,
diff --git a/publisher/src/stores/ReportStore.test.tsx b/publisher/src/stores/ReportStore.test.tsx
index 07212a837..6ca061b2e 100644
--- a/publisher/src/stores/ReportStore.test.tsx
+++ b/publisher/src/stores/ReportStore.test.tsx
@@ -15,13 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import { ReportOverview } from "@justice-counts/common/types";
import { render, screen } from "@testing-library/react";
import { runInAction } from "mobx";
import React from "react";
import mockJSON from "../mocks/reportOverviews.json";
import Reports from "../pages/Reports";
-import { ReportOverview } from "../shared/types";
import { rootStore, StoreProvider } from ".";
const mockUnorderedReportsMap: { [reportID: string]: ReportOverview } = {};
diff --git a/publisher/src/stores/ReportStore.ts b/publisher/src/stores/ReportStore.ts
index 2fcb4a429..b5f5a3d3c 100644
--- a/publisher/src/stores/ReportStore.ts
+++ b/publisher/src/stores/ReportStore.ts
@@ -15,6 +15,13 @@
// along with this program. If not, see .
// =============================================================================
+import {
+ Metric,
+ Report,
+ ReportOverview,
+ ReportStatus,
+ UpdatedMetricsValues,
+} from "@justice-counts/common/types";
import {
IReactionDisposer,
makeAutoObservable,
@@ -24,13 +31,6 @@ import {
import { UploadedFileStatus } from "../components/DataUpload";
import { MetricSettings } from "../components/MetricsView";
-import {
- Metric,
- Report,
- ReportOverview,
- ReportStatus,
- UpdatedMetricsValues,
-} from "../shared/types";
import { groupBy } from "../utils/helperUtils";
import API from "./API";
import UserStore from "./UserStore";
diff --git a/publisher/src/stores/UserStore.ts b/publisher/src/stores/UserStore.ts
index 897553460..71219903e 100644
--- a/publisher/src/stores/UserStore.ts
+++ b/publisher/src/stores/UserStore.ts
@@ -14,12 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
// =============================================================================
+import { UserAgency } from "@justice-counts/common/types";
import { makeAutoObservable, runInAction, when } from "mobx";
import { makePersistable } from "mobx-persist-store";
import { APP_METADATA_CLAIM, AuthStore } from "../components/Auth";
import { showToast } from "../components/Toast";
-import { UserAgency } from "../shared/types";
import API from "./API";
type UserSettingsRequestBody = {
diff --git a/publisher/src/utils/dateUtils.ts b/publisher/src/utils/dateUtils.ts
index a3ec6e09b..1ae5a6cc1 100644
--- a/publisher/src/utils/dateUtils.ts
+++ b/publisher/src/utils/dateUtils.ts
@@ -15,7 +15,7 @@
// along with this program. If not, see .
// =============================================================================
-import { ReportFrequency } from "../shared/types";
+import { ReportFrequency } from "@justice-counts/common/types";
export const monthsByName = [
"January",
diff --git a/publisher/src/utils/helperUtils.ts b/publisher/src/utils/helperUtils.ts
index 41bf9f106..e658828c9 100644
--- a/publisher/src/utils/helperUtils.ts
+++ b/publisher/src/utils/helperUtils.ts
@@ -15,10 +15,9 @@
// along with this program. If not, see .
// =============================================================================
+import { MetricContext } from "@justice-counts/common/types";
import { debounce, memoize } from "lodash";
-import { MetricContext } from "../shared/types";
-
export const isPositiveNumber = (value: string | number) => {
if (typeof value === "string") {
return (value.trim() !== "" && Number(value) === 0) || Number(value) > 0;