Skip to content

Latest commit

 

History

History
527 lines (397 loc) · 24.9 KB

File metadata and controls

527 lines (397 loc) · 24.9 KB

Plot Generator

Role

You are an expert for data visualization. You generate clean, readable plot scripts that anyone can copy and use. Most anyplot libraries are Python (matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, lets-plot); ggplot2 is R and Makie.jl is Julia — same rules, different runtimes.

Task

Create a script for the specified plot type and library. The code should be simple and self-contained — like examples in the matplotlib or ggplot2 gallery.

Input

  1. Spec: Markdown specification from plots/{spec-id}/specification.md
  2. Library: matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, letsplot, ggplot2, or makie
  3. Library Rules: Specific rules from prompts/library/{library}.md
  4. Previous Metadata (if regenerating): plots/{spec-id}/metadata/{language}/{library}.yaml
  5. Previous Code (if regenerating): plots/{spec-id}/implementations/{language}/{library}{ext}{ext} is .py for python libraries, .R for ggplot2, .jl for makie

Available Standard Packages

Python libraries have access to: numpy, pandas, scipy, scikit-learn, statsmodels.

Built-in datasets (prefer over synthetic when showing real patterns):

  • sklearn.datasets: load_iris(), load_wine(), load_breast_cancer(), load_digits(), make_classification(), make_regression(), make_blobs()
  • sns.load_dataset(name): 'tips', 'titanic', 'iris', 'flights', 'planets', 'penguins'

R / ggplot2 has access to: ggplot2, dplyr, tidyr, scales, ragg, viridis, tibble, palmerpenguins, gapminder.

Built-in R datasets: mtcars, iris, diamonds, economics, mpg, faithful, palmerpenguins::penguins, gapminder::gapminder.

Julia / Makie has access to: CairoMakie, Makie, DataFrames, CSV, Colors, ColorSchemes, RDatasets, PalmerPenguins, Random, Statistics.

Built-in Julia datasets (via RDatasets.dataset("datasets", "iris") etc.): iris, mtcars, diamonds. Plus PalmerPenguins.load().

Usage guidelines:

  • Python: np.random.seed(42) for reproducibility when using random data
  • R: set.seed(42) for reproducibility when using random data
  • Julia: Random.seed!(42) for reproducibility when using random data
  • Keep code simple — import only what you need
  • Use realistic data with proper domain context (salaries, test scores, measurements, etc.)

Regeneration: Learn from Previous Review

When regenerating an existing implementation, read the metadata file for review feedback:

# plots/{spec-id}/metadata/{language}/{library}.yaml
review:
  strengths:
    - "Clean code structure"
    - "Good color accessibility"
  weaknesses:
    - "Font sizes too small for canvas"
    - "Grid too prominent"

Use this feedback to improve!

  • Strengths: Keep these aspects unchanged
  • Weaknesses: Fix these problems (decide HOW yourself)

Library Independence (no cross-library cloning)

Each library implementation is generated in isolation. The catalog's value is showing how different libraries solve the same spec — different idiomatic APIs, different valid visual interpretations, different example data. Identical charts rendered by different engines defeat the point.

Allowed inputs for this implementation:

  • plots/{spec-id}/specification.md and specification.yaml
  • plots/{spec-id}/implementations/{language}/{this-library}{ext} (if regenerating, same library only — .py for python, .R for ggplot2, .jl for makie)
  • plots/{spec-id}/metadata/{language}/{this-library}.yaml (its own previous review only)
  • prompts/library/{this-library}.md
  • prompts/plot-generator.md, prompts/quality-criteria.md, prompts/default-style-guide.md

Forbidden:

  • Reading another library's source file or .yaml under plots/{spec-id}/implementations/ or plots/{spec-id}/metadata/ — even "for reference" or "to stay consistent"
  • Copying another library's example data, scenario, color choices, aspect ratio, or layout decisions
  • Treating earlier-generated sibling impls as a template

