{"spec_id":"bar-spine","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nbar-spine: Spine Plot for Two-Variable Proportions\nLibrary: pygal 3.1.3 | Python 3.13.15\nQuality: 91/100 | Updated: 2026-09-27\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove current directory from path to avoid collision with this filename\n_cwd = sys.path[0] if sys.path[0] else \".\"\nif _cwd in sys.path:\n    sys.path.remove(_cwd)\n\nimport pygal\nfrom pygal.style import Style\n\n\nsys.path.insert(0, _cwd)\n\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\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\nIMPRINT = (\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\")\n\n# Data: Titanic passenger survival by passenger class\nclass_names = [\"1st Class\", \"2nd Class\", \"3rd Class\"]\nsurvived_counts = [200, 119, 181]\nnot_survived_counts = [123, 158, 528]\nclass_totals = [s + n for s, n in zip(survived_counts, not_survived_counts, strict=True)]\ngrand_total = sum(class_totals)\n\n# Bar widths proportional to marginal (class) frequencies\nwidths = [t / grand_total for t in class_totals]\nx_ranges = []\ncumulative = 0.0\nfor w in widths:\n    x_ranges.append((cumulative, cumulative + w))\n    cumulative += w\n\n# Conditional proportions within each bar (spine plot segments)\nsurvive_props = [s / t for s, t in zip(survived_counts, class_totals, strict=True)]\nnot_survive_props = [1.0 - sp for sp in survive_props]\n\n# Title fontsize scales with title length off the 67-char mandated baseline\nTITLE = \"Titanic Survival by Passenger Class · bar-spine · python · pygal · anyplot.ai\"\ntitle_font_size = round(66 * min(1.0, 67 / len(TITLE)))\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK_SOFT,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT,\n    opacity=1,\n    stroke_opacity=1,\n    stroke_width=1.5,\n    # Serif title distinguishes the headline from the monospace data labels\n    # below, rather than leaving every text element in the library-default face.\n    title_font_family='Georgia, \"Times New Roman\", serif',\n    title_font_size=title_font_size,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n)\n\nchart = pygal.Histogram(\n    style=custom_style,\n    width=3200,\n    height=1800,\n    title=TITLE,\n    y_title=\"Survival Rate (%)\",\n    show_legend=True,\n    show_x_guides=False,\n    show_y_guides=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=2,\n    print_values=True,\n    print_values_position=\"top\",\n    truncate_label=-1,\n)\n\n# Spine plot using overlapping Histogram bars:\n# Series 1 \"Survived\" (Imprint green #009E73): full-height background bar (value=1.0)\n# Series 2 \"Not Survived\" (Imprint lavender #C475FD): overlay bar from y=0 to not_survive_prop\n# → lavender covers the bottom portion; green is visible at top (survive_prop)\n# opacity=1 keeps the overlay fully solid so the covered green never bleeds through.\nsurvived_data = [\n    {\"value\": (1.0, x_min, x_max), \"label\": f\"{cls} — {sp:.1%} survived\"}\n    for cls, sp, (x_min, x_max) in zip(class_names, survive_props, x_ranges, strict=True)\n]\nnot_survived_data = [\n    {\"value\": (nsp, x_min, x_max), \"label\": f\"{cls} — {nsp:.1%} not survived\"}\n    for cls, nsp, (x_min, x_max) in zip(class_names, not_survive_props, x_ranges, strict=True)\n]\n\n# print_values_position=\"top\" anchors each series' label just above its own\n# rect's top edge. \"Survived\" spans the full [0, 1] range, so its label lands\n# above the plot as a per-bar headline. \"Not Survived\" only rises to\n# not_survive_prop, so its label sits right at the green/purple boundary —\n# inside the visible green sliver, next to the segment it describes.\nchart.add(\"Survived\", survived_data, formatter=lambda _v, index=None, **_kw: f\"{survive_props[index]:.0%} survived\")\nchart.add(\n    \"Not Survived\",\n    not_survived_data,\n    formatter=lambda _v, index=None, **_kw: f\"{not_survive_props[index]:.0%} not survived\",\n)\n\n# Y-axis in percentage format\nchart.y_labels = [\n    {\"label\": \"0%\", \"value\": 0},\n    {\"label\": \"25%\", \"value\": 0.25},\n    {\"label\": \"50%\", \"value\": 0.50},\n    {\"label\": \"75%\", \"value\": 0.75},\n    {\"label\": \"100%\", \"value\": 1.0},\n]\n\n# X-axis labels centered under each variable-width bar (spec requirement)\nchart.x_labels = [\n    {\"label\": f\"{cls} ({w:.1%})\", \"value\": (x_min + x_max) / 2}\n    for cls, w, (x_min, x_max) in zip(class_names, widths, x_ranges, strict=True)\n]\n\nchart.render_to_png(f\"plot-{THEME}.png\")\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}