{"spec_id":"bump-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nbump-basic: Basic Bump Chart\nLibrary: plotly 6.7.0 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-05-29\n\"\"\"\n\nimport os\n\nimport plotly.graph_objects as go\n\n\n# Theme tokens — Imprint palette chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nGRID = \"rgba(26,26,23,0.15)\" if THEME == \"light\" else \"rgba(240,239,232,0.15)\"\n\n# Imprint categorical palette positions 1-6\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\"]\n\n# Data - Formula 1 driver standings over a season\ndrivers = [\"Verstappen\", \"Hamilton\", \"Norris\", \"Leclerc\", \"Piastri\", \"Sainz\"]\nraces = [\"Bahrain\", \"Jeddah\", \"Melbourne\", \"Suzuka\", \"Miami\", \"Imola\", \"Monaco\", \"Silverstone\"]\n\nrankings = {\n    \"Verstappen\": [1, 1, 1, 1, 1, 2, 3, 2],\n    \"Hamilton\": [4, 3, 4, 3, 3, 3, 1, 1],\n    \"Norris\": [5, 5, 3, 4, 2, 1, 2, 3],\n    \"Leclerc\": [2, 2, 2, 2, 4, 4, 4, 4],\n    \"Piastri\": [3, 4, 5, 5, 5, 5, 5, 5],\n    \"Sainz\": [6, 6, 6, 6, 6, 6, 6, 6],\n}\n\ncolors = {d: IMPRINT_PALETTE[i] for i, d in enumerate(drivers)}\n\n# Visual hierarchy — emphasize dynamic storylines, mute stable ones\nrank_changes = {d: max(r) - min(r) for d, r in rankings.items()}\nline_widths = {d: 5 if rank_changes[d] >= 3 else 3 if rank_changes[d] >= 2 else 2 for d in drivers}\nmarker_sizes = {d: 16 if rank_changes[d] >= 3 else 12 if rank_changes[d] >= 2 else 10 for d in drivers}\nopacities = {d: 1.0 if rank_changes[d] >= 3 else 0.85 if rank_changes[d] >= 2 else 0.65 for d in drivers}\n\n# Figure\nfig = go.Figure()\n\nfor driver in drivers:\n    ranks = rankings[driver]\n    color = colors[driver]\n    fig.add_trace(\n        go.Scatter(\n            x=races,\n            y=ranks,\n            mode=\"lines+markers\",\n            name=driver,\n            line={\"width\": line_widths[driver], \"color\": color},\n            marker={\"size\": marker_sizes[driver], \"color\": color, \"line\": {\"width\": 2, \"color\": PAGE_BG}},\n            opacity=opacities[driver],\n            showlegend=False,\n            hovertemplate=\"<b>%{text}</b><br>%{x}: P%{y}<extra></extra>\",\n            text=[driver] * len(races),\n        )\n    )\n    # End-of-line label — bold for high-movement drivers\n    fig.add_annotation(\n        x=races[-1],\n        y=ranks[-1],\n        text=f\"  <b>{driver}</b>\" if rank_changes[driver] >= 3 else f\"  {driver}\",\n        showarrow=False,\n        xanchor=\"left\",\n        font={\"size\": 12, \"color\": color},\n        opacity=opacities[driver],\n    )\n\n# Layout with inverted Y-axis (rank 1 at top)\ntitle = \"bump-basic · python · plotly · anyplot.ai\"\nfig.update_layout(\n    autosize=False,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    title={\"text\": title, \"font\": {\"size\": 16, \"color\": INK}, \"x\": 0.02, \"xanchor\": \"left\"},\n    xaxis={\n        \"title\": {\"text\": \"Race\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"showgrid\": False,\n        \"linecolor\": INK_SOFT,\n        \"tickcolor\": INK_SOFT,\n    },\n    yaxis={\n        \"title\": {\"text\": \"Championship Position\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"autorange\": \"reversed\",\n        \"tickmode\": \"linear\",\n        \"tick0\": 1,\n        \"dtick\": 1,\n        \"gridcolor\": GRID,\n        \"gridwidth\": 1,\n        \"showgrid\": True,\n        \"zeroline\": False,\n        \"linecolor\": INK_SOFT,\n    },\n    margin={\"r\": 130, \"t\": 60, \"l\": 80, \"b\": 60},\n    hoverlabel={\"font_size\": 12, \"bgcolor\": ELEVATED_BG, \"bordercolor\": INK_SOFT},\n    font={\"color\": INK},\n)\n\n# PNG — static export at exact 3200×1800 canvas\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\n\n# HTML — add range slider for race-by-race exploration (interactive only)\nfig.update_xaxes(rangeslider={\"visible\": True, \"bgcolor\": ELEVATED_BG, \"bordercolor\": INK_SOFT, \"thickness\": 0.06})\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\", config={\"scrollZoom\": True, \"displayModeBar\": True})\n"}