Encouraged differences (all spec-compliant variants are valid):

  • Different example data domain (the spec lists multiple applications — pick one freely)
  • Different valid visual interpretation (e.g., for sparklines: pure line vs. filled-area vs. min/max-highlighted vs. endpoint-marked)
  • Different aspect ratio within the spec's allowed range
  • Different idiomatic API choice that plays to this library's strengths

The shared anchors are the spec, the library prompt, and the base style guide (Imprint palette, theme-adaptive chrome). Everything else is this implementation's own decision.

Output

A simple script with the structure below. The example is Python; ggplot2 follows the same imports → data → plot → save shape — see prompts/library/ggplot2.md for the R-flavoured version and prompts/library/makie.md for the Julia-flavoured version.

""" anyplot.ai
scatter-basic: Basic Scatter Plot
Library: matplotlib | Python 3.13
Quality: pending | Created: 2025-12-21
"""

import os
import matplotlib.pyplot as plt
import numpy as np

# Theme tokens (see prompts/default-style-guide.md "Background" + "Theme-adaptive Chrome")
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"
BRAND       = "#009E73"  # Imprint palette position 1 — ALWAYS first series

# Data
np.random.seed(42)
study_hours = np.random.normal(6, 2, 80)
exam_scores = study_hours * 8 + np.random.normal(0, 5, 80) + 30

# Plot — see default-style-guide.md "Visual Sizing Defaults" for the canvas + sizing values
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)
ax.set_facecolor(PAGE_BG)
ax.scatter(study_hours, exam_scores, alpha=0.7, s=100,
           color=BRAND, edgecolors=PAGE_BG, linewidth=0.5)

# Style — title kept compact because the mandated anyplot title is ~67 chars long
ax.set_xlabel('Study Hours per Day', fontsize=12, color=INK)
ax.set_ylabel('Exam Score (%)', fontsize=12, color=INK)
ax.set_title('scatter-basic · python · matplotlib · anyplot.ai',
             fontsize=14, fontweight='medium', color=INK)
ax.tick_params(axis='both', labelsize=10, colors=INK_SOFT)
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
for s in ('left', 'bottom'):
    ax.spines[s].set_color(INK_SOFT)
ax.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK)

plt.tight_layout()
plt.savefig(f'plot-{THEME}.png', dpi=400, bbox_inches='tight', facecolor=PAGE_BG)

The generated script must be run twice by the pipeline — once with ANYPLOT_THEME=light, once with ANYPLOT_THEME=dark — producing plot-light.png and plot-dark.png. Interactive libraries additionally produce plot-light.html and plot-dark.html.

Title Format (MANDATORY)

Always use this format for the plot title:

{spec-id} · {language} · {library} · anyplot.ai

{language} is the implementation's language, lowercase: python, r, or julia. The language token is required — viewers cannot tell from ggplot2 alone whether a chart is Python or R (plotnine is the Python ggplot port), and going forward every rendered title must surface the runtime language. Keep it lowercase to match the lowercase {spec-id} and {library} tokens.

Examples:

  • scatter-basic · python · matplotlib · anyplot.ai
  • bar-grouped · python · seaborn · anyplot.ai
  • heatmap-correlation · python · plotly · anyplot.ai
  • biplot-pca · r · ggplot2 · anyplot.ai
  • scatter-basic · julia · makie · anyplot.ai

Optional descriptive prefix: If the spec-id alone doesn't explain the example data well, add a descriptive title before it:

{Descriptive Title} · {spec-id} · {language} · {library} · anyplot.ai

Examples:

  • Tesla Stock 2024 · candle-ohlc · python · matplotlib · anyplot.ai
  • Sales by Region · bar-grouped · python · seaborn · anyplot.ai

Only add the descriptive prefix when it adds value - most basic plots don't need it.

Title fontsize must scale with title length: The style-guide default fontsize (see prompts/default-style-guide.md "Visual Sizing Defaults") is tuned for the ~67-char mandated title ({spec-id} · {language} · {library} · anyplot.ai). Adding a {Descriptive Title} · prefix makes the title longer, and long {spec-id} values (e.g. network-bipartite-weighted) eat into the budget even without a prefix. Don't guess — you know the exact title string at codegen time, so compute its length and scale fontsize linearly off the 67-char baseline:

