claude/vigorous-stonebraker-41cc2d
needs attentionviewing older commitd28c6da · incrementalpre-PRreviewed 2026-07-10 16:59 UTC0H · 3M · 10L · 3I- Purpose
- Atlas is a public-facing data visualization map for Mexico's energy grid (CFE network, distributed generation, tariffs). This branch extends the 'Evolución' panel to support a rich DG (generación distribuida) sub-panel.
- Goal
- Add multi-dimensional DG visualization: MW/contracts/avg-system-size metrics, cumulative vs annual mode, size-band breakdown (bar chart) and region/state map breakdown — giving users a complete picture of Mexico's solar DG deployment evolution.
- Sub-goals
- SG-1: Add DG panel HTML controls (break/metric/mode/state selectors)
- SG-2: Implement DG computation helpers (dgSizeVal, dgStateVal, dgNatAvg, etc.)
- SG-3: Implement drawDGSizeChart and drawDGRegionChart with interactive bar charts
- SG-4: Wire region map coloring for DG mode
- SG-5: Update popup handler to show DG-appropriate units
- What
- Replaced the single drawDGSize() function (annual MW only) with a full DG sub-system: EVO state expanded with dgBreak/dgMetric/dgMode/dgBand/dgState; 15 new helper functions; buildDG() dispatcher; two new draw functions for size and region charts; HTML controls for all new dimensions; popup handler updated.
- Why
- Previous DG visualization only showed annual MW additions by size band. The new panel exposes 3 metric types × 2 modes × 2 breakdown views = 12 chart configurations, matching the depth of the users/energy panels.
- Areas
- apps/web/public/atlas/index.html+128−38apps/web/public/atlas/data/atlas.json+1−1
- Blast
- 2 files, +129/−39 total. Fully self-contained in the Atlas static HTML visualization — no TypeScript, no Next.js, no API changes. Zero impact on platform or backend.
Findings · 17
correctness3
Negative kW/system in annual avg mode renders point outside SVG viewBox
apps/web/public/atlas/index.html
dgStateVal() returns raw dm/dc*1000 in annual avg mode even when dm<0 (MW decreased due to retroactive data corrections). The Y function Y=v=>DGmT+DGph-v/max*DGph places negative values above the chart's top edge (y > DGH=190), so they fall outside the viewBox and disappear, creating a misleading visual drop toward zero. dgNatAvgState has the same issue — dm=mw-pmw is not clamped. Fix: return Math.max(0, dm/dc*1000) or null when dm<0.
drawDGRegionMap ignores its keys parameter — implicit coupling to EVO.idx
apps/web/public/atlas/index.html
drawDGRegionMap(keys) receives a keys array but never uses it; all lookups go through dgStateVal(nm, EVO.idx) which reads EVO.data.dgstate.periods internally. Not a crash today since EVO.idx is clamped before the call and buildDG always passes dgstate.periods. But a future caller passing a different keys array expecting control over the displayed period will be silently ignored. Remove the parameter or use it.
regs.indexOf(r) in inner loop — color mismatch if region names contain duplicates
apps/web/public/atlas/index.html
In drawDGRegionChart, regs.indexOf(r) is called inside regs.forEach() for both bar color and legend color. If two regions ever share a name, indexOf always returns the first index for both, causing the second region's bars to get the wrong color while the legend has the correct one. Fix: capture the forEach callback index (r, ri) and use ri directly.
security4
XSS via unsanitized GeoJSON properties in map popups (new evo-fill handler)
apps/web/public/atlas/index.html
The new evo-fill click handler interpolates p.name (from GeoJSON feature properties) directly into setHTML() without escaping. Pre-existing handlers do the same for demand, generation, and tariff popups. Data comes from static same-origin JSON files today, but ATLAS_DEMAND_URL and ATLAS_PND_URL are fetched from raw.githubusercontent.com — a supply-chain compromise of those files or the atlas-data branch could inject arbitrary HTML into popups. Fix: escape p.name and all string properties before interpolation into setHTML.
XSS via unsanitized JSON keys inserted into dropdown innerHTML
apps/web/public/atlas/index.html
State names from dg_by_state.json (new in this commit), zone names from demand JSON, and other JSON-keyed strings are inserted into <select> innerHTML via template literals without escaping. A malicious JSON value like '</option><script>...' would execute. The state dropdown populates from Object.keys(EVO.data.dgstate.byState) — those are static data today, but the pattern is fragile. Fix: build option elements via document.createElement/textContent, or escape <, >, &, " before inserting.
External cross-origin fetches (GitHub raw) without integrity verification
apps/web/public/atlas/index.html
ATLAS_DEMAND_URL and ATLAS_PND_URL point to raw.githubusercontent.com and are fetched without SRI or signature verification. A compromise of the batuenergy/atlas-sen data branch would allow injecting values into rendered HTML. Lower severity since the repo is trusted infrastructure, but worth noting as a persistent external dependency for an HTML file that renders fetched strings into the DOM.
SVG innerHTML injection pattern is fragile — region name strings not escaped
apps/web/public/atlas/index.html
evoLegend() builds <span> elements with region names from dgstate JSON and sets them via echart.innerHTML. If a region name contained </span><img src=x onerror=...> it would execute. Values are currently static data, but the pattern has no escape layer. SVG innerHTML also permits <script> and inline handlers in some browser contexts.
conventions4
DGph/DGpw use literal arithmetic instead of named constants
apps/web/public/atlas/index.html
const DGph=190-10-18 and DGpw=860-46-12 repeat the raw numbers rather than referencing DGH, DGmT, DGW, DGmL, DGmR. The literal 18 is a bottom margin with no named constant (DGmB never defined). The existing demand chart uses ph=Hh-mT-mB with all four symbols in scope — the same pattern should apply here. If DGH or any margin is adjusted, DGph/DGpw silently become stale.
DG_REGPAL breaks the EVO_ prefix convention for EVO-panel constants
apps/web/public/atlas/index.html
All sibling constants are EVO_-prefixed (EVO_TF, EVO_SZ, EVO_SZC, EVO_RAMP, EVO_UNIT). The new region palette DG_REGPAL uses a different prefix. Rename to EVO_REGPAL to stay consistent.
e_metric handler not updated to use the new setSeg helper
apps/web/public/atlas/index.html
The new setSeg(id,b) helper is used for e_break, e_dgmetric, and e_mode, but the pre-existing e_metric handler still uses the old inlined toggle pattern. This leaves an inconsistency — a future maintainer is unsure which idiom is canonical.
regs.indexOf(r) called twice per region — forEach index already available
apps/web/public/atlas/index.html
regs.forEach(r => { ... DG_REGPAL[regs.indexOf(r)%DG_REGPAL.length] ... }) calls indexOf inside the callback when (r, ri) => { DG_REGPAL[ri%DG_REGPAL.length] } would be O(1) and match the forEach((sz, i)) pattern already used in the size-stacked-bar path.
tests3
Annual-mode delta not clamped in dgNatAvgState — negative national average silently masked
apps/web/public/atlas/index.html
dgNatAvgState computes dm = mw - pmw without Math.max(0,...). A data revision reducing national total MW in a period produces a negative dm, and therefore negative kW/system for that period. The SVG rect height is clamped via Math.max(0.4,...) so it renders as a near-zero bar, hiding the underlying negative value. dgSizeVal has the clamp; dgNatAvgState should match.
Division-by-zero guard pattern duplicated across three functions with different logic
apps/web/public/atlas/index.html
dgNatAvg, dgNatAvgState, and dgStateVal each implement the divide-by-contracts guard with slightly different conditions (dc>0 vs truthy c vs truthy s[key].contratos). No test validates all three return null (not NaN) on dc=0. A future edit may break one while the others pass visual inspection.
Pure computation functions are extraction candidates for unit testing
apps/web/public/atlas/index.html
dgSizeVal, dgNatAvg, dgStateVal, dgRegionVal, dgNatAvgState are pure computations over a data structure with no DOM access. They could be extracted to atlas-dg.js and tested with a simple Vitest fixture. The 12 logical branches (3 metric types × 2 modes × 2 break types) make manual correctness verification unreliable as the dataset evolves.
improvement3
Dropdown re-init guard may not re-apply selected value on re-entry
apps/web/public/atlas/index.html
buildDG() sets stSel.value=EVO.dgState inside the options.length<=1 guard. On subsequent calls (e.g. switch away from region break and back), the guard skips re-population and therefore also skips re-asserting stSel.value. Works today because onchange keeps EVO.dgState in sync, but silently breaks if EVO.dgState is reset externally. Move stSel.value=EVO.dgState outside the guard.
Shared chart setup duplicated verbatim in drawDGSizeChart and drawDGRegionChart
apps/web/public/atlas/index.html
Both functions open with identical const n=keys.length,cur=Math.min(EVO.idx,n-1),slot=DGpw/n,cx=i=>DGmL+(i+0.5)*slot,bw=Math.max(2,slot*0.62). A dgChartSetup(keys) helper returning {n,cur,slot,cx,bw} would ensure a change to the bar-width coefficient (0.62) applies to both charts and reduce noise in both functions.
statesInRegion called on every dgRegionVal invocation — pre-computable
apps/web/public/atlas/index.html
drawDGRegionChart computes series[r]=keys.map((k,i)=>dgRegionVal(r,i)), and dgRegionVal calls statesInRegion(r) each time. statesInRegion does Object.keys(B).filter(...) — a full scan of all states. Pre-computing a regionToStates map once per drawDGRegionChart call would remove n_regions×n_periods redundant scans. Not a real perf issue at current data sizes, but it's free to fix.
History · 10 commits
- a081002safeincremental0H · 0M · 0L2026-07-14 01:23
- 64dc83dneeds attentionincremental0H · 1M · 7L2026-07-13 20:03
- ec4eea3needs attentionincremental1H · 3M · 7L2026-07-13 18:42
- f4b4129safeincremental0H · 0M · 3L2026-07-13 04:34
- 8ea0c8asafeincremental0H · 0M · 3L2026-07-13 04:21
- 37ce28asafeincremental0H · 2M · 3L2026-07-10 17:22
- d28c6daneeds attentionincremental0H · 3M · 10L2026-07-10 16:59current
- 754a65dneeds attentionincremental0H · 3M · 4L2026-07-10 16:39
- a9190b2safeincremental0H · 0M · 5L2026-07-10 16:14
- 0bf12a0blockedfull1H · 2M · 4L2026-07-10 16:06