Skip to content

Commit 650d208

Browse files
committed
Convert us-zipcodes from GeoJSON to TopoJSON for 76% size reduction
- Replace 384 MB us-zipcodes.json with 91 MB us-zipcodes.topojson - Enables GitHub compatibility (under 100 MB limit) - Add topojson-client package for client-side conversion - Update fetchGeoJson to detect and convert TopoJSON to GeoJSON for ECharts - Update .gitignore to track .topojson instead of .geojson - Update process-zipcodes.py to generate both formats - Successfully downloaded all 50 US states (32,936 total ZIP codes) Compression results: - Original GeoJSON with simplification: 384 MB - TopoJSON with mapshaper: 91 MB (76% reduction) - Individual state files: 1-26 MB each (not committed)
1 parent 0b1ffc3 commit 650d208

File tree

6 files changed

+108
-24
lines changed

6 files changed

+108
-24
lines changed

.gitignore

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ coverage/
5353
result-cache/
5454

5555
# Large GeoJSON files (for local development only, not committed to git)
56-
# Exception: us-zipcodes.json (merged all-states) is committed to git (~105 MB)
56+
# Exception: us-zipcodes.topojson (merged all-states, 91 MB) is committed to git
5757
# Individual state files (us-al-zipcodes.json, us-ca-zipcodes.json, etc.) are git-ignored
5858
# These can be regenerated locally using scripts/process-zipcodes.py
5959
/exec/java-exec/src/main/resources/webapp/public/geojson/us-*-zipcodes.json
60-
!/exec/java-exec/src/main/resources/webapp/public/geojson/us-zipcodes.json
60+
/exec/java-exec/src/main/resources/webapp/public/geojson/us-zipcodes.json
61+
!/exec/java-exec/src/main/resources/webapp/public/geojson/us-zipcodes.topojson
6162
/exec/java-exec/src/main/resources/webapp/public/geojson/*.zip
6263
/exec/java-exec/src/main/resources/webapp/public/geojson/cb_*
63-
/exec/java-exec/src/main/resources/webapp/public/geojson/*-topo.json

exec/java-exec/src/main/resources/webapp/package-lock.json

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

exec/java-exec/src/main/resources/webapp/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@
3737
"react-router-dom": "^6.21.0",
3838
"regression": "^2.0.1",
3939
"rehype-raw": "^7.0.0",
40-
"sql-formatter": "^15.0.2"
40+
"sql-formatter": "^15.0.2",
41+
"topojson-client": "^3.1.0"
4142
},
4243
"devDependencies": {
4344
"@testing-library/jest-dom": "^6.9.1",

exec/java-exec/src/main/resources/webapp/public/geojson/us-zipcodes.topojson

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

exec/java-exec/src/main/resources/webapp/scripts/process-zipcodes.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,27 @@ def write_geojson_file(path, features, minify=True):
279279
# Pretty-printed for debugging
280280
json.dump(geojson, f, indent=2)
281281

282+
283+
def convert_geojson_to_topojson(geojson_path, topojson_path):
284+
"""
285+
Convert GeoJSON to TopoJSON using mapshaper (5-10x compression).
286+
TopoJSON uses delta encoding and shared arcs.
287+
"""
288+
try:
289+
# Use mapshaper command-line tool
290+
# -o format=topojson specifies output format
291+
# -o quantization=1e4 sets coordinate precision
292+
subprocess.run([
293+
'mapshaper', geojson_path,
294+
'-o', 'format=topojson',
295+
'-o', 'quantization=1e4',
296+
topojson_path
297+
], check=True, capture_output=True, text=True)
298+
return True
299+
except (FileNotFoundError, subprocess.CalledProcessError) as e:
300+
# mapshaper not installed, skip TopoJSON conversion
301+
return False
302+
282303
def create_state_files(state_groups):
283304
"""Create individual state ZIP code files"""
284305
print("Creating state-level ZIP code files...")
@@ -323,6 +344,18 @@ def main():
323344
# Create merged all-states file (committed to git)
324345
create_merged_file(all_features)
325346

347+
# Convert to TopoJSON for better compression
348+
print("Converting to TopoJSON format...")
349+
merged_geojson = GEOJSON_DIR / 'us-zipcodes.json'
350+
merged_topojson = GEOJSON_DIR / 'us-zipcodes.topojson'
351+
if convert_geojson_to_topojson(str(merged_geojson), str(merged_topojson)):
352+
geojson_size = os.path.getsize(merged_geojson) / 1024 / 1024
353+
topojson_size = os.path.getsize(merged_topojson) / 1024 / 1024
354+
compression = (1 - topojson_size / geojson_size) * 100
355+
print(f" ✓ {merged_topojson.name}: {topojson_size:.1f} MB ({compression:.1f}% smaller)")
356+
else:
357+
print(" ⚠ mapshaper not found, skipping TopoJSON conversion")
358+
326359
print("\n✓ Processing complete!\n")
327360
print(f"Summary:")
328361
print(f" Total US ZIP codes: {len(all_features):,}")

exec/java-exec/src/main/resources/webapp/src/api/geojson.ts

Lines changed: 53 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,55 +16,89 @@
1616
* limitations under the License.
1717
*/
1818

