Skip to content

Commit

Permalink
Add place type to app (#682)
Browse files Browse the repository at this point in the history
This is really helpful to e.g. look at state-level reform. We update the
counter to say the place type if exactly one matches.
  • Loading branch information
Eric-Arellano authored Dec 29, 2024
1 parent f08cc6c commit 9e135fd
Show file tree
Hide file tree
Showing 16 changed files with 3,477 additions and 22 deletions.
3,299 changes: 3,299 additions & 0 deletions data/core.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions scripts/generateDataSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function createLegacyCsv(data: ProcessedCompleteEntry[]): string {
place: entry.place.name,
state: entry.place.state,
country: entry.place.country,
place_type: entry.place.type,
all_minimums_repealed: toBoolean(entry.place.repeal),
status: entry.unifiedPolicy.status,
policy_change: entry.unifiedPolicy.policy,
Expand Down Expand Up @@ -54,6 +55,7 @@ export function createAnyPolicyCsv(data: ProcessedCompleteEntry[]): string {
place: entry.place.name,
state: entry.place.state,
country: entry.place.country,
place_type: entry.place.type,
population: entry.place.pop,
lat: entry.place.coord[1],
long: entry.place.coord[0],
Expand Down Expand Up @@ -89,6 +91,7 @@ export function createReformCsv(
state: entry.place.state,
country: entry.place.country,
population: entry.place.pop,
place_type: entry.place.type,
lat: entry.place.coord[1],
long: entry.place.coord[0],
all_minimums_repealed: toBoolean(entry.place.repeal),
Expand Down
4 changes: 1 addition & 3 deletions scripts/lib/directus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
QueryFilter,
} from "@directus/sdk";

import { PolicyType, ReformStatus } from "../../src/js/types.js";
import { PlaceType, PolicyType, ReformStatus } from "../../src/js/types.js";

export const CITATIONS_FILES_FOLDER = "f085de08-b747-4251-973d-1752ccc29649";

Expand Down Expand Up @@ -56,8 +56,6 @@ export interface Schema {
policy_records_citations: PolicyRecordCitationJunction[];
}

export type PlaceType = "city" | "county" | "state" | "country";

export type Place = {
name: string;
state: string | null;
Expand Down
3 changes: 3 additions & 0 deletions scripts/syncDirectus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ async function readPlacesAndEnsureCoordinates(
"name",
"state",
"country_code",
"type",
"population",
"complete_minimums_repeal",
"coordinates",
Expand Down Expand Up @@ -447,6 +448,7 @@ function combineData(
name: place.name!,
state: place.state!,
country: place.country_code!,
type: place.type!,
pop: place.population!,
repeal: place.complete_minimums_repeal!,
coord: place.coordinates!.coordinates,
Expand Down Expand Up @@ -490,6 +492,7 @@ async function saveCoreData(
name: entry.place.name,
state: entry.place.state,
country: entry.place.country,
type: entry.place.type,
pop: entry.place.pop,
coord: entry.place.coord,
repeal: entry.place.repeal,
Expand Down
13 changes: 13 additions & 0 deletions src/js/FilterState.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isEqual } from "lodash-es";
import {
PlaceId,
PlaceType,
PolicyType,
ProcessedCoreEntry,
ProcessedCorePolicy,
Expand Down Expand Up @@ -39,6 +40,7 @@ export interface FilterState {
searchInput: string | null;
policyTypeFilter: PolicyTypeFilter;
allMinimumsRemovedToggle: boolean;
placeType: Set<string>;
includedPolicyChanges: Set<string>;
scope: Set<string>;
landUse: Set<string>;
Expand Down Expand Up @@ -83,6 +85,7 @@ interface CacheEntry {
matchedPlaces: Record<PlaceId, PlaceMatch>;
matchedCountries: Set<string>;
matchedPolicyTypesForAnyPolicy: Set<PolicyType>;
matchedPlaceTypes: Set<PlaceType>;
numMatchedPolicyRecordsForSinglePolicy: number;
}

Expand Down Expand Up @@ -124,6 +127,10 @@ export class PlaceFilterManager {
return this.ensureCache().matchedCountries;
}

get matchedPlaceTypes(): Set<PlaceType> {
return this.ensureCache().matchedPlaceTypes;
}

/// The policy types the matched places have.
///
/// This is only set when the policy type is 'any parking reform'.
Expand Down Expand Up @@ -161,12 +168,14 @@ export class PlaceFilterManager {
const matchedPlaces: Record<PlaceId, PlaceMatch> = {};
const matchedCountries = new Set<string>();
const matchedPolicyTypes = new Set<PolicyType>();
const matchedPlaceTypes = new Set<PlaceType>();
let numMatchedPolicyRecords = 0;
for (const placeId in this.entries) {
const match = this.getPlaceMatch(placeId);
if (!match) continue;
matchedPlaces[placeId] = match;
matchedCountries.add(this.entries[placeId].place.country);
matchedPlaceTypes.add(this.entries[placeId].place.type);
if (match.type === "single policy") {
numMatchedPolicyRecords += match.matchingIndexes.length;
}
Expand All @@ -183,6 +192,7 @@ export class PlaceFilterManager {
matchedPlaces,
matchedCountries,
matchedPolicyTypesForAnyPolicy: matchedPolicyTypes,
matchedPlaceTypes,
numMatchedPolicyRecordsForSinglePolicy: numMatchedPolicyRecords,
};
return this.cache;
Expand All @@ -191,6 +201,9 @@ export class PlaceFilterManager {
private matchesPlace(place: ProcessedPlace): boolean {
const filterState = this.state.getValue();

const isPlaceType = filterState.placeType.has(place.type);
if (!isPlaceType) return false;

const isCountry = filterState.country.has(place.country);
if (!isCountry) return false;

Expand Down
27 changes: 22 additions & 5 deletions src/js/counters.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isEqual } from "lodash-es";

import { FilterState, PlaceFilterManager } from "./FilterState";
import { PolicyType } from "./types";
import { PlaceType, PolicyType } from "./types";

export function determineHtml(
view: "table" | "map",
Expand All @@ -10,6 +10,7 @@ export function determineHtml(
numPolicyRecords: number,
matchedPolicyTypes: Set<PolicyType>,
matchedCountries: Set<string>,
matchedPlaceTypes: Set<PlaceType>,
): string {
if (!numPlaces) {
return "No places selected — use the filter or search icons";
Expand All @@ -26,17 +27,31 @@ export function determineHtml(
country = `the ${country}`;
}

const placesWord = numPlaces === 1 ? "place" : "places";
const recordsWord = numPolicyRecords === 1 ? "record" : "records";
let placeDescription;
if (isEqual(matchedPlaceTypes, new Set(["city"]))) {
const label = numPlaces === 1 ? "city" : "cities";
placeDescription = `${label} in ${country}`;
} else if (isEqual(matchedPlaceTypes, new Set(["county"]))) {
const label = numPlaces === 1 ? "county" : "counties";
placeDescription = `${label} in ${country}`;
} else if (isEqual(matchedPlaceTypes, new Set(["state"]))) {
const label = numPlaces === 1 ? "state" : "states";
placeDescription = `${label} in ${country}`;
} else if (isEqual(matchedPlaceTypes, new Set(["country"]))) {
placeDescription = numPlaces === 1 ? "country" : "countries";
} else {
const label = numPlaces === 1 ? "place" : "places";
placeDescription = `${label} in ${country}`;
}
const prefix = `Showing ${numPlaces} ${placeDescription} with`;

// We only show the number of policy records when it's useful information to the user
// because it would otherwise be noisy.
const recordsWord = numPolicyRecords === 1 ? "record" : "records";
const showRecords = view === "table" && numPlaces !== numPolicyRecords;
const multipleRecordsExplanation =
"because some places have multiple records";

const prefix = `Showing ${numPlaces} ${placesWord} in ${country} with`;

if (state.policyTypeFilter === "legacy reform") {
const suffix = state.allMinimumsRemovedToggle
? "all parking minimums removed"
Expand Down Expand Up @@ -148,6 +163,7 @@ export default function initCounters(manager: PlaceFilterManager): void {
manager.numMatchedPolicyRecords,
manager.matchedPolicyTypes,
manager.matchedCountries,
manager.matchedPlaceTypes,
);
tableCounter.innerHTML = determineHtml(
"table",
Expand All @@ -156,6 +172,7 @@ export default function initCounters(manager: PlaceFilterManager): void {
manager.numMatchedPolicyRecords,
manager.matchedPolicyTypes,
manager.matchedCountries,
manager.matchedPlaceTypes,
);
});
}
25 changes: 18 additions & 7 deletions src/js/filterOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { initPopulationSlider } from "./populationSlider";

// Keep in alignment with FilterState.
type FilterGroupKey =
| "placeType"
| "includedPolicyChanges"
| "scope"
| "landUse"
Expand All @@ -20,6 +21,7 @@ type FilterGroupKey =
| "year";

const DESELECTED_BY_DEFAULT: Record<FilterGroupKey, Set<string>> = {
placeType: new Set(),
includedPolicyChanges: new Set(),
scope: new Set(),
landUse: new Set(),
Expand All @@ -32,13 +34,15 @@ export class FilterOptions {
readonly options: Record<FilterGroupKey, string[]>;

constructor(entries: ProcessedCoreEntry[]) {
const placeType = new Set<string>();
const policy = new Set<string>();
const scope = new Set<string>();
const landUse = new Set<string>();
const status = new Set<string>();
const country = new Set<string>();
const year = new Set<string>();
entries.forEach((entry) => {
placeType.add(entry.place.type);
status.add(entry.unifiedPolicy.status);
country.add(entry.place.country);
year.add(
Expand All @@ -49,6 +53,7 @@ export class FilterOptions {
entry.unifiedPolicy.land.forEach((v) => landUse.add(v));
});
this.options = {
placeType: Array.from(placeType).sort(),
includedPolicyChanges: Array.from(policy).sort(),
scope: Array.from(scope).sort(),
landUse: Array.from(landUse).sort(),
Expand Down Expand Up @@ -465,18 +470,18 @@ export function initFilterOptions(
policyTypeFilter !== "any parking reform",
});
initFilterGroup(filterManager, filterOptions, filterPopup, {
htmlName: "land-use",
filterStateKey: "landUse",
legend: "Affected land use",
htmlName: "scope",
filterStateKey: "scope",
legend: "Reform scope",
hide: ({ policyTypeFilter, allMinimumsRemovedToggle }) =>
policyTypeFilter === "any parking reform" ||
(allMinimumsRemovedToggle &&
policyTypeFilter !== "reduce parking minimums"),
});
initFilterGroup(filterManager, filterOptions, filterPopup, {
htmlName: "scope",
filterStateKey: "scope",
legend: "Reform scope",
htmlName: "land-use",
filterStateKey: "landUse",
legend: "Affected land use",
hide: ({ policyTypeFilter, allMinimumsRemovedToggle }) =>
policyTypeFilter === "any parking reform" ||
(allMinimumsRemovedToggle &&
Expand All @@ -497,11 +502,17 @@ export function initFilterOptions(
});

// Options about the Place
initPopulationSlider(filterManager, filterPopup);
initFilterGroup(filterManager, filterOptions, filterPopup, {
htmlName: "country",
filterStateKey: "country",
legend: "Country",
preserveCapitalization: true,
});
initFilterGroup(filterManager, filterOptions, filterPopup, {
htmlName: "place-type",
filterStateKey: "placeType",
legend: "Jurisdiction",
useTwoColumns: true,
});
initPopulationSlider(filterManager, filterPopup);
}
1 change: 1 addition & 0 deletions src/js/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export default async function initApp(): Promise<void> {
searchInput: null,
policyTypeFilter: revampEnabled ? "any parking reform" : "legacy reform",
allMinimumsRemovedToggle: true,
placeType: filterOptions.default("placeType"),
includedPolicyChanges: filterOptions.default("includedPolicyChanges"),
scope: filterOptions.default("scope"),
landUse: filterOptions.default("landUse"),
Expand Down
2 changes: 2 additions & 0 deletions src/js/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const PLACE_COLUMNS: ColumnDefinition[] = [
},
{ title: "State", field: "state", width: 70 },
{ title: "Country", field: "country", width: 110 },
{ title: "Jurisdiction", field: "placeType", width: 80 },
{
title: "Population",
field: "population",
Expand Down Expand Up @@ -163,6 +164,7 @@ export default function initTable(
place: entry.place.name,
state: entry.place.state,
country: entry.place.country,
placeType: entry.place.type,
population: entry.place.pop.toLocaleString("en-us"),
url: entry.place.url,
};
Expand Down
2 changes: 2 additions & 0 deletions src/js/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ export class Date {
}

export type PlaceId = string;
export type PlaceType = "city" | "county" | "state" | "country";

export interface RawPlace {
// Full name of the town, city, county, province, state, or country.
name: string;
// State or province abbreviation. Not set for countries.
state: string | null;
country: string;
type: PlaceType;
pop: number;
// [long, lat]
coord: [number, number];
Expand Down
11 changes: 11 additions & 0 deletions tests/app/FilterState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ test.describe("PlaceFilterManager.matchedPolicyRecords()", () => {
landUse: new Set(["all uses", "commercial", "other"]),
status: new Set(["implemented", "passed"]),
country: new Set(["United States", "Brazil"]),
placeType: new Set(["city", "county"]),
year: new Set(["2023", "2024"]),
populationSliderIndexes: [0, POPULATION_MAX_INDEX],
};
Expand All @@ -28,6 +29,7 @@ test.describe("PlaceFilterManager.matchedPolicyRecords()", () => {
name: "Place 1",
state: "",
country: "United States",
type: "city",
pop: 48100,
repeal: false,
coord: [0, 0],
Expand Down Expand Up @@ -56,6 +58,7 @@ test.describe("PlaceFilterManager.matchedPolicyRecords()", () => {
name: "Place 2",
state: "",
country: "Brazil",
type: "county",
pop: 400,
repeal: true,
coord: [0, 0],
Expand Down Expand Up @@ -160,6 +163,14 @@ test.describe("PlaceFilterManager.matchedPolicyRecords()", () => {
manager.update({
populationSliderIndexes: DEFAULT_STATE.populationSliderIndexes,
});

manager.update({ placeType: new Set(["county"]) });
expect(manager.matchedPlaces).toEqual({
"Place 2": expectedPlace2Match,
});
manager.update({
placeType: DEFAULT_STATE.placeType,
});
});

test("reduce minimums", () => {
Expand Down
Loading

0 comments on commit 9e135fd

Please sign in to comment.