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
29 changes: 29 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# EditorConfig — https://editorconfig.org
# Mirrors the enforced formatters: ruff (Python, 4-space, 120 cols) and the
# frontend's 2-space TS/TSX convention. Editors pick this up automatically.

root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space

[*.py]
indent_size = 4
max_line_length = 120

[*.{ts,tsx,js,jsx,mjs,css,html}]
indent_size = 2

[*.{yml,yaml,json}]
indent_size = 2

[*.md]
# Trailing whitespace is meaningful in Markdown (hard line breaks).
trim_trailing_whitespace = false

[Makefile]
indent_style = tab
5 changes: 4 additions & 1 deletion .github/ISSUE_TEMPLATE/report-plot-issue.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Report Plot Issue
description: Report a problem with an existing plot (specification or implementation)
labels: ["report-pending"]
# No auto-applied labels: `report-pending` triggers a write-privileged Claude
# agent (report-validate.yml), so a maintainer must review the issue and add
# the label manually. Auto-labeling let any unauthenticated issue author feed
# prompt text straight into that agent.
body:
- type: markdown
attributes:
Expand Down
7 changes: 5 additions & 2 deletions .github/ISSUE_TEMPLATE/request-new-plot.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
name: Request New Plot
description: Propose a new plot type for anyplot
labels: ["spec-request"]
# No auto-applied labels: `spec-request` triggers a write-privileged Claude
# agent (spec-create.yml), so a maintainer must review the issue and add the
# label manually. Auto-labeling let any unauthenticated issue author feed
# prompt text straight into that agent.
body:
- type: markdown
attributes:
Expand All @@ -12,7 +15,7 @@ body:
2. Assign a spec ID (e.g., `scatter-regression-linear`)
3. Create the specification for review

**After submission:** A maintainer will review and add the `approved` label to start code generation.
**After submission:** A maintainer will review your request and add the `spec-request` label to start AI spec creation, then the `approved` label to start code generation.

- type: textarea
id: description
Expand Down
10 changes: 5 additions & 5 deletions .github/actions/setup-julia/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ runs:
steps:
- name: Install Julia
# julia-actions/setup-julia ships every sub-action from one repo and
# tags them together; `@v2` is the maintained moving tag. Dependabot
# will pin to a SHA on first run.
uses: julia-actions/setup-julia@v2
# tags them together; pinned to the v2 tag's commit SHA like every
# workflow-level action in this repo (Dependabot keeps the pin fresh).
uses: julia-actions/setup-julia@4c0cb0fce8556fdb04a90347310e5db8b1f98fb9 # v2
with:
version: ${{ inputs.julia-version }}

Expand All @@ -34,9 +34,9 @@ runs:
libpng-dev

# Speed up subsequent runs — Julia precompilation is slow on cold start.
# `julia-actions/cache@v2` keys on Project.toml + Manifest.toml.
# `julia-actions/cache` keys on Project.toml + Manifest.toml.
- name: Cache Julia depot
uses: julia-actions/cache@v2
uses: julia-actions/cache@d10a6fd8f31b12404a54613ebad242900567f2b9 # v2

- name: Install Julia packages
# `Pkg.instantiate()` restores the exact versions pinned in
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/setup-node/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ runs:
using: "composite"
steps:
- name: Install Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ inputs.node-version }}
# Restore npm's download cache keyed on the committed lockfile so cold
Expand Down
7 changes: 4 additions & 3 deletions .github/actions/setup-r/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ runs:
using: "composite"
steps:
- name: Install R
# r-lib/actions ships every sub-action from a single repo and tags them together;
# `@v2` is the maintained moving tag. Dependabot will pin to a SHA on first run.
uses: r-lib/actions/setup-r@v2
# r-lib/actions ships every sub-action from a single repo and tags them
# together; pinned to the v2 tag's commit SHA like every workflow-level
# action in this repo (Dependabot keeps the pin fresh).
uses: r-lib/actions/setup-r@d3c5be51b12e724e68f33216ca3c148b66d5f0b6 # v2
with:
r-version: ${{ inputs.r-version }}
use-public-rspm: true
Expand Down
37 changes: 20 additions & 17 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ This file provides guidance to GitHub Copilot when working with code in this rep

## Project Overview

**anyplot** is an AI-powered platform for Python data visualization that automatically discovers, generates, tests, and maintains plotting examples. The platform is specification-driven: every plot starts as a library-agnostic Markdown spec, then AI generates implementations for all supported libraries.
**anyplot** is an AI-powered platform for data visualization that automatically discovers, generates, tests, and maintains plotting examples. The platform is specification-driven: every plot starts as a library-agnostic Markdown spec, then AI generates implementations for all supported libraries.

**Supported Libraries** (9 total):
- matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, lets-plot
**Supported Libraries** (15 total, per `LIBRARIES_METADATA` in `core/constants.py`):
- Python (8): matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, lets-plot
- R (1): ggplot2
- Julia (1): Makie.jl
- JavaScript (5): Chart.js, D3.js, Apache ECharts, Highcharts, MUI X Charts (React, `.tsx`) — rendered via the Node + Playwright browser harness

