feat(climate-sim): ✨ Update StatsDashboard component with enhanced visualizations and improve sprite generation tooling for better asset handling
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
e1880d53b1
commit
670cc24d7a
6 changed files with 55 additions and 41 deletions
|
|
@ -52,6 +52,39 @@ interface MetricDef {
|
|||
phaseBands?: PhaseBand[]
|
||||
}
|
||||
|
||||
// ── Formatters ───────────────────────────────────────────────────────────────
|
||||
|
||||
const fmt3 = (v: number) => v.toFixed(3)
|
||||
const fmt2 = (v: number) => v.toFixed(2)
|
||||
const fmt4 = (v: number) => v.toFixed(4)
|
||||
const fmtDelta3 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(3)
|
||||
const fmtDelta2 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(2)
|
||||
const fmtDelta4 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(4)
|
||||
|
||||
// ── Metric catalog (single source of truth) ─────────────────────────────────
|
||||
|
||||
const METRIC_CATALOG: Record<string, MetricDef> = {
|
||||
// -- Climate / atmosphere --
|
||||
sea_level: { key: 'sea_level', label: 'Sea Lvl', tooltip: 'Elevation threshold for water. Rises with warming, falls with cooling.', color: '#5C6BC0', getValue: (s) => s.sea_level, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
et: { key: 'evapotrans', label: 'ET', tooltip: 'Average evapotranspiration across land. Moisture recycled by vegetation per turn.', color: '#80DEEA', getValue: (s) => s.avg_evapotranspiration, formatValue: fmt4, formatDelta: fmtDelta4 },
|
||||
albedo: { key: 'albedo', label: 'Albedo', tooltip: 'Global average surface reflectivity (0=absorbs all, 1=reflects all). Ice/snow raise it, forests/water lower it.', color: '#BDBDBD', getValue: (s) => s.avg_albedo, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
aerosol: { key: 'aerosol', label: 'Aerosol', tooltip: 'Global average sulfate aerosol opacity. Volcanic eruptions inject aerosols that cool and dry the atmosphere.', color: '#90A4AE', getValue: (s) => s.avg_aerosol, formatValue: fmt4, formatDelta: fmtDelta4 },
|
||||
// -- Land ecology --
|
||||
land_canopy: { key: 'land_canopy', label: 'Flora', tooltip: 'Average tree canopy cover across land (0=barren, 1=full canopy). Drives succession and shading.', color: '#2E7D32', getValue: (s) => s.avg_land_canopy, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
land_under: { key: 'land_undergrowth', label: 'Undergrowth', tooltip: 'Average ground vegetation across land (0=bare, 1=dense). Drives food yield and habitat quality.', color: '#66BB6A', getValue: (s) => s.avg_land_undergrowth, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
land_fungi: { key: 'land_fungi', label: 'Fungi', tooltip: 'Average mycorrhizal network across land (0=none, 1=dense). Accelerates forest regrowth, boosts ecosystem resilience.', color: '#8D6E63', getValue: (s) => s.avg_land_fungi, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
land_habitat: { key: 'land_habitat', label: 'Fauna', tooltip: 'Average fauna habitat suitability across land (0=inhospitable, 1=thriving). Combines flora density, moisture, temperature.', color: '#A1887F', getValue: (s) => s.avg_land_habitat, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
land_quality: { key: 'land_quality', label: 'Quality', tooltip: 'Average tile quality across land (1=Prolific, 5=Epic). Ecology composite of flora health, fauna diversity, biome stability.', color: '#FFD54F', getValue: (s) => s.avg_land_quality, formatValue: fmt2, formatDelta: fmtDelta2 },
|
||||
// -- Water ecology --
|
||||
water_reef: { key: 'water_reef', label: 'Flora', tooltip: 'Average reef health across water tiles (0=dead, 1=pristine). Bleaching from high temps (>0.75). Dead reefs halve fish capacity.', color: '#29B6F6', getValue: (s) => s.avg_water_reef, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
water_fish: { key: 'water_fish', label: 'Fauna', tooltip: 'Average fish stock across water tiles (0=empty, 100+=abundant). Logistic reproduction, temperature-scaled.', color: '#26C6DA', getValue: (s) => s.avg_water_fish, formatValue: fmt2, formatDelta: fmtDelta2 },
|
||||
water_quality:{ key: 'water_quality', label: 'Quality', tooltip: 'Average tile quality across water tiles (1=Prolific, 5=Epic). Reflects marine ecosystem health.', color: '#42A5F5', getValue: (s) => s.avg_water_quality, formatValue: fmt2, formatDelta: fmtDelta2 },
|
||||
}
|
||||
|
||||
const m = (key: string): MetricDef => METRIC_CATALOG[key]
|
||||
|
||||
// ── Layout selections ────────────────────────────────────────────────────────
|
||||
|
||||
const PRIMARY_METRICS: MetricDef[] = [
|
||||
{
|
||||
key: 'temp', label: 'Temp', tooltip: 'Average land temperature (0=frozen, 1=scorching). Driven by solar input, albedo, and orbital cycles.', color: '#E85D3A',
|
||||
|
|
@ -69,44 +102,14 @@ const PRIMARY_METRICS: MetricDef[] = [
|
|||
m('sea_level'),
|
||||
]
|
||||
|
||||
// ── Metric catalog (single source of truth) ─────────────────────────────────
|
||||
// Each metric defined ONCE. Dashboard layouts select by key.
|
||||
|
||||
const fmt3 = (v: number) => v.toFixed(3)
|
||||
const fmt2 = (v: number) => v.toFixed(2)
|
||||
const fmt4 = (v: number) => v.toFixed(4)
|
||||
const fmtDelta3 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(3)
|
||||
const fmtDelta2 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(2)
|
||||
const fmtDelta4 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(4)
|
||||
|
||||
const METRIC_CATALOG: Record<string, MetricDef> = {
|
||||
// -- Climate / atmosphere --
|
||||
sea_level: { key: 'sea_level', label: 'Sea Lvl', tooltip: 'Elevation threshold for water. Rises with warming, falls with cooling.', color: '#5C6BC0', getValue: (s) => s.sea_level, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
et: { key: 'evapotrans', label: 'ET', tooltip: 'Average evapotranspiration across land. Moisture recycled by vegetation per turn.', color: '#80DEEA', getValue: (s) => s.avg_evapotranspiration, formatValue: fmt4, formatDelta: fmtDelta4 },
|
||||
albedo: { key: 'albedo', label: 'Albedo', tooltip: 'Global average surface reflectivity (0=absorbs all, 1=reflects all). Ice/snow raise it, forests/water lower it.', color: '#BDBDBD', getValue: (s) => s.avg_albedo, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
aerosol: { key: 'aerosol', label: 'Aerosol', tooltip: 'Global average sulfate aerosol opacity. Volcanic eruptions inject aerosols that cool and dry the atmosphere.', color: '#90A4AE', getValue: (s) => s.avg_aerosol, formatValue: fmt4, formatDelta: fmtDelta4 },
|
||||
// -- Land ecology --
|
||||
land_canopy: { key: 'land_canopy', label: 'Canopy', tooltip: 'Average tree canopy cover across land (0=barren, 1=full canopy). Drives succession and shading.', color: '#2E7D32', getValue: (s) => s.avg_land_canopy, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
land_under: { key: 'land_undergrowth', label: 'Undergrowth', tooltip: 'Average ground vegetation across land (0=bare, 1=dense). Drives food yield and habitat quality.', color: '#66BB6A', getValue: (s) => s.avg_land_undergrowth, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
land_fungi: { key: 'land_fungi', label: 'Fungi', tooltip: 'Average mycorrhizal network across land (0=none, 1=dense). Accelerates forest regrowth, boosts ecosystem resilience.', color: '#8D6E63', getValue: (s) => s.avg_land_fungi, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
land_habitat: { key: 'land_habitat', label: 'Habitat', tooltip: 'Average habitat suitability across land (0=inhospitable, 1=thriving). Combines flora density, moisture, temperature.', color: '#A1887F', getValue: (s) => s.avg_land_habitat, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
land_quality: { key: 'land_quality', label: 'Quality', tooltip: 'Average tile quality across land (1=Prolific, 5=Epic). Ecology composite of flora health, fauna diversity, biome stability.', color: '#FFD54F', getValue: (s) => s.avg_land_quality, formatValue: fmt2, formatDelta: fmtDelta2 },
|
||||
// -- Water ecology --
|
||||
water_reef: { key: 'water_reef', label: 'Reef', tooltip: 'Average reef health across water tiles (0=dead, 1=pristine). Bleaching from high temps (>0.75). Dead reefs halve fish capacity.', color: '#29B6F6', getValue: (s) => s.avg_water_reef, formatValue: fmt3, formatDelta: fmtDelta3 },
|
||||
water_fish: { key: 'water_fish', label: 'Fish', tooltip: 'Average fish stock across water tiles (0=empty, 100+=abundant). Logistic reproduction, temperature-scaled.', color: '#26C6DA', getValue: (s) => s.avg_water_fish, formatValue: fmt2, formatDelta: fmtDelta2 },
|
||||
water_quality:{ key: 'water_quality', label: 'Quality', tooltip: 'Average tile quality across water tiles (1=Prolific, 5=Epic). Reflects marine ecosystem health.', color: '#42A5F5', getValue: (s) => s.avg_water_quality, formatValue: fmt2, formatDelta: fmtDelta2 },
|
||||
}
|
||||
|
||||
const m = (key: string): MetricDef => METRIC_CATALOG[key]
|
||||
|
||||
// Life mode: LAND column (fauna quality first, then flora quality, then details)
|
||||
// Life mode: LAND column — Fauna, Flora, Quality, then details
|
||||
const LIFE_LEFT: MetricDef[] = [
|
||||
m('land_habitat'), m('land_quality'), m('land_canopy'), m('land_under'), m('land_fungi'),
|
||||
m('land_habitat'), m('land_canopy'), m('land_quality'), m('land_under'), m('land_fungi'),
|
||||
]
|
||||
|
||||
// Life mode: WATER column (fauna quality first, then flora quality, then details)
|
||||
// Life mode: WATER column — Fauna, Flora, Quality
|
||||
const LIFE_RIGHT: MetricDef[] = [
|
||||
m('water_fish'), m('water_quality'), m('water_reef'),
|
||||
m('water_fish'), m('water_reef'), m('water_quality'),
|
||||
]
|
||||
|
||||
// Environment mode: left column (energy budget)
|
||||
|
|
@ -359,7 +362,9 @@ function TerrainDistChart({ stats, currentTurn, onScrub }: StatsDashboardProps):
|
|||
|
||||
// Match total height of primary + compact sections
|
||||
// Height matches the tallest possible content (life mode with headers)
|
||||
const maxCompactRows = Math.max(LIFE_LEFT.length, LIFE_RIGHT.length) + 1
|
||||
const lifeRows = Math.max(LIFE_LEFT.length, LIFE_RIGHT.length) + 1 // +1 for header
|
||||
const envRows = Math.max(ENV_LEFT.length, ENV_RIGHT.length)
|
||||
const maxCompactRows = Math.max(lifeRows, envRows)
|
||||
const chartH = PRIMARY_METRICS.length * (PRIMARY_H + 3)
|
||||
+ 6
|
||||
+ maxCompactRows * (COMPACT_H + 3)
|
||||
|
|
|
|||
BIN
m2_life_dashboard_working.png
Normal file
BIN
m2_life_dashboard_working.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
|
|
@ -189,22 +189,31 @@ def create_app(
|
|||
@app.get("/api/stream/variants")
|
||||
async def stream_variants() -> StreamingResponse:
|
||||
async def event_generator():
|
||||
# Start from the highest known variant ID
|
||||
current = registry.get_recent_variants(limit=1)
|
||||
last_id = current[0]["variant_id"] if current else 0
|
||||
last_rating_snapshot = ""
|
||||
poll_count = 0
|
||||
while True:
|
||||
await asyncio.sleep(3)
|
||||
new_variants = registry.get_recent_variants(limit=10, since_id=last_id)
|
||||
if new_variants:
|
||||
last_id = max(v["variant_id"] for v in new_variants)
|
||||
yield f"data: {json.dumps(new_variants)}\n\n"
|
||||
else:
|
||||
# Also check for rating updates on recent variants
|
||||
continue
|
||||
|
||||
# Check for rating updates every ~15s (5 cycles)
|
||||
poll_count += 1
|
||||
if poll_count % 5 == 0:
|
||||
updated = registry.get_recent_variants(limit=30)
|
||||
if updated:
|
||||
snapshot = "|".join(
|
||||
f"{v['variant_id']}:{v['rating']}" for v in updated
|
||||
)
|
||||
if snapshot != last_rating_snapshot:
|
||||
last_rating_snapshot = snapshot
|
||||
yield f"data: {json.dumps(updated)}\n\n"
|
||||
else:
|
||||
yield ": keepalive\n\n"
|
||||
continue
|
||||
|
||||
yield ": keepalive\n\n"
|
||||
|
||||
return StreamingResponse(
|
||||
event_generator(),
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Add table
Reference in a new issue