{"spec_id":"line-multi","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\nline-multi: Multi-Line Comparison Plot\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 92/100 | Updated: 2026-08-05\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_line,\n    geom_point,\n    geom_text,\n    ggplot,\n    labs,\n    scale_color_manual,\n    scale_fill_manual,\n    scale_x_continuous,\n    theme,\n    theme_minimal,\n)\n\n\n# Theme tokens\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\n# Imprint palette (brand green always first)\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\"]\n\n# Data - Monthly sales for 4 product lines over 12 months, category order\n# matches the year-end ranking so the legend reads top-to-bottom like the lines\nnp.random.seed(42)\nmonths = np.arange(1, 13)\nmonth_labels = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\nelectronics = 100 + months * 9 + np.cumsum(np.random.normal(0, 3, 12)) * 0.4\naccessories = 85 + months * 3.2 + np.cumsum(np.random.normal(0, 2.5, 12)) * 0.4\nfurniture = 95 + np.sin(months * 0.4) * 6 + np.cumsum(np.random.normal(0, 2, 12)) * 0.3\nclothing = 128 - months * 3.8 + np.cumsum(np.random.normal(0, 2.5, 12)) * 0.4\n\n# Long-format DataFrame for plotnine\ndf = pd.DataFrame(\n    {\n        \"Month\": np.tile(months, 4),\n        \"Sales\": np.concatenate([electronics, accessories, furniture, clothing]),\n        \"Product\": np.repeat([\"Electronics\", \"Accessories\", \"Furniture\", \"Clothing\"], 12),\n    }\n)\ndf[\"Product\"] = pd.Categorical(\n    df[\"Product\"], categories=[\"Electronics\", \"Accessories\", \"Furniture\", \"Clothing\"], ordered=True\n)\n\ncolor_map = dict(zip([\"Electronics\", \"Accessories\", \"Furniture\", \"Clothing\"], IMPRINT, strict=True))\nbrand_only = df[df[\"Product\"] == \"Electronics\"]\n\n# Direct end-of-line value labels (year-end sales) so the crossover story reads\n# without relying on the legend alone. Labels are spread apart with a minimum\n# gap so the close Furniture/Clothing pair at month 12 doesn't collide.\nend_labels = (\n    df.loc[df[\"Month\"] == 12, [\"Product\", \"Sales\"]].sort_values(\"Sales\", ascending=False).reset_index(drop=True)\n)\nend_labels[\"label_y\"] = end_labels[\"Sales\"]\nmin_gap = 14\nfor i in range(1, len(end_labels)):\n    if end_labels.loc[i - 1, \"label_y\"] - end_labels.loc[i, \"label_y\"] < min_gap:\n        end_labels.loc[i, \"label_y\"] = end_labels.loc[i - 1, \"label_y\"] - min_gap\nend_labels[\"Month\"] = 12\nend_labels[\"Label\"] = end_labels[\"Sales\"].round(0).astype(int).astype(str)\n\n# Plot - base lines for all series, brand series (Electronics) redrawn\n# thicker on top to lead the eye, plus direct end-of-line value labels\nplot = (\n    ggplot(df, aes(x=\"Month\", y=\"Sales\", color=\"Product\", group=\"Product\"))\n    + geom_line(size=1.3)\n    + geom_point(aes(fill=\"Product\"), shape=\"o\", color=PAGE_BG, stroke=0.6, size=2.9)\n    + geom_line(data=brand_only, size=2.1, show_legend=False)\n    + geom_text(\n        data=end_labels,\n        mapping=aes(x=\"Month\", y=\"label_y\", label=\"Label\", color=\"Product\"),\n        ha=\"left\",\n        nudge_x=0.35,\n        size=2.8,\n        fontweight=\"bold\",\n        show_legend=False,\n    )\n    + scale_color_manual(values=color_map)\n    + scale_fill_manual(values=color_map, guide=None)\n    + scale_x_continuous(breaks=months, labels=month_labels, expand=(0.02, 0, 0.1, 0.6))\n    + labs(\n        x=\"Month\", y=\"Sales (thousands USD)\", title=\"line-multi · python · plotnine · anyplot.ai\", color=\"Product Line\"\n    )\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.12),\n        axis_line=element_blank(),\n        text=element_text(size=7, color=INK_SOFT),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        plot_title=element_text(size=12, color=INK, ha=\"center\"),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.5),\n        legend_key=element_rect(fill=PAGE_BG, color=None),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=9, color=INK),\n        legend_position=\"right\",\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}