Skip to content

fix: implement dash-leaflet #235, address #272#276

Open
t-kramer wants to merge 4 commits into
developmentfrom
dash-leaflet-implementation
Open

fix: implement dash-leaflet #235, address #272#276
t-kramer wants to merge 4 commits into
developmentfrom
dash-leaflet-implementation

Conversation

@t-kramer
Copy link
Copy Markdown
Contributor

@t-kramer t-kramer commented May 21, 2026

Hi @FedericoTartarini,

I implemented #235 as a fix for #272 and the remaining error from #273 where the select page took ages to load. Using Dash-Leaflet and GeoJSON, everything seems to load much faster (at least locally).

This is a PR into development. Test it and see how you like it. I made minor tweaks to the default cluster colors (Sorry, I had to add a CSS file 😜) to match the overall tool UI.

Using this new way of visualizing weather files on the map, along with updating our repository of OneBuilding climate file URLs I did two weeks ago, we’ll have almost double the weather files in Clima with this update (compared to 2022 when we last pulled from OneBuilding).

Note: There are about 18k future weather files for Canada (around 32x per location with different iterations). I added a one-liner to filter them out for now. This issue describes why. I think at this point this is the best solution to avoid confusion.

Let me know if you have any questions! PR summary below✌🏼

@stefanoschiavon @giobetti

