From 2f41c38cc420a4be24ab7cf78b391f7c8079f140 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Mon, 26 Jan 2026 20:42:52 +0000 Subject: [PATCH] Fix loading times --- frontend/src/components/Map.tsx | 3 ++- frontend/src/types.ts | 1 - server/main.py | 12 ++++++++++- server/routes/hexagons.py | 36 +++++++++++++++++++-------------- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/frontend/src/components/Map.tsx b/frontend/src/components/Map.tsx index 5628b96..5cd173b 100644 --- a/frontend/src/components/Map.tsx +++ b/frontend/src/components/Map.tsx @@ -164,7 +164,8 @@ export default function Map({ data, onViewChange }: MapProps) { getFillColor: (d) => priceToColor(d.avg_price), extruded: false, pickable: true, - opacity: 0.7, + opacity: 0.5, + highPrecision: true, }), ], [data] diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 561d293..ecfe433 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -37,5 +37,4 @@ export interface ViewChangeParams { export interface ApiResponse { features: HexagonData[]; - truncated: boolean; } diff --git a/server/main.py b/server/main.py index 14cf941..511016c 100644 --- a/server/main.py +++ b/server/main.py @@ -1,3 +1,4 @@ +from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware @@ -5,7 +6,16 @@ from fastapi.staticfiles import StaticFiles from server.routes import hexagons -app = FastAPI(title="Property Map API") + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup: preload all parquet files + hexagons.preload_dataframes() + yield + # Shutdown: nothing to clean up + + +app = FastAPI(title="Property Map API", lifespan=lifespan) app.add_middleware( CORSMiddleware, diff --git a/server/routes/hexagons.py b/server/routes/hexagons.py index 40835e9..b4cc24f 100644 --- a/server/routes/hexagons.py +++ b/server/routes/hexagons.py @@ -1,8 +1,11 @@ +import math from functools import lru_cache from fastapi import APIRouter, Query, HTTPException import polars as pl import h3 +from tqdm import tqdm + from server.config import ( AGGREGATES_DIR, VALID_RESOLUTIONS, @@ -19,6 +22,12 @@ router = APIRouter() _df_cache: dict[int, pl.DataFrame] = {} +def preload_dataframes() -> None: + """Load all resolution dataframes into cache on startup.""" + for resolution in tqdm(VALID_RESOLUTIONS, desc="Loading parquet files"): + get_cached_df(resolution) + + def get_cached_df(resolution: int) -> pl.DataFrame | None: """Get cached dataframe for resolution, loading from disk if needed.""" if resolution not in _df_cache: @@ -48,8 +57,8 @@ def query_hexagons_cached( min_price: int, max_price: int, bounds_tuple: tuple[float, float, float, float], -) -> tuple[list[dict], bool]: - """Cached query - returns (features, truncated).""" +) -> list[dict]: + """Cached query - returns features list.""" south, west, north, east = bounds_tuple df = get_cached_df(resolution) @@ -86,12 +95,6 @@ def query_hexagons_cached( (pl.col("avg_price") >= min_price) & (pl.col("avg_price") <= max_price) ) - # Limit results - MAX_HEXAGONS = 50000 - truncated = len(df) >= MAX_HEXAGONS - if truncated: - df = df.limit(MAX_HEXAGONS) - # Build response efficiently using Polars df = df.select( [ @@ -104,7 +107,7 @@ def query_hexagons_cached( ] ) - return df.to_dicts(), truncated + return df.to_dicts() @router.get("/hexagons") @@ -136,15 +139,18 @@ async def get_hexagons( ) # Round bounds to reduce cache misses (0.01 degree ≈ 1km precision) + # Always expand bounds (floor for min, ceil for max) to prevent hexagons + # popping in when crossing rounding boundaries + precision = 0.01 bounds_tuple = ( - round(south, 2), - round(west, 2), - round(north, 2), - round(east, 2), + math.floor(south / precision) * precision, + math.floor(west / precision) * precision, + math.ceil(north / precision) * precision, + math.ceil(east / precision) * precision, ) # Convert prices to int for cache key hashability - features, truncated = query_hexagons_cached( + features = query_hexagons_cached( resolution, min_year, max_year, @@ -153,4 +159,4 @@ async def get_hexagons( bounds_tuple, ) - return {"features": features, "truncated": truncated} + return {"features": features}