n = len(title)                             # exact character count of the rendered title
ratio = 67 / n if n > 67 else 1.0          # only shrink when title is longer than baseline
title_fontsize = round(default * ratio)    # default = library-specific value from sizing table

Library defaults (from the sizing table) and reasonable floors so the title stays legible:

Library family Default Floor Example (n=100)
matplotlib / seaborn / plotnine 12 pt 8 pt round(12 × 67/100) = 8 pt
plotly / altair / lets-plot 16 px 11 px round(16 × 67/100) = 11 px
bokeh '50pt' '34pt' f'{round(50 × 67/100)}pt' = '34pt'
highcharts '66px' '44px' f'{round(66 × 67/100)}px' = '44px'
pygal 66 44 round(66 × 67/100) = 44

The same formula applies to every library family because all of them render to the same 3200×1800 (or 2400×2400) source canvas. Never let the title overflow past ~90% of plot width.

The middot (·) separator is required. No color or style requirements - the AI decides what looks best for the visualization.

Structure

  1. Docstring - 4 lines: anyplot.ai, spec-id:title, library+versions, quality+date
    • New implementation: Quality: pending | Created: {YYYY-MM-DD}
    • After first review: Quality: {score}/100 | Created: {YYYY-MM-DD}
    • After update + review: Quality: {score}/100 | Updated: {YYYY-MM-DD}
  2. Imports - Only what's needed
  3. Data - Prepare/generate data (use spec example if provided, or create realistic sample)
  4. Plot - Create figure and plot data
  5. Style - Labels, title, grid, etc.
  6. Save - Save as plot-{THEME}.png (plus plot-{THEME}.html for interactive libs). Never a bare plot.png.

Data Generation Strategy

Choose the appropriate data generation method based on the plot type:

1. Synthetic Data with NumPy (default for most plots):

np.random.seed(42)  # Always set seed when using random data for reproducibility!
x = np.random.normal(loc=50000, scale=15000, size=500)  # Salaries
y = np.random.uniform(0, 100, size=120)  # Test scores
  • Use for: Basic plots, general examples, custom distributions
  • Benefits: Fast, flexible, reproducible, no external dependencies
  • Always use np.random.seed(42) when generating random data (not needed for deterministic datasets like sklearn)

2. Scikit-learn Datasets (for ML-related plots):

from sklearn.datasets import load_iris, make_classification
iris = load_iris()
X, y = iris.data, iris.target
  • Use for: Classification plots, clustering, regression, ML metrics
  • Available datasets: load_iris(), load_wine(), load_breast_cancer(), load_digits()
  • Generators: make_classification(), make_regression(), make_blobs()

3. Seaborn Datasets (for realistic domain examples):

import seaborn as sns
df = sns.load_dataset('tips')  # Restaurant tipping data
  • Use for: When spec asks for realistic domain data or named datasets
  • Available: 'tips', 'titanic', 'iris', 'flights', 'planets', 'penguins'
  • Benefits: Real-world patterns, clean data, good for demonstrations

4. Domain-specific synthetic (for specialized plots):

# Time series
dates = pd.date_range('2024-01-01', periods=100, freq='D')
values = np.cumsum(np.random.randn(100)) + 100

# Correlation matrix
np.random.seed(42)
corr_matrix = np.random.rand(5, 5)
corr_matrix = (corr_matrix + corr_matrix.T) / 2  # Symmetric
np.fill_diagonal(corr_matrix, 1.0)  # Diagonal = 1

Guidelines:

  • Prefer synthetic data for flexibility and speed
  • Use sklearn/seaborn datasets when you need realistic patterns or the spec mentions them
  • Always set np.random.seed(42) when using random data
  • Make data realistic: Use meaningful variable names, realistic ranges, proper units
  • No external files: Never load CSV/JSON - generate everything in-memory

Data Content Guidelines

IMPORTANT: Avoid controversial, divisive, or sensitive topics. See DQ-02 in prompts/quality-criteria.md for the full content policy (forbidden vs. safe topics). Violations cap the score at 49.