PR Summary

  • Replaced px.scatter_mapbox (Plotly) with dash-leaflet dl.GeoJSON(cluster=True) on the select page. The previous approach serialised all ~100K location markers into a single Plotly figure JSON on every page visit (~50 MB); the new approach serves a static, browser-cached gzip file (~1.5 MB) and renders only the visible clusters at any zoom level (= much, much faster)
  • Added scripts/build_locations_geojson.py to pre-process both source files (epw_location.json + one_building.csv) into a single assets/data/locations.geojson.gz. Future-climate scenario files (_Future/ URLs, ~18K rows (Canada only) are excluded for now and will be handled in a dedicated feature.
  • As before, all marker tooltips show name, lat/lon, and source for all locations; OneBuilding markers additionally show period, elevation, time zone, 99% Heating DB, and 1% Cooling DB.
  • Two marker colours are preserved: purple (#3a0ca3) for EnergyPlus, blue (#4895ef) for OneBuilding.
  • I tried to align the cluster colors with overall tool looks

Use

  • Select page map loads immediately on / with clustered markers visible
  • Zooming in breaks clusters into individual points with correct colours
  • Hovering a point shows the full tooltip
  • Clicking a point opens the confirmation modal with the location name
  • Clicking Yes downloads the EPW and enables all other tabs as before
  • Uploading a local EPW file still works
Screenshot 2026-05-21 at 4 11 38 PM

Summary by CodeRabbit

  • New Features

    • Upgraded location map with enhanced clustering visualization and styled markers
    • Improved location information display with interactive tooltips
    • Optimized location data loading for faster map performance
  • Tests

    • Updated map container selector in test suite

Review Change Stack

@t-kramer t-kramer added the enhancement New feature or request label May 21, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: df42d8a1-e2a5-4e05-b5a8-e3ede8a5c608

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR integrates Leaflet-based interactive maps into the location selection page. It introduces a complete GeoJSON data pipeline, a Flask serving endpoint, dash-leaflet rendering with custom marker tooltips, and updates modal interactions to work with the new map click data structure.

Changes

Dash-Leaflet Map Integration

Layer / File(s) Summary
GeoJSON Asset Build & Serving Pipeline
scripts/build_locations_geojson.py, app.py
Standalone script builds compressed GeoJSON from EnergyPlus and OneBuilding sources, and Flask endpoint serves it with gzip/cache-control headers at /geojson/locations.
Map Framework, Styling & JS Extensions
Pipfile, assets/cluster.css, assets/dashExtensions_default.js, pages/select.py
Adds dash-leaflet package, cluster styling CSS, and custom JS extension function0 to render circle markers with source-based colors and tooltips from feature properties.
Map Page Layout & GeoJSON Rendering
pages/select.py
Integrates dl.Map with cache-busted GeoJSON URL and custom pointToLayer callback to render clustered location markers on the select page.
Modal Callbacks for Map Click Handling
pages/select.py
Updates modal callbacks to extract location URL and title from clicked feature properties, and integrates location info fetching after user confirms.
Code Cleanup & Simplification
pages/lib/global_element_ids.py, pages/select.py
Removes unused SKELETON_GRAPH_CONTAINER element ID and simplifies switch_si_ip unit converter return logic.
Test Map Selector Update
tests/test_select.py
Updates test to use new #map-container selector instead of previous #tab-one-map.

🎯 3 (Moderate) | ⏱️ ~25 minutes

🐰 A leaflet map now sprouts on the page,
With clusters that bloom where the markers all gape,
GeoJSON flows gently through pipe and through stage,
Tooltips dance softly on each featured engage! 🗺️✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: implementing dash-leaflet (a primary architectural change replacing Plotly with Leaflet for map rendering) and addressing issues #235 and #272.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dash-leaflet-implementation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@giobetti
Copy link
Copy Markdown
Contributor

giobetti commented May 21, 2026 via email

@stefanoschiavon
Copy link
Copy Markdown

@t-kramer great work! Greenlight from me.

@FedericoTartarini
Copy link
Copy Markdown
Contributor

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@FedericoTartarini
Copy link
Copy Markdown
Contributor

I love the new implementation. Fantastic work!!!! Even if you used CSS ;)

While loading the application locally I have noticed the following issue as you can see some of the circles overlap can we fix that or shall we ignore that? I guess that we can fix it by extending the range of a grouping so two groups are not so close.
image

I'm going to review the rest of the code now. I've also asked code rabbit to do a review.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app.py`:
- Around line 15-27: The endpoint serve_locations_geojson currently returns
Cache-Control: no-cache which forces revalidation; update the response headers
returned by serve_locations_geojson to use cache-friendly directives for a
static, versioned gzipped asset (e.g., "Cache-Control": "public,
max-age=31536000, immutable") while keeping "Content-Encoding": "gzip" (and
leave conditional send_file behavior as-is) so browsers/CDNs can aggressively
cache the file.

In `@pages/select.py`:
- Around line 34-36: The module-level expression that builds _GEO_URL calls
os.path.getmtime at import time and will raise if
"assets/data/locations.geojson.gz" is missing; wrap that logic (the _GEO_URL
construction) in a safe check or try/except: first check os.path.exists(...) or
catch FileNotFoundError around os.path.getmtime and fall back to a safe default
(e.g. omit the ?v= param or use 0) so importing the module won't fail in
fresh/dev environments; update the code that references _GEO_URL to rely on this
guarded value.
- Around line 40-60: The tooltip HTML in _point_to_layer (created via assign)
directly concatenates unescaped feature.properties values (e.g., p.title,
p.period, p.elev, p.tz, p.heat99, p.cool1) and can lead to HTML injection; fix
by escaping these property strings before building the html string (or using a
safe DOM/text API) and then pass the sanitized string to marker.bindTooltip;
locate the assign-created function named _point_to_layer and ensure every
insertion of p.<...> is sanitized (e.g., replace <, >, &, " and ' with HTML
entities) before concatenation.

In `@Pipfile`:
- Line 20: Update the Pipfile to pin dash-leaflet to a Dash 3–compatible release
(replace dash-leaflet = "*" with dash-leaflet == "1.1.3" or another 1.1.x+
version) and then regenerate Pipfile.lock so the lockfile and Pipfile agree
(current Pipfile.lock uses 1.0.15 which is pre-Dash-3); target dash-leaflet
1.1.3 to align with Dash 3 compatibility and run pipenv lock to produce an
updated lockfile.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b966501-4f00-4f8b-ab11-1ec3fe66545a

📥 Commits

Reviewing files that changed from the base of the PR and between 67beae0 and af32e6e.

⛔ Files ignored due to path filters (2)
  • Pipfile.lock is excluded by !**/*.lock
  • assets/data/locations.geojson.gz is excluded by !**/*.gz
📒 Files selected for processing (8)
  • Pipfile
  • app.py
  • assets/cluster.css
  • assets/dashExtensions_default.js
  • pages/lib/global_element_ids.py
  • pages/select.py
  • scripts/build_locations_geojson.py
  • tests/test_select.py
💤 Files with no reviewable changes (1)
  • pages/lib/global_element_ids.py

Comment thread app.py
Comment on lines +15 to +27
@app.server.route("/geojson/locations")
def serve_locations_geojson():
return (
send_file(
"assets/data/locations.geojson.gz",
mimetype="application/json",
as_attachment=False,
download_name="locations.geojson",
conditional=True,
),
200,
{"Content-Encoding": "gzip", "Cache-Control": "no-cache"},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use cache-friendly headers for the GeoJSON endpoint.

Line 26 sets Cache-Control: no-cache, which forces revalidation and undercuts the map-load performance gain from serving a static gzipped asset with versioned URLs.

💡 Suggested change
 `@app.server.route`("/geojson/locations")
 def serve_locations_geojson():
     return (
         send_file(
             "assets/data/locations.geojson.gz",
             mimetype="application/json",
             as_attachment=False,
             download_name="locations.geojson",
             conditional=True,
         ),
         200,
-        {"Content-Encoding": "gzip", "Cache-Control": "no-cache"},
+        {
+            "Content-Encoding": "gzip",
+            "Cache-Control": "public, max-age=31536000, immutable",
+        },
     )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app.py` around lines 15 - 27, The endpoint serve_locations_geojson currently
returns Cache-Control: no-cache which forces revalidation; update the response
headers returned by serve_locations_geojson to use cache-friendly directives for
a static, versioned gzipped asset (e.g., "Cache-Control": "public,
max-age=31536000, immutable") while keeping "Content-Encoding": "gzip" (and
leave conditional send_file behavior as-is) so browsers/CDNs can aggressively
cache the file.

Comment thread pages/select.py
Comment on lines +34 to +36
_GEO_URL = "/geojson/locations?v=" + str(
int(os.path.getmtime("assets/data/locations.geojson.gz"))
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard _GEO_URL generation against missing asset at import time.

Line 35 calls os.path.getmtime(...) during module import. If the file is absent in a fresh/dev environment, the whole page import fails.

🛠️ Suggested fallback
-_GEO_URL = "/geojson/locations?v=" + str(
-    int(os.path.getmtime("assets/data/locations.geojson.gz"))
-)
+try:
+    _geo_mtime = int(os.path.getmtime("assets/data/locations.geojson.gz"))
+except OSError:
+    _geo_mtime = 0
+_GEO_URL = f"/geojson/locations?v={_geo_mtime}"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/select.py` around lines 34 - 36, The module-level expression that
builds _GEO_URL calls os.path.getmtime at import time and will raise if
"assets/data/locations.geojson.gz" is missing; wrap that logic (the _GEO_URL
construction) in a safe check or try/except: first check os.path.exists(...) or
catch FileNotFoundError around os.path.getmtime and fall back to a safe default
(e.g. omit the ?v= param or use 0) so importing the module won't fail in
fresh/dev environments; update the code that references _GEO_URL to rely on this
guarded value.

Comment thread pages/select.py
Comment on lines +40 to +60
_point_to_layer = assign("""function(feature, latlng, ctx) {
const p = feature.properties;
const color = p.source === "ep" ? "#3a0ca3" : "#4895ef";
const marker = L.circleMarker(latlng, {
radius: 5, color: color, fillColor: color, fillOpacity: 0.8, weight: 1
});

let html = '<b>' + (p.title || '') + '</b><br/>'
+ 'Lat: ' + latlng.lat.toFixed(2) + ', Lon: ' + latlng.lng.toFixed(2) + '<br/>';
if (p.source === 'ob') {
html += 'Period: ' + (p.period || 'N/A') + '<br/>'
+ 'Elevation: ' + (p.elev || 'N/A') + ' m<br/>'
+ 'Time zone: GMT' + (p.tz || 'N/A') + '<br/>'
+ '99% Heating DB: ' + (p.heat99 || 'N/A') + '<br/>'
+ '1% Cooling DB: ' + (p.cool1 || 'N/A') + '<br/>'
+ 'Source: Climate.OneBuilding.Org';
} else {
html += 'Source: EnergyPlus';
}
marker.bindTooltip(html, {sticky: true, opacity: 0.9});
return marker;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Escape tooltip content before injecting into HTML.

Lines 47–55 concatenate unescaped feature.properties values into HTML. If any upstream value contains HTML, this can inject markup into the tooltip.

🔐 Suggested hardening
 _point_to_layer = assign("""function(feature, latlng, ctx) {
     const p = feature.properties;
+    const esc = (v) => String(v ?? "")
+      .replace(/&/g, "&amp;")
+      .replace(/</g, "&lt;")
+      .replace(/>/g, "&gt;")
+      .replace(/"/g, "&quot;")
+      .replace(/'/g, "&`#39`;");
     const color = p.source === "ep" ? "`#3a0ca3`" : "`#4895ef`";
@@
-    let html = '<b>' + (p.title || '') + '</b><br/>'
+    let html = '<b>' + esc(p.title || '') + '</b><br/>'
              + 'Lat: ' + latlng.lat.toFixed(2) + ', Lon: ' + latlng.lng.toFixed(2) + '<br/>';
     if (p.source === 'ob') {
-        html += 'Period: '         + (p.period || 'N/A') + '<br/>'
-              + 'Elevation: '      + (p.elev   || 'N/A') + ' m<br/>'
-              + 'Time zone: GMT'   + (p.tz     || 'N/A') + '<br/>'
-              + '99% Heating DB: ' + (p.heat99 || 'N/A') + '<br/>'
-              + '1% Cooling DB: '  + (p.cool1  || 'N/A') + '<br/>'
+        html += 'Period: '         + esc(p.period || 'N/A') + '<br/>'
+              + 'Elevation: '      + esc(p.elev   || 'N/A') + ' m<br/>'
+              + 'Time zone: GMT'   + esc(p.tz     || 'N/A') + '<br/>'
+              + '99% Heating DB: ' + esc(p.heat99 || 'N/A') + '<br/>'
+              + '1% Cooling DB: '  + esc(p.cool1  || 'N/A') + '<br/>'
               + 'Source: Climate.OneBuilding.Org';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pages/select.py` around lines 40 - 60, The tooltip HTML in _point_to_layer
(created via assign) directly concatenates unescaped feature.properties values
(e.g., p.title, p.period, p.elev, p.tz, p.heat99, p.cool1) and can lead to HTML
injection; fix by escaping these property strings before building the html
string (or using a safe DOM/text API) and then pass the sanitized string to
marker.bindTooltip; locate the assign-created function named _point_to_layer and
ensure every insertion of p.<...> is sanitized (e.g., replace <, >, &, " and '
with HTML entities) before concatenation.

Comment thread Pipfile
dash-iconify = "*"
scipy = "==1.12.0"
kgcpy = "*"
dash-leaflet = "*"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the latest stable dash-leaflet release, and which versions are compatible with Dash 3.x?

💡 Result:

Latest stable dash-leaflet release: 1.1.3 [1][2]. Dash 3.x compatibility: - dash-leaflet 1.1.3 declares a dependency constraint of dash >=3.0.0, so it is compatible with Dash 3.x releases [1][2]. - The dash-leaflet project also documents that versions 1.1.1+ were released as “Dash 3 compatible” and notes a specific incompatibility issue related to Dash 3 / React 18.3 that was addressed via updates [3]. Practical takeaways: - Use dash-leaflet==1.1.3 with Dash 3.x (>=3.0.0) [1][2].

Citations:


🏁 Script executed:

rg -n "dash-leaflet\s*=|\[packages\]|\[dev-packages\]|^dash\s*=" -S .

Repository: CenterForTheBuiltEnvironment/clima

Length of output: 188


🌐 Web query:

dash-leaflet latest version PyPI and compatibility with dash 3.x

💡 Result:

Latest dash-leaflet version on PyPI (as of 2026-05-26): 1.1.3 [1]. Compatibility with Dash 3.x: - dash-leaflet 1.1.3 declares a dependency on dash >= 3.0.0, so it is intended to work with Dash 3.x (and will not allow Dash 2.x) [1][2]. - The dash-leaflet 1.1.3 metadata also indicates Python >= 3.11 [1][2]. - Dash 3.0 is documented by Plotly as removing/altering some older component stubs and includes breaking changes; if your app uses any libraries/components that Dash 3 requires to be updated, you may need to update those libraries too [3]. Recommended install (to match the intended constraints): - pip install "dash>=3" "dash-leaflet==1.1.3" [1][2].

Citations:


🏁 Script executed:

ls -la

Repository: CenterForTheBuiltEnvironment/clima

Length of output: 1897


🏁 Script executed:

rg -n "dash-leaflet" -S .

Repository: CenterForTheBuiltEnvironment/clima

Length of output: 155


🏁 Script executed:

python3 - <<'PY'
import itertools, re, sys
path="Pipfile.lock"
start=200
end=260
with open(path,'r',encoding='utf-8') as f:
    for i,line in enumerate(f,1):
        if start<=i<=end:
            print(f"{i}:{line.rstrip()}")
PY

Repository: CenterForTheBuiltEnvironment/clima

Length of output: 2832


🌐 Web query:

dash-leaflet 1.0.15 dependency on dash (Dash >=?) PyPI

💡 Result:

dash-leaflet 1.0.15 declares a dependency on Dash with the constraint dash >=2.13.0 on PyPI. [1] So the answer to “Dash >=?” is: Dash >= 2.13.0. [1]

Citations:


🌐 Web query:

dash-leaflet 1.0.15 Dash 3 compatibility

💡 Result:

Dash-leaflet 1.0.15 is not considered “Dash 3 compatible” on its own. Instead, Dash 3 compatibility was implemented starting from dash-leaflet 1.1.0 (and related 1.1.x releases), while 1.0.15 only declares compatibility with Dash >=2.13.0. Evidence: - The PyPI page for dash-leaflet 1.0.15 shows a dependency constraint of dash >=2.13.0 (no Dash 3-specific minimum is stated for 1.0.15). [1] - The project changelog/compare history between 1.0.15 and 1.1.0 explicitly notes “Working in Dash 3 compatibility” and “Update dependency to Dash 3.0.0” as part of the 1.1.0 change. [2] - There is a GitHub issue reporting incompatibility when using dash=3 with dash-leaflet (and discussing fixes that were released in later 1.1.x versions), indicating that older 1.0.x builds can be problematic with Dash 3. [3] Practical takeaway: - If you need Dash 3 compatibility, use dash-leaflet 1.1.0 or later (for example, 1.1.3). [2][4] Install guidance: - pip install dash>=3 dash-leaflet>=1.1.0

Citations:


Pin dash-leaflet and align it with Dash 3.2.

  • Pipfile uses dash-leaflet = "*" (drift risk); Pipfile.lock currently pins dash-leaflet==1.0.15.
  • dash-leaflet==1.0.15 is not the Dash 3–compatible line (Dash 3 compatibility started in dash-leaflet 1.1.0+). Upgrade to a Dash 3–compatible version (e.g., dash-leaflet==1.1.3) and regenerate Pipfile.lock.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Pipfile` at line 20, Update the Pipfile to pin dash-leaflet to a Dash
3–compatible release (replace dash-leaflet = "*" with dash-leaflet == "1.1.3" or
another 1.1.x+ version) and then regenerate Pipfile.lock so the lockfile and
Pipfile agree (current Pipfile.lock uses 1.0.15 which is pre-Dash-3); target
dash-leaflet 1.1.3 to align with Dash 3 compatibility and run pipenv lock to
produce an updated lockfile.

@FedericoTartarini
Copy link
Copy Markdown
Contributor

@t-kramer Could you please fix the issue from code rabbit and the issue identified by Claude below? Some are repeated. I also formatted the .js code with Prettier and pushed the changes.

  1. Cache-Control: no-cache defeats the version-busted URL (app.py:18)

{"Content-Encoding": "gzip", "Cache-Control": "no-cache"}

The URL already has a mtime-based cache buster (?v=), meaning the URL changes every time the file is rebuilt. This makes the resource effectively immutable per URL. Using no-cache forces the browser to revalidate on every request, throwing away the benefit. Change to:

{"Content-Encoding": "gzip", "Cache-Control": "public, max-age=31536000, immutable"}

  1. dashExtensions_default.js conflicts with the inline assign() call (pages/select.py:40)

assign() from dash_extensions.javascript generates/updates assets/dashExtensions_default.js at runtime. Committing a hand-written version of that file alongside the assign() call means the two can diverge, and the runtime-generated version will silently overwrite the committed one (or vice versa depending on the Dash version). Either remove the committed JS file and rely solely on assign(), or remove the assign() call and load the JS directly — don't maintain both.

  1. Magic number 2585 in the build script (scripts/build_locations_geojson.py:42)

for feat in epw_data["features"][:2585]:

This is unexplained. Extract it as a named constant at the top of the file with a comment explaining why that specific count is used (e.g. matching the old behaviour, or excluding a known bad tail).

  1. Relative paths for the GeoJSON file

Both app.py ("assets/data/locations.geojson.gz") and pages/select.py (os.path.getmtime("assets/data/locations.geojson.gz")) use relative paths. These break if the process working directory is ever not the repo root. Use Path(file).parent / "assets" / "data" / "locations.geojson.gz" for robustness.

  1. Wildcard version for dash-leaflet in Pipfile

dash-leaflet = "*"

The lock file pins 1.0.15, which is good, but the wildcard in Pipfile means pipenv update could silently pull a breaking major release. Pin to at least ">=1.0.15,<2".

  1. Missing newline at end of cluster.css and dashExtensions_default.js

Both new files are missing a trailing newline. Minor, but inconsistent with the rest of the repo.


Minor Notes

  • df.iterrows() in the build script — for a one-off data-prep script this is fine, but for ~18K rows it adds a few seconds. Not blocking.
  • SKELETON_GRAPH_CONTAINER removal — worth confirming no other file references this ID before merging (a quick grep over the codebase).
  • Bundled dependency bumps in Pipfile.lock (certifi, click, idna, urllib3, zipp) are unrelated to this feature. Not a blocker, but worth calling out for reviewers.
  • Test coverage — the updated test checks #map-container is visible, which is correct. Testing click-to-modal or cluster interaction would be ideal but is non-trivial for Leaflet in Playwright; current coverage is acceptable.

Copy link
Copy Markdown
Contributor

@FedericoTartarini FedericoTartarini left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@t-kramer Great work again. I left a few comments in line with the text. Please tag me once you have addressed my comments and the comments from Claude and Code Rabbit so I can do a second review. You can close open comments as you address them. Thank you so much for working on this.

Comment thread pages/select.py
# for individual point features, never for cluster markers, so properties are always complete.
_point_to_layer = assign("""function(feature, latlng, ctx) {
const p = feature.properties;
const color = p.source === "ep" ? "#3a0ca3" : "#4895ef";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The color string is duplicated in here and in dashExtension_defaults.py. Could you please check for code duplication, not only in the color string, but in other parts of the code? Perhaps we can export them and save them as a constant and import them in both files.

@@ -0,0 +1,51 @@
window.dashExtensions = Object.assign({}, window.dashExtensions, {
default: {
function0: function (feature, latlng, ctx) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function0 is not used and can be removed.

if (p.source === "ob") {
html +=
"Period: " +
(p.period || "N/A") +
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PyCharm is complaining that this may not be defined, the same for the other attributes of p. Can we give a more meaningful name to P? Perhaps we can call it "Properties of XXX"

default: {
function0: function (feature, latlng, ctx) {
const p = feature.properties;
const color = p.source === "ep" ? "#3a0ca3" : "#4895ef";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checking that the source is equal to "ep" which is a string, it is quite dangerous because we may miss this in the future, can we find a more elegant solution? for example to define a class with the two different sources.

@@ -0,0 +1,97 @@
"""
Build assets/data/locations.geojson.gz from the two source data files.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a bit more information to the docstring at the top so we know what this file is for and also describe it in the readme.

@FedericoTartarini
Copy link
Copy Markdown
Contributor

@t-kramer So sorry for tagging you again but I found a few more issue. I am also tagging @stefanoschiavon and @giobetti to know their opinion.

  1. I think the main issue is that we should stop grouping the weather file after a certain level of zoom. I know that this can be done because I implemented it before in JavaScript, but I don't know exactly how to do it. For example, please try to look at the San Francisco International Airport And you will see that the group remains even if I zoom at the level of the runway. Please let me know if you have a preference on this but I think we can stop the grouping when we are can approximately see 100 by 100 Km. See screenshot below for reference when we can stop the grouping.
image
  1. Please go to Florence, zoom in and you will see that it shows like a number 5 but as you keep zooming in then there is only one weather file for that location. The same issue is also there for San Francisco International Airport.
image
  1. I'm also wondering whether we should filter out some weather files because now it's becoming a bit confusing. For SFO we have so many files. I know that we have this because some different weather files are for a different period but if you go on San Francisco International Airport some of the files do not have information about the reference period which defeats the purpose of having these files and may create confusion.
image
  1. In SFO if I click on the group I get this flower shape, but I do not get the same if I zoom in.
image
  1. we should also limit the max zoom out, to avoid the following.
image
  1. Can we also limit the pan of the map? We don't really want that user can keep dragging the map left or right and be left with an empty map I know this is a very minor thing and maybe no one will do it but it will be great if we can fix it. I know in JavaScript it can be done and we just have to say that the map does not repeat.
image

@t-kramer
Copy link
Copy Markdown
Contributor Author

@FedericoTartarini, four of your comments point to the same issue: we’re using longitude and latitude to define the location for the weather file, which often overlaps since we have multiple files for the same location. Dash Leaflet solves this by stacking them and exposing them in a “flower” shape, but it’s not ideal because the flower only appears when clicking on the group, not when zooming in. I’ll check for a built-in fix or settings we can adjust. Alternatively, we could introduce “jitter” by slightly adjusting the coordinates to keep files at roughly the same location, like SFO, but not directly on top of each other. I’ll report back on our options. But as I said in the PR, this is a problem we will encounter much more frequently in the future (see my example with Canadian future weather files with x34 versions for each location), so I think we should find a robust solution. I don't consider filtering or only displaying a selection of weather files a good solution long-term.

I also agree with your other comments on limiting zoom. I’ll look into that too.

@FedericoTartarini
Copy link
Copy Markdown
Contributor

@t-kramer I like adding a bit of noise. I think I was already doing this in the past.

I agree that manually filtering is not a good solution and I definitely don't know how to solve the issue, but at the same time, providing 12 weather files for a single location is going to be very confusing for our users. They will start wondering how much they differ? Do I need to click on all of them and compare them? For future weather files, the explanation and the inclusion is a bit easier, because if we can show the different periods and the climate model in the tooltip, then it's going to be easier to select one versus the other. But the distinction is not very clear for historical data, which do not show the reference period.

@giobetti
Copy link
Copy Markdown
Contributor

giobetti commented May 27, 2026 via email

@t-kramer
Copy link
Copy Markdown
Contributor Author

@FedericoTartarini @giobetti

Thanks for sharing, I agree. Let me think about inclusion/exclusion criteria for historical weather data. Starting by filtering out files with lots of missing metadata (e.g. reference period) might be a good option. I'll think about it and get back to you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants