diff --git a/plots/shap-waterfall/implementations/python/highcharts.py b/plots/shap-waterfall/implementations/python/highcharts.py new file mode 100644 index 0000000000..7965200888 --- /dev/null +++ b/plots/shap-waterfall/implementations/python/highcharts.py @@ -0,0 +1,241 @@ +""" anyplot.ai +shap-waterfall: SHAP Waterfall Plot for Feature Attribution +Library: highcharts unknown | Python 3.13.13 +Quality: 86/100 | Created: 2026-05-07 +""" + +import base64 +import json +import os +import tempfile +import time +import urllib.request +from pathlib import Path + +from selenium import webdriver +from selenium.webdriver.chrome.options import Options + + +# Theme tokens +THEME = os.getenv("ANYPLOT_THEME", "light") +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" +GRID = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)" + +BRAND = "#009E73" # Okabe-Ito pos 1 — baseline & prediction bars +POSITIVE_COLOR = "#D55E00" # Okabe-Ito pos 2 — positive SHAP (pushes risk up) +NEGATIVE_COLOR = "#0072B2" # Okabe-Ito pos 3 — negative SHAP (pushes risk down) + +# Data — credit scoring model explaining a single loan application +# Features sorted by absolute SHAP value (largest contribution first = top of chart) +BASE_VALUE = 0.35 # Expected probability of default across all applicants + +features = [ + "Credit Score", + "Debt-to-Income", + "Annual Income", + "Loan Amount", + "Employment Years", + "Payment History", + "Open Accounts", + "Credit Inquiries", + "Credit Age", + "Savings Balance", +] +shap_values = [-0.18, +0.15, -0.12, +0.09, -0.07, -0.05, +0.04, +0.03, -0.02, -0.02] +FINAL_VALUE = round(BASE_VALUE + sum(shap_values), 4) + +# Build waterfall data: base bar → feature deltas → isSum final +categories = ["E[f(x)] Baseline", *features, "f(x) Prediction"] + +data_points = [{"y": BASE_VALUE, "color": BRAND}] +for sv in shap_values: + data_points.append({"y": sv, "color": POSITIVE_COLOR if sv > 0 else NEGATIVE_COLOR}) +data_points.append({"isSum": True, "color": BRAND}) + +# Chart configuration (JSON-serializable; JS functions injected via string replace) +chart_config = { + "chart": { + "type": "waterfall", + "inverted": True, + "width": 4800, + "height": 2700, + "backgroundColor": PAGE_BG, + "marginLeft": 340, + "marginRight": 220, + "marginTop": 130, + "marginBottom": 220, + "style": {"fontFamily": "Arial, sans-serif", "color": INK}, + }, + "title": { + "text": "Credit Default Risk · shap-waterfall · highcharts · anyplot.ai", + "style": {"fontSize": "28px", "fontWeight": "normal", "color": INK}, + "align": "left", + "x": 340, + }, + "xAxis": { + "categories": categories, + "title": {"text": "Feature", "style": {"fontSize": "22px", "color": INK}}, + "labels": {"style": {"fontSize": "20px", "color": INK_SOFT}}, + "lineColor": INK_SOFT, + "tickColor": INK_SOFT, + "gridLineColor": GRID, + }, + "yAxis": { + "title": {"text": "Probability of Default", "style": {"fontSize": "22px", "color": INK}}, + "labels": {"style": {"fontSize": "18px", "color": INK_SOFT}, "formatter": "__YAXIS_FORMATTER__"}, + "lineColor": INK_SOFT, + "tickColor": INK_SOFT, + "gridLineColor": GRID, + "gridLineWidth": 1, + "min": -0.02, + "max": 0.50, + "plotLines": [ + { + "value": BASE_VALUE, + "color": INK_SOFT, + "width": 2, + "dashStyle": "Dash", + "zIndex": 2, + "label": { + "text": f"Baseline {BASE_VALUE:.2f}", + "align": "right", + "rotation": 0, + "x": -6, + "y": -12, + "style": {"fontSize": "16px", "color": INK_SOFT}, + }, + }, + { + "value": FINAL_VALUE, + "color": BRAND, + "width": 2, + "dashStyle": "ShortDot", + "zIndex": 2, + "label": { + "text": f"Prediction {FINAL_VALUE:.2f}", + "align": "right", + "rotation": 0, + "x": -6, + "y": -12, + "style": {"fontSize": "16px", "color": BRAND}, + }, + }, + ], + }, + "legend": {"enabled": False}, + "tooltip": {"enabled": False}, + "plotOptions": { + "waterfall": { + "lineWidth": 2, + "lineColor": INK_SOFT, + "borderWidth": 0, + "groupPadding": 0.05, + "pointPadding": 0.08, + "dataLabels": { + "enabled": True, + "formatter": "__DATA_LABEL_FORMATTER__", + "style": {"fontSize": "18px", "fontWeight": "bold", "color": INK, "textOutline": "none"}, + "inside": False, + }, + } + }, + "series": [{"name": "SHAP Attribution", "data": data_points}], +} + +# Inject JavaScript formatter functions (not JSON-serializable, so replace placeholders) +config_json = json.dumps(chart_config) + +yaxis_formatter = """function() { + return Highcharts.numberFormat(this.value, 2); +}""" +config_json = config_json.replace('"__YAXIS_FORMATTER__"', yaxis_formatter) + +data_label_formatter = """function() { + if (this.point.isSum) { + return 'f(x) = ' + Highcharts.numberFormat(this.y, 2); + } + if (this.point.index === 0) { + return 'E[f(x)] = ' + Highcharts.numberFormat(this.y, 2); + } + var sign = this.y > 0 ? '+' : ''; + return sign + Highcharts.numberFormat(this.y, 3); +}""" +config_json = config_json.replace('"__DATA_LABEL_FORMATTER__"', data_label_formatter) + + +# Download Highcharts JS with multiple CDN fallbacks +def download_js(paths, timeout=15): + cdn_bases = [ + "https://cdn.jsdelivr.net/npm/highcharts@11/", + "https://unpkg.com/highcharts@11/", + "https://code.highcharts.com/", + ] + for path in paths: + for base in cdn_bases: + url = base + path + for attempt in range(2): + try: + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + with urllib.request.urlopen(req, timeout=timeout) as resp: + return resp.read().decode("utf-8") + except Exception: + if attempt == 0: + time.sleep(1) + return None + + +highcharts_js = download_js(["highcharts.js"]) +if highcharts_js is None: + raise RuntimeError("Failed to download highcharts.js from all CDNs") + +# Waterfall chart type lives in highcharts-more.js +highcharts_more_js = download_js(["highcharts-more.js"]) +if highcharts_more_js is None: + raise RuntimeError("Failed to download highcharts-more.js from all CDNs") + +# Generate HTML with inline Highcharts JS (core + more module for waterfall type) +html_content = f""" + +
+ + + + + + + + +""" + +# Save interactive HTML artifact +with open(f"plot-{THEME}.html", "w", encoding="utf-8") as f: + f.write(html_content) + +# Screenshot via headless Chrome with CDP for full-resolution capture +with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f: + f.write(html_content) + temp_path = f.name + +chrome_options = Options() +chrome_options.add_argument("--headless=new") +chrome_options.add_argument("--no-sandbox") +chrome_options.add_argument("--disable-dev-shm-usage") +chrome_options.add_argument("--disable-gpu") +chrome_options.add_argument("--hide-scrollbars") +chrome_options.add_argument("--force-device-scale-factor=1") + +driver = webdriver.Chrome(options=chrome_options) +driver.get(f"file://{temp_path}") +time.sleep(5) + +screenshot_config = {"captureBeyondViewport": True, "clip": {"x": 0, "y": 0, "width": 4800, "height": 2700, "scale": 1}} +result = driver.execute_cdp_cmd("Page.captureScreenshot", screenshot_config) +with open(f"plot-{THEME}.png", "wb") as f: + f.write(base64.b64decode(result["data"])) +driver.quit() + +Path(temp_path).unlink() diff --git a/plots/shap-waterfall/metadata/python/highcharts.yaml b/plots/shap-waterfall/metadata/python/highcharts.yaml new file mode 100644 index 0000000000..0839827531 --- /dev/null +++ b/plots/shap-waterfall/metadata/python/highcharts.yaml @@ -0,0 +1,250 @@ +library: highcharts +language: python +specification_id: shap-waterfall +created: '2026-05-07T11:47:19Z' +updated: '2026-05-07T12:06:19Z' +generated_by: claude-sonnet +workflow_run: 25493326647 +issue: 5237 +python_version: 3.13.13 +library_version: unknown +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/shap-waterfall/python/highcharts/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/shap-waterfall/python/highcharts/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/shap-waterfall/python/highcharts/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/shap-waterfall/python/highcharts/plot-dark.html +quality_score: 86 +review: + strengths: + - Native Highcharts waterfall chart type with isSum flag correctly models the cumulative + SHAP attribution chain from baseline to final prediction + - Semantic Okabe-Ito color assignment (orange = positive/risk-up, blue = negative/risk-down, + green = anchors) creates an immediate, intuitive visual narrative that needs no + legend + - 'Complete theme-adaptive chrome: all INK, INK_SOFT, GRID, and PAGE_BG tokens applied + to every axis, label, and background element in both renders' + - Multi-CDN fallback download logic with retry ensures robust CI execution even + when a CDN is unreachable + - Dual plotLines (baseline dashed, prediction dotted) with branded label styling + fulfil the spec's labeled reference lines requirement + weaknesses: + - Title prefix 'Credit Default Risk · ' is non-standard; must be exactly 'shap-waterfall + · highcharts · anyplot.ai' + - 'yAxis.max: 0.50 wastes ~30% of canvas width to the right of the Baseline 0.35 + line; trim to ~0.40-0.42' + - Data label fontSize 18px is slightly small for a 4800x2700 canvas; 20-22px would + improve legibility + - download_js() helper function violates KISS; inline the CDN logic or simplify + to a single URL + - Raw JSON construction bypasses highcharts_core Python API; LM-01 capped at 3/5 + image_description: |- + Light render (plot-light.png): + Background: Warm off-white #FAF8F1 — correct, not pure white + Chrome: Title visible as dark ink at top-left; y-axis feature labels (Credit Score, Debt-to-Income, etc.) readable as dark text; x-axis tick labels (0.00, 0.05, ... 0.45) readable; axis titles ("Probability of Default", "Feature") clear; reference line labels ("Baseline 0.35", "Prediction 0.20") legible + Data: Baseline bar in brand green #009E73 spanning ~0 to 0.35; feature bars in orange #D55E00 (positive SHAP) and blue #0072B2 (negative SHAP); prediction bar in green #009E73 spanning ~0 to 0.20; SHAP value labels (+0.150, -0.180, etc.) shown beside each bar + Legibility verdict: PASS + + Dark render (plot-dark.png): + Background: Warm near-black #1A1A17 — correct, not pure black + Chrome: Title appears as light-colored text against dark background; y-axis feature labels readable as light gray #B8B7B0; x-axis tick labels visible as light gray; axis titles in light #F0EFE8; grid lines very subtle; reference line labels legible ("Prediction 0.20" in green, "Baseline 0.35" in soft gray); no dark-on-dark text failures detected + Data: Bar colors identical to light render — green #009E73 for baseline/prediction, orange #D55E00 for positive SHAP, blue #0072B2 for negative SHAP; all bars clearly distinguishable against dark background + Legibility verdict: PASS + criteria_checklist: + visual_quality: + score: 28 + max: 30 + items: + - id: VQ-01 + name: Text Legibility + score: 7 + max: 8 + passed: true + comment: All font sizes explicitly set (title 28px, axis 22px, ticks 18-20px, + data labels 18px); data labels slightly small, 20-22px would be better + - id: VQ-02 + name: No Overlap + score: 6 + max: 6 + passed: true + comment: No text or element collisions in either render + - id: VQ-03 + name: Element Visibility + score: 6 + max: 6 + passed: true + comment: All bars sized and padded well, clearly distinct in both themes + - id: VQ-04 + name: Color Accessibility + score: 2 + max: 2 + passed: true + comment: Orange/blue are high-contrast and CVD-safe; green reference bars + unambiguous + - id: VQ-05 + name: Layout & Canvas + score: 3 + max: 4 + passed: true + comment: 'Chart fills canvas well but yAxis.max: 0.50 extends 15+ points beyond + Baseline 0.35, wasting right-side canvas' + - id: VQ-06 + name: Axis Labels & Title + score: 2 + max: 2 + passed: true + comment: Probability of Default (x-axis) and Feature (y-axis) are descriptive + - id: VQ-07 + name: Palette Compliance + score: 2 + max: 2 + passed: true + comment: 'Okabe-Ito positions 1-3 used correctly; backgrounds #FAF8F1/#1A1A17; + chrome tokens applied to all elements' + design_excellence: + score: 13 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 5 + max: 8 + passed: true + comment: 'Above defaults: semantic color coding for direction, brand green + for reference bars, clean typography; not yet publication-level' + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: 'borderWidth: 0 removes bar outlines, legend/tooltip disabled, subtle + grid; top/right axes still present' + - id: DE-03 + name: Data Storytelling + score: 4 + max: 6 + passed: true + comment: Color instantly signals contribution direction; magnitude-ordered + features guide the eye; clear flow from baseline to prediction + spec_compliance: + score: 14 + max: 15 + items: + - id: SC-01 + name: Plot Type + score: 5 + max: 5 + passed: true + comment: 'Native Highcharts waterfall type with inverted: true — correct horizontal + waterfall' + - id: SC-02 + name: Required Features + score: 4 + max: 4 + passed: true + comment: Cumulative bars, positive/negative color coding, base value bar, + prediction bar, numeric SHAP labels, horizontal layout, reference lines, + magnitude-sorted features, native connector lines + - id: SC-03 + name: Data Mapping + score: 3 + max: 3 + passed: true + comment: Features on y-axis, probability of default on x-axis, all 10 features + shown + - id: SC-04 + name: Title & Legend + score: 2 + max: 3 + passed: false + comment: Title has non-standard prefix 'Credit Default Risk · '; required + format is 'shap-waterfall · highcharts · anyplot.ai' + data_quality: + score: 15 + max: 15 + items: + - id: DQ-01 + name: Feature Coverage + score: 6 + max: 6 + passed: true + comment: Both positive and negative contributors shown; baseline and prediction + bookend bars complete the picture + - id: DQ-02 + name: Realistic Context + score: 5 + max: 5 + passed: true + comment: Credit default risk model for single loan application; all feature + names domain-appropriate; neutral business scenario + - id: DQ-03 + name: Appropriate Scale + score: 4 + max: 4 + passed: true + comment: Base value 0.35, final prediction 0.20, SHAP values ±0.18 are all + realistic for probability-scale credit model + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 2 + max: 3 + passed: false + comment: download_js() helper function defined; KISS requires flat script + with no functions/classes + - id: CQ-02 + name: Reproducibility + score: 2 + max: 2 + passed: true + comment: All data hardcoded; fully deterministic + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: All 9 imports are used + - id: CQ-04 + name: Code Elegance + score: 2 + max: 2 + passed: true + comment: JS formatter injection via string-replace is pragmatic; multi-CDN + fallback justified; no fake UI + - id: CQ-05 + name: Output & API + score: 1 + max: 1 + passed: true + comment: Saves plot-{THEME}.png and plot-{THEME}.html correctly + library_mastery: + score: 7 + max: 10 + items: + - id: LM-01 + name: Idiomatic Usage + score: 3 + max: 5 + passed: true + comment: Uses native Highcharts waterfall type correctly but bypasses highcharts_core + Python API in favour of raw JSON construction + - id: LM-02 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: 'isSum: true for prediction total bar, inverted: true for horizontal + orientation, plotLines with styled labels, CDP-based full-resolution screenshot + — all Highcharts-distinctive' + verdict: APPROVED +impl_tags: + dependencies: + - selenium + techniques: + - html-export + patterns: + - data-generation + - iteration-over-groups + dataprep: [] + styling: []