19+
import * as topojson from 'topojson-client';
20+
1921
/**
2022
* Fetch GeoJSON for a map by ID from the public/geojson/ directory.
23+
* Supports both .geojson and .topojson formats.
24+
* TopoJSON is automatically converted to GeoJSON for ECharts compatibility.
2125
* The base URL is relative to the page location to work in both dev and prod.
2226
*/
2327
export async function fetchGeoJson(mapId: string): Promise<object> {
2428
try {
25-
// Use relative path that works regardless of deployment location
26-
// In dev: served by Vite from public/
27-
// In prod: served from webapp resources
28-
const url = `/sqllab/geojson/${mapId}.json`;
29+
// Try TopoJSON first (more compact), fall back to GeoJSON
30+
let data = await tryFetchMap(mapId, '.topojson');
31+
if (!data) {
32+
data = await tryFetchMap(mapId, '.json');
33+
}
34+
35+
if (!data || typeof data !== 'object') {
36+
throw new Error(`Invalid GeoJSON for map ${mapId}`);
37+
}
38+
39+
// If it's TopoJSON, convert to GeoJSON
40+
if ('objects' in data) {
41+
console.debug(`[GeoJSON] Converting TopoJSON to GeoJSON for ${mapId}`);
42+
return convertTopoJsonToGeoJson(data as topojson.Topology);
43+
}
44+
45+
return data;
46+
} catch (error) {
47+
console.error(`[GeoJSON] Error loading ${mapId}:`, error);
48+
throw error;
49+
}
50+
}
2951

52+
async function tryFetchMap(mapId: string, ext: string): Promise<unknown> {
53+
try {
54+
const url = `/sqllab/geojson/${mapId}${ext}`;
3055
console.debug(`[GeoJSON] Fetching from: ${url}`);
3156

3257
const response = await fetch(url);
33-
3458
if (!response.ok) {
35-
console.error(`[GeoJSON] 404 for ${url} - trying fallback paths`);
36-
3759
// Try alternative paths
3860
const altPaths = [
39-
`/geojson/${mapId}.json`,
40-
`./geojson/${mapId}.json`,
61+
`/geojson/${mapId}${ext}`,
62+
`./geojson/${mapId}${ext}`,
4163
];
4264

4365
for (const altPath of altPaths) {
4466
try {
4567
const altResponse = await fetch(altPath);
4668
if (altResponse.ok) {
4769
console.log(`[GeoJSON] Found at fallback path: ${altPath}`);
48-
const data = await altResponse.json();
49-
return data;
70+
return altResponse.json();
5071
}
5172
} catch {
5273
// Continue to next fallback
5374
}
5475
}
55-
56-
throw new Error(`HTTP ${response.status} - map file not found at ${url}`);
76+
return null;
5777
}
5878

59-
const data = await response.json();
79+
return response.json();
80+
} catch (error) {
81+
console.debug(`[GeoJSON] Error trying ${ext}:`, error);
82+
return null;
83+
}
84+
}
6085

61-
if (!data || typeof data !== 'object') {
62-
throw new Error(`Invalid GeoJSON for map ${mapId}`);
86+
function convertTopoJsonToGeoJson(
87+
topoData: topojson.Topology
88+
): Record<string, unknown> {
89+
try {
90+
// Get the first object from the topology (usually 'features' or a similar key)
91+
const objectKey = Object.keys(topoData.objects)[0];
92+
if (!objectKey) {
93+
throw new Error('TopoJSON has no objects');
6394
}
6495

65-
return data;
96+
// Convert the topology object to GeoJSON FeatureCollection
97+
const geoJson = topojson.feature(topoData, topoData.objects[objectKey]);
98+
console.debug(`[GeoJSON] Converted TopoJSON with key "${objectKey}"`);
99+
return geoJson;
66100
} catch (error) {
67-
console.error(`[GeoJSON] Error loading ${mapId}:`, error);
68-
throw error;
101+
console.error(`[GeoJSON] TopoJSON conversion failed:`, error);
102+
throw new Error(`Failed to convert TopoJSON: ${error}`);
69103
}
70104
}

0 commit comments

Comments
 (0)