{"spec_id":"slope-basic","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\nslope-basic: Basic Slope Chart (Slopegraph)\nLibrary: plotly 6.9.0 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-07-25\n\"\"\"\n\nimport os\n\nimport plotly.graph_objects as go\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nGRID = \"rgba(26,26,23,0.10)\" if THEME == \"light\" else \"rgba(240,239,232,0.10)\"\n\n# Imprint palette: brand green = growth, matte red = decline (finance/industrial\n# gain-loss semantic exception — see default-style-guide.md \"Semantic exception\")\nCOLOR_UP = \"#009E73\"\nCOLOR_DOWN = \"#AE3030\"\n\n# Data - US manufacturing sector output ($M), 2023 vs 2024\nsectors = [\n    \"Aerospace\",\n    \"Electronics\",\n    \"Pharmaceuticals\",\n    \"Machinery\",\n    \"Food Processing\",\n    \"Plastics\",\n    \"Chemicals\",\n    \"Metals\",\n    \"Automotive\",\n    \"Textiles\",\n]\n\noutput_2023 = [145, 260, 190, 130, 275, 80, 205, 155, 320, 95]\noutput_2024 = [210, 310, 225, 175, 290, 100, 195, 140, 285, 70]\n\ncolors = [COLOR_UP if end > start else COLOR_DOWN for start, end in zip(output_2023, output_2024, strict=True)]\n\n# Emphasize the biggest movers with bolder lines/markers (data-driven, not decorative)\npct_changes = [abs(end - start) / start for start, end in zip(output_2023, output_2024, strict=True)]\nlo, hi = min(pct_changes), max(pct_changes)\nspread = hi - lo\nemphasis = [(c - lo) / spread if spread else 0.5 for c in pct_changes]\nline_widths = [2.0 + e * 2.0 for e in emphasis]\nmarker_sizes = [9 + e * 6 for e in emphasis]\n\n\nall_values = output_2023 + output_2024\ny_min, y_max = min(all_values), max(all_values)\ny_pad = (y_max - y_min) * 0.18\ny_axis_range = [y_min - y_pad, y_max + y_pad]\nmin_label_gap = (y_axis_range[1] - y_axis_range[0]) * 0.052\n\n# Anti-overlap label placement: push stacked label positions apart while\n# keeping the stack centered on the original values (left/right run the same\n# pass independently, since the two label columns never interact).\nleft_label_y = []\nfor values in (output_2023, output_2024):\n    order = sorted(range(len(values)), key=lambda i: values[i])\n    stacked = [values[i] for i in order]\n    for i in range(1, len(stacked)):\n        if stacked[i] - stacked[i - 1] < min_label_gap:\n            stacked[i] = stacked[i - 1] + min_label_gap\n    for i in range(len(stacked) - 2, -1, -1):\n        if stacked[i + 1] - stacked[i] < min_label_gap:\n            stacked[i] = stacked[i + 1] - min_label_gap\n    adjusted = [0.0] * len(values)\n    for position, i in enumerate(order):\n        adjusted[i] = stacked[position]\n    left_label_y.append(adjusted)\nleft_label_y, right_label_y = left_label_y\n\n# Label columns: leader-line anchors sit exactly at the xaxis range bounds (the\n# same x position as the axis spine), so label text - regardless of how long a\n# sector name is - always renders into the margin whitespace and never crosses\n# back over the spine into the plot area. Margins are sized from the longest\n# label string in each column so no text clips the canvas edge either.\nx_axis_range = [-0.5, 1.5]\nleft_label_text = [f\"{sector}: ${value}M\" for sector, value in zip(sectors, output_2023, strict=True)]\nright_label_text = [f\"${value}M: {sector}\" for sector, value in zip(sectors, output_2024, strict=True)]\nlabel_fontsize = 11\nchar_width = 0.62 * label_fontsize\nleft_margin = round(24 + char_width * max(len(t) for t in left_label_text))\nright_margin = round(24 + char_width * max(len(t) for t in right_label_text))\nleft_anchor_x = x_axis_range[0] - 0.02\nright_anchor_x = x_axis_range[1] + 0.02\n\n# Plot\nfig = go.Figure()\n\nfor i, sector in enumerate(sectors):\n    fig.add_trace(\n        go.Scatter(\n            x=[0, 1],\n            y=[output_2023[i], output_2024[i]],\n            mode=\"lines+markers\",\n            line={\"color\": colors[i], \"width\": line_widths[i]},\n            marker={\"size\": marker_sizes[i], \"color\": colors[i]},\n            name=sector,\n            showlegend=False,\n            hovertemplate=(f\"{sector}<br>2023: ${output_2023[i]}M<br>2024: ${output_2024[i]}M<extra></extra>\"),\n        )\n    )\n\n# Labels at 2023 (left side) — leader line points from the (possibly nudged)\n# label position back to the true data value, so decluttering never disconnects\n# a label from the entity it describes.\nfor i in range(len(sectors)):\n    fig.add_annotation(\n        x=0,\n        y=output_2023[i],\n        ax=left_anchor_x,\n        ay=left_label_y[i],\n        axref=\"x\",\n        ayref=\"y\",\n        xref=\"x\",\n        yref=\"y\",\n        text=left_label_text[i],\n        showarrow=True,\n        arrowhead=0,\n        arrowwidth=1,\n        arrowcolor=colors[i],\n        xanchor=\"right\",\n        yanchor=\"middle\",\n        align=\"right\",\n        font={\"size\": label_fontsize, \"color\": colors[i]},\n    )\n\n# Labels at 2024 (right side)\nfor i in range(len(sectors)):\n    fig.add_annotation(\n        x=1,\n        y=output_2024[i],\n        ax=right_anchor_x,\n        ay=right_label_y[i],\n        axref=\"x\",\n        ayref=\"y\",\n        xref=\"x\",\n        yref=\"y\",\n        text=right_label_text[i],\n        showarrow=True,\n        arrowhead=0,\n        arrowwidth=1,\n        arrowcolor=colors[i],\n        xanchor=\"left\",\n        yanchor=\"middle\",\n        align=\"left\",\n        font={\"size\": label_fontsize, \"color\": colors[i]},\n    )\n\n# Style\ntitle_text = \"Manufacturing Output by Sector · slope-basic · python · plotly · anyplot.ai\"\ntitle_fontsize = round(14 * min(1.0, 67 / len(title_text)))\n\nfig.update_layout(\n    autosize=False,\n    width=800,\n    height=450,\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    title={\"text\": title_text, \"font\": {\"size\": title_fontsize, \"color\": INK}, \"x\": 0.5, \"xanchor\": \"center\"},\n    xaxis={\n        \"tickmode\": \"array\",\n        \"tickvals\": [0, 1],\n        \"ticktext\": [\"2023\", \"2024\"],\n        \"tickfont\": {\"size\": 13, \"color\": INK_SOFT},\n        \"range\": x_axis_range,\n        \"showgrid\": False,\n        \"zeroline\": False,\n        \"linecolor\": INK_SOFT,\n        \"mirror\": False,\n    },\n    yaxis={\n        # Numeric ticks are hidden: every entity is already directly labeled with\n        # its exact value at both endpoints, and the tick-number column would sit\n        # in the same margin band as those labels and collide with them.\n        \"showticklabels\": False,\n        \"range\": y_axis_range,\n        \"showgrid\": True,\n        \"gridwidth\": 1,\n        \"gridcolor\": GRID,\n        \"zeroline\": False,\n        \"linecolor\": INK_SOFT,\n        \"mirror\": False,\n    },\n    margin={\"l\": left_margin, \"r\": right_margin, \"t\": 45, \"b\": 35},\n)\n\n# Units label placed above the plot instead of a rotated axis title, so it\n# never competes with the wide left-side entity label column for margin space.\nfig.add_annotation(\n    x=0,\n    y=1,\n    xref=\"paper\",\n    yref=\"paper\",\n    xanchor=\"left\",\n    yanchor=\"bottom\",\n    text=\"Output ($M)\",\n    showarrow=False,\n    font={\"size\": 13, \"color\": INK},\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}