{"spec_id":"lollipop-grouped","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nlollipop-grouped: Grouped Lollipop Chart\nLibrary: pygal 3.1.0 | Python 3.13.13\nQuality: 85/100 | Updated: 2026-05-17\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent local filename from shadowing the pygal package by removing script dir from path\n_script_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path[:] = [p for p in sys.path if os.path.abspath(p) != _script_dir]\n\nimport pygal\nfrom pygal.style import Style\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_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Okabe-Ito palette (first series is always #009E73)\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data: Quarterly revenue (in millions) by product line across regions\ncategories = [\"North\", \"South\", \"East\", \"West\"]\nseries_names = [\"Electronics\", \"Furniture\", \"Apparel\"]\nseries_values = [[42, 35, 48, 31], [28, 32, 25, 38], [15, 22, 18, 26]]\n\n# Build color list: repeating Okabe-Ito colors for each lollipop\nall_colors = []\nfor _i, color in enumerate(IMPRINT[: len(series_names)]):\n    all_colors.extend([color] * len(categories))\n\n# Custom style for 4800x2700 canvas with theme-adaptive chrome\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=tuple(all_colors),\n    title_font_size=28,\n    label_font_size=22,\n    major_label_font_size=18,\n    legend_font_size=16,\n    value_font_size=14,\n)\n\n# Create XY chart for lollipop visualization\nchart = pygal.XY(\n    width=4800,\n    height=2700,\n    style=custom_style,\n    title=\"lollipop-grouped · Python · pygal · anyplot.ai\",\n    x_title=\"Region\",\n    y_title=\"Revenue ($ millions)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    show_y_guides=True,\n    show_x_guides=False,\n    dots_size=22,\n    stroke_width=3,\n    margin=80,\n    range=(0, 55),\n    xrange=(0.3, 4.7),\n    show_minor_x_labels=False,\n    truncate_legend=-1,\n)\n\n# Position offsets for grouped lollipops within each category\noffsets = [-0.2, 0, 0.2]\n\n# Add each lollipop as its own series (prevents connecting lines)\nfor series_idx, (name, values) in enumerate(zip(series_names, series_values, strict=True)):\n    for cat_idx, val in enumerate(values):\n        x_pos = cat_idx + 1 + offsets[series_idx]\n        # Each lollipop is a separate series: stem from 0 to value\n        lollipop_data = [{\"value\": (x_pos, 0), \"node\": {\"r\": 0}}, {\"value\": (x_pos, val), \"node\": {\"r\": 22}}]\n        # Only first lollipop of each series shows in legend\n        show_label = name if cat_idx == 0 else None\n        chart.add(show_label, lollipop_data)\n\n# Custom x-axis labels at category positions\nchart.x_labels = [1, 2, 3, 4]\nchart.x_labels_major = [1, 2, 3, 4]\n\n# X-axis label mapping\nlabel_map = {1: \"North\", 2: \"South\", 3: \"East\", 4: \"West\"}\nchart.x_value_formatter = lambda x: label_map.get(int(x), \"\") if x == int(x) else \"\"\n\n# Save as PNG and HTML with theme suffix\nchart.render_to_png(f\"plot-{THEME}.png\")\nchart.render_to_file(f\"plot-{THEME}.html\")\n"}