Skip to content

Commit

Permalink
Make city stats generic (#309)
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric-Arellano authored Jan 11, 2025
1 parent 24f0262 commit ffd0b64
Show file tree
Hide file tree
Showing 14 changed files with 70 additions and 52 deletions.
12 changes: 2 additions & 10 deletions packages/ct/data/city-stats.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,15 @@
"hartford": {
"name": "Hartford",
"percentage": "26%",
"cityType": "core",
"population": "121,054",
"urbanizedAreaPopulation": "977,158",
"parkingScore": "53",
"reforms": "adopted",
"url": "https://parkingreform.org/mandates-map/city_detail/Hartford_CT.html",
"contribution": null
"url": "https://parkingreform.org/mandates-map/city_detail/Hartford_CT.html"
},
"new-haven": {
"name": "New Haven",
"percentage": "34%",
"cityType": "principal",
"population": "134,023",
"urbanizedAreaPopulation": "561,456",
"parkingScore": null,
"reforms": "adopted",
"url": "https://parkingreform.org/mandates-map/city_detail/NewHaven_CT.html",
"contribution": null
"url": "https://parkingreform.org/mandates-map/city_detail/NewHaven_CT.html"
}
}
4 changes: 3 additions & 1 deletion packages/ct/src/js/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import type {
CityBoundaries,
} from "@prn-parking-lots/shared/src/js/model/types";

import type { CityStats } from "./types";

import UNTYPED_CITY_STATS_DATA from "../../data/city-stats.json" with { type: "json" };
const CITY_STATS_DATA: CityStatsCollection = UNTYPED_CITY_STATS_DATA;
const CITY_STATS_DATA: CityStatsCollection<CityStats> = UNTYPED_CITY_STATS_DATA;

// @ts-expect-error TypeScript doesn't understand GeoJSON.
import UNTYPED_CITY_BOUNDARIES_GEOJSON from "../../data/city-boundaries.geojson" with { type: "json" };
Expand Down
6 changes: 4 additions & 2 deletions packages/ct/src/js/dropdownGroups.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { DropdownGroup } from "@prn-parking-lots/shared/src/js/city-ui/dropdownUtils";
import { CityStatsCollection } from "@prn-parking-lots/shared/src/js/model/types";
import type { CityStatsCollection } from "@prn-parking-lots/shared/src/js/model/types";

import type { CityStats } from "./types";

