{"spec_id":"slope-basic","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nslope-basic: Basic Slope Chart (Slopegraph)\nLibrary: matplotlib 3.11.1 | Python 3.13.14\nQuality: 93/100 | Updated: 2026-07-25\n\"\"\"\n\nimport os\n\nimport matplotlib.patheffects as patheffects\nimport matplotlib.pyplot as plt\nimport matplotlib.ticker as mticker\nfrom matplotlib.lines import Line2D\nfrom matplotlib.transforms import blended_transform_factory\n\n\n# Theme\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\"\n\nCOLOR_INC = \"#009E73\"  # Imprint green — profit/gain semantic anchor\nCOLOR_DEC = \"#AE3030\"  # Imprint matte red — loss/decrease semantic anchor\n\n# Data: quarterly revenue for a consumer-electronics product line\nproducts = [\n    \"Power Bank\",\n    \"Bluetooth Speaker\",\n    \"Fitness Tracker\",\n    \"Thermostat\",\n    \"Robot Vacuum\",\n    \"ANC Headphones\",\n    \"Action Camera\",\n    \"Video Doorbell\",\n]\nq1_sales = [3.0, 6.5, 10.0, 13.5, 17.0, 20.5, 24.0, 27.5]\nq4_sales = [5.5, 4.0, 14.0, 11.0, 20.0, 17.5, 28.0, 25.0]\nchanges = [q4 - q1 for q1, q4 in zip(q1_sales, q4_sales, strict=True)]\n\n# Label collision avoidance: sort by value and nudge positions if too close\nmin_gap = 1.8\n\nq1_indexed = sorted(enumerate(q1_sales), key=lambda x: x[1])\nq1_label_pos = [0.0] * len(q1_sales)\nfor i, (orig_idx, val) in enumerate(q1_indexed):\n    if i == 0:\n        q1_label_pos[orig_idx] = val\n    else:\n        prev_idx = q1_indexed[i - 1][0]\n        if val - q1_label_pos[prev_idx] < min_gap:\n            q1_label_pos[orig_idx] = q1_label_pos[prev_idx] + min_gap\n        else:\n            q1_label_pos[orig_idx] = val\n\nq4_indexed = sorted(enumerate(q4_sales), key=lambda x: x[1])\nq4_label_pos = [0.0] * len(q4_sales)\nfor i, (orig_idx, val) in enumerate(q4_indexed):\n    if i == 0:\n        q4_label_pos[orig_idx] = val\n    else:\n        prev_idx = q4_indexed[i - 1][0]\n        if val - q4_label_pos[prev_idx] < min_gap:\n            q4_label_pos[orig_idx] = q4_label_pos[prev_idx] + min_gap\n        else:\n            q4_label_pos[orig_idx] = val\n\n# Plot — canonical landscape canvas: figsize x dpi = 3200x1800px, no bbox_inches=\"tight\"\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\nfig.subplots_adjust(left=0.29, right=0.71, top=0.83, bottom=0.1)\n\nx_positions = [0, 1]\n\n# Vertical column lines at axis positions — structural anchors for the slopegraph\nfor x in x_positions:\n    ax.axvline(x, color=INK_SOFT, linewidth=0.8, alpha=0.35, zorder=0)\n\n# Blended transform (axes-fraction x, data y): labels sit a fixed fraction of the\n# axes width outside the plotted columns regardless of the data's y-range, so the\n# collision-nudge math above only ever needs to reason in data (y) space.\nlabel_transform = blended_transform_factory(ax.transAxes, ax.transData)\nlabel_stroke = [patheffects.withStroke(linewidth=2, foreground=PAGE_BG)]\n\nfor i, (product, q1, q4, change) in enumerate(zip(products, q1_sales, q4_sales, changes, strict=True)):\n    color = COLOR_INC if change >= 0 else COLOR_DEC\n    ax.plot(\n        x_positions,\n        [q1, q4],\n        marker=\"o\",\n        markersize=8,\n        linewidth=2.5,\n        color=color,\n        markeredgecolor=PAGE_BG,\n        markeredgewidth=1.2,\n    )\n\n    # Left label: product name + Q1 value; dotted stub if the label was nudged to avoid a collision\n    lpos = q1_label_pos[i]\n    if abs(lpos - q1) > 0.05:\n        ax.plot(\n            [-0.09, -0.09], [q1, lpos], color=color, linewidth=0.7, alpha=0.4, linestyle=\":\", transform=label_transform\n        )\n    ax.text(\n        -0.16,\n        lpos,\n        f\"{product}: ${q1:.1f}M\",\n        ha=\"right\",\n        va=\"center\",\n        fontsize=8,\n        color=color,\n        fontweight=\"bold\",\n        transform=label_transform,\n        clip_on=False,\n        path_effects=label_stroke,\n    )\n\n    # Right label: product name + Q4 value; dotted stub if the label was nudged to avoid a collision\n    rpos = q4_label_pos[i]\n    if abs(rpos - q4) > 0.05:\n        ax.plot(\n            [1.09, 1.09], [q4, rpos], color=color, linewidth=0.7, alpha=0.4, linestyle=\":\", transform=label_transform\n        )\n    ax.text(\n        1.16,\n        rpos,\n        f\"{product}: ${q4:.1f}M\",\n        ha=\"left\",\n        va=\"center\",\n        fontsize=8,\n        color=color,\n        fontweight=\"bold\",\n        transform=label_transform,\n        clip_on=False,\n        path_effects=label_stroke,\n    )\n\n# Style\nax.set_xlim(-0.05, 1.05)\nax.set_xticks(x_positions)\nax.set_xticklabels([\"Q1 2024\", \"Q4 2024\"], fontsize=10, fontweight=\"bold\", color=INK)\n\ntitle = \"slope-basic · matplotlib · anyplot.ai\"\ntitle_fontsize = max(8, round(12 * 67 / len(title))) if len(title) > 67 else 12\nax.set_title(title, fontsize=title_fontsize, fontweight=\"medium\", color=INK)\n\nax.tick_params(axis=\"x\", length=0)\nax.tick_params(axis=\"y\", labelsize=8, labelcolor=INK_SOFT, colors=INK_SOFT)\n\n# FuncFormatter for y-axis: show units inline as \"$XM\" for self-documenting tick labels\nax.yaxis.set_major_formatter(mticker.FuncFormatter(lambda val, _: f\"${val:.0f}M\"))\n\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nax.spines[\"bottom\"].set_visible(False)\nax.spines[\"left\"].set_color(INK_SOFT)\n\nax.grid(True, axis=\"y\", alpha=0.2, linewidth=0.8, color=INK)\n\n# Legend centered below title\nlegend_elements = [\n    Line2D(\n        [0], [0], color=COLOR_INC, linewidth=2.5, marker=\"o\", markersize=7, markeredgecolor=PAGE_BG, label=\"Increase\"\n    ),\n    Line2D(\n        [0], [0], color=COLOR_DEC, linewidth=2.5, marker=\"o\", markersize=7, markeredgecolor=PAGE_BG, label=\"Decrease\"\n    ),\n]\nleg = ax.legend(\n    handles=legend_elements, loc=\"upper center\", bbox_to_anchor=(0.5, 1.14), fontsize=8, frameon=True, ncol=2\n)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}