diff --git a/OwnersRefactor.md b/OwnersRefactor.md
deleted file mode 100644
index 313f0b6e..00000000
--- a/OwnersRefactor.md
+++ /dev/null
@@ -1,162 +0,0 @@
-# Building Owners Performance Refactor
-
-## Problem
-
-The `BuildingOwner.vue` template currently has a performance issue:
-
-- It pulls **all ~3000 buildings** via GraphQL (lines 201-232)
-- Then filters them **client-side** in JavaScript (lines 89-106) using the `BuildingsCustomInfo` constant
-- This loads unnecessary data and performs filtering in the browser instead of at build time
-
-## Current Architecture
-
-1. Building owner mappings are stored in `src/constants/buildings-custom-info.constant.vue`
-2. The `BuildingOwner.vue` template queries all buildings
-3. Client-side code loops through `BuildingsCustomInfo` to find matching building IDs
-4. Filters the full building list to only show buildings for the current owner
-
-## Proposed Solution
-
-Add a new data pipeline step that bakes owner information into the building data CSV, allowing GraphQL to filter at query time.
-
-### Implementation Steps
-
-#### 1. Create New Pipeline Script
-
-**File:** `src/data/scripts/add_building_owners.py`
-
-This script should:
-
-- Parse `src/constants/buildings-custom-info.constant.vue` to extract the owner mappings
- - Look for lines like `'251330': { owner: BuildingOwners.depaul.key }`
- - Extract the building ID and owner key
-- Read the processed building CSV from the previous pipeline step
-- Add an `Owner` column to each building row based on the mappings
-- Output the updated CSV for the next pipeline step
-
-**Parsing Strategy:**
-
-- Use regex to find patterns like `'buildingId': { owner: BuildingOwners.ownerKey.key }`
-- Build a dictionary mapping building IDs to owner keys
-- Handle both simple owner assignments and complex objects with tags/links
-
-#### 2. Update Pipeline
-
-**File:** `run_all.py`
-
-Add the new step to the `pipeline_steps` list after `process_data`:
-
-```python
-pipeline_steps = [
- {
- "module": "src.data.scripts.clean_and_split_data",
- "description": "clean_and_split_data",
- },
- {"module": "src.data.scripts.process_data", "description": "process_data"},
- {
- "module": "src.data.scripts.add_building_owners",
- "description": "add_building_owners",
- },
- {
- "module": "src.data.scripts.add_context_by_property_type",
- "description": "add_context_by_property_type",
- },
- # ... rest of pipeline
-]
-```
-
-#### 3. Update BuildingOwner.vue GraphQL Query
-
-**File:** `src/templates/BuildingOwner.vue`
-
-Replace the current query (lines 201-232) with:
-
-```graphql
-
- query($ownerId: String!) {
- allBuilding(
- filter: { Owner: { eq: $ownerId } }
- sortBy: "GHGIntensity"
- ) {
- edges {
- node {
- slugSource
- ID
- DataYear
- PropertyName
- Address
- path
- PrimaryPropertyType
- GHGIntensity
- GHGIntensityRank
- GHGIntensityPercentileRank
- TotalGHGEmissions
- TotalGHGEmissionsRank
- TotalGHGEmissionsPercentileRank
- AvgPercentileLetterGrade
- NaturalGasUse
- DistrictSteamUse
- DataAnomalies
- GrossFloorArea
- GrossFloorAreaRank
- GrossFloorAreaPercentileRank
- YearBuilt
- Owner
- }
- }
- }
- }
-
-```
-
-#### 4. Simplify BuildingOwner.vue Logic
-
-Remove the client-side filtering logic (lines 89-106):
-
-```typescript
-// DELETE this method - no longer needed
-filterBuildings(ownerId: string): void {
- // ... filtering logic
-}
-```
-
-Update `created()` to use the pre-filtered results:
-
-```typescript
-created(): void {
- const ownerId: string = this.$context.ownerId;
-
- if (BuildingOwners[ownerId]) {
- this.currOwner = BuildingOwners[ownerId];
-
- // Buildings are already filtered by GraphQL
- this.buildingsFiltered = this.$static.allBuilding.edges;
- this.calculateOwnedBuildingStats();
- }
-}
-```
-
-## Benefits
-
-1. **Performance**: Only load ~20-50 buildings per owner instead of ~3000
-2. **Consistency**: Owner data is part of the building data model, just like other fields
-3. **Queryability**: Owner information can be filtered/sorted in GraphQL queries
-4. **Build-time optimization**: Filtering happens during static site generation, not in the browser
-5. **Similar pattern**: Matches how `FirstYearReported` and `LastYearReported` were added for `LatestUpdates.vue`
-
-## Implementation Complexity
-
-**Medium** - Requires:
-
-- Python script to parse TypeScript constant file (regex-based)
-- CSV data manipulation
-- GraphQL query updates
-- TypeScript interface updates (add `Owner?: string` to `IBuilding`)
-
-## Testing Checklist
-
-- [ ] Pipeline runs successfully with new step
-- [ ] Building CSV has `Owner` column with correct values
-- [ ] Building owner pages load correctly for all owners
-- [ ] Performance improvement is measurable (check network tab for reduced payload)
-- [ ] All existing building owner functionality works (stats, tables, etc.)
diff --git a/src/pages/AllElectric.vue b/src/pages/AllElectric.vue
index 547451ec..b7aff729 100644
--- a/src/pages/AllElectric.vue
+++ b/src/pages/AllElectric.vue
@@ -57,7 +57,7 @@ export default class AllElectric extends Vue {}
-
+
query {
allBuilding(
filter: {
@@ -106,15 +106,15 @@ export default class AllElectric extends Vue {}
}
}
}
-
+
- Chicago's {{ $static.allBuilding.edges.length }} All Electric Buildings
+ Chicago's {{ $page.allBuilding.edges.length }} All Electric Buildings
- Cleanest {{ $static.allBuilding.edges.length }} Buildings by Greenhouse
+ Cleanest {{ $page.allBuilding.edges.length }} Buildings by Greenhouse
Gas Intensity
@@ -115,7 +115,7 @@ export default class CleanestBuildings extends Vue {
-
+
diff --git a/src/pages/GeothermalBuildings.vue b/src/pages/GeothermalBuildings.vue
index 12fbc30f..440d9211 100644
--- a/src/pages/GeothermalBuildings.vue
+++ b/src/pages/GeothermalBuildings.vue
@@ -32,16 +32,16 @@ interface IBuildingEdge {
})
export default class GeothermalBuildings extends Vue {
/** Set by Gridsome to results of GraphQL query */
- readonly $static!: { allBuilding: { edges: Array } };
+ readonly $page!: { allBuilding: { edges: Array } };
buildingsFiltered: Array = [];
created(): void {
validateTaggedBuildings(
BuildingTags.hasGeothermalHeatPump,
- this.$static.allBuilding.edges.map((e) => e.node.ID.toString()),
+ this.$page.allBuilding.edges.map((e) => e.node.ID.toString()),
);
- this.buildingsFiltered = this.$static.allBuilding.edges;
+ this.buildingsFiltered = this.$page.allBuilding.edges;
}
}
@@ -51,7 +51,7 @@ export default class GeothermalBuildings extends Vue {
performance optimization. The IDs MUST match buildings tagged with hasGeothermalHeatPump in
buildings-custom-info.constant.vue (validated at runtime via validateTaggedBuildings).
-->
-
+
query {
# PERFORMANCE OPTIMIZATION: Hard-coded filter for buildings with hasGeothermalHeatPump tag
# These IDs MUST match buildings-custom-info.constant.vue (validated at runtime)
@@ -87,7 +87,7 @@ export default class GeothermalBuildings extends Vue {
}
}
}
-
+
diff --git a/src/pages/LargeOwners.vue b/src/pages/LargeOwners.vue
index e4f9adcf..e651f7a1 100644
--- a/src/pages/LargeOwners.vue
+++ b/src/pages/LargeOwners.vue
@@ -32,7 +32,7 @@ export default class LargeOwners extends Vue {
}
-
+
query {
allBuilding(sortBy: "GHGIntensity", limit: 50) {
edges {
@@ -64,7 +64,7 @@ export default class LargeOwners extends Vue {
}
}
}
-
+
diff --git a/src/pages/LatestUpdates.vue b/src/pages/LatestUpdates.vue
index 117822f0..11f5ff3b 100644
--- a/src/pages/LatestUpdates.vue
+++ b/src/pages/LatestUpdates.vue
@@ -34,7 +34,7 @@ export default class LatestUpdates extends Vue {
readonly Links: typeof Links = Links;
/** Set by Gridsome to results of GraphQL query */
- readonly $static!: {
+ readonly $page!: {
newBuildings: { edges: Array };
stoppedReporting: { edges: Array };
consistentReporters: { totalCount: number };
@@ -47,14 +47,14 @@ export default class LatestUpdates extends Vue {
get newBuildings(): Array<{ node: IBuilding }> {
// Already filtered by GraphQL, just sort by square footage
- return this.$static.newBuildings.edges
+ return this.$page.newBuildings.edges
.slice()
.sort((a, b) => b.node.GrossFloorArea - a.node.GrossFloorArea);
}
get consistentReportersCount(): number {
// Already calculated by GraphQL totalCount
- return this.$static.consistentReporters.totalCount;
+ return this.$page.consistentReporters.totalCount;
}
get netChangeInReporting(): number {
@@ -63,14 +63,14 @@ export default class LatestUpdates extends Vue {
get stoppedReportingBuildings(): Array<{ node: IBuilding }> {
// Already filtered by GraphQL, just sort by square footage
- return this.$static.stoppedReporting.edges
+ return this.$page.stoppedReporting.edges
.slice()
.sort((a, b) => b.node.GrossFloorArea - a.node.GrossFloorArea);
}
}
-
+
query {
# New buildings: FirstYearReported equals latest year (2023)
newBuildings: allBuilding(filter: { FirstYearReported: { eq: 2023 } }) {
@@ -126,7 +126,7 @@ export default class LatestUpdates extends Vue {
FirstYearReported
LastYearReported
}
-
+
diff --git a/src/pages/NeverSubmitted.vue b/src/pages/NeverSubmitted.vue
index d87d7e80..a2376f95 100644
--- a/src/pages/NeverSubmitted.vue
+++ b/src/pages/NeverSubmitted.vue
@@ -37,7 +37,7 @@ import { generatePageMeta } from '../constants/meta-helpers.vue';
export default class NeverSubmitted extends Vue {}
-
+
query {
allBuilding(
filter: { FirstYearReported: { eq: null } },
@@ -73,16 +73,15 @@ export default class NeverSubmitted extends Vue {}
}
}
}
-
+
- Chicago's {{ $static.allBuilding.edges.length }} Never Submitted
- Buildings
+ Chicago's {{ $page.allBuilding.edges.length }} Never Submitted Buildings
@@ -104,12 +103,12 @@ export default class NeverSubmitted extends Vue {}
diff --git a/src/pages/PropertyTypes.vue b/src/pages/PropertyTypes.vue
index 4c677c68..73a560c2 100644
--- a/src/pages/PropertyTypes.vue
+++ b/src/pages/PropertyTypes.vue
@@ -37,6 +37,9 @@ interface IPropertyTypeSummary {
imgUrl: string | null;
}
+/** The fields the property type list can be sorted by */
+type PropertyTypeSortField = 'buildingCount' | 'totalGHGEmissions';
+
/**
* Total emissions across every property type we have stats for, used as the
* denominator for each type's share. This covers a few types that don't have
@@ -66,14 +69,30 @@ const CitywideTotalEmissions = getCitywideTotalEmissions(
},
})
export default class PropertyTypes extends Vue {
- /** Set by Gridsome to results of the static GraphQL query */
- readonly $static!: { allBuilding: { edges: Array } };
+ /** Set by Gridsome to results of the page GraphQL query */
+ readonly $page!: { allBuilding: { edges: Array } };
+
+ /** The sort options shown in the toggle, in display order */
+ readonly SortOptions: Array<{ field: PropertyTypeSortField; label: string }> =
+ [
+ { field: 'buildingCount', label: 'Most Buildings' },
+ { field: 'totalGHGEmissions', label: 'Highest Emissions' },
+ ];
+
+ /** All property types that have a generated page, unsorted */
+ propertyTypes: Array = [];
/**
- * All property types that have a generated page, sorted by building count
- * descending, since the largest categories are the most useful to browse
+ * The field to sort by, descending. Defaults to building count since the
+ * largest categories are the most useful to browse
*/
- propertyTypes: Array = [];
+ sortField: PropertyTypeSortField = 'buildingCount';
+
+ get sortedPropertyTypes(): Array {
+ return [...this.propertyTypes].sort(
+ (a, b) => b[this.sortField] - a[this.sortField],
+ );
+ }
created(): void {
const featuredBuildings = this.buildFeaturedBuildingMap();
@@ -104,7 +123,8 @@ export default class PropertyTypes extends Vue {
: null,
};
})
- .sort((a, b) => b.buildingCount - a.buildingCount);
+ // Ignore types with no buildings
+ .filter((type) => type.buildingCount > 0);
}
/**
@@ -115,7 +135,7 @@ export default class PropertyTypes extends Vue {
private buildFeaturedBuildingMap(): Record {
const featuredBuildings: Record = {};
- this.$static.allBuilding.edges.forEach(({ node }) => {
+ this.$page.allBuilding.edges.forEach(({ node }) => {
const propertyType = node.PrimaryPropertyType;
if (
@@ -161,7 +181,7 @@ export default class PropertyTypes extends Vue {
Sorted by GrossFloorArea descending so we can pick the largest building with
a photo for each property type in a single pass
-->
-
+
query {
allBuilding(sortBy: "GrossFloorArea") {
edges {
@@ -175,7 +195,7 @@ export default class PropertyTypes extends Vue {
}
}
}
-
+
@@ -201,8 +221,24 @@ export default class PropertyTypes extends Vue {
city as a whole is trending.
+
+
+
+
-
+
} };
+ readonly $page!: { allBuilding: { edges: Array } };
buildingsFiltered: Array = [];
created(): void {
validateTaggedBuildings(
BuildingTags.hasRetrofitCaseStudy,
- this.$static.allBuilding.edges.map((e) => e.node.ID.toString()),
+ this.$page.allBuilding.edges.map((e) => e.node.ID.toString()),
);
- this.buildingsFiltered = this.$static.allBuilding.edges;
+ this.buildingsFiltered = this.$page.allBuilding.edges;
}
}
@@ -55,7 +55,7 @@ export default class ChicagoRetrofitParticipants extends Vue {
This page grabs all buildings and then filters by owner on the client-side, since that data isn't
baked into the actual building CSV
-->
-
+
query {
# PERFORMANCE OPTIMIZATION: Hard-coded filter for buildings with hasRetrofitCaseStudy tag
# These IDs MUST match buildings-custom-info.constant.vue (validated at runtime)
@@ -91,7 +91,7 @@ export default class ChicagoRetrofitParticipants extends Vue {
}
}
}
-
+
diff --git a/src/pages/Search.vue b/src/pages/Search.vue
index a81d372c..eb12e46b 100644
--- a/src/pages/Search.vue
+++ b/src/pages/Search.vue
@@ -72,13 +72,11 @@ export default class Search extends Vue {
};
/** Set by Gridsome to results of GraphQL query */
- readonly $static!: {
+ readonly $page!: {
allBuilding: { edges: Array };
allBenchmark: { edges: Array<{ node: IHistoricData }> };
};
- // readonly $page!: { allBuilding: { edges: Array } };
-
/** The search query */
searchFilter = '';
@@ -143,11 +141,11 @@ export default class Search extends Vue {
this.buildNormalizedCache();
// Make sure on load we have some data
- this.setSearchResults(this.$static.allBuilding.edges);
+ this.setSearchResults(this.$page.allBuilding.edges);
}
buildNormalizedCache(): void {
- for (const edge of this.$static.allBuilding.edges) {
+ for (const edge of this.$page.allBuilding.edges) {
this.normalizedCache.set(edge, {
name: normalizeForSearch(edge.node.PropertyName ?? ''),
address: normalizeForSearch(edge.node.Address ?? ''),
@@ -284,12 +282,12 @@ export default class Search extends Vue {
// If no filters are active, return all results
if (activeFilters.length === 0) {
this.hasFilteredResults = false;
- this.setSearchResults(this.$static.allBuilding.edges);
+ this.setSearchResults(this.$page.allBuilding.edges);
return;
}
// Apply all active filters in sequence
- let buildingsResults: Array = this.$static.allBuilding.edges;
+ let buildingsResults: Array = this.$page.allBuilding.edges;
for (const filter of activeFilters) {
buildingsResults = filter.apply(buildingsResults);
}
@@ -318,7 +316,7 @@ export default class Search extends Vue {
if (this.hasFilteredResults) {
buildingsToSort = [...this.searchResults];
} else {
- buildingsToSort = [...this.$static.allBuilding.edges];
+ buildingsToSort = [...this.$page.allBuilding.edges];
}
this.runSort(buildingsToSort);
@@ -370,7 +368,7 @@ export default class Search extends Vue {
private initializeHistoricalDataIndex(): void {
this.historicalDataIndex.clear();
- for (const edge of this.$static.allBenchmark.edges) {
+ for (const edge of this.$page.allBenchmark.edges) {
const buildingId = edge.node.ID;
if (!this.historicalDataIndex.has(buildingId)) {
this.historicalDataIndex.set(buildingId, []);
@@ -419,8 +417,11 @@ export default class Search extends Vue {
}
-
+
query {
+ # TODO: Querying all historic benchmarks ships 10k+ records to the client just
+ # to detect new buildings. Precompute an "is new building" flag on Building
+ # instead of querying allBenchmark.
# Search page only needs core BuildingsTable fields (no conditional fields)
allBuilding(sortBy: "GHGIntensity") {
edges {
@@ -457,7 +458,7 @@ export default class Search extends Vue {
}
}
}
-
+
diff --git a/src/pages/TopElectricityUsers.vue b/src/pages/TopElectricityUsers.vue
index 4b1b2585..d8a7db59 100644
--- a/src/pages/TopElectricityUsers.vue
+++ b/src/pages/TopElectricityUsers.vue
@@ -35,7 +35,7 @@ export default class TopElectricityUsers extends Vue {}
-
+
query {
allBuilding(sortBy: "ElectricityUse", limit: 50) {
edges {
@@ -68,15 +68,15 @@ export default class TopElectricityUsers extends Vue {}
}
}
}
-
+
- Top {{ $static.allBuilding.edges.length }} Electricity Users
+ Top {{ $page.allBuilding.edges.length }} Electricity Users
- Top {{ $static.allBuilding.edges.length }} Buildings by Greenhouse Gas
+ Top {{ $page.allBuilding.edges.length }} Buildings by Greenhouse Gas
Emissions