Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions plots/boxen-basic/implementations/python/pygal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
""" anyplot.ai
boxen-basic: Basic Boxen Plot (Letter-Value Plot)
Library: pygal 3.1.0 | Python 3.13.13
Quality: 61/100 | Created: 2026-05-17
"""

import importlib
import os
import sys

import numpy as np


script_dir = sys.path[0]
sys.path.pop(0)
pygal = importlib.import_module("pygal")
Style = importlib.import_module("pygal.style").Style
sys.path.insert(0, script_dir)

THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

OKABE_ITO = ("#009E73", "#D55E00", "#0072B2", "#CC79A7", "#E69F00", "#56B4E9", "#F0E442")

np.random.seed(42)
endpoints = ["API-Auth", "API-Search", "API-Report", "API-Download"]

api_auth = np.concatenate([np.random.gamma(shape=2, scale=20, size=3500), np.random.uniform(200, 1000, size=200)])

api_search = np.concatenate([np.random.gamma(shape=1.5, scale=30, size=3500), np.random.uniform(300, 1200, size=200)])

api_report = np.concatenate([np.random.gamma(shape=2.5, scale=25, size=3500), np.random.uniform(250, 900, size=200)])

api_download = np.concatenate([np.random.gamma(shape=2, scale=35, size=3500), np.random.uniform(400, 1500, size=200)])

datasets = [api_auth, api_search, api_report, api_download]


def get_letter_values(data):
"""Calculate letter values at multiple quantile levels"""
quantiles = {
"sixteenths": (np.percentile(data, 6.25), np.percentile(data, 93.75)),
"eighths": (np.percentile(data, 12.5), np.percentile(data, 87.5)),
"quartiles": (np.percentile(data, 25), np.percentile(data, 75)),
"median": (np.percentile(data, 50), np.percentile(data, 50)),
}
return quantiles


def get_outliers(data):
"""Get points beyond sixteenths"""
q_lower = np.percentile(data, 6.25)
q_upper = np.percentile(data, 93.75)
return data[(data < q_lower) | (data > q_upper)].tolist()


custom_style = Style(
background=PAGE_BG,
plot_background=PAGE_BG,
foreground=INK,
foreground_strong=INK,
foreground_subtle=INK_MUTED,
colors=OKABE_ITO,
title_font_size=28,
label_font_size=18,
major_label_font_size=16,
legend_font_size=16,
value_font_size=14,
stroke_width=3,
)

boxen_chart = pygal.XY(
title="boxen-basic · pygal · anyplot.ai",
x_title="Endpoint",
y_title="Response Time (ms)",
width=4800,
height=2700,
style=custom_style,
show_legend=True,
range=(0, 1200),
show_dots=True,
dots_size=6,
)

for endpoint_idx, endpoint in enumerate(endpoints):
data = datasets[endpoint_idx]
lv = get_letter_values(data)

# Add data points for each letter value level to create nested boxes
# Sixteenths - outermost box
s_lower, s_upper = lv["sixteenths"]
boxen_chart.add(f"{endpoint}: Sixteenths", [(endpoint_idx, s_lower), (endpoint_idx, s_upper)], stroke_width=2)

# Eighths - middle outer box
e_lower, e_upper = lv["eighths"]
boxen_chart.add(f"{endpoint}: Eighths", [(endpoint_idx, e_lower), (endpoint_idx, e_upper)], stroke_width=2)

# Quartiles - middle inner box (IQR)
q_lower, q_upper = lv["quartiles"]
boxen_chart.add(f"{endpoint}: Quartiles", [(endpoint_idx, q_lower), (endpoint_idx, q_upper)], stroke_width=4)

# Median - innermost
m_val = lv["median"][0]
boxen_chart.add(f"{endpoint}: Median", [(endpoint_idx, m_val)], stroke_width=6)

# Outliers
outliers = get_outliers(data)
if outliers:
outlier_x = [endpoint_idx + np.random.normal(0, 0.06) for _ in outliers]
outlier_points = list(zip(outlier_x, outliers, strict=False))
boxen_chart.add(f"{endpoint}: Outliers", outlier_points, dots_size=4)

boxen_chart.x_labels = endpoints

boxen_chart.render_to_png(f"plot-{THEME}.png")

