supply-chain-env / visualize.py
ragavrida's picture
feat: real data wired in, visual map, all 5 improvements complete
5e5efc0
#!/usr/bin/env python3
"""
Live Supply Chain Map — visual dashboard for judges.
Shows a world map with:
- Ports (green=open, red=closed, yellow=reduced)
- Shipping routes (blue=open, red=blocked)
- Shipments moving along routes
- Disruptions highlighted
- Real-time stats panel
Generates an HTML file that opens in browser.
Run: python3 visualize.py
"""
import json
import webbrowser
import os
from world import SupplyChainWorld
from real_data import REAL_PORTS, REAL_ROUTES
def generate_map_html(world: SupplyChainWorld, filename: str = "supply_chain_map.html") -> str:
"""Generate an interactive HTML map of the supply chain."""
# Port data with status colors
ports_js = []
for p in REAL_PORTS:
status = world.ports.get(p["id"], {}).get("status", "open")
color = {"open": "#22c55e", "closed": "#ef4444", "reduced": "#eab308"}.get(status, "#666")
throughput = p.get("throughput_teu", 0)
ports_js.append({
"id": p["id"], "name": p["name"], "lat": p["lat"], "lon": p["lon"],
"status": status, "color": color, "throughput": throughput,
"country": p.get("country", ""),
})
# Route data with status
routes_js = []
for r in REAL_ROUTES:
src_port = next((p for p in REAL_PORTS if p["id"] == r["from"]), None)
dst_port = next((p for p in REAL_PORTS if p["id"] == r["to"]), None)
if not src_port or not dst_port:
continue
route_info = world.routes.get(r["from"], {}).get(r["to"], {})
status = route_info.get("status", "open")
color = "#3b82f6" if status == "open" else "#ef4444"
routes_js.append({
"from_lat": src_port["lat"], "from_lon": src_port["lon"],
"to_lat": dst_port["lat"], "to_lon": dst_port["lon"],
"cost": r["cost_per_teu"], "days": r["transit_days"],
"mode": r.get("mode", "ocean"), "status": status, "color": color,
"carrier": r.get("carrier", ""),
})
# Shipment positions
shipments_js = []
for s in world.shipments.values():
port = world.ports.get(s.current_location, {})
p_data = next((p for p in REAL_PORTS if p["id"] == s.current_location), None)
if p_data:
color = {"pending": "#fbbf24", "in_transit": "#3b82f6", "delivered": "#22c55e", "lost": "#ef4444"}.get(s.status, "#666")
shipments_js.append({
"id": s.id, "product": s.product, "value": s.value_usd,
"lat": p_data["lat"], "lon": p_data["lon"],
"status": s.status, "color": color,
})
# Disruptions
disruptions_js = [
{"type": d.type, "severity": d.severity, "desc": d.description,
"active": d.active, "start": d.start_day, "end": d.end_day}
for d in world.disruptions
]
# Stats
total_value = sum(s.value_usd for s in world.shipments.values())
stats = {
"day": world.day, "total_days": world.total_days,
"pending": sum(1 for s in world.shipments.values() if s.status == "pending"),
"in_transit": sum(1 for s in world.shipments.values() if s.status == "in_transit"),
"delivered": sum(1 for s in world.shipments.values() if s.status == "delivered"),
"lost": sum(1 for s in world.shipments.values() if s.status == "lost"),
"delivered_value": world.delivered_value,
"lost_value": world.lost_value,
"shipping_cost": world.total_shipping_cost,
"total_value": total_value,
}
html = f"""<!DOCTYPE html>
<html><head>
<meta charset="utf-8">
<title>SupplyChainEnv — Live Map</title>
<style>
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; }}
.header {{ padding: 16px 24px; background: #1e293b; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center; }}
.header h1 {{ font-size: 20px; color: #f8fafc; }}
.header .badge {{ background: #22c55e; color: #000; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 600; }}
.container {{ display: flex; height: calc(100vh - 56px); }}
.map {{ flex: 1; position: relative; background: #1e293b; overflow: hidden; }}
.sidebar {{ width: 360px; background: #0f172a; border-left: 1px solid #334155; overflow-y: auto; padding: 16px; }}
svg {{ width: 100%; height: 100%; }}
.stat-card {{ background: #1e293b; border-radius: 8px; padding: 12px 16px; margin-bottom: 8px; }}
.stat-label {{ font-size: 11px; color: #94a3b8; text-transform: uppercase; letter-spacing: 1px; }}
.stat-value {{ font-size: 24px; font-weight: 700; margin-top: 2px; }}
.stat-value.green {{ color: #22c55e; }}
.stat-value.red {{ color: #ef4444; }}
.stat-value.blue {{ color: #3b82f6; }}
.stat-value.yellow {{ color: #eab308; }}
.disruption {{ background: #1e293b; border-left: 3px solid #ef4444; border-radius: 4px; padding: 8px 12px; margin-bottom: 6px; font-size: 13px; }}
.disruption.active {{ border-color: #ef4444; background: #291515; }}
.disruption.upcoming {{ border-color: #eab308; }}
.section-title {{ font-size: 13px; font-weight: 600; color: #94a3b8; margin: 16px 0 8px; text-transform: uppercase; letter-spacing: 1px; }}
.port-label {{ font-size: 10px; fill: #cbd5e1; text-anchor: middle; pointer-events: none; }}
.tooltip {{ position: absolute; background: #1e293b; border: 1px solid #475569; border-radius: 6px; padding: 8px 12px; font-size: 12px; pointer-events: none; display: none; z-index: 10; }}
</style>
</head><body>
<div class="header">
<h1>SupplyChainEnv — Global Trade Network</h1>
<span class="badge">Day {stats['day']}/{stats['total_days']} | {world.difficulty.upper()}</span>
</div>
<div class="container">
<div class="map">
<svg viewBox="-180 -90 360 180" preserveAspectRatio="xMidYMid meet">
<rect x="-180" y="-90" width="360" height="180" fill="#0c1222"/>
<!-- Simplified world outline -->
<ellipse cx="0" cy="0" rx="170" ry="80" fill="none" stroke="#1e293b" stroke-width="0.5"/>
<!-- Routes -->
{"".join(f'<line x1="{r["from_lon"]}" y1="{-r["from_lat"]}" x2="{r["to_lon"]}" y2="{-r["to_lat"]}" stroke="{r["color"]}" stroke-width="{"0.8" if r["status"]=="open" else "1.2"}" opacity="{"0.4" if r["status"]=="open" else "0.8"}" stroke-dasharray="{"" if r["status"]=="open" else "2,2"}"/>' for r in routes_js)}
<!-- Ports -->
{"".join(f'<circle cx="{p["lon"]}" cy="{-p["lat"]}" r="2.5" fill="{p["color"]}" stroke="#fff" stroke-width="0.3"/><text x="{p["lon"]}" y="{-p["lat"]-4}" class="port-label">{p["name"]}</text>' for p in ports_js)}
<!-- Shipments -->
{"".join(f'<circle cx="{s["lon"]}" cy="{-s["lat"]}" r="1.5" fill="{s["color"]}" opacity="0.9"><animate attributeName="r" values="1;2.5;1" dur="2s" repeatCount="indefinite"/></circle>' for s in shipments_js)}
</svg>
</div>
<div class="sidebar">
<div class="stat-card"><div class="stat-label">Shipments Pending</div><div class="stat-value yellow">{stats['pending']}</div></div>
<div class="stat-card"><div class="stat-label">In Transit</div><div class="stat-value blue">{stats['in_transit']}</div></div>
<div class="stat-card"><div class="stat-label">Delivered</div><div class="stat-value green">{stats['delivered']} (${stats['delivered_value']:,.0f})</div></div>
<div class="stat-card"><div class="stat-label">Lost</div><div class="stat-value red">{stats['lost']} (${stats['lost_value']:,.0f})</div></div>
<div class="stat-card"><div class="stat-label">Shipping Cost</div><div class="stat-value">${stats['shipping_cost']:,.0f}</div></div>
<div class="stat-card"><div class="stat-label">Total Cargo Value</div><div class="stat-value">${stats['total_value']:,.0f}</div></div>
<div class="section-title">Active Disruptions</div>
{"".join(f'<div class="disruption {"active" if d["active"] else "upcoming"}"><strong>{d["type"].upper()}</strong> [{d["severity"]}]<br/>{d["desc"][:80]}<br/>Day {d["start"]}-{d["end"]}</div>' for d in disruptions_js)}
<div class="section-title">Legend</div>
<div style="font-size:12px; line-height:1.8;">
<span style="color:#22c55e;">&#9679;</span> Port Open &nbsp;
<span style="color:#ef4444;">&#9679;</span> Port Closed &nbsp;
<span style="color:#eab308;">&#9679;</span> Reduced<br/>
<span style="color:#3b82f6;">&#9644;</span> Route Open &nbsp;
<span style="color:#ef4444;">&#9644;</span> Route Blocked<br/>
<span style="color:#fbbf24;">&#9679;</span> Pending &nbsp;
<span style="color:#3b82f6;">&#9679;</span> In Transit &nbsp;
<span style="color:#22c55e;">&#9679;</span> Delivered
</div>
<div class="section-title" style="margin-top:20px;">Data Sources</div>
<div style="font-size:11px; color:#64748b; line-height:1.6;">
Port throughput: UNCTAD 2023<br/>
Shipping rates: Freightos Baltic Index Q1 2024<br/>
Disruptions: Lloyd's List, WHO, USGS 2017-2024<br/>
LPI scores: World Bank 2023
</div>
</div>
</div>
</body></html>"""
with open(filename, "w") as f:
f.write(html)
return os.path.abspath(filename)
def main():
"""Run the demo agent and generate map at each stage."""
from server.supply_chain_environment import SupplyChainEnvironment
from models import SupplyChainAction
def tool(name, args=None):
return SupplyChainAction(action_type="ToolCallAction", tool_name=name, arguments=args or {})
env = SupplyChainEnvironment()
env.reset(seed=42, difficulty="hard")
# Route shipments
obs = env.step(tool("view_shipments"))
for s in obs.tool_result["shipments"]:
if s["status"] == "pending":
path_obs = env.step(tool("find_path", {"from_port": s["current_location"], "to_warehouse": s["destination"]}))
path = path_obs.tool_result.get("path")
if path:
env.step(tool("route_shipment", {"shipment_id": s["id"], "route": path}))
# Advance 15 days for interesting state
for _ in range(15):
obs = env.step(tool("advance_day"))
if obs.done:
break
# Generate map
path = generate_map_html(env._world)
print(f"Map generated: {path}")
webbrowser.open(f"file://{path}")
if __name__ == "__main__":
main()