{"spec_id":"bar-pareto","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nbar-pareto: Pareto Chart with Cumulative Line\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-06-20\n\"\"\"\n\nimport os\nimport re\n\nimport cairosvg\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens (Imprint style guide — default-style-guide.md)\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# Imprint palette — first series always #009E73\nCOLOR_VITAL = \"#009E73\"  # Imprint pos 1 — vital-few bars\nCOLOR_TRIVIAL = INK_MUTED  # semantic muted anchor — trivial-many (de-emphasised)\nCOLOR_LINE = \"#4467A3\"  # Imprint pos 3 — cumulative percentage line\nCOLOR_THRESHOLD = \"#DDCC77\"  # Imprint amber — 80% warning threshold\n\n# Data — manufacturing defect analysis, sorted descending by count\ncategories = [\"Scratches\", \"Dents\", \"Misalignment\", \"Cracks\", \"Discoloration\", \"Warping\", \"Contamination\", \"Burrs\"]\ncounts = [142, 98, 84, 56, 42, 31, 18, 9]\n\ntotal = sum(counts)\ncumulative_pct = []\nrunning = 0\nfor c in counts:\n    running += c\n    cumulative_pct.append(round(running / total * 100, 1))\n\nvital_few_count = sum(1 for p in cumulative_pct if p <= 80.0)\n\ntitle = \"bar-pareto · python · pygal · anyplot.ai\"\n\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=(COLOR_VITAL,),\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    value_font_size=36,\n    legend_font_size=44,\n    stroke_width=2.5,\n)\n\nchart = pygal.Bar(\n    width=3200,\n    height=1800,\n    title=title,\n    x_title=\"Defect Type\",\n    y_title=\"Frequency (Count)\",\n    style=custom_style,\n    show_legend=False,\n    print_values=True,\n    print_values_position=\"top\",\n    value_formatter=lambda x: f\"{x:,.0f}\",\n    show_y_guides=False,\n    show_x_guides=False,\n    margin=20,\n    margin_bottom=180,\n    margin_left=200,\n    margin_right=420,\n    spacing=18,\n    rounded_bars=6,\n    truncate_label=-1,\n    x_label_rotation=30,\n    js=[],\n)\n\nchart.x_labels = categories\nchart.add(\"Defect Count\", counts)\n\n# Render SVG then inject cumulative-line overlays\nsvg_str = chart.render().decode(\"utf-8\")\n\n# Recolor trivial-many bars via inline style override\nbar_rects = list(re.finditer(r'<rect\\s[^>]*class=\"rect reactive tooltip-trigger\"[^>]*/>', svg_str))\nfor i in reversed(range(len(bar_rects))):\n    if i >= vital_few_count:\n        m = bar_rects[i]\n        old_rect = m.group(0)\n        new_rect = old_rect.replace(\n            'class=\"rect reactive tooltip-trigger\"',\n            f'class=\"rect reactive tooltip-trigger\" style=\"fill:{COLOR_TRIVIAL};stroke:{COLOR_TRIVIAL}\"',\n        )\n        svg_str = svg_str[: m.start()] + new_rect + svg_str[m.end() :]\n\n# Extract bar geometry for overlay coordinate mapping\nbar_matches_pos = re.findall(r'<rect\\s[^>]*class=\"rect reactive tooltip-trigger\"[^>]*/>', svg_str)\n\nbar_centers_x = []\nbar_tops_y = []\nbar_bottoms_y = []\n\nfor bar_svg in bar_matches_pos:\n    x_m = re.search(r'\\bx=\"([^\"]+)\"', bar_svg)\n    y_m = re.search(r'\\by=\"([^\"]+)\"', bar_svg)\n    w_m = re.search(r'\\bwidth=\"([^\"]+)\"', bar_svg)\n    h_m = re.search(r'\\bheight=\"([^\"]+)\"', bar_svg)\n    if x_m and y_m and w_m and h_m:\n        x = float(x_m.group(1))\n        y = float(y_m.group(1))\n        w = float(w_m.group(1))\n        h = float(h_m.group(1))\n        bar_centers_x.append(x + w / 2)\n        bar_tops_y.append(y)\n        bar_bottoms_y.append(y + h)\n\n# Derive plot coordinate system from bar geometry\ny_base = max(bar_bottoms_y)\npixels_per_count = (y_base - min(bar_tops_y)) / max(counts)\n\ny_label_values = [int(m) for m in re.findall(r\"<text[^>]*>(\\d+)</text>\", svg_str)]\ny_axis_max = max(v for v in y_label_values if v <= 200) if y_label_values else 160\n\ny_plot_top = y_base - pixels_per_count * y_axis_max\nplot_height = y_base - y_plot_top\n\n# Cumulative percentage pixel coordinates (centred on each bar top)\ncum_points = []\nfor i, pct in enumerate(cumulative_pct):\n    cx = bar_centers_x[i]\n    cy = y_base - (pct / 100.0) * plot_height\n    cum_points.append((cx, cy))\n\n# 80% reference dashed line\ny_80 = y_base - 0.80 * plot_height\nx_left = bar_centers_x[0] - 80\nx_right = bar_centers_x[-1] + 90\n\nref_line_svg = f'''\n<g id=\"reference-80\">\n  <line x1=\"{x_left:.1f}\" y1=\"{y_80:.1f}\" x2=\"{x_right:.1f}\" y2=\"{y_80:.1f}\"\n    stroke=\"{COLOR_THRESHOLD}\" stroke-width=\"4\" stroke-dasharray=\"20,12\" opacity=\"0.9\" />\n</g>\n'''\n\n# Cumulative line, dots, and percentage labels\npoints_str = \" \".join(f\"{x:.1f},{y:.1f}\" for x, y in cum_points)\n\ncumulative_svg = f'''\n<g id=\"cumulative-overlay\">\n  <polyline points=\"{points_str}\"\n    fill=\"none\" stroke=\"{COLOR_LINE}\" stroke-width=\"5\"\n    stroke-linecap=\"round\" stroke-linejoin=\"round\" />\n'''\n\nfor i, (cx, cy) in enumerate(cum_points):\n    pct_val = cumulative_pct[i]\n    bar_top = bar_tops_y[i]\n    lx = cx + 22\n    ly = cy - 28\n    anchor = \"start\"\n    if abs(cy - bar_top) < 70:\n        ly = min(cy - 50, bar_top - 38)\n    if i >= len(cum_points) - 2:\n        lx = cx - 22\n        anchor = \"end\"\n    if i == 1:\n        lx = cx + 28\n        ly = cy - 32\n    # Prevent label from creeping into the title / top margin area\n    if ly < y_plot_top + 50:\n        ly = cy + 42  # flip below the dot\n    cumulative_svg += f'''\n  <circle cx=\"{cx:.1f}\" cy=\"{cy:.1f}\" r=\"10\"\n    fill=\"{COLOR_LINE}\" stroke=\"{PAGE_BG}\" stroke-width=\"3\" />\n  <text x=\"{lx:.1f}\" y=\"{ly:.1f}\"\n    text-anchor=\"{anchor}\" fill=\"{COLOR_LINE}\"\n    font-size=\"32\" font-family=\"sans-serif\" font-weight=\"bold\">{pct_val:.0f}%</text>\n'''\n\ncumulative_svg += \"</g>\"\n\n# Secondary y-axis on the right for cumulative percentage\nright_edge = bar_centers_x[-1] + 90\nsec_axis_svg = '<g id=\"secondary-y-axis\">'\n\nfor pct in [0, 20, 40, 60, 80]:\n    y_pos = y_base - (pct / 100.0) * plot_height\n    sec_axis_svg += f'''\n  <line x1=\"{right_edge:.1f}\" y1=\"{y_pos:.1f}\"\n    x2=\"{right_edge + 12:.1f}\" y2=\"{y_pos:.1f}\"\n    stroke=\"{COLOR_LINE}\" stroke-width=\"2\" />\n  <text x=\"{right_edge + 22:.1f}\" y=\"{y_pos + 12:.1f}\"\n    fill=\"{COLOR_LINE}\" font-size=\"34\" font-family=\"sans-serif\"\n    text-anchor=\"start\">{pct}%</text>\n'''\n\nsec_axis_svg += f'''\n  <line x1=\"{right_edge:.1f}\" y1=\"{y_base:.1f}\"\n    x2=\"{right_edge:.1f}\" y2=\"{y_plot_top:.1f}\"\n    stroke=\"{COLOR_LINE}\" stroke-width=\"2\" />\n'''\n\nmid_y = (y_base + y_plot_top) / 2\ntitle_x = right_edge + 100\nsec_axis_svg += f'''\n  <text x=\"{title_x:.1f}\" y=\"{mid_y:.1f}\"\n    fill=\"{COLOR_LINE}\" font-size=\"42\" font-family=\"sans-serif\"\n    text-anchor=\"middle\"\n    transform=\"rotate(-90, {title_x:.1f}, {mid_y:.1f})\">Cumulative %</text>\n'''\nsec_axis_svg += \"</g>\"\n\n# Bottom legend centred below the bars\nlegend_cx = (bar_centers_x[0] + bar_centers_x[-1]) / 2\nlegend_y = y_base + 110\n\nlegend_svg = f'''\n<g id=\"pareto-legend\">\n  <rect x=\"{legend_cx - 510:.1f}\" y=\"{legend_y - 14:.1f}\"\n    width=\"28\" height=\"28\" rx=\"4\" fill=\"{COLOR_VITAL}\" />\n  <text x=\"{legend_cx - 472:.1f}\" y=\"{legend_y + 10:.1f}\"\n    fill=\"{INK}\" font-size=\"32\" font-family=\"sans-serif\">Vital Few</text>\n\n  <rect x=\"{legend_cx - 306:.1f}\" y=\"{legend_y - 14:.1f}\"\n    width=\"28\" height=\"28\" rx=\"4\" fill=\"{COLOR_TRIVIAL}\" />\n  <text x=\"{legend_cx - 268:.1f}\" y=\"{legend_y + 10:.1f}\"\n    fill=\"{INK}\" font-size=\"32\" font-family=\"sans-serif\">Trivial Many</text>\n\n  <line x1=\"{legend_cx - 38:.1f}\" y1=\"{legend_y:.1f}\"\n    x2=\"{legend_cx + 10:.1f}\" y2=\"{legend_y:.1f}\"\n    stroke=\"{COLOR_LINE}\" stroke-width=\"4\" />\n  <circle cx=\"{legend_cx - 14:.1f}\" cy=\"{legend_y:.1f}\" r=\"7\"\n    fill=\"{COLOR_LINE}\" stroke=\"{PAGE_BG}\" stroke-width=\"2\" />\n  <text x=\"{legend_cx + 22:.1f}\" y=\"{legend_y + 10:.1f}\"\n    fill=\"{INK}\" font-size=\"32\" font-family=\"sans-serif\">Cumulative %</text>\n\n  <line x1=\"{legend_cx + 240:.1f}\" y1=\"{legend_y:.1f}\"\n    x2=\"{legend_cx + 290:.1f}\" y2=\"{legend_y:.1f}\"\n    stroke=\"{COLOR_THRESHOLD}\" stroke-width=\"3\" stroke-dasharray=\"12,7\" />\n  <text x=\"{legend_cx + 302:.1f}\" y=\"{legend_y + 10:.1f}\"\n    fill=\"{INK}\" font-size=\"32\" font-family=\"sans-serif\">80% Threshold</text>\n</g>\n'''\n\n# Inject all overlays before the closing </svg> tag\ninjection = ref_line_svg + cumulative_svg + sec_axis_svg + legend_svg\nsvg_str = svg_str.replace(\"</svg>\", injection + \"\\n</svg>\")\n\n# Save\ncairosvg.svg2png(\n    bytestring=svg_str.encode(\"utf-8\"), write_to=f\"plot-{THEME}.png\", output_width=3200, output_height=1800\n)\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(svg_str.encode(\"utf-8\"))\n"}