export default function createDropdownGroups(
data: CityStatsCollection,
data: CityStatsCollection<CityStats>,
): DropdownGroup[] {
return [
{
Expand Down
2 changes: 1 addition & 1 deletion packages/ct/src/js/scorecard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
formatHeader,
formatReformLine,
} from "@prn-parking-lots/shared/src/js/city-ui/scorecard";
import { CityStats } from "@prn-parking-lots/shared/src/js/model/types";
import type { CityStats } from "./types";

export default function formatScorecard(stats: CityStats): ScorecardValues {
const header = formatHeader({
Expand Down
3 changes: 3 additions & 0 deletions packages/ct/src/js/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { CommonCityStats } from "@prn-parking-lots/shared/src/js/model/types";

export type CityStats = CommonCityStats;
4 changes: 3 additions & 1 deletion packages/primary/src/js/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import type {
CityBoundaries,
} from "@prn-parking-lots/shared/src/js/model/types";

import type { CityStats } from "./types";

import UNTYPED_CITY_STATS_DATA from "../../data/city-stats.json" with { type: "json" };
const CITY_STATS_DATA: CityStatsCollection = UNTYPED_CITY_STATS_DATA;
const CITY_STATS_DATA: CityStatsCollection<CityStats> = UNTYPED_CITY_STATS_DATA;

// @ts-expect-error TypeScript doesn't understand GeoJSON.
import UNTYPED_CITY_BOUNDARIES_GEOJSON from "../../data/city-boundaries.geojson" with { type: "json" };
Expand Down
6 changes: 4 additions & 2 deletions packages/primary/src/js/dropdownGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import {
DropdownGroup,
DropdownChoiceId,
} from "@prn-parking-lots/shared/src/js/city-ui/dropdownUtils";
import { CityStatsCollection } from "@prn-parking-lots/shared/src/js/model/types";
import type { CityStatsCollection } from "@prn-parking-lots/shared/src/js/model/types";

import type { CityStats } from "./types";

export default function createDropdownGroups(
data: CityStatsCollection,
data: CityStatsCollection<CityStats>,
): DropdownGroup[] {
const official: DropdownChoiceId[] = [];
const community: DropdownChoiceId[] = [];
Expand Down
2 changes: 1 addition & 1 deletion packages/primary/src/js/scorecard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
formatHeader,
formatReformLine,
} from "@prn-parking-lots/shared/src/js/city-ui/scorecard";
import { CityStats } from "@prn-parking-lots/shared/src/js/model/types";
import type { CityStats } from "./types";

export default function formatScorecard(stats: CityStats): ScorecardValues {
let header = formatHeader({
Expand Down
8 changes: 8 additions & 0 deletions packages/primary/src/js/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { CommonCityStats } from "@prn-parking-lots/shared/src/js/model/types";

export type CityStats = CommonCityStats & {
cityType: string;
urbanizedAreaPopulation: string;
parkingScore: string | null;
contribution: string | null;
};
12 changes: 7 additions & 5 deletions packages/shared/src/js/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,20 @@ import {
import ParkingLotLoader from "./map-layers/ParkingLotLoader";

import { extractCityIdFromUrl } from "./model/cityId";
import type { CityId, DataSet } from "./model/types";
import type { CityId, DataSet, BaseCityStats } from "./model/types";

import { initViewState } from "./state/ViewState";

interface Args {
data: DataSet;
interface Args<T extends BaseCityStats> {
data: DataSet<T>;
initialCity: CityId;
dropdownGroups: DropdownGroup[];
scorecardFormatter: ScorecardFormatter;
scorecardFormatter: ScorecardFormatter<T>;
}

export default async function bootstrapApp(args: Args): Promise<void> {
export default async function bootstrapApp<T extends BaseCityStats>(
args: Args<T>,
): Promise<void> {
initIcons();
maybeDisableFullScreenIcon();
initAbout();
Expand Down
12 changes: 7 additions & 5 deletions packages/shared/src/js/city-ui/scorecard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CityEntryCollection, CityStats } from "../model/types";
import type { BaseCityStats, CityEntryCollection } from "../model/types";
import Observable from "../state/Observable";
import { ViewStateObservable } from "../state/ViewState";

Expand All @@ -7,7 +7,9 @@ export interface ScorecardValues {
listEntries: string[];
}

export type ScorecardFormatter = (stats: CityStats) => ScorecardValues;
export type ScorecardFormatter<T extends BaseCityStats> = (
stats: T,
) => ScorecardValues;

function generateScorecard(values: ScorecardValues): string {
const accordion = `<div class="scorecard-accordion">
Expand Down Expand Up @@ -94,10 +96,10 @@ function initAccordion(): void {
isExpanded.initialize();
}

export default function subscribeScorecard(
export default function subscribeScorecard<T extends BaseCityStats>(
viewState: ViewStateObservable,
cityEntries: CityEntryCollection,
scorecardFormatter: ScorecardFormatter,
cityEntries: CityEntryCollection<T>,
scorecardFormatter: ScorecardFormatter<T>,
): void {
viewState.subscribe(({ cityId }) => {
const scorecardContainer = document.querySelector(".scorecard-container");
Expand Down
9 changes: 5 additions & 4 deletions packages/shared/src/js/map-layers/citiesLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,20 @@ import type {
CityEntryCollection,
CityStatsCollection,
CityBoundaries,
BaseCityStats,
} from "../model/types";
import { ViewStateObservable } from "../state/ViewState";
import { STYLES } from "../layout/map";

/**
* Load the cities from GeoJson and associate each city with its layer and scorecard entry.
*/
export function createCitiesLayer(
export function createCitiesLayer<T extends BaseCityStats>(
map: Map,
cityBoundaries: CityBoundaries,
cityStatsData: CityStatsCollection,
): [GeoJSON, CityEntryCollection] {
const cityEntries: CityEntryCollection = {};
cityStatsData: CityStatsCollection<T>,
): [GeoJSON, CityEntryCollection<T>] {
const cityEntries: CityEntryCollection<T> = {};
const boundaries = geoJSON(cityBoundaries, {
style() {
return STYLES.cities;
Expand Down
10 changes: 5 additions & 5 deletions packages/shared/src/js/mapPosition.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ImageOverlay, Map } from "leaflet";

import type { CityEntryCollection } from "./model/types";
import type { BaseCityStats, CityEntryCollection } from "./model/types";
import { ViewStateObservable } from "./state/ViewState";
import ParkingLotLoader from "./map-layers/ParkingLotLoader";

Expand All @@ -18,10 +18,10 @@ function snapToCity(map: Map, layer: ImageOverlay): void {
map.setView(translatedCenter);
}

export function subscribeSnapToCity(
export function subscribeSnapToCity<T extends BaseCityStats>(
viewState: ViewStateObservable,
map: Map,
cityEntries: CityEntryCollection,
cityEntries: CityEntryCollection<T>,
): void {
viewState.subscribe((state) => {
if (!state.shouldSnapMap) return;
Expand All @@ -34,10 +34,10 @@ export function subscribeSnapToCity(
*
* Regardless of if the city is chosen, ensure its parking lots are loaded when in view.
*/
export function setCityByMapPosition(
export function setCityByMapPosition<T extends BaseCityStats>(
viewState: ViewStateObservable,
map: Map,
cityEntries: CityEntryCollection,
cityEntries: CityEntryCollection<T>,
parkingLotLoader: ParkingLotLoader,
): void {
map.on("moveend", () => {
Expand Down
32 changes: 17 additions & 15 deletions packages/shared/src/js/model/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,35 +11,37 @@ import type {
/// (The state code is missing for state-specific maps like CT.)
export type CityId = string;

export interface CityStats {
export interface BaseCityStats {
name: string;
percentage: string;
cityType: string;
population: string;
urbanizedAreaPopulation: string;
parkingScore: string | null;
reforms: string | null;
url: string | null;
contribution: string | null;
}

export type CityStatsCollection = Record<CityId, CityStats>;
export type CityStatsCollection<T extends BaseCityStats> = Record<CityId, T>;

export interface CityEntry {
stats: CityStats;
export interface CityEntry<T extends BaseCityStats> {
stats: T;
layer: ImageOverlay;
}

export type CityEntryCollection = Record<CityId, CityEntry>;
export type CommonCityStats = BaseCityStats & {
percentage: string;
reforms: string | null;
url: string | null;
population: string;
};

export type CityEntryCollection<T extends BaseCityStats> = Record<
CityId,
CityEntry<T>
>;

export type CityBoundaries = FeatureCollection<Polygon, GeoJsonProperties>;

export interface ParkingLotGeoJSONModules {
[key: string]: () => Promise<Feature<Geometry>>;
}

export interface DataSet {
stats: CityStatsCollection;
export interface DataSet<T extends BaseCityStats> {
stats: CityStatsCollection<T>;
boundaries: CityBoundaries;
parkingLots: ParkingLotGeoJSONModules;
}

0 comments on commit ffd0b64

Please sign in to comment.