{"spec_id":"slope-basic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nslope-basic: Basic Slope Chart (Slopegraph)\nLibrary: letsplot 4.11.0 | Python 3.13.14\nQuality: 94/100 | Updated: 2026-07-26\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_point,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsave,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_manual,\n    scale_linetype_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\n\n\nLetsPlot.setup_html()\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\"\nGRID_COLOR = \"#C8C7BF\" if THEME == \"light\" else \"#333330\"\n\n# Imprint palette: brand green = increase, matte red = decrease\nCOLOR_INCREASE = \"#009E73\"\nCOLOR_DECREASE = \"#AE3030\"\n\n# Consumer electronics quarterly sales ($K): Q1 vs Q4.\n# Values are spread with a minimum gap of ~20 at both endpoints so the\n# entity + value labels never sit close enough to overlap.\ndata = {\n    \"entity\": [\n        \"Streaming Devices\",\n        \"Gaming Consoles\",\n        \"Laptops\",\n        \"Tablets\",\n        \"Smart TVs\",\n        \"Headphones\",\n        \"Monitors\",\n        \"Speakers\",\n        \"Cameras\",\n        \"Smartphones\",\n    ],\n    \"Q1\": [50, 72, 95, 118, 140, 162, 184, 206, 228, 250],\n    \"Q4\": [55, 90, 140, 160, 222, 245, 115, 200, 270, 180],\n}\ndf = pd.DataFrame(data)\ndf[\"change\"] = df[\"Q4\"] - df[\"Q1\"]\ndf[\"direction\"] = df[\"change\"].apply(lambda x: \"Increase\" if x > 0 else \"Decrease\")\ndf[\"abs_change\"] = df[\"change\"].abs()\n\n# Top 3 movers by absolute change — receive visual emphasis\ntop_movers = set(df.nlargest(3, \"abs_change\")[\"entity\"])\n\n# Segment data\ndf_segments = pd.DataFrame(\n    {\n        \"entity\": df[\"entity\"].values,\n        \"x_start\": [0] * 10,\n        \"x_end\": [1] * 10,\n        \"y_start\": df[\"Q1\"].values,\n        \"y_end\": df[\"Q4\"].values,\n        \"direction\": df[\"direction\"].values,\n        \"change\": df[\"change\"].values,\n    }\n)\nseg_normal = df_segments[~df_segments[\"entity\"].isin(top_movers)].reset_index(drop=True)\nseg_top = df_segments[df_segments[\"entity\"].isin(top_movers)].reset_index(drop=True)\n\n# Points (both endpoints)\ndf_long = pd.DataFrame(\n    {\"entity\": df[\"entity\"].tolist() * 2, \"value\": df[\"Q1\"].tolist() + df[\"Q4\"].tolist(), \"x\": [0] * 10 + [1] * 10}\n).merge(df[[\"entity\", \"direction\"]], on=\"entity\")\n\n# Endpoint labels: entity name + value for legibility\ndf_left = pd.DataFrame(\n    {\n        \"entity\": df[\"entity\"].values,\n        \"value\": df[\"Q1\"].values,\n        \"x\": [0] * 10,\n        \"label\": [f\"{e} (${v}K)\" for e, v in zip(df[\"entity\"], df[\"Q1\"], strict=False)],\n    }\n)\ndf_right = pd.DataFrame(\n    {\n        \"entity\": df[\"entity\"].values,\n        \"value\": df[\"Q4\"].values,\n        \"x\": [1] * 10,\n        \"label\": [f\"{e} (${v}K)\" for e, v in zip(df[\"entity\"], df[\"Q4\"], strict=False)],\n    }\n)\n\n# Custom tooltip format — letsplot-specific interactive feature\n_tooltip_normal = (\n    layer_tooltips()\n    .title(\"@entity\")\n    .line(\"Q1 Sales|$@{y_start}K\")\n    .line(\"Q4 Sales|$@{y_end}K\")\n    .line(\"Change|@{change}K\")\n)\n_tooltip_top = (\n    layer_tooltips()\n    .title(\"@entity  ★ Top mover\")\n    .line(\"Q1 Sales|$@{y_start}K\")\n    .line(\"Q4 Sales|$@{y_end}K\")\n    .line(\"Change|@{change}K\")\n)\n\nplot = (\n    ggplot()\n    # Background lines: dimmed to let top movers stand out\n    + geom_segment(\n        data=seg_normal,\n        mapping=aes(x=\"x_start\", y=\"y_start\", xend=\"x_end\", yend=\"y_end\", color=\"direction\", linetype=\"direction\"),\n        size=1.8,\n        alpha=0.55,\n        tooltips=_tooltip_normal,\n    )\n    # Top-mover lines: bold, fully opaque — emphasises the biggest Q1→Q4 changes\n    + geom_segment(\n        data=seg_top,\n        mapping=aes(x=\"x_start\", y=\"y_start\", xend=\"x_end\", yend=\"y_end\", color=\"direction\"),\n        size=3.5,\n        alpha=1.0,\n        tooltips=_tooltip_top,\n    )\n    + geom_point(data=df_long, mapping=aes(x=\"x\", y=\"value\", color=\"direction\"), size=6)\n    + geom_text(\n        data=df_left, mapping=aes(x=\"x\", y=\"value\", label=\"label\"), hjust=1, nudge_x=-0.07, size=4.2, color=INK_SOFT\n    )\n    + geom_text(\n        data=df_right, mapping=aes(x=\"x\", y=\"value\", label=\"label\"), hjust=0, nudge_x=0.07, size=4.2, color=INK_SOFT\n    )\n    + scale_color_manual(values={\"Increase\": COLOR_INCREASE, \"Decrease\": COLOR_DECREASE})\n    + scale_linetype_manual(values={\"Increase\": \"solid\", \"Decrease\": \"dashed\"}, guide=\"none\")\n    + scale_x_continuous(breaks=[0, 1], labels=[\"Q1 Sales ($K)\", \"Q4 Sales ($K)\"], limits=[-1.05, 2.05])\n    + scale_y_continuous(limits=[20, 300])\n    + labs(title=\"slope-basic · python · letsplot · anyplot.ai\", x=\"\", y=\"Sales ($K)\", color=\"Change\")\n    + theme_minimal()\n    + theme(\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        # color matches fill so the rect draws no border (theme_minimal has no\n        # spines by default — the \"remove all spines\" clean-look alternative)\n        panel_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_grid_major_y=element_line(color=GRID_COLOR, size=0.3),\n        panel_grid_major_x=element_blank(),\n        panel_grid_minor=element_blank(),\n        plot_title=element_text(size=16, color=INK),\n        axis_title_y=element_text(size=12, color=INK),\n        axis_title_x=element_blank(),\n        axis_text_x=element_text(size=12, color=INK_SOFT),\n        axis_text_y=element_text(size=10, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),\n        legend_title=element_text(size=12, color=INK),\n        legend_text=element_text(size=10, color=INK_SOFT),\n    )\n    + ggsize(800, 450)\n)\n\n# Save PNG: scale=4 on ggsize(800, 450) -> 3200 x 1800 px (canonical landscape canvas)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\n\n# Save HTML for letsplot interactive tooltips\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}