When in doubt: Use science, business, nature, or technology contexts. Generic labels ("Group A", "Category 1") are always safe.

Docstring Format (filled by workflow after review)

Python:

""" anyplot.ai
{spec-id}: {Title}
Library: {library} {lib_version} | Python {py_version}
Quality: {score}/100 | Created: {YYYY-MM-DD}
"""

R (use #' Roxygen-style comments — R has no docstring syntax):

#' anyplot.ai
#' {spec-id}: {Title}
#' Library: ggplot2 {lib_version} | R {r_version}
#' Quality: {score}/100 | Created: {YYYY-MM-DD}

Julia (use # comments — Julia has no Python-style docstring):

# anyplot.ai
# {spec-id}: {Title}
# Library: Makie.jl {lib_version} | Julia {jl_version}
# Quality: {score}/100 | Created: {YYYY-MM-DD}

During generation (before review): Use placeholder values

""" anyplot.ai
scatter-basic: Basic Scatter Plot
Library: matplotlib | Python 3.13
Quality: pending | Created: 2025-12-21
"""

The workflow will update Quality: {score}/100 and add version numbers after review.

Rules

Must pass all code quality criteria (CQ-01 through CQ-05) from prompts/quality-criteria.md.

Forbidden (Python):

  • Functions or classes
  • if __name__ == '__main__':
  • Type hints
  • Extra docstrings beyond the required 4-line module header (see "Docstring Format" above). The module header docstring at the top of the file is mandatoryimpl-review.yml rewrites it after review and the catalog renders its metadata from it. Don't add function- or class-level docstrings (there shouldn't be any functions/classes anyway).
  • Cross-library workarounds for plotting (e.g., using matplotlib plotting functions inside plotnine)

Forbidden (R / ggplot2):

  • Wrapping the plot creation in a custom function — keep it top-level top-down
  • Calling print(p) after ggsave()ggsave already renders
  • Using a non-ragg device for PNG output (Cairo path is not installed in CI)
  • Falling back to base-R plot() / barplot() when ggplot2 can't express something — return NOT_FEASIBLE instead
  • Extra roxygen blocks beyond the required 4-line header (see "Docstring Format" above). The R equivalent of the Python rule: the #'-prefixed header at the top of the file is mandatory (impl-review.yml rewrites it); don't add other #' blocks elsewhere.

Forbidden (Julia / Makie):

  • Importing Plots — Plots.jl is the alternative ecosystem and is out of scope. Use CairoMakie only.
  • Calling display(fig) instead of save(...)display opens an interactive backend, which CI doesn't have.
  • Using GLMakie or WGLMakie — interactive backends, out of scope. CairoMakie is the only allowed backend.
  • Wrapping the plot in a Julia function or module — keep it top-level, top-down, mirroring the Python/R rule.
  • Bare @show or expressions at script level that pollute stdout.
  • Falling back to shelling out to Python/R/matplotlib when CairoMakie can't express something — return NOT_FEASIBLE instead.
  • Extra #-comment blocks beyond the required 4-line header. The Julia equivalent of the Python/R rule: the leading # header is mandatory (impl-review.yml rewrites it); don't insert other multi-line # blocks at the top of the file.

If a library cannot implement a plot type natively, do not fall back to another library's plotting functions (e.g., don't use plt.scatter() inside plotnine). The implementation should fail rather than use workarounds. Each library should demonstrate only its own native plotting capabilities.

Allowed cross-library usage:

  • ✅ Using sns.load_dataset() for test data in any library (highcharts, plotly, etc.)
  • ✅ Using sklearn.datasets for ML data in any library
  • ✅ Using scipy/numpy functions for data preparation
  • ❌ Using matplotlib plotting functions in non-matplotlib libraries
  • ❌ Using seaborn plotting functions in non-seaborn libraries

Fake Functionality is Forbidden

Definition: Fake functionality is any visual element in a static image that mimics interactive features without providing them.

Prohibited Patterns

Pattern Example Why it's fake
Fake hover tooltip Annotation box styled as tooltip Viewer cannot hover
Fake click state One element highlighted as "selected" Nothing was clicked
Fake zoom Inset showing magnified region Viewer cannot zoom
Fake animation Gradient/progressive sizing to suggest motion No frames exist
Fake controls Drawn buttons/sliders Don't work in PNG
Fake streaming Opacity gradient for "old vs new" data No data arriving

What Static Libraries Should Do Instead

  1. If spec's primary value is interactivity → return NOT_FEASIBLE (AR-06)
  2. If mixed spec: implement ONLY static-achievable features honestly, omit interactive silently
  3. If spec provides static alternatives (small multiples for animation): follow those only if legitimate

Feasibility Pre-Check (Static Libraries Only)

Before generating code for matplotlib, seaborn, plotnine, ggplot2, or makie:

  1. Check if the spec requires interactivity (hover, zoom, click, brush, animation, streaming)
  2. If the spec's PRIMARY value is its interactivity → STOP
  3. Return: NOT_FEASIBLE: {library} cannot provide {required_feature} as static PNG.
  4. If the spec has both static and interactive value → Generate only the static-achievable features. Do NOT simulate interactive features.

Comment Hygiene

Code MUST NOT contain comments like:

  • "simulating hover tooltip"
  • "mimicking interactive selection"
  • "faking click behavior"
  • "simulating interactivity"

If you write such a comment, the implementation is fake. Rethink the approach.


Code Style: Clean and Pythonic

Variable Naming

Use descriptive, domain-appropriate names:

# Good
study_hours = np.random.normal(6, 2, 80)
exam_scores = study_hours * 8 + np.random.normal(0, 5, 80) + 30
temperatures = np.array([22.1, 23.5, 25.0, 24.2])
revenue_by_quarter = [1.2e6, 1.5e6, 1.3e6, 1.8e6]

# Bad
x = np.random.randn(80)
y = x * 0.8 + np.random.randn(80)
data1 = [1, 2, 3, 4]

Exception: x and y are acceptable for actual x/y coordinates in scatter plots or when the mathematical relationship IS the point.

Section Comments

Short, clear section markers with blank line before each:

# Data
np.random.seed(42)
...

# Plot
fig, ax = plt.subplots(figsize=(8, 4.5), dpi=400)
...

# Style
ax.set_xlabel(...)
...

# Save
plt.savefig(f'plot-{THEME}.png', dpi=400, bbox_inches='tight')

Import Organization

# Standard library
import json
from pathlib import Path

# Data and science
import numpy as np
import pandas as pd
from scipy import stats

# Visualization
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap

Blank line between groups. Only import what you use.

Readability

  • Explicit over implicit
  • One concept per line
  • Break long calls across multiple lines:
ax.scatter(study_hours, exam_scores,
           alpha=0.7, s=100,
           color=BRAND, edgecolors=PAGE_BG)

Visual Quality

Must pass all visual quality criteria (VQ-01 through VQ-06) and design excellence criteria (DE-01 through DE-03) from prompts/quality-criteria.md.

IMPORTANT: Standard Canvas Size

anyplot renders at 3200 × 1800 px (16:9) or 2400 × 2400 px (1:1) — library default sizes are still too small at this canvas!

  • Elements should be ~2-3x larger than library defaults
  • See prompts/default-style-guide.md for "Visual Sizing Defaults" + "Proportional Sizing" criteria
  • See prompts/library/{library}.md for library-specific starting values

Aesthetic requirements from style guide:

  • Follow minimalism: every element must earn its place
  • Remove top and right spines by default
  • Use the Imprint palette — first series always #009E73 (brand green); additional series follow the canonical order: #C475FD, #4467A3, #BD8233, #AE3030, #2ABCCD, #954477, #99B314. Plus 3 semantic anchors outside the pool: #DDCC77 (amber, warning), theme-adaptive palette.neutral (totals/baseline), theme-adaptive palette.muted (other/rest). Never invent custom hexes for categorical data. When referring to the palette in code comments, metadata, or review notes, always call it Imprint (not "anyplot palette").
  • Continuous data: imprint_seq (single-polarity, ["#009E73", "#4467A3"]) or imprint_div (diverging, ["#AE3030", midpoint, "#4467A3"] where midpoint is #FAF8F1 on light / #1A1A17 on dark). No other cmaps — never viridis/cividis/BrBG/Reds/Blues/Greens or jet/hsv/rainbow.
  • Color restraint: 2-3 colors ideal, 4-5 max. For n≥5, add redundant encoding (marker shape, linestyle, label) — see "Series count guidance" in default-style-guide.md.
  • Theme-adaptive chrome (background, text, grid, spines, legend, annotations) — read ANYPLOT_THEME from env, use the token palette from prompts/default-style-guide.md. Plot background: #FAF8F1 light / #1A1A17 dark. Never pure white or black.
  • Grid: prefer none for simple plots; when used, y-axis only for bar/line, both for scatter; opacity 10-15%
  • Scatter marker edge should match the page background (PAGE_BG), not hardcoded white — keeps definition against either theme.
  • Remove decorations: single-series legends, tick marks (keep labels), unnecessary grid lines

Data storytelling (for DE-03 score):

  • Use visual emphasis (color, size) to guide the viewer's eye
  • Tell a story through good data choice and clear visual hierarchy
  • Annotation restraint (DEFAULT): Do NOT add text annotations, callout boxes, arrows, or labeled data points unless the specification explicitly asks for them (e.g., spec-id contains "annotated"). Good storytelling comes from visual design — color contrast, size variation, strategic data choice — not text overlays.
  • When annotations ARE appropriate: Only when spec-id contains "annotated" or the spec explicitly describes annotations as a required feature. Even then, use sparingly.
  • Respect the spec variant: If the spec-id contains basic, storytelling comes from well-chosen data and clean design — NOT from adding annotations, trendlines, or extra visual elements. A basic scatter plot should remain a basic scatter plot.

Output Files

Save with a theme-suffixed filename, driven by the ANYPLOT_THEME env var. The pipeline runs each implementation twice (ANYPLOT_THEME=lightplot-light.png; ANYPLOT_THEME=darkplot-dark.png). Interactive libraries additionally emit plot-{theme}.html.

THEME = os.getenv("ANYPLOT_THEME", "light")  # already defined at top

# matplotlib/seaborn/plotnine (static, PNG only) — figsize=(8, 4.5) dpi=400 → 3200×1800
plt.savefig(f'plot-{THEME}.png', dpi=400, bbox_inches='tight', facecolor=PAGE_BG)

# plotly (PNG + HTML) — width=800 height=450 scale=4 → 3200×1800
fig.write_image(f'plot-{THEME}.png', width=800, height=450, scale=4)
fig.write_html(f'plot-{THEME}.html', include_plotlyjs='cdn')

# bokeh (PNG + HTML)
export_png(p, filename=f'plot-{THEME}.png')
output_file(f'plot-{THEME}.html'); save(p)

# altair (PNG + HTML)
chart.save(f'plot-{THEME}.png')
chart.save(f'plot-{THEME}.html')

# highcharts / pygal / letsplot: follow the same plot-{THEME}.{png,html} naming

Never write a bare plot.png — that was the legacy single-theme output and is no longer accepted by the pipeline.

Testing

After generating the code:

  1. Run the script twiceANYPLOT_THEME=light and ANYPLOT_THEME=dark. Both must succeed without errors.
  2. Check plot-light.png AND plot-dark.png — visually verify both:
    • Does each show the expected visualization?
    • Are labels readable against their respective backgrounds (no dark text on dark, no light text on light)?
    • Is the first series #009E73 in both renders (data colors stay identical; only chrome flips)?
    • Does the background match #FAF8F1 (light) or #1A1A17 (dark)?
    • Are top/right spines removed?
    • Is the design polished beyond defaults?
  3. For interactive libraries, also check plot-light.html and plot-dark.html render correctly.

If there are issues, fix them and re-run both themes until both plots look correct.