**Core Principle**: Community proposes plot ideas via GitHub Issues → AI generates code → AI quality review → Deployed.

Expand Down Expand Up @@ -87,19 +90,19 @@ Everything for one plot type lives in a single directory:
plots/{spec-id}/
├── specification.md # Library-agnostic description
├── specification.yaml # Tags, created, issue, suggested
├── metadata/
── python/ # Per-library metadata (one file per library)
├── matplotlib.yaml
├── seaborn.yaml
└── ...
└── implementations/
── python/ # Library implementations (one file per library)
├── matplotlib.py
├── seaborn.py
└── ...
├── metadata/ # Per-library metadata, grouped by language
── python/{matplotlib,seaborn,…}.yaml
│ ├── r/ggplot2.yaml
│ ├── julia/makie.yaml
│ └── javascript/{chartjs,d3,echarts,highcharts,muix}.yaml
└── implementations/ # Library implementations, grouped by language
── python/{matplotlib,seaborn,…}.py
├── r/ggplot2.R
├── julia/makie.jl
└── javascript/{chartjs,d3,echarts,highcharts}.js + muix.tsx
```

The `python/` subdirectory is a deliberate forward-compatibility layer; non-Python implementation languages would live as siblings (e.g. `implementations/r/`) when introduced. All paths in code, prompts, and metadata refer to the language-prefixed form: `plots/{spec-id}/implementations/python/{library}.py` and `plots/{spec-id}/metadata/python/{library}.yaml`.
Implementations and metadata are grouped by language subdirectory. All paths in code, prompts, and metadata refer to the language-prefixed form: `plots/{spec-id}/implementations/{language}/{library}{ext}` and `plots/{spec-id}/metadata/{language}/{library}.yaml`.

Example: `plots/scatter-basic/` contains everything for the basic scatter plot.

Expand Down Expand Up @@ -135,9 +138,9 @@ Examples: `scatter-basic`, `scatter-color-mapped`, `bar-grouped-horizontal`, `he
### Implementation Labels

- **`generate:{library}`** - Trigger single library generation (e.g., `generate:matplotlib`)
- **`generate:all`** - Trigger all 9 libraries via bulk-generate
- **`generate:all`** - Trigger all 15 libraries via bulk-generate
- **`impl:{library}:done`** - Implementation merged to main
- **`impl:{library}:failed`** - Max retries exhausted (3 attempts)
- **`impl:{library}:failed`** - Max retries exhausted (4 repair attempts)

### PR Labels (set by workflows)

Expand Down Expand Up @@ -217,7 +220,7 @@ ax.scatter(x, y, alpha=0.7, s=50, color='#306998')
ax.set_title('Basic Scatter Plot')