with open(f"plot-{THEME}.html", "wb") as f:
f.write(boxen_chart.render())
230 changes: 230 additions & 0 deletions plots/boxen-basic/metadata/python/pygal.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
library: pygal
language: python
specification_id: boxen-basic
created: '2026-05-17T08:58:34Z'
updated: '2026-05-17T09:37:03Z'
generated_by: claude-haiku
workflow_run: 25986442262
issue: 3414
language_version: 3.13.13
library_version: 3.1.0
preview_url_light: https://storage.googleapis.com/anyplot-images/plots/boxen-basic/python/pygal/plot-light.png
preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/boxen-basic/python/pygal/plot-dark.png
preview_html_light: https://storage.googleapis.com/anyplot-images/plots/boxen-basic/python/pygal/plot-light.html
preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/boxen-basic/python/pygal/plot-dark.html
quality_score: 61
review:
strengths:
- Theme tokens correctly applied (warm off-white and near-black backgrounds)
- Data colors remain identical across light and dark renders
- API response time scenario is realistic and well-chosen
- Shows outliers and multiple quantile levels as specified
- Clean code structure with reproducible seed
- Correctly formatted title
weaknesses:
- 'Legend clutter: 16 entries (4 endpoints × 4 quantile levels) severely impacts
readability; legend should be simplified or reorganized'
- Boxen plot is approximation using line segments rather than nested rectangles
- doesn't match specification's 'nested boxes with decreasing width'
- Color cycling per level rather than per endpoint causes same quantile level to
have different colors across endpoints, confusing the encoding
- Minimal design refinement; uses library defaults with no visual hierarchy or aesthetic
sophistication
- Small outlier markers (4px) are barely visible at full resolution
image_description: |-
Light render (plot-light.png):
Background: Warm off-white (#FAF8F1) ✓
Chrome: Title ("boxen-basic · pygal · anyplot.ai"), axis labels ("Endpoint", "Response Time (ms)"), and tick labels all clearly readable against light background. All text is dark-colored and well-positioned.
Data: Uses four colored marker sets (cyan, orange, blue, pink) with line segments representing quantile levels. Each endpoint (API-Auth, API-Search, API-Report, API-Download) is shown with nested line segments for sixteenths, eighths, quartiles, and median, plus outlier points with jitter. Colors appear to follow Okabe-Ito palette order but are applied inconsistently (cycling per level rather than per endpoint).
Legend: MAJOR ISSUE - Contains 16 entries (4 endpoints × 4 quantile types), creating severe visual clutter and overlap on the left side. Legend is hard to parse.
Legibility verdict: PARTIAL FAIL - While text elements are readable, the legend clutter significantly impairs usability.

Dark render (plot-dark.png):
Background: Warm near-black (#1A1A17) ✓
Chrome: Title, axis labels, and tick labels are all light-colored and readable against the dark background. No "dark-on-dark" failures observed.
Data: Data colors are identical to light render (cyan, orange, blue, pink line segments for each quantile level and outliers). Line visibility is good against dark background.
Legend: Same 16-entry clutter issue as light render, but text is now light-colored and readable against dark background.
Legibility verdict: PARTIAL FAIL - Chrome is theme-correct, but legend clutter reduces overall usability in both renders.
criteria_checklist:
visual_quality:
score: 19
max: 30
items:
- id: VQ-01
name: Text Legibility
score: 6
max: 8
passed: true
comment: All text readable but could be more prominent at 4800×2700 resolution
- id: VQ-02
name: No Overlap
score: 1
max: 6
passed: false
comment: 'CRITICAL: Legend has 16 entries creating severe overlap and clutter'
- id: VQ-03
name: Element Visibility
score: 4
max: 6
passed: true
comment: Line segments visible but thin; outlier points (4px) are quite small
- id: VQ-04
name: Color Accessibility
score: 1
max: 2
passed: false
comment: Palette cycles per level not per endpoint; same level has different
colors across endpoints
- id: VQ-05
name: Layout & Canvas
score: 4
max: 4
passed: true
comment: Excellent canvas utilization with balanced margins
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: Clear descriptive labels with units
- id: VQ-07
name: Palette Compliance
score: 1
max: 2
passed: false
comment: Okabe-Ito palette defined but applied via color cycling; theme chrome
is correct in both renders but palette is misapplied
design_excellence:
score: 6
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 2
max: 8
passed: false
comment: Generic defaults with no design thought; color cycling creates visual
confusion
- id: DE-02
name: Visual Refinement
score: 2
max: 6
passed: false
comment: Library defaults with minimal customization
- id: DE-03
name: Data Storytelling
score: 2
max: 6
passed: false
comment: Data displayed but no visual hierarchy or emphasis
spec_compliance:
score: 8
max: 15
items:
- id: SC-01
name: Plot Type
score: 2
max: 5
passed: false
comment: Uses XY scatter as workaround; not true boxen plot with nested rectangles
- id: SC-02
name: Required Features
score: 2
max: 4
passed: false
comment: Shows quantile levels and outliers but 'nested boxes with decreasing
width' not clearly represented
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: X/Y mapping correct (endpoint vs response time)
- id: SC-04
name: Title & Legend
score: 1
max: 3
passed: false
comment: Title correct format; legend is confusing with 16 cluttered entries
data_quality:
score: 14
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 5
max: 6
passed: true
comment: Shows outliers and varying distributions well
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: API response times by endpoint is realistic, neutral scenario
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: Response times 0-1200ms are realistic and plausible
code_quality:
score: 10
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: Simple, straightforward
- id: CQ-02
name: Reproducibility
score: 2
max: 2
passed: true
comment: Uses np.random.seed(42)
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
comment: Only necessary imports
- id: CQ-04
name: Code Elegance
score: 2
max: 2
passed: true
comment: Clean, no fake UI
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: Correct plot-{THEME}.png and .html output
library_mastery:
score: 4
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 3
max: 5
passed: true
comment: Uses XY chart and Style correctly; workaround approach is reasonable
given pygal limitations
- id: LM-02
name: Distinctive Features
score: 1
max: 5
passed: false
comment: Generic usage without distinctive library features
verdict: APPROVED
impl_tags:
dependencies: []
techniques: []
patterns:
- data-generation
- iteration-over-groups
dataprep: []
styling: []
Loading