plt.tight_layout()
plt.savefig('plot.png', dpi=300, bbox_inches='tight')
plt.savefig(f'plot-{THEME}.png', dpi=300, bbox_inches='tight') # THEME = os.getenv('ANYPLOT_THEME', 'light')
```

**Header format:** 4-line docstring with anyplot.ai branding, spec-id, library version, quality score.
Expand Down
9 changes: 8 additions & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ updates:

# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
# "/" only scans .github/workflows — the composite actions' own `uses:`
# pins (now SHA-pinned like the workflows) need their directories listed
# explicitly to stay covered.
directories:
- "/"
- "/.github/actions/setup-node"
- "/.github/actions/setup-r"
- "/.github/actions/setup-julia"
schedule:
interval: "weekly"
day: "monday"
Expand Down
23 changes: 23 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!--
Automated pipeline PRs (spec-create, impl-*, auto-polish, daily-regen) and
Dependabot bumps ignore this template — they are summarized at release time.
-->

## Summary

<!-- 1-3 bullet points describing the changes -->

-

## Test plan

<!-- How were the changes verified? Check the applicable verify loops. -->

- [ ]

## Checklist

- [ ] `CHANGELOG.md` updated under `[Unreleased]` (Keep-a-Changelog categories,
bold-titled bullets, PR ref) — required for every non-pipeline PR
- [ ] Related documentation updated (e.g. `docs/reference/`, `docs/workflows/`,
`docs/contributing.md`) if behavior changed
42 changes: 33 additions & 9 deletions .github/workflows/impl-generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1021,20 +1021,44 @@ jobs:

# Count previous failures via hidden marker comments (more reliable than workflow runs).
# Paginate so the marker is found even on issues with >30 comments
# (which is common because all 9 library impls land on the same issue).
# (which is common because all 15 library impls land on the same issue).
#
# FAIL CLOSED: if the count call errors (rate limit, transient API
# failure), we must NOT assume zero prior failures — a zero fallback
# turns the 3-attempt cap into an infinite self-retry loop (each
# retry adds a failure comment, which makes the count call slower and
# more rate-limit-prone, which re-triggers the zero fallback; issue
# #1010 flooded ~1,200 Actions runs in 38h exactly this way).
# A failed count is therefore treated as cap-reached: no auto-retry.
# NB: --paginate applies --jq to EACH page separately, so the filter
# emits one per-page count per line; awk sums them into one number
# (the old code broke on issues with >100 comments for this reason).
# pipefail (Actions bash default) makes a gh failure fail the whole
# pipeline, so the fail-closed else-branch still triggers.
MARKER="<!-- impl-fail:${SPEC_ID}:${LIBRARY} -->"
FAILURE_COUNT=$(gh api --paginate "repos/${{ github.repository }}/issues/${ISSUE}/comments?per_page=100" \
--jq "[.[] | select(.body != null and (.body | contains(\"$MARKER\")))] | length" 2>/dev/null || echo "0")

echo "::notice::Previous failures for ${LIBRARY}/${SPEC_ID}: $FAILURE_COUNT"
if FAILURE_COUNT=$(gh api --paginate "repos/${{ github.repository }}/issues/${ISSUE}/comments?per_page=100" \
--jq "[.[] | select(.body != null and (.body | contains(\"$MARKER\")))] | length" \
| awk '{ sum += $1 } END { print sum + 0 }'); then
echo "::notice::Previous failures for ${LIBRARY}/${SPEC_ID}: $FAILURE_COUNT"
else
echo "::warning::Failure-count API call failed — failing closed (treating retry cap as reached, no auto-retry)"
FAILURE_COUNT=999
fi

# FAILURE_COUNT counts marker comments BEFORE this run.
# 0 → this is attempt 1 fail, 1 → attempt 2 fail, 2 → attempt 3 fail.
ATTEMPT=$((FAILURE_COUNT + 1))

# After 2 previous failures (= this is attempt 3) → mark as failed
# After 2 previous failures (= this is attempt 3) → mark as failed.
# Also entered when the failure count could not be determined
# (FAILURE_COUNT=999, fail-closed — see above).
if [ "$FAILURE_COUNT" -ge 2 ]; then
echo "::warning::Marking $LIBRARY as failed after 3 generation attempts"
if [ "$FAILURE_COUNT" -eq 999 ]; then
CAP_REASON="the failure counter could not be read (failed closed — auto-retry disabled)"
else
CAP_REASON="3 generation attempts"
fi
echo "::warning::Marking $LIBRARY as failed: $CAP_REASON"

# Create failed label if needed
gh label create "impl:${LIBRARY}:failed" --color "d73a4a" \
Expand All @@ -1047,9 +1071,9 @@ jobs:

# Post final failure comment with marker
gh issue comment "$ISSUE" --body "${MARKER}
## :x: ${LIBRARY} Failed (Attempt 3/3)
## :x: ${LIBRARY} Failed (retry cap reached)

The **${LIBRARY}** implementation for \`${SPEC_ID}\` failed after 3 attempts.
The **${LIBRARY}** implementation for \`${SPEC_ID}\` was marked failed after ${CAP_REASON}.

**Reason:** Claude Code failed to create the implementation file.

Expand Down
11 changes: 8 additions & 3 deletions .github/workflows/impl-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -352,9 +352,14 @@ jobs:
ISSUE: ${{ steps.issue.outputs.number }}
SPEC_ID: ${{ steps.extract.outputs.specification_id }}
run: |
# All supported libraries (mirrors core/constants.py SUPPORTED_LIBRARIES).
# ggplot2 (R) and makie (Julia) are non-Python entries; everything else is Python.
LIBRARIES="matplotlib seaborn plotly bokeh altair plotnine pygal highcharts letsplot ggplot2 makie"
# All supported libraries, read from core/constants.py so this list
# can never drift again (a stale hardcoded 11-library copy used to
# close issues while the 4 JS libraries were still pending).
LIBRARIES=$(python3 -c "from core.constants import SUPPORTED_LIBRARIES; print(' '.join(sorted(SUPPORTED_LIBRARIES)))")
if [ -z "$LIBRARIES" ]; then
echo "::error::Could not read SUPPORTED_LIBRARIES from core/constants.py"
exit 1
fi

# Get current labels on the issue
LABELS=$(gh issue view "$ISSUE" --json labels -q '.labels[].name' 2>/dev/null || echo "")
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/report-validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ run-name: "Validate Report: issue #${{ github.event.issue.number }}"

# Validates and structures user-submitted issue reports
# Flow:
# 1. User submits report → report-pending label auto-added
# 1. User submits report → maintainer reviews and adds report-pending label
# (label is NOT auto-applied: adding it requires triage rights, which is
# the gate that keeps unauthenticated issue text out of this Claude agent)
# 2. AI validates spec/impl exists, analyzes issue, posts structured comment
# 3. Labels updated: report-validated + category + report:spec/impl

Expand Down
Loading