From ede6e3330179b120186b09f29cc9993ab312c344 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 19:56:03 +0000 Subject: [PATCH 1/6] feat(ggplot2): add R/ggplot2 library + multi-language pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires anyplot for its first non-Python implementation language. Tier 2 / Phase 3 of the library-expansion roadmap — unlocks the R / academic audience and validates the multi-language pipeline. What's added - core/constants.py: SUPPORTED_LANGUAGES gains "r", LANGUAGES_METADATA gains the R entry (.R extension, runtime 4.4), SUPPORTED_LIBRARIES + LIBRARIES_METADATA gain ggplot2 with language_id=r - prompts/library/ggplot2.md: ggplot2-specific generation rules — Okabe-Ito mapping, theme-adaptive chrome tokens, ANYPLOT_THEME env handling, ragg PNG device, NOT_FEASIBLE policy for interactivity - .github/actions/setup-r: composite action installing R + system libs for ragg/systemfonts, restoring packages from renv.lock, and smoke-testing a ggplot2 render - renv.lock: pinned ggplot2 + tidyverse helpers + dataset packages (palmerpenguins, gapminder) against a Posit RSPM snapshot - Frontend CodeHighlighter: registers Prism r grammar, accepts a language prop, falls back to plain text for unknown languages Workflow changes (extension- and language-aware) - impl-generate.yml: ggplot2 added to library choices; LANGUAGE + EXT derived from LIBRARY; R setup step conditional on language; version detection via Rscript packageVersion for R libs; all .py paths replaced by ${LIBRARY}${EXT} - impl-repair.yml: same derive_lang step; conditional R setup; conditional Python plotting deps; extension-aware paths and prompt vars - impl-review.yml: derives language + ext from library; staging GCS path, metadata path, and impl-file path all use the derived values - impl-merge.yml: branch parser derives LANGUAGE + EXT from LIBRARY; validates correct impl file path - bulk-generate.yml: ggplot2 added to choices and ALL_LIBRARIES - prompts/workflow-prompts/impl-generate-claude.md + impl-repair-claude.md: target paths and run commands made extension-aware (Python uses python, R uses Rscript) - prompts/plot-generator.md: role/wording generalised; R-specific datasets, reproducibility seed, docstring style, forbidden-patterns list added Tests - tests/unit/core/test_constants.py: covers ggplot2 entry, R language metadata, language_id integrity across libraries - tests/unit/api/test_debug.py + test_routers.py: library_stats length assertions switched to len(SUPPORTED_LIBRARIES) so adding a new library doesn't require a test edit - app/src/components/CodeHighlighter.test.tsx: covers r language + plain-text fallback After this lands, gh workflow run impl-generate.yml -f specification_id= -f library=ggplot2 produces R code and renders plot-light.png / plot-dark.png through the existing review/merge pipeline. --- .github/actions/setup-r/action.yml | 61 ++++ .github/workflows/bulk-generate.yml | 5 +- .github/workflows/impl-generate.yml | 118 +++++--- .github/workflows/impl-merge.yml | 21 +- .github/workflows/impl-repair.yml | 43 ++- .github/workflows/impl-review.yml | 30 +- app/src/components/CodeHighlighter.test.tsx | 22 +- app/src/components/CodeHighlighter.tsx | 15 +- app/src/components/SpecTabs.tsx | 4 +- app/src/pages/SpecPage.tsx | 1 + core/constants.py | 22 +- prompts/library/ggplot2.md | 269 ++++++++++++++++++ prompts/plot-generator.md | 42 ++- .../workflow-prompts/impl-generate-claude.md | 53 +++- .../workflow-prompts/impl-repair-claude.md | 19 +- renv.lock | 170 +++++++++++ tests/unit/api/test_debug.py | 3 +- tests/unit/api/test_routers.py | 3 +- tests/unit/core/test_constants.py | 65 ++++- 19 files changed, 874 insertions(+), 92 deletions(-) create mode 100644 .github/actions/setup-r/action.yml create mode 100644 prompts/library/ggplot2.md create mode 100644 renv.lock diff --git a/.github/actions/setup-r/action.yml b/.github/actions/setup-r/action.yml new file mode 100644 index 0000000000..51d51a0dd1 --- /dev/null +++ b/.github/actions/setup-r/action.yml @@ -0,0 +1,61 @@ +name: "Setup R + ggplot2" +description: >- + Installs R, the system libraries needed for headless plot rendering, and the + package set used by anyplot's R implementations (ggplot2 + tidyverse helpers + + ragg device + dataset packages). Packages are restored from renv.lock at + the repository root so versions stay pinned. +inputs: + r-version: + description: "R version to install" + required: false + default: "4.4.1" + working-directory: + description: "Directory containing renv.lock (defaults to the repo root)" + required: false + default: "." + +runs: + using: "composite" + steps: + - name: Install R + uses: r-lib/actions/setup-r@e2e21ddc06d49ebc3007bf9d52aa66f8d4b1d8f3 # v2 + with: + r-version: ${{ inputs.r-version }} + use-public-rspm: true + + # ragg needs libfreetype/libpng/libtiff/libjpeg headers; systemfonts needs + # libfontconfig1. These are pre-built on ubuntu-latest as binaries via + # Posit Package Manager, but keeping the apt fallback makes the action + # robust against future runner-image changes. + - name: Install system dependencies for ragg / systemfonts + shell: bash + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libcurl4-openssl-dev \ + libssl-dev \ + libxml2-dev \ + libfontconfig1-dev \ + libfreetype-dev \ + libpng-dev \ + libtiff5-dev \ + libjpeg-dev \ + libharfbuzz-dev \ + libfribidi-dev + + - name: Install R packages from renv.lock + uses: r-lib/actions/setup-renv@e2e21ddc06d49ebc3007bf9d52aa66f8d4b1d8f3 # v2 + with: + working-directory: ${{ inputs.working-directory }} + + - name: Smoke-test ggplot2 renders a PNG + shell: bash + run: | + Rscript -e ' + library(ggplot2); library(ragg) + tmp <- tempfile(fileext = ".png") + ggsave(tmp, ggplot(mtcars, aes(wt, mpg)) + geom_point(), + device = ragg::agg_png, width = 4, height = 3, dpi = 100) + stopifnot(file.exists(tmp), file.info(tmp)$size > 0) + cat("ok: ", tmp, " (", file.info(tmp)$size, " bytes)\n", sep = "") + ' diff --git a/.github/workflows/bulk-generate.yml b/.github/workflows/bulk-generate.yml index 8283efae26..6a16bab2a2 100644 --- a/.github/workflows/bulk-generate.yml +++ b/.github/workflows/bulk-generate.yml @@ -28,6 +28,7 @@ on: - pygal - highcharts - letsplot + - ggplot2 dry_run: description: "List what would be generated without executing" type: boolean @@ -52,9 +53,9 @@ on: default: '{}' env: - ALL_LIBRARIES: "matplotlib seaborn plotly bokeh altair plotnine pygal highcharts letsplot" + ALL_LIBRARIES: "matplotlib seaborn plotly bokeh altair plotnine pygal highcharts letsplot ggplot2" -# Serialise bulk-generate runs. Each run paces its own 9 dispatches with +# Serialise bulk-generate runs. Each run paces its own dispatches with # `pace_seconds`; letting two runs overlap would interleave their # dispatches and stack the Claude queue, which is exactly what the pacing # exists to avoid. Second run waits for first to finish instead. diff --git a/.github/workflows/impl-generate.yml b/.github/workflows/impl-generate.yml index 2ffc23446a..677fe6d888 100644 --- a/.github/workflows/impl-generate.yml +++ b/.github/workflows/impl-generate.yml @@ -29,6 +29,7 @@ on: - pygal - highcharts - letsplot + - ggplot2 issue_number: description: "Issue number (optional, for tracking)" required: false @@ -111,16 +112,27 @@ jobs: MODEL="sonnet" fi - # Language: only python supported today. Future multi-language work will derive this per spec. - LANGUAGE="python" + # Derive LANGUAGE + EXT from LIBRARY. Mirrors core/constants.py LIBRARIES_METADATA; + # new non-Python entries get listed here too. + case "$LIBRARY" in + ggplot2) + LANGUAGE="r" + EXT=".R" + ;; + *) + LANGUAGE="python" + EXT=".py" + ;; + esac echo "specification_id=$SPEC_ID" >> $GITHUB_OUTPUT echo "library=$LIBRARY" >> $GITHUB_OUTPUT echo "language=$LANGUAGE" >> $GITHUB_OUTPUT + echo "ext=$EXT" >> $GITHUB_OUTPUT echo "issue_number=$ISSUE" >> $GITHUB_OUTPUT echo "model=$MODEL" >> $GITHUB_OUTPUT - echo "::notice::Generating $LANGUAGE/$LIBRARY for $SPEC_ID (issue: ${ISSUE:-none}, model: ${MODEL})" + echo "::notice::Generating $LANGUAGE/$LIBRARY for $SPEC_ID (issue: ${ISSUE:-none}, model: ${MODEL}, ext: ${EXT})" - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 @@ -189,7 +201,8 @@ jobs: gh issue edit "$ISSUE" --add-label "impl:${LIBRARY}:pending" 2>/dev/null || true # ======================================================================== - # Setup: Python and dependencies + # Setup: Python and dependencies (always installed — needed for image + # processing + metadata writing even for non-Python implementations). # ======================================================================== - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 @@ -204,7 +217,8 @@ jobs: sudo apt-get update sudo apt-get install -y pngquant - - name: Install dependencies + - name: Install Python plotting dependencies + if: steps.inputs.outputs.language == 'python' env: LIBRARY: ${{ steps.inputs.outputs.library }} run: | @@ -213,12 +227,25 @@ jobs: # Source of truth: pyproject.toml lib-${LIBRARY} extras uv pip install -e ".[lib-${LIBRARY}]" ruff pillow pyyaml + - name: Install Python helper deps (for non-Python implementations) + if: steps.inputs.outputs.language != 'python' + run: | + # ggplot2 / future non-Python libs render via their own runtime, + # but the pipeline still uses Python for metadata + image post-processing. + uv venv .venv + source .venv/bin/activate + uv pip install pillow pyyaml + - name: Setup Chrome for Highcharts if: steps.inputs.outputs.library == 'highcharts' uses: browser-actions/setup-chrome@4f8e94349a351df0f048634f25fec36c3c91eded # v2 with: chrome-version: stable + - name: Setup R + ggplot2 + if: steps.inputs.outputs.language == 'r' + uses: ./.github/actions/setup-r + # ======================================================================== # Generate: Create implementation branch and code # ======================================================================== @@ -244,9 +271,10 @@ jobs: SPEC_ID: ${{ steps.inputs.outputs.specification_id }} LANGUAGE: ${{ steps.inputs.outputs.language }} LIBRARY: ${{ steps.inputs.outputs.library }} + EXT: ${{ steps.inputs.outputs.ext }} run: | METADATA_FILE="plots/${SPEC_ID}/metadata/${LANGUAGE}/${LIBRARY}.yaml" - IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}.py" + IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}${EXT}" if [ -f "$METADATA_FILE" ] && [ -f "$IMPL_FILE" ]; then echo "is_regeneration=true" >> $GITHUB_OUTPUT @@ -349,6 +377,7 @@ jobs: Variables for this run: - LANGUAGE: ${{ steps.inputs.outputs.language }} - LIBRARY: ${{ steps.inputs.outputs.library }} + - EXT: ${{ steps.inputs.outputs.ext }} - SPEC_ID: ${{ steps.inputs.outputs.specification_id }} - IS_REGENERATION: ${{ steps.existing.outputs.is_regeneration }} @@ -368,6 +397,7 @@ jobs: Variables for this run: - LANGUAGE: ${{ steps.inputs.outputs.language }} - LIBRARY: ${{ steps.inputs.outputs.library }} + - EXT: ${{ steps.inputs.outputs.ext }} - SPEC_ID: ${{ steps.inputs.outputs.specification_id }} - IS_REGENERATION: ${{ steps.existing.outputs.is_regeneration }} @@ -380,6 +410,7 @@ jobs: SPEC_ID: ${{ steps.inputs.outputs.specification_id }} LANGUAGE: ${{ steps.inputs.outputs.language }} LIBRARY: ${{ steps.inputs.outputs.library }} + EXT: ${{ steps.inputs.outputs.ext }} ISSUE: ${{ steps.issue.outputs.number }} BRANCH: ${{ steps.branch.outputs.branch }} MODEL: ${{ steps.inputs.outputs.model }} @@ -424,39 +455,59 @@ jobs: mkdir -p "$METADATA_DIR" - # Get Python version (e.g., "3.13.1" from "Python 3.13.1") - PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}') + # Runtime version: Python for python libs, R for R libs. We still record + # a 'python_version' field in metadata for backwards compatibility; for R + # implementations it carries the R version instead (the DB / frontend treat + # it as a free-form runtime version string). + if [ "$LANGUAGE" = "r" ]; then + PYTHON_VERSION=$(Rscript -e 'cat(as.character(getRversion()))' 2>/dev/null || echo "unknown") + else + PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}') + fi - # Get library version from pip (use explicit venv path) + # Get library version. Python libs read via `pip show`; R libs via + # packageVersion() inside Rscript. Names that differ between catalogue + # id and registry id are mapped explicitly. get_pip_version() { .venv/bin/pip show "$1" 2>/dev/null | grep -i "^Version:" | awk '{print $2}' } + get_r_version() { + Rscript -e "cat(as.character(packageVersion('$1')))" 2>/dev/null + } - case "$LIBRARY" in - letsplot) - LIBRARY_VERSION=$(get_pip_version "lets-plot") - ;; - highcharts) - LIBRARY_VERSION=$(get_pip_version "highcharts-core") - ;; - *) - LIBRARY_VERSION=$(get_pip_version "$LIBRARY") - ;; - esac - - # Fallback if version is empty - if [ -z "$LIBRARY_VERSION" ]; then - echo "::warning::Could not get version for $LIBRARY, trying alternative method" - # Map library names to Python module names + if [ "$LANGUAGE" = "r" ]; then + LIBRARY_VERSION=$(get_r_version "$LIBRARY") + else case "$LIBRARY" in - letsplot) PYTHON_MODULE="lets_plot" ;; - plotnine) PYTHON_MODULE="plotnine" ;; - highcharts) PYTHON_MODULE="highcharts_core" ;; - *) PYTHON_MODULE="$LIBRARY" ;; + letsplot) + LIBRARY_VERSION=$(get_pip_version "lets-plot") + ;; + highcharts) + LIBRARY_VERSION=$(get_pip_version "highcharts-core") + ;; + *) + LIBRARY_VERSION=$(get_pip_version "$LIBRARY") + ;; esac - LIBRARY_VERSION=$(.venv/bin/python -c "import $PYTHON_MODULE; print(getattr($PYTHON_MODULE, '__version__', 'unknown'))" 2>/dev/null || echo "unknown") + + # Fallback if version is empty + if [ -z "$LIBRARY_VERSION" ]; then + echo "::warning::Could not get version for $LIBRARY, trying alternative method" + # Map library names to Python module names + case "$LIBRARY" in + letsplot) PYTHON_MODULE="lets_plot" ;; + plotnine) PYTHON_MODULE="plotnine" ;; + highcharts) PYTHON_MODULE="highcharts_core" ;; + *) PYTHON_MODULE="$LIBRARY" ;; + esac + LIBRARY_VERSION=$(.venv/bin/python -c "import $PYTHON_MODULE; print(getattr($PYTHON_MODULE, '__version__', 'unknown'))" 2>/dev/null || echo "unknown") + fi + fi + + if [ -z "$LIBRARY_VERSION" ]; then + LIBRARY_VERSION="unknown" fi - echo "::notice::Library version: $LIBRARY = $LIBRARY_VERSION" + echo "::notice::Library version: $LIBRARY = $LIBRARY_VERSION ($LANGUAGE runtime $PYTHON_VERSION)" # Interactive libraries additionally produce HTML previews (one per theme) HAS_HTML="false" @@ -527,7 +578,7 @@ jobs: git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}.py" + IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}${EXT}" # Verify implementation file exists in the repository (Claude should have committed it) if ! git ls-files --error-unmatch "$IMPL_FILE" >/dev/null 2>&1; then @@ -625,6 +676,7 @@ jobs: SPEC_ID: ${{ steps.inputs.outputs.specification_id }} LANGUAGE: ${{ steps.inputs.outputs.language }} LIBRARY: ${{ steps.inputs.outputs.library }} + EXT: ${{ steps.inputs.outputs.ext }} ISSUE: ${{ steps.issue.outputs.number }} BRANCH: ${{ steps.branch.outputs.branch }} run: | @@ -643,7 +695,7 @@ jobs: Implements the **${LANGUAGE}/${LIBRARY}** version of \`${SPEC_ID}\`. - **File:** \`plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}.py\`" + **File:** \`plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}${EXT}\`" if [ -n "$ISSUE" ]; then BODY="${BODY} diff --git a/.github/workflows/impl-merge.yml b/.github/workflows/impl-merge.yml index 1007ae099d..f20249b156 100644 --- a/.github/workflows/impl-merge.yml +++ b/.github/workflows/impl-merge.yml @@ -100,13 +100,25 @@ jobs: SPEC_ID=$(echo "$BRANCH" | cut -d'/' -f2) LIBRARY=$(echo "$BRANCH" | cut -d'/' -f3) - # Language: only python supported today. If future branches encode language, - # parse it from the branch name here. - LANGUAGE="python" + # Derive LANGUAGE + EXT from LIBRARY. Mirrors core/constants.py + # LIBRARIES_METADATA — extend this case when new non-Python libraries + # are added. We don't encode language in the branch name to keep + # existing branches and tooling working. + case "$LIBRARY" in + ggplot2) + LANGUAGE="r" + EXT=".R" + ;; + *) + LANGUAGE="python" + EXT=".py" + ;; + esac echo "specification_id=$SPEC_ID" >> $GITHUB_OUTPUT echo "library=$LIBRARY" >> $GITHUB_OUTPUT echo "language=$LANGUAGE" >> $GITHUB_OUTPUT + echo "ext=$EXT" >> $GITHUB_OUTPUT - name: Validate PR completeness before merge if: steps.check.outputs.should_run == 'true' @@ -115,13 +127,14 @@ jobs: SPEC_ID: ${{ steps.extract.outputs.specification_id }} LANGUAGE: ${{ steps.extract.outputs.language }} LIBRARY: ${{ steps.extract.outputs.library }} + EXT: ${{ steps.extract.outputs.ext }} BRANCH: ${{ steps.check.outputs.branch }} PR_NUM: ${{ steps.check.outputs.pr_number }} run: | # Fetch the PR branch to check its contents git fetch origin "$BRANCH" - IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}.py" + IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}${EXT}" META_FILE="plots/${SPEC_ID}/metadata/${LANGUAGE}/${LIBRARY}.yaml" # Check if implementation file exists on the PR branch diff --git a/.github/workflows/impl-repair.yml b/.github/workflows/impl-repair.yml index 822ff6ddd2..a3fe660eb2 100644 --- a/.github/workflows/impl-repair.yml +++ b/.github/workflows/impl-repair.yml @@ -65,6 +65,25 @@ jobs: git fetch origin "$BRANCH" git checkout "$BRANCH" + - name: Derive language + extension from library + id: lang + env: + LIBRARY: ${{ inputs.library }} + run: | + case "$LIBRARY" in + ggplot2) + LANGUAGE="r" + EXT=".R" + ;; + *) + LANGUAGE="python" + EXT=".py" + ;; + esac + echo "language=$LANGUAGE" >> $GITHUB_OUTPUT + echo "ext=$EXT" >> $GITHUB_OUTPUT + echo "::notice::Repair: $LANGUAGE/$LIBRARY (ext: $EXT)" + - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: @@ -73,7 +92,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - - name: Install dependencies + - name: Install Python plotting dependencies + if: steps.lang.outputs.language == 'python' env: LIBRARY: ${{ inputs.library }} run: | @@ -82,12 +102,23 @@ jobs: # Source of truth: pyproject.toml lib-${LIBRARY} extras uv pip install -e ".[lib-${LIBRARY}]" ruff pillow pyyaml + - name: Install Python helper deps (for non-Python implementations) + if: steps.lang.outputs.language != 'python' + run: | + uv venv .venv + source .venv/bin/activate + uv pip install pillow pyyaml + - name: Setup Chrome for Highcharts if: inputs.library == 'highcharts' uses: browser-actions/setup-chrome@4f8e94349a351df0f048634f25fec36c3c91eded # v2 with: chrome-version: stable + - name: Setup R + ggplot2 + if: steps.lang.outputs.language == 'r' + uses: ./.github/actions/setup-r + - name: Extract AI feedback from PR id: feedback env: @@ -122,8 +153,9 @@ jobs: Read `prompts/workflow-prompts/impl-repair-claude.md` and follow those instructions. Variables for this run: - - LANGUAGE: python + - LANGUAGE: ${{ steps.lang.outputs.language }} - LIBRARY: ${{ inputs.library }} + - EXT: ${{ steps.lang.outputs.ext }} - SPEC_ID: ${{ inputs.specification_id }} - ATTEMPT: ${{ inputs.attempt }} - BRANCH: ${{ env.branch }} @@ -141,8 +173,9 @@ jobs: Read `prompts/workflow-prompts/impl-repair-claude.md` and follow those instructions. Variables for this run: - - LANGUAGE: python + - LANGUAGE: ${{ steps.lang.outputs.language }} - LIBRARY: ${{ inputs.library }} + - EXT: ${{ steps.lang.outputs.ext }} - SPEC_ID: ${{ inputs.specification_id }} - ATTEMPT: ${{ inputs.attempt }} - BRANCH: ${{ env.branch }} @@ -152,7 +185,7 @@ jobs: env: SPEC_ID: ${{ inputs.specification_id }} LIBRARY: ${{ inputs.library }} - LANGUAGE: python + LANGUAGE: ${{ steps.lang.outputs.language }} run: | IMPL_DIR="plots/${SPEC_ID}/implementations/${LANGUAGE}" @@ -193,7 +226,7 @@ jobs: env: SPEC_ID: ${{ inputs.specification_id }} LIBRARY: ${{ inputs.library }} - LANGUAGE: python + LANGUAGE: ${{ steps.lang.outputs.language }} run: | IMPL_DIR="plots/${SPEC_ID}/implementations/${LANGUAGE}" STAGING_PATH="gs://anyplot-images/staging/${SPEC_ID}/${LANGUAGE}/${LIBRARY}" diff --git a/.github/workflows/impl-review.yml b/.github/workflows/impl-review.yml index 551db52a7d..2afc4e10ce 100644 --- a/.github/workflows/impl-review.yml +++ b/.github/workflows/impl-review.yml @@ -95,6 +95,24 @@ jobs: echo "::notice::Reviewing PR #$PR_NUMBER for $LIBRARY implementation of $SPEC_ID (branch: $HEAD_REF, model: $MODEL)" + - name: Derive language + extension from library + id: lang + env: + LIBRARY: ${{ steps.pr.outputs.library }} + run: | + case "$LIBRARY" in + ggplot2) + LANGUAGE="r" + EXT=".R" + ;; + *) + LANGUAGE="python" + EXT=".py" + ;; + esac + echo "language=$LANGUAGE" >> $GITHUB_OUTPUT + echo "ext=$EXT" >> $GITHUB_OUTPUT + - name: Checkout PR code run: | git fetch origin ${{ steps.pr.outputs.head_sha }} @@ -142,7 +160,7 @@ jobs: env: SPEC_ID: ${{ steps.pr.outputs.specification_id }} LIBRARY: ${{ steps.pr.outputs.library }} - LANGUAGE: python + LANGUAGE: ${{ steps.lang.outputs.language }} run: | mkdir -p plot_images gsutil -m cp "gs://anyplot-images/staging/${SPEC_ID}/${LANGUAGE}/${LIBRARY}/*" plot_images/ 2>/dev/null || true @@ -350,12 +368,13 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SPEC_ID: ${{ steps.pr.outputs.specification_id }} LIBRARY: ${{ steps.pr.outputs.library }} - LANGUAGE: python + LANGUAGE: ${{ steps.lang.outputs.language }} + EXT: ${{ steps.lang.outputs.ext }} SCORE: ${{ steps.score.outputs.score }} BRANCH: ${{ steps.pr.outputs.branch }} run: | METADATA_FILE="plots/${SPEC_ID}/metadata/${LANGUAGE}/${LIBRARY}.yaml" - IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}.py" + IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}${EXT}" # Configure git auth and checkout the PR branch git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" @@ -600,7 +619,8 @@ jobs: PR_NUM: ${{ steps.pr.outputs.pr_number }} SPEC_ID: ${{ steps.pr.outputs.specification_id }} LIBRARY: ${{ steps.pr.outputs.library }} - LANGUAGE: python + LANGUAGE: ${{ steps.lang.outputs.language }} + EXT: ${{ steps.lang.outputs.ext }} SCORE: ${{ steps.score.outputs.score }} ATTEMPT: ${{ steps.attempts.outputs.display }} ATTEMPT_COUNT: ${{ steps.attempts.outputs.count }} @@ -664,7 +684,7 @@ jobs: gh pr close "$PR_NUM" # Remove old implementation from main if it exists - IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}.py" + IMPL_FILE="plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}${EXT}" META_FILE="plots/${SPEC_ID}/metadata/${LANGUAGE}/${LIBRARY}.yaml" git fetch origin main diff --git a/app/src/components/CodeHighlighter.test.tsx b/app/src/components/CodeHighlighter.test.tsx index 39f27bae95..f836ddb1b3 100644 --- a/app/src/components/CodeHighlighter.test.tsx +++ b/app/src/components/CodeHighlighter.test.tsx @@ -28,6 +28,10 @@ vi.mock('react-syntax-highlighter/dist/esm/languages/prism/python', () => ({ default: {}, })); +vi.mock('react-syntax-highlighter/dist/esm/languages/prism/r', () => ({ + default: {}, +})); + import CodeHighlighter from './CodeHighlighter'; describe('CodeHighlighter', () => { @@ -44,11 +48,27 @@ describe('CodeHighlighter', () => { expect(highlighter).toHaveTextContent('plt.show()'); }); - it('sets language to python', () => { + it('defaults to python when no language prop given', () => { render(); expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute( 'data-language', 'python' ); }); + + it('uses r grammar when language is "r"', () => { + render(); + expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute( + 'data-language', + 'r' + ); + }); + + it('falls back to plain text for unknown languages', () => { + render(); + expect(screen.getByTestId('syntax-highlighter')).toHaveAttribute( + 'data-language', + 'text' + ); + }); }); diff --git a/app/src/components/CodeHighlighter.tsx b/app/src/components/CodeHighlighter.tsx index 4f191e4dad..1564d4f2b3 100644 --- a/app/src/components/CodeHighlighter.tsx +++ b/app/src/components/CodeHighlighter.tsx @@ -1,8 +1,17 @@ import SyntaxHighlighter from 'react-syntax-highlighter/dist/esm/prism-light'; import python from 'react-syntax-highlighter/dist/esm/languages/prism/python'; +import r from 'react-syntax-highlighter/dist/esm/languages/prism/r'; import { typography } from '../theme'; SyntaxHighlighter.registerLanguage('python', python); +SyntaxHighlighter.registerLanguage('r', r); + +// Map anyplot language IDs → Prism grammar names. Anything we don't know about +// falls back to plain text so the block still renders, just unhighlighted. +const PRISM_LANGUAGE: Record = { + python: 'python', + r: 'r', +}; // Theme-aware Okabe-Ito syntax theme. All colors come from CSS variables in // tokens.css so the block adapts to light (paper) and dark modes. Comments @@ -45,12 +54,14 @@ const okabeItoTheme: Record = { interface CodeHighlighterProps { code: string; + language?: string; } -export default function CodeHighlighter({ code }: CodeHighlighterProps) { +export default function CodeHighlighter({ code, language = 'python' }: CodeHighlighterProps) { + const prismLanguage = PRISM_LANGUAGE[language.toLowerCase()] ?? 'text'; return ( ) => void; // Overview mode - only show Spec tab overviewMode?: boolean; @@ -179,6 +180,7 @@ export function SpecTabs({ criteriaChecklist, generatedAt, libraryId, + language, onTrackEvent, overviewMode = false, highlightedTags = [], @@ -263,7 +265,7 @@ export function SpecTabs({ {code} }> - + ) : null; diff --git a/app/src/pages/SpecPage.tsx b/app/src/pages/SpecPage.tsx index 07d4b2afdf..f98a25d1c4 100644 --- a/app/src/pages/SpecPage.tsx +++ b/app/src/pages/SpecPage.tsx @@ -532,6 +532,7 @@ export function SpecPage() { criteriaChecklist={currentImpl?.review_criteria_checklist} generatedAt={currentImpl?.generated_at} libraryId={selectedLibrary || ''} + language={currentImpl?.language || urlLanguage || 'python'} onTrackEvent={trackEvent} highlightedTags={highlightedTags} /> diff --git a/core/constants.py b/core/constants.py index 446a36c8b3..d0469a8068 100644 --- a/core/constants.py +++ b/core/constants.py @@ -14,11 +14,11 @@ # Canonical set of all supported plotting libraries (IDs) SUPPORTED_LIBRARIES = frozenset( - ["altair", "bokeh", "highcharts", "letsplot", "matplotlib", "plotly", "plotnine", "pygal", "seaborn"] + ["altair", "bokeh", "ggplot2", "highcharts", "letsplot", "matplotlib", "plotly", "plotnine", "pygal", "seaborn"] ) # Supported programming languages -SUPPORTED_LANGUAGES = frozenset(["python"]) +SUPPORTED_LANGUAGES = frozenset(["python", "r"]) # Language metadata for database seeding (analog to LIBRARIES_METADATA) LANGUAGES_METADATA = [ @@ -29,7 +29,15 @@ "runtime_version": "3.13", "documentation_url": "https://www.python.org", "description": "The default language for anyplot plot implementations.", - } + }, + { + "id": "r", + "name": "R", + "file_extension": ".R", + "runtime_version": "4.4", + "documentation_url": "https://www.r-project.org", + "description": "R is a statistical computing environment widely used in academia, biotech, and finance research.", + }, ] # Map from language id → file extension used by sync_to_postgres for discovery @@ -53,6 +61,14 @@ "documentation_url": "https://bokeh.org", "description": "Interactive visualization library that makes it simple to create common plots, while also handling custom or specialized use-cases. Work in Python close to all the PyData tools you're already familiar with.", }, + { + "id": "ggplot2", + "name": "ggplot2", + "language_id": "r", + "version": "3.5.1", + "documentation_url": "https://ggplot2.tidyverse.org", + "description": "The de facto standard for data visualization in R. ggplot2 is an implementation of the grammar of graphics: declarative, layered charts that compose with a small set of primitives (geoms, aesthetics, scales, facets, themes).", + }, { "id": "highcharts", "name": "Highcharts", diff --git a/prompts/library/ggplot2.md b/prompts/library/ggplot2.md new file mode 100644 index 0000000000..b31f5af664 --- /dev/null +++ b/prompts/library/ggplot2.md @@ -0,0 +1,269 @@ +# ggplot2 + +ggplot2 is the canonical implementation of the grammar of graphics in R. This +is anyplot's first R-language entry; the file extension is **`.R`** and the +runtime is **Rscript**, not Python. + +## IMPORTANT: No Workarounds + +**If ggplot2 cannot implement a plot type natively, DO NOT shell out to +matplotlib, Python, or another backend.** + +ggplot2 is a grammar-of-graphics library. It does NOT support out of the box: + +- 3D plots (no wireframes, surfaces, 3D scatter) +- Network / graph visualizations (needs `ggraph` — out of scope here) +- Geographic maps (needs `sf` / `ggmap` — out of scope here) +- True interactivity (hover, zoom, brush, animation) + +If the specification requires features outside ggplot2's grammar, the +implementation should FAIL rather than fall back to another library. Each +library implementation must use only that library's native capabilities. + +## Interactive Spec Handling + +ggplot2 produces **static PNG only**. When implementing specs that mention +interactive features: + +- Specs whose PRIMARY value is interactivity (hover, zoom, click, brush) → + **NOT_FEASIBLE** +- Specs with animation → use small multiples / `facet_wrap` as a static + alternative +- Mixed specs (static + interactive) → implement static features only, omit + interactive silently +- **NEVER** simulate tooltips, hover states, or controls. See AR-08 in + `prompts/quality-criteria.md`. + +--- + +## Imports + +```r +library(ggplot2) +library(dplyr) +library(tidyr) +library(scales) +library(ragg) # high-quality PNG device +``` + +Optional dataset packages available in the CI runtime: `palmerpenguins`, +`gapminder`. Plus everything bundled with base R / ggplot2: `mtcars`, `iris`, +`diamonds`, `economics`, `mpg`, `faithful`. + +## Reproducibility + +```r +set.seed(42) +``` + +## Create Plot + +```r +p <- ggplot(df, aes(x = col_x, y = col_y)) + + geom_point() + + labs(x = x_label, y = y_label, title = plot_title) + + theme_minimal(base_size = 14) +``` + +## Figure Size & Sizing for 4800×2700 px + +ggplot2 inherits sizes via `theme(... base_size = ...)`. Override individual +elements for the anyplot canvas: + +```r +p <- p + + theme( + text = element_text(size = 14), # base text + axis.title = element_text(size = 20), # axis labels + axis.text = element_text(size = 16), # tick labels + plot.title = element_text(size = 24), + legend.text = element_text(size = 16), + legend.title = element_text(size = 18) + ) + +# Element sizes inside geoms (~3-4× ggplot2 defaults so they survive the +# 4800×2700 canvas): +# geom_point(size = 4) +# geom_line(linewidth = 1.5) +``` + +## Save (PNG, both themes) + +Use the `ragg` device — it antialiases properly and ignores `Cairo` system +deps: + +```r +ggsave( + filename = sprintf("plot-%s.png", THEME), + plot = p, + device = ragg::agg_png, + width = 16, + height = 9, + units = "in", + dpi = 300 +) +``` + +## Colors + +Use the Okabe-Ito palette (see `prompts/default-style-guide.md` "Categorical +Palette"). First series is **always** `#009E73`. + +```r +OKABE_ITO <- c( + "#009E73", # 1 — first categorical series + "#D55E00", # 2 + "#0072B2", # 3 + "#CC79A7", # 4 + "#E69F00", # 5 + "#56B4E9", # 6 + "#F0E442" # 7 +) + +# Single-series +geom_point(color = OKABE_ITO[1]) + +# Multi-series — categorical +scale_color_manual(values = OKABE_ITO) +scale_fill_manual(values = OKABE_ITO) + +# Continuous — NOT Okabe-Ito: +scale_color_viridis_c(option = "viridis") # sequential +scale_color_viridis_c(option = "cividis") # sequential CVD-safe +scale_fill_distiller(palette = "BrBG") # diverging +``` + +Never use `rainbow()`, `heat.colors()`, `terrain.colors()`, `topo.colors()` or +`hsv()` based palettes — they all fail CVD and luminance ordering. + +## Theme-adaptive Chrome (ggplot2 mapping) + +Read `ANYPLOT_THEME` from the environment and flip chrome tokens. Data colors +stay identical between themes — only the surrounding chrome changes. + +```r +THEME <- Sys.getenv("ANYPLOT_THEME", "light") +PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17" +ELEVATED_BG <- if (THEME == "light") "#FFFDF6" else "#242420" +INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8" +INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0" + +anyplot_theme <- theme_minimal(base_size = 14) + + theme( + plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG), + panel.background = element_rect(fill = PAGE_BG, color = NA), + panel.grid.major = element_line(color = INK, linewidth = 0.3), + panel.grid.minor = element_line(color = INK, linewidth = 0.2), + panel.border = element_rect(color = INK_SOFT, fill = NA), + axis.title = element_text(color = INK), + axis.text = element_text(color = INK_SOFT), + axis.line = element_line(color = INK_SOFT), + plot.title = element_text(color = INK), + plot.subtitle = element_text(color = INK_SOFT), + legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT), + legend.text = element_text(color = INK_SOFT), + legend.title = element_text(color = INK) + ) + +# `panel.grid.*` `linewidth` is the line width in mm. ggplot2 does not expose +# `alpha` for grid lines, so use a faint INK color and rely on the natural +# contrast against PAGE_BG instead. + +p <- ggplot(df, aes(x, y)) + + geom_point(color = OKABE_ITO[1], size = 4) + + anyplot_theme +``` + +## Geoms (most common) + +```r +geom_point() # scatter +geom_line() # line +geom_col() # bar from precomputed values (stat = "identity") +geom_bar() # bar from counts (stat = "count") +geom_boxplot() # boxplot +geom_violin() # violin +geom_histogram() # histogram +geom_density() # density +geom_tile() # heatmap / raster +geom_smooth() # regression / loess overlay +geom_errorbar() # error bars +facet_wrap(~group) # small multiples +facet_grid(rows ~ cols) +``` + +## Script Skeleton + +A complete ggplot2 implementation reads `ANYPLOT_THEME`, generates data, builds +the plot, and saves both themed PNGs by being invoked twice with different env +values. The same single `.R` file handles both themes — no two-file split. + +```r +library(ggplot2) +library(dplyr) +library(ragg) + +set.seed(42) + +# --- Theme tokens ----------------------------------------------------------- +THEME <- Sys.getenv("ANYPLOT_THEME", "light") +PAGE_BG <- if (THEME == "light") "#FAF8F1" else "#1A1A17" +INK <- if (THEME == "light") "#1A1A17" else "#F0EFE8" +INK_SOFT <- if (THEME == "light") "#4A4A44" else "#B8B7B0" +OKABE_ITO <- c("#009E73", "#D55E00", "#0072B2", "#CC79A7", + "#E69F00", "#56B4E9", "#F0E442") + +# --- Data ------------------------------------------------------------------- +df <- tibble::tibble( + x = rnorm(100), + y = rnorm(100) +) + +# --- Plot ------------------------------------------------------------------- +p <- ggplot(df, aes(x, y)) + + geom_point(color = OKABE_ITO[1], size = 4, alpha = 0.7) + + labs(title = "Basic Scatter", x = "X", y = "Y") + + theme_minimal(base_size = 14) + + theme( + plot.background = element_rect(fill = PAGE_BG, color = PAGE_BG), + panel.background = element_rect(fill = PAGE_BG, color = NA), + panel.grid.major = element_line(color = INK, linewidth = 0.3), + panel.grid.minor = element_line(color = INK, linewidth = 0.2), + axis.title = element_text(color = INK, size = 20), + axis.text = element_text(color = INK_SOFT, size = 16), + plot.title = element_text(color = INK, size = 24) + ) + +# --- Save ------------------------------------------------------------------- +ggsave( + filename = sprintf("plot-%s.png", THEME), + plot = p, + device = ragg::agg_png, + width = 16, + height = 9, + units = "in", + dpi = 300 +) +``` + +## Output Files + +- Implementation: `plots/{spec-id}/implementations/r/ggplot2.R` — executed + twice with different `ANYPLOT_THEME`. +- Generated artifacts: `plot-light.png` + `plot-dark.png` (ggplot2 is PNG-only + in this catalog; no HTML variant). + +## R-Specific Gotchas + +- Use `linewidth =` for `geom_line()` and `element_line()` in modern ggplot2 + (≥ 3.4). `size =` triggers a deprecation warning. +- `theme()` chains compose right-to-left: a later `theme()` overrides an + earlier one. Start with `theme_minimal(base_size = ...)`, then layer + `anyplot_theme`. +- `ggsave()` resets the active graphics device. Use `device = ragg::agg_png` + explicitly — the platform default Cairo path is not installed in the CI + image. +- Use `tibble::tibble(...)` (or plain `data.frame(...)`) — `dplyr::tibble` was + removed in dplyr ≥ 1.0; `tibble::tibble` is the correct path. +- Avoid `print(p)` at script end — `ggsave()` already renders the plot, and an + extra `print()` opens an unused interactive device. diff --git a/prompts/plot-generator.md b/prompts/plot-generator.md index 99a1fdfcbb..6bb94e94f0 100644 --- a/prompts/plot-generator.md +++ b/prompts/plot-generator.md @@ -2,30 +2,35 @@ ## Role -You are a Python expert for data visualization. You generate clean, readable plot scripts that anyone can copy and use. +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** — same rules, different runtime. ## Task -Create a Python script for the specified plot type and library. The code should be simple and self-contained - like examples in the matplotlib gallery. +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, or letsplot +2. **Library**: matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, letsplot, or ggplot2 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}.py` +5. **Previous Code** (if regenerating): `plots/{spec-id}/implementations/{language}/{library}{ext}` — `{ext}` is `.py` for python libraries, `.R` for ggplot2 ## Available Standard Packages -All plot implementations have access to: `numpy`, `pandas`, `scipy`, `scikit-learn`, `statsmodels`. +**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`. + **Usage guidelines:** -- Always use `np.random.seed(42)` for reproducibility when using random data +- Python: `np.random.seed(42)` for reproducibility when using random data +- R: `set.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.) @@ -57,13 +62,13 @@ 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}.py` (if regenerating, same library only) +- `plots/{spec-id}/implementations/{language}/{this-library}{ext}` (if regenerating, same library only — `.py` for python, `.R` for ggplot2) - `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 `.py` or `.yaml` under `plots/{spec-id}/implementations/` or `plots/{spec-id}/metadata/` — even "for reference" or "to stay consistent" +- 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 @@ -79,7 +84,7 @@ implementation's own decision. ## Output -A simple Python script with this structure: +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. ```python """ anyplot.ai @@ -228,6 +233,7 @@ np.fill_diagonal(corr_matrix, 1.0) # Diagonal = 1 ### Docstring Format (filled by workflow after review) +Python: ```python """ anyplot.ai {spec-id}: {Title} @@ -236,6 +242,14 @@ Quality: {score}/100 | Created: {YYYY-MM-DD} """ ``` +R (use `#'` Roxygen-style comments — R has no docstring syntax): +```r +#' anyplot.ai +#' {spec-id}: {Title} +#' Library: ggplot2 {lib_version} | R {r_version} +#' Quality: {score}/100 | Created: {YYYY-MM-DD} +``` + **During generation** (before review): Use placeholder values ```python """ anyplot.ai @@ -251,12 +265,18 @@ The workflow will update `Quality: {score}/100` and add version numbers after re Must pass all code quality criteria (CQ-01 through CQ-05) from `prompts/quality-criteria.md`. -**Forbidden:** +**Forbidden (Python):** - Functions or classes - `if __name__ == '__main__':` - Type hints or docstrings (keep it simple) - 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 + > 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:** @@ -291,7 +311,7 @@ Must pass all code quality criteria (CQ-01 through CQ-05) from `prompts/quality- ### Feasibility Pre-Check (Static Libraries Only) -Before generating code for **matplotlib**, **seaborn**, or **plotnine**: +Before generating code for **matplotlib**, **seaborn**, **plotnine**, or **ggplot2**: 1. Check if the spec requires interactivity (hover, zoom, click, brush, animation, streaming) 2. If the spec's PRIMARY value is its interactivity → **STOP** diff --git a/prompts/workflow-prompts/impl-generate-claude.md b/prompts/workflow-prompts/impl-generate-claude.md index 68243066ed..41a67eb158 100644 --- a/prompts/workflow-prompts/impl-generate-claude.md +++ b/prompts/workflow-prompts/impl-generate-claude.md @@ -2,10 +2,17 @@ **YOUR PRIMARY TASK: Create a working plot implementation file.** -You MUST create: `plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py` +You MUST create: `plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}{EXT}` This is NOT optional. The workflow will FAIL if this file does not exist after you finish. +The `{EXT}` value depends on `{LANGUAGE}`: + +| LANGUAGE | EXT | Runner | +|----------|------|-----------------------| +| `python` | `.py` | `python` (in `.venv`) | +| `r` | `.R` | `Rscript` | + --- **Variables:** @@ -28,7 +35,7 @@ Read these files to understand the requirements: When regenerating an existing implementation, you MUST read these BEFORE writing any code: 1. `/tmp/anyplot-prev-review.md` — structured review from the previous attempt (image description, strengths, weaknesses, failed criteria checklist). The workflow extracts this automatically from the previous `metadata/{LANGUAGE}/{LIBRARY}.yaml`. -2. `plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py` — the previous implementation. +2. `plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}{EXT}` — the previous implementation (`.py` for python, `.R` for r). **Default regen mindset: incremental improvement, not rewrite.** @@ -41,7 +48,7 @@ When regenerating an existing implementation, you MUST read these BEFORE writing ### Library Independence — DO NOT read sibling implementations This implementation is for **one library only**. Never read another library's -`.py` or `.yaml` under `plots/{SPEC_ID}/implementations/` or +source file or `.yaml` under `plots/{SPEC_ID}/implementations/` or `plots/{SPEC_ID}/metadata/` — not even "for reference" or "to stay consistent with the catalog". Each library is an independent interpretation of the spec; identical charts rendered by different engines defeat the catalog's purpose. @@ -84,7 +91,7 @@ entirely — there is nothing to apply. ### Feasibility Check (Static Libraries Only) -If LIBRARY is **matplotlib**, **seaborn**, or **plotnine**, AND the specification mentions interactive features (hover, zoom, click, brush, animation, streaming): +If LIBRARY is **matplotlib**, **seaborn**, **plotnine**, or **ggplot2**, AND the specification mentions interactive features (hover, zoom, click, brush, animation, streaming): 1. Is the spec's PRIMARY value its interactivity? 2. If YES → Do NOT generate. Report: `NOT_FEASIBLE: {LIBRARY} cannot provide {required_feature} as static PNG.` @@ -95,14 +102,18 @@ If LIBRARY is **matplotlib**, **seaborn**, or **plotnine**, AND the specificatio **You MUST use the Write tool to create:** ``` -plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py +plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}{EXT} ``` +where `{EXT}` is `.py` for `python` and `.R` for `r`. + The script MUST: - Follow the KISS structure: imports → data → plot → save - Read `ANYPLOT_THEME` from the environment (`"light"` or `"dark"`, default `"light"`) and render accordingly. The same single script file handles both themes. + - Python: `os.getenv("ANYPLOT_THEME", "light")` + - R: `Sys.getenv("ANYPLOT_THEME", "light")` - Save output as `plot-{THEME}.png` (theme-suffixed, based on the env var). -- For interactive libraries (plotly, bokeh, altair, highcharts, pygal, letsplot): also save `plot-{THEME}.html`. +- For interactive libraries (plotly, bokeh, altair, highcharts, pygal, letsplot): also save `plot-{THEME}.html`. ggplot2 is PNG-only, no HTML variant. - Use `#009E73` (Okabe-Ito position 1) as the **first categorical series**, always. Multi-series follows the canonical order: `#D55E00`, `#0072B2`, `#CC79A7`, `#E69F00`, `#56B4E9`, `#F0E442`. - For continuous data: `viridis`/`cividis` (sequential) or `BrBG` (diverging). Never `jet`/`hsv`/`rainbow`. - Plot backgrounds: `#FAF8F1` (light) / `#1A1A17` (dark). Never pure `#FFFFFF` or `#000000`. @@ -112,7 +123,9 @@ The script MUST: ## Step 3: Test and fix (up to 3 attempts) -Run the implementation **twice**, once per theme: +Run the implementation **twice**, once per theme. + +**Python (`LANGUAGE=python`)**: ```bash source .venv/bin/activate cd plots/{SPEC_ID}/implementations/{LANGUAGE} @@ -120,6 +133,13 @@ MPLBACKEND=Agg ANYPLOT_THEME=light python {LIBRARY}.py MPLBACKEND=Agg ANYPLOT_THEME=dark python {LIBRARY}.py ``` +**R (`LANGUAGE=r`)**: +```bash +cd plots/{SPEC_ID}/implementations/{LANGUAGE} +ANYPLOT_THEME=light Rscript {LIBRARY}.R +ANYPLOT_THEME=dark Rscript {LIBRARY}.R +``` + Both runs must succeed and produce `plot-light.png` / `plot-dark.png` (plus `plot-light.html` / `plot-dark.html` for interactive libs). If either fails, fix and try again (max 3 attempts). ## Step 4: Visual self-check (BOTH renders) @@ -133,20 +153,29 @@ Look at `plot-light.png` AND `plot-dark.png`: ## Step 5: Format the code +**Python (`LANGUAGE=python`)**: ```bash source .venv/bin/activate ruff format plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py ruff check --fix plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py ``` +**R (`LANGUAGE=r`)**: no formatter is required by CI. Keep the code idiomatic +ggplot2 style (4-space indent, `<-` for assignment, `=` only in arguments). If +`styler` is available, you may run +`Rscript -e 'styler::style_file("plots/{SPEC_ID}/implementations/r/{LIBRARY}.R")'` +— but a missing styler is not a failure. + ## Step 6: Verify file exists (CRITICAL) Before committing, verify the implementation file exists: ```bash -ls -la plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py +ls -la plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}{EXT} ``` +`{EXT}` is `.py` for python, `.R` for r. + **If the file does NOT exist, you MUST go back to Step 2 and create it!** ## Step 7: Commit @@ -154,7 +183,7 @@ ls -la plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py ```bash git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" -git add plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py +git add plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}{EXT} git commit -m "feat({LIBRARY}): implement {SPEC_ID}" git push -u origin implementation/{SPEC_ID}/{LIBRARY} ``` @@ -174,10 +203,10 @@ Pass the multi-line message via `-F -` or a heredoc so git preserves the body. ## Final Check Before finishing, confirm: -1. ✅ `plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py` exists -2. ✅ `plot-light.png` AND `plot-dark.png` were generated successfully (plus `plot-light.html` / `plot-dark.html` for interactive libs) +1. ✅ `plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}{EXT}` exists (`.py` for python, `.R` for r) +2. ✅ `plot-light.png` AND `plot-dark.png` were generated successfully (plus `plot-light.html` / `plot-dark.html` for interactive libs — ggplot2 is PNG-only) 3. ✅ First categorical series renders in `#009E73` in both themes 4. ✅ Changes were committed and pushed -5. ✅ If regenerating: `/tmp/anyplot-prev-review.md` and the previous `.py` were read, and each weakness / failed criterion was either addressed or consciously kept (explained in the commit body) +5. ✅ If regenerating: `/tmp/anyplot-prev-review.md` and the previous source file were read, and each weakness / failed criterion was either addressed or consciously kept (explained in the commit body) If any of these failed, DO NOT report success. diff --git a/prompts/workflow-prompts/impl-repair-claude.md b/prompts/workflow-prompts/impl-repair-claude.md index 0d2b11e064..0474169718 100644 --- a/prompts/workflow-prompts/impl-repair-claude.md +++ b/prompts/workflow-prompts/impl-repair-claude.md @@ -39,10 +39,11 @@ Read both sources to understand what needs to be fixed: ## Step 3: Read current implementation -`plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py` +`plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}{EXT}` — `{EXT}` is `.py` +for python libraries and `.R` for ggplot2. **Do NOT read sibling-library implementations under -`plots/{SPEC_ID}/implementations/`** (other libraries' `.py` or `.yaml`). +`plots/{SPEC_ID}/implementations/`** (other libraries' source or `.yaml`). Each library is an independent interpretation; copying data scenarios, color choices, layout, or aspect ratio from a sibling defeats the point of having multiple libraries in the catalog. See `prompts/plot-generator.md` → @@ -57,6 +58,7 @@ Based on the AI feedback, fix: ## Step 5: Test the fix (BOTH themes) +**Python (`LANGUAGE=python`)**: ```bash source .venv/bin/activate cd plots/{SPEC_ID}/implementations/{LANGUAGE} @@ -64,6 +66,13 @@ MPLBACKEND=Agg ANYPLOT_THEME=light python {LIBRARY}.py MPLBACKEND=Agg ANYPLOT_THEME=dark python {LIBRARY}.py ``` +**R (`LANGUAGE=r`)**: +```bash +cd plots/{SPEC_ID}/implementations/{LANGUAGE} +ANYPLOT_THEME=light Rscript {LIBRARY}.R +ANYPLOT_THEME=dark Rscript {LIBRARY}.R +``` + Both renders must succeed. ## Step 6: Visual self-check @@ -72,18 +81,22 @@ View `plot-light.png` AND `plot-dark.png`. Verify the failed criteria are now fi ## Step 7: Format the code +**Python (`LANGUAGE=python`)**: ```bash source .venv/bin/activate ruff format plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py ruff check --fix plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py ``` +**R (`LANGUAGE=r`)**: no formatter is required by CI. Keep idiomatic ggplot2 +style (4-space indent, `<-` for assignment). + ## Step 8: Commit and push ```bash git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" -git add plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}.py +git add plots/{SPEC_ID}/implementations/{LANGUAGE}/{LIBRARY}{EXT} git commit -m "fix({LIBRARY}): address review feedback for {SPEC_ID} Attempt {ATTEMPT}/3 - fixes based on AI review" diff --git a/renv.lock b/renv.lock new file mode 100644 index 0000000000..7416e4b2ed --- /dev/null +++ b/renv.lock @@ -0,0 +1,170 @@ +{ + "R": { + "Version": "4.4.1", + "Repositories": [ + { + "Name": "CRAN", + "URL": "https://packagemanager.posit.co/cran/__linux__/jammy/2025-01-15" + } + ] + }, + "Packages": { + "renv": { + "Package": "renv", + "Version": "1.0.11", + "Source": "Repository", + "Repository": "CRAN" + }, + "ggplot2": { + "Package": "ggplot2", + "Version": "3.5.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "glue", + "grDevices", + "grid", + "gtable", + "isoband", + "lifecycle", + "MASS", + "mgcv", + "rlang", + "scales", + "stats", + "tibble", + "vctrs", + "withr" + ] + }, + "ragg": { + "Package": "ragg", + "Version": "1.3.3", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": ["systemfonts", "textshaping"] + }, + "systemfonts": { + "Package": "systemfonts", + "Version": "1.1.0", + "Source": "Repository", + "Repository": "CRAN" + }, + "textshaping": { + "Package": "textshaping", + "Version": "0.4.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": ["cpp11", "systemfonts"] + }, + "dplyr": { + "Package": "dplyr", + "Version": "1.1.4", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "generics", + "glue", + "lifecycle", + "magrittr", + "methods", + "pillar", + "R6", + "rlang", + "tibble", + "tidyselect", + "utils", + "vctrs" + ] + }, + "tidyr": { + "Package": "tidyr", + "Version": "1.3.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "dplyr", + "glue", + "lifecycle", + "magrittr", + "purrr", + "rlang", + "stringr", + "tibble", + "tidyselect", + "utils", + "vctrs" + ] + }, + "tibble": { + "Package": "tibble", + "Version": "3.2.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "fansi", + "lifecycle", + "magrittr", + "methods", + "pillar", + "pkgconfig", + "rlang", + "utils", + "vctrs" + ] + }, + "scales": { + "Package": "scales", + "Version": "1.3.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": [ + "R", + "cli", + "farver", + "glue", + "labeling", + "lifecycle", + "munsell", + "R6", + "RColorBrewer", + "rlang", + "viridisLite" + ] + }, + "viridis": { + "Package": "viridis", + "Version": "0.6.5", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": ["R", "ggplot2", "gridExtra", "viridisLite"] + }, + "viridisLite": { + "Package": "viridisLite", + "Version": "0.4.2", + "Source": "Repository", + "Repository": "CRAN" + }, + "palmerpenguins": { + "Package": "palmerpenguins", + "Version": "0.1.1", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": ["R"] + }, + "gapminder": { + "Package": "gapminder", + "Version": "1.0.0", + "Source": "Repository", + "Repository": "CRAN", + "Requirements": ["R", "tibble"] + } + } +} diff --git a/tests/unit/api/test_debug.py b/tests/unit/api/test_debug.py index 755353dcee..c097e256ae 100644 --- a/tests/unit/api/test_debug.py +++ b/tests/unit/api/test_debug.py @@ -21,6 +21,7 @@ from api.main import app, fastapi_app from api.routers.debug import require_admin from core.config import settings +from core.constants import SUPPORTED_LIBRARIES from core.database import get_db @@ -127,7 +128,7 @@ def test_debug_status_empty_db(self, db_client) -> None: assert data["missing_preview_specs"] == [] assert data["missing_tags_specs"] == [] assert data["oldest_specs"] == [] - assert len(data["library_stats"]) == 9 # All 9 supported libraries + assert len(data["library_stats"]) == len(SUPPORTED_LIBRARIES) def test_debug_status_with_specs_and_impls(self, db_client) -> None: """Debug status should compute library stats and coverage from specs/impls.""" diff --git a/tests/unit/api/test_routers.py b/tests/unit/api/test_routers.py index e79d44a5e2..89ecb1fc4d 100644 --- a/tests/unit/api/test_routers.py +++ b/tests/unit/api/test_routers.py @@ -17,6 +17,7 @@ _calculate_or_counts, _image_matches_groups, ) +from core.constants import SUPPORTED_LIBRARIES from core.database import get_db from tests.conftest import TEST_IMAGE_URL @@ -1576,7 +1577,7 @@ def test_dashboard_with_db(self, client: TestClient, mock_spec) -> None: assert data["total_implementations"] == 1 assert data["total_lines_of_code"] == 500 assert data["total_interactive"] == 0 - assert len(data["library_stats"]) == 9 + assert len(data["library_stats"]) == len(SUPPORTED_LIBRARIES) assert isinstance(data["coverage_matrix"], list) assert isinstance(data["score_distribution"], dict) assert isinstance(data["tag_distribution"], dict) diff --git a/tests/unit/core/test_constants.py b/tests/unit/core/test_constants.py index a432b13b48..051b4a5cd8 100644 --- a/tests/unit/core/test_constants.py +++ b/tests/unit/core/test_constants.py @@ -7,6 +7,8 @@ from core.constants import ( ATTEMPT_LABELS, INTERACTIVE_LIBRARIES, + LANGUAGE_FILE_EXTENSIONS, + LANGUAGES_METADATA, LIBRARIES_METADATA, LIBRARY_LABELS, QUALITY_LABELS, @@ -16,6 +18,7 @@ QUALITY_THRESHOLD_GOOD, QUALITY_THRESHOLD_NEEDS_WORK, STATUS_LABELS, + SUPPORTED_LANGUAGES, SUPPORTED_LIBRARIES, get_library_label, is_interactive_library, @@ -26,13 +29,24 @@ class TestSupportedLibraries: """Tests for SUPPORTED_LIBRARIES constant.""" - def test_contains_all_nine_libraries(self) -> None: - """Should contain exactly 9 supported libraries.""" - assert len(SUPPORTED_LIBRARIES) == 9 + def test_contains_all_libraries(self) -> None: + """Should contain every catalog library (8 Python + ggplot2).""" + assert len(SUPPORTED_LIBRARIES) == 10 def test_contains_expected_libraries(self) -> None: """Should contain all expected library IDs.""" - expected = {"altair", "bokeh", "highcharts", "letsplot", "matplotlib", "plotly", "plotnine", "pygal", "seaborn"} + expected = { + "altair", + "bokeh", + "ggplot2", + "highcharts", + "letsplot", + "matplotlib", + "plotly", + "plotnine", + "pygal", + "seaborn", + } assert SUPPORTED_LIBRARIES == expected def test_is_frozenset(self) -> None: @@ -43,9 +57,9 @@ def test_is_frozenset(self) -> None: class TestLibrariesMetadata: """Tests for LIBRARIES_METADATA constant.""" - def test_contains_all_nine_libraries(self) -> None: - """Should contain metadata for all 9 libraries.""" - assert len(LIBRARIES_METADATA) == 9 + def test_contains_all_libraries(self) -> None: + """Should contain metadata for every supported library.""" + assert len(LIBRARIES_METADATA) == len(SUPPORTED_LIBRARIES) def test_ids_match_supported_libraries(self) -> None: """All metadata IDs should match SUPPORTED_LIBRARIES.""" @@ -54,7 +68,7 @@ def test_ids_match_supported_libraries(self) -> None: def test_each_library_has_required_fields(self) -> None: """Each library should have all required fields.""" - required_fields = {"id", "name", "version", "documentation_url", "description"} + required_fields = {"id", "name", "language_id", "version", "documentation_url", "description"} for lib in LIBRARIES_METADATA: assert required_fields.issubset(lib.keys()), f"Missing fields in {lib.get('id', 'unknown')}" @@ -64,6 +78,11 @@ def test_documentation_urls_are_valid(self) -> None: url = lib["documentation_url"] assert url.startswith("http://") or url.startswith("https://"), f"Invalid URL for {lib['id']}: {url}" + def test_ggplot2_is_r_language(self) -> None: + """ggplot2 is the catalog's first non-Python entry.""" + ggplot2 = next(lib for lib in LIBRARIES_METADATA if lib["id"] == "ggplot2") + assert ggplot2["language_id"] == "r" + class TestInteractiveLibraries: """Tests for INTERACTIVE_LIBRARIES constant.""" @@ -201,3 +220,33 @@ def test_static_libraries(self) -> None: static = SUPPORTED_LIBRARIES - INTERACTIVE_LIBRARIES for lib in static: assert is_interactive_library(lib) is False + + +class TestSupportedLanguages: + """Tests for SUPPORTED_LANGUAGES + LANGUAGES_METADATA.""" + + def test_contains_python_and_r(self) -> None: + """Catalog currently supports Python and R.""" + assert SUPPORTED_LANGUAGES == {"python", "r"} + + def test_metadata_ids_match_supported(self) -> None: + """Every LANGUAGES_METADATA entry must appear in SUPPORTED_LANGUAGES.""" + metadata_ids = {lang["id"] for lang in LANGUAGES_METADATA} + assert metadata_ids == SUPPORTED_LANGUAGES + + def test_each_language_has_required_fields(self) -> None: + """Each language must declare id, name, file_extension, runtime_version.""" + required = {"id", "name", "file_extension", "runtime_version", "documentation_url", "description"} + for lang in LANGUAGES_METADATA: + assert required.issubset(lang.keys()), f"Missing fields in language {lang.get('id', 'unknown')}" + + def test_file_extensions_map(self) -> None: + """LANGUAGE_FILE_EXTENSIONS exposes the same data as a mapping.""" + assert LANGUAGE_FILE_EXTENSIONS["python"] == ".py" + assert LANGUAGE_FILE_EXTENSIONS["r"] == ".R" + + def test_every_library_references_known_language(self) -> None: + """No library may point at a language we don't list in LANGUAGES_METADATA.""" + known = SUPPORTED_LANGUAGES + for lib in LIBRARIES_METADATA: + assert lib["language_id"] in known, f"{lib['id']} has unknown language_id {lib['language_id']}" From 1d7aee6d11a87830c5d0b8610a83a2473c43f262 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 20:00:27 +0000 Subject: [PATCH 2/6] chore(daily-regen): drop hardcoded "9 libs" log line bulk-generate.yml's ALL_LIBRARIES is now 10 (ggplot2 added in the parent commit), and any future addition would re-stale this string. --- .github/workflows/daily-regen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/daily-regen.yml b/.github/workflows/daily-regen.yml index 66283411d2..565678e6b4 100644 --- a/.github/workflows/daily-regen.yml +++ b/.github/workflows/daily-regen.yml @@ -284,7 +284,7 @@ jobs: MODEL: ${{ inputs.model || 'haiku' }} CHANGE_REQUESTS: ${{ steps.collect.outputs.change_requests }} run: | - echo "::notice::Dispatching bulk-generate for ${SPEC_ID} (all 9 libs, model=${MODEL})" + echo "::notice::Dispatching bulk-generate for ${SPEC_ID} (all libs, model=${MODEL})" gh workflow run bulk-generate.yml \ --repo "${{ github.repository }}" \ -f specification_id="${SPEC_ID}" \ From ba7885100a2938e65c355a7ab08b68b76b9eca8c Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 20:15:26 +0000 Subject: [PATCH 3/6] fix(review): address Copilot review on PR #6944 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three Copilot review comments resolved. 1. python_version no longer abused for R version (impl-generate.yml comment) - Adds a new language_version column to impls (Alembic migration 3a7e1b5c0c4f), backfilled from python_version for existing rows. - impl-generate.yml writes both: python_version stays the pipeline Python (3.13), language_version carries the implementation's own runtime — Python for python libs, R for ggplot2. - sync_to_postgres reads language_version with a fallback to python_version so legacy rows keep displaying correctly. - API schemas, specs / insights / mcp routers thread the new field through. - PlotOfTheDay frontend renders {Python|R} {language_version || python_version} so ggplot2 entries show "R 4.4.1" instead of "Python 4.4.1". 2. R header rewrite in impl-review.yml (was Python-only) - The "Update implementation header" step now branches on LANGUAGE: Python files still get a triple-quoted docstring, R files get a roxygen-style #' block and "R {version}" runtime label. - If the R file has no existing #' header (e.g. first review pass), one is prepended instead of silently no-op'ing. 3. test_constants docstring lied about library count - "8 Python + ggplot2" claimed 9 libs but there are 10 (9 Python + 1 R). Length assertion was also redundant with test_contains_expected_libraries, which already checks the exact set. - Removed the redundant length assertions; the equality check on the expected set is the single source of truth. Test fixtures (test_routers, test_tools, PlotOfTheDay tests) updated to set the new language_version attribute so Pydantic validation passes. --- .github/workflows/impl-generate.yml | 17 +++-- .github/workflows/impl-review.yml | 70 ++++++++++++++----- ...e1b5c0c4f_add_language_version_to_impls.py | 42 +++++++++++ api/mcp/server.py | 2 + api/routers/insights.py | 2 + api/routers/specs.py | 1 + api/schemas.py | 1 + app/src/components/PlotOfTheDay.test.tsx | 1 + app/src/components/PlotOfTheDay.tsx | 3 +- .../components/PlotOfTheDayTerminal.test.tsx | 1 + app/src/hooks/usePlotOfTheDay.ts | 1 + automation/scripts/sync_to_postgres.py | 13 +++- core/database/models.py | 6 ++ tests/unit/api/mcp/test_tools.py | 1 + tests/unit/api/test_routers.py | 2 + tests/unit/core/test_constants.py | 10 +-- 16 files changed, 136 insertions(+), 37 deletions(-) create mode 100644 alembic/versions/3a7e1b5c0c4f_add_language_version_to_impls.py diff --git a/.github/workflows/impl-generate.yml b/.github/workflows/impl-generate.yml index 677fe6d888..1afaf34785 100644 --- a/.github/workflows/impl-generate.yml +++ b/.github/workflows/impl-generate.yml @@ -455,14 +455,15 @@ jobs: mkdir -p "$METADATA_DIR" - # Runtime version: Python for python libs, R for R libs. We still record - # a 'python_version' field in metadata for backwards compatibility; for R - # implementations it carries the R version instead (the DB / frontend treat - # it as a free-form runtime version string). + # python_version = the Python interpreter that ran THIS pipeline (always 3.13). + # language_version = the implementation's own runtime — same as python_version for + # Python libs, the R version for ggplot2. The frontend renders language_version with + # the right language label, so we never mislabel R as "Python". + PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}') if [ "$LANGUAGE" = "r" ]; then - PYTHON_VERSION=$(Rscript -e 'cat(as.character(getRversion()))' 2>/dev/null || echo "unknown") + LANGUAGE_VERSION=$(Rscript -e 'cat(as.character(getRversion()))' 2>/dev/null || echo "unknown") else - PYTHON_VERSION=$(python3 --version 2>&1 | awk '{print $2}') + LANGUAGE_VERSION="$PYTHON_VERSION" fi # Get library version. Python libs read via `pip show`; R libs via @@ -507,7 +508,7 @@ jobs: if [ -z "$LIBRARY_VERSION" ]; then LIBRARY_VERSION="unknown" fi - echo "::notice::Library version: $LIBRARY = $LIBRARY_VERSION ($LANGUAGE runtime $PYTHON_VERSION)" + echo "::notice::Library version: $LIBRARY = $LIBRARY_VERSION ($LANGUAGE runtime $LANGUAGE_VERSION, pipeline python $PYTHON_VERSION)" # Interactive libraries additionally produce HTML previews (one per theme) HAS_HTML="false" @@ -526,6 +527,7 @@ jobs: run_id = ${{ github.run_id }} issue = int('$ISSUE' or '0') py_ver = '$PYTHON_VERSION' + lang_ver = '$LANGUAGE_VERSION' lib_ver = '$LIBRARY_VERSION' has_html = '$HAS_HTML' == 'true' metadata_file = '$METADATA_FILE' @@ -559,6 +561,7 @@ jobs: 'workflow_run': run_id, 'issue': issue, 'python_version': py_ver, + 'language_version': lang_ver, 'library_version': lib_ver, # Theme-aware preview URLs (Phase C). Both PNG variants are always emitted. 'preview_url_light': f'{base_url}/plot-light.png', diff --git a/.github/workflows/impl-review.yml b/.github/workflows/impl-review.yml index 2afc4e10ce..64d51bcf46 100644 --- a/.github/workflows/impl-review.yml +++ b/.github/workflows/impl-review.yml @@ -492,9 +492,15 @@ jobs: # Update implementation header with quality score if [ -f "$IMPL_FILE" ]; then - # Get library and python versions from metadata + # Get library version + the runtime version of the implementation language + # (Python for python libs, R for ggplot2). The script-level header in the + # impl file should display the impl's own runtime, not the pipeline Python. LIBRARY_VERSION=$(python3 -c "import yaml; print(yaml.safe_load(open('$METADATA_FILE'))['library_version'])" 2>/dev/null || echo "unknown") - PYTHON_VERSION=$(python3 -c "import yaml; print(yaml.safe_load(open('$METADATA_FILE'))['python_version'])" 2>/dev/null || echo "3.13") + LANGUAGE_VERSION=$(python3 -c " + import yaml + data = yaml.safe_load(open('$METADATA_FILE')) + print(data.get('language_version') or data.get('python_version') or 'unknown') + " 2>/dev/null || echo "unknown") DATE_INFO=$(python3 -c " import yaml data = yaml.safe_load(open('$METADATA_FILE')) @@ -510,39 +516,65 @@ jobs: TITLE=$(python3 -c "import yaml; print(yaml.safe_load(open('plots/${SPEC_ID}/specification.yaml'))['title'])" 2>/dev/null || echo "${SPEC_ID}") # Replace old header using Python. - # All inputs are passed via env (NOT shell-interpolated into the - # Python source), and the heredoc uses single-quoted 'EOF' so bash - # does not expand $TITLE. This blocks the prior triple-quoted - # literal injection (a TITLE containing ''' would have escaped - # the string and executed arbitrary Python in the runner). - export IMPL_FILE SPEC_ID TITLE LIBRARY LIBRARY_VERSION PYTHON_VERSION SCORE DATE_INFO + # Inputs come through env (NOT shell-interpolated into Python source) and the + # heredoc uses single-quoted 'EOF' so bash does not expand $TITLE. That blocks + # triple-quote literal injection (a TITLE containing ''' would otherwise + # escape the string and execute arbitrary Python in the runner). + # + # Per-language header style: + # - Python: triple-quoted docstring (""") + # - R: roxygen-style #' block (R has no docstring syntax) + export IMPL_FILE LANGUAGE SPEC_ID TITLE LIBRARY LIBRARY_VERSION LANGUAGE_VERSION SCORE DATE_INFO python3 - <<'EOF' import os import re impl_file = os.environ["IMPL_FILE"] + language = os.environ["LANGUAGE"] spec_id = os.environ["SPEC_ID"] title = os.environ["TITLE"] library = os.environ["LIBRARY"] lib_version = os.environ["LIBRARY_VERSION"] - py_version = os.environ["PYTHON_VERSION"] + lang_version = os.environ["LANGUAGE_VERSION"] score = os.environ["SCORE"] date_info = os.environ["DATE_INFO"] - # Sanitize title to prevent triple-quote injection - title = title.replace('"""', '\"\"\"') - - new_header = f'''""" anyplot.ai - {spec_id}: {title} - Library: {library} {lib_version} | Python {py_version} - Quality: {score}/100 | {date_info} - """''' + # Human-readable runtime label for the header. Extend this map when a new + # language joins the catalog. + RUNTIME_LABEL = {"python": "Python", "r": "R"}.get(language, language.capitalize()) with open(impl_file, "r") as f: content = f.read() - pattern = r'^""".*?"""' - new_content = re.sub(pattern, new_header, content, count=1, flags=re.DOTALL) + if language == "r": + # Sanitize title — newlines would break the comment block. R header is + # roxygen-style #' so triple-quote injection isn't a concern. + title_safe = title.replace("\n", " ") + new_header = ( + "#' anyplot.ai\n" + f"#' {spec_id}: {title_safe}\n" + f"#' Library: {library} {lib_version} | {RUNTIME_LABEL} {lang_version}\n" + f"#' Quality: {score}/100 | {date_info}" + ) + # Match a leading run of #' lines (the existing roxygen header). Anchored to + # start of file. If no header exists we fall back to prepending one. + pattern = r"\A(?:#'[^\n]*\n)+" + if re.match(pattern, content): + new_content = re.sub(pattern, new_header + "\n", content, count=1) + else: + new_content = new_header + "\n" + content + else: + # Python: triple-quoted module docstring at the top of the file. + title_safe = title.replace('"""', '\\"\\"\\"') + new_header = ( + '""" anyplot.ai\n' + f"{spec_id}: {title_safe}\n" + f"Library: {library} {lib_version} | {RUNTIME_LABEL} {lang_version}\n" + f"Quality: {score}/100 | {date_info}\n" + '"""' + ) + pattern = r'^""".*?"""' + new_content = re.sub(pattern, new_header, content, count=1, flags=re.DOTALL) with open(impl_file, "w") as f: f.write(new_content) diff --git a/alembic/versions/3a7e1b5c0c4f_add_language_version_to_impls.py b/alembic/versions/3a7e1b5c0c4f_add_language_version_to_impls.py new file mode 100644 index 0000000000..148754042d --- /dev/null +++ b/alembic/versions/3a7e1b5c0c4f_add_language_version_to_impls.py @@ -0,0 +1,42 @@ +"""add_language_version_to_impls + +Adds a `language_version` column to the `impls` table to record the runtime +version of the implementation's own language — Python interpreter for python +libraries, R interpreter for ggplot2. The existing `python_version` column +keeps its original meaning ("Python that ran the impl-generate pipeline", +i.e. 3.13) so multi-language entries no longer abuse it to carry an R +version. + +The new column is nullable: old rows stay correct via the fallback chain +(`language_version → python_version`) wired in the API. + +Revision ID: 3a7e1b5c0c4f +Revises: f2d9c8a1b4e0 +Create Date: 2026-05-16 + +""" + +from typing import Sequence + +import sqlalchemy as sa + +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = "3a7e1b5c0c4f" +down_revision: str | None = "f2d9c8a1b4e0" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + + +def upgrade() -> None: + op.add_column("impls", sa.Column("language_version", sa.String(), nullable=True)) + + # Backfill: for existing rows (all python today) the language runtime IS the + # Python interpreter, so seed language_version from python_version. + op.execute("UPDATE impls SET language_version = python_version WHERE language_version IS NULL") + + +def downgrade() -> None: + op.drop_column("impls", "language_version") diff --git a/api/mcp/server.py b/api/mcp/server.py index 667b8ed9a9..92317601c1 100644 --- a/api/mcp/server.py +++ b/api/mcp/server.py @@ -292,6 +292,7 @@ async def get_spec_detail(spec_id: str) -> dict[str, Any]: generated_at=impl.generated_at.isoformat() if impl.generated_at else None, generated_by=impl.generated_by, python_version=impl.python_version, + language_version=impl.language_version or impl.python_version, library_version=impl.library_version, review_strengths=impl.review_strengths or [], review_weaknesses=impl.review_weaknesses or [], @@ -391,6 +392,7 @@ async def get_implementation(spec_id: str, library: str) -> dict[str, Any]: generated_at=impl.generated_at.isoformat() if impl.generated_at else None, generated_by=impl.generated_by, python_version=impl.python_version, + language_version=impl.language_version or impl.python_version, library_version=impl.library_version, review_strengths=impl.review_strengths or [], review_weaknesses=impl.review_weaknesses or [], diff --git a/api/routers/insights.py b/api/routers/insights.py index e7c502b5b1..2986811338 100644 --- a/api/routers/insights.py +++ b/api/routers/insights.py @@ -115,6 +115,7 @@ class PlotOfTheDayResponse(BaseModel): code: str | None = None library_version: str | None = None python_version: str | None = None + language_version: str | None = None date: str @@ -420,6 +421,7 @@ async def _build_potd(spec_repo: SpecRepository, impl_repo: ImplRepository) -> P code=strip_noqa_comments(full_impl.code) if full_impl and full_impl.code else None, library_version=full_impl.library_version if full_impl else None, python_version=full_impl.python_version if full_impl else None, + language_version=(full_impl.language_version or full_impl.python_version) if full_impl else None, date=today, ) diff --git a/api/routers/specs.py b/api/routers/specs.py index 7d92bb13b8..874e670297 100644 --- a/api/routers/specs.py +++ b/api/routers/specs.py @@ -89,6 +89,7 @@ async def _build_spec_detail(db: AsyncSession, spec_id: str) -> SpecDetailRespon updated=impl.updated.isoformat() if impl.updated else None, generated_by=impl.generated_by, python_version=impl.python_version, + language_version=impl.language_version or impl.python_version, library_version=impl.library_version, review_strengths=impl.review_strengths or [], review_weaknesses=impl.review_weaknesses or [], diff --git a/api/schemas.py b/api/schemas.py index 0d47a317e6..ba64a9cb0c 100644 --- a/api/schemas.py +++ b/api/schemas.py @@ -31,6 +31,7 @@ class ImplementationResponse(BaseModel): updated: str | None = None generated_by: str | None = None python_version: str | None = None + language_version: str | None = None library_version: str | None = None # Review fields review_strengths: list[str] = [] diff --git a/app/src/components/PlotOfTheDay.test.tsx b/app/src/components/PlotOfTheDay.test.tsx index da41229e79..05f5368c21 100644 --- a/app/src/components/PlotOfTheDay.test.tsx +++ b/app/src/components/PlotOfTheDay.test.tsx @@ -36,6 +36,7 @@ const mockData = { image_description: 'Shows data points with clear labels', library_version: '3.8.0', python_version: '3.12', + language_version: '3.12', date: '2026-04-11', }; diff --git a/app/src/components/PlotOfTheDay.tsx b/app/src/components/PlotOfTheDay.tsx index 0d218251df..b49a89209c 100644 --- a/app/src/components/PlotOfTheDay.tsx +++ b/app/src/components/PlotOfTheDay.tsx @@ -29,6 +29,7 @@ interface PlotOfTheDayData { image_description: string | null; library_version: string | null; python_version: string | null; + language_version: string | null; date: string; } @@ -254,7 +255,7 @@ export function PlotOfTheDay() { │ - {data.library_name}{data.library_version && data.library_version !== 'unknown' ? ` ${data.library_version}` : ''} · Python {data.python_version || '3.13'} + {data.library_name}{data.library_version && data.library_version !== 'unknown' ? ` ${data.library_version}` : ''} · {data.language === 'r' ? 'R' : 'Python'} {data.language_version || data.python_version || (data.language === 'r' ? '4.4' : '3.13')} diff --git a/app/src/components/PlotOfTheDayTerminal.test.tsx b/app/src/components/PlotOfTheDayTerminal.test.tsx index 051dbe25f5..7a3ce45e28 100644 --- a/app/src/components/PlotOfTheDayTerminal.test.tsx +++ b/app/src/components/PlotOfTheDayTerminal.test.tsx @@ -32,6 +32,7 @@ const potd = { image_description: null, library_version: '3.8.0', python_version: '3.12', + language_version: '3.12', date: '2026-04-25', }; diff --git a/app/src/hooks/usePlotOfTheDay.ts b/app/src/hooks/usePlotOfTheDay.ts index c45ca48083..494593e5f6 100644 --- a/app/src/hooks/usePlotOfTheDay.ts +++ b/app/src/hooks/usePlotOfTheDay.ts @@ -16,6 +16,7 @@ export interface PlotOfTheDayData { quality_score?: number; library_version?: string | null; python_version?: string | null; + language_version?: string | null; date?: string; } diff --git a/automation/scripts/sync_to_postgres.py b/automation/scripts/sync_to_postgres.py index fd6ff09922..33722b5ae4 100644 --- a/automation/scripts/sync_to_postgres.py +++ b/automation/scripts/sync_to_postgres.py @@ -366,8 +366,18 @@ def scan_plot_directory(plot_dir: Path) -> dict | None: "preview_url_dark": preview_url_dark, "preview_html_light": preview_html_light, "preview_html_dark": preview_html_dark, - # Versions (from metadata YAML, filled by workflow) + # Versions (from metadata YAML, filled by workflow). + # language_version is the implementation's own runtime version (Python for + # python libs, R for ggplot2). python_version stays the pipeline Python. "python_version": current.get("python_version") or impl_meta.get("python_version"), + "language_version": ( + current.get("language_version") + or impl_meta.get("language_version") + # Legacy entries (pre-ggplot2) only carried python_version; fall back so + # existing rows keep displaying correctly until they re-sync. + or current.get("python_version") + or impl_meta.get("python_version") + ), "library_version": current.get("library_version") or impl_meta.get("library_version"), # Generation metadata "generated_at": generated_at, @@ -412,6 +422,7 @@ def _chunked(iterable, size): "preview_html_light", "preview_html_dark", "python_version", + "language_version", "library_version", "generated_at", "updated", diff --git a/core/database/models.py b/core/database/models.py index 4e5c7e2107..8f5de04d52 100644 --- a/core/database/models.py +++ b/core/database/models.py @@ -142,7 +142,13 @@ class Impl(Base): preview_html = synonym("preview_html_light") # Creation versions (filled by workflow) + # python_version is the Python interpreter that ran the impl-generate pipeline (3.13 today). + # language_version is the runtime version of the implementation's own language — same + # as python_version for Python libs, the R version for ggplot2. The frontend renders + # `{language_name} {language_version}`; old rows without language_version fall back to + # python_version (kept as a synonym for backwards compatibility). python_version: Mapped[str | None] = mapped_column(String, nullable=True) # e.g., "3.13" + language_version: Mapped[str | None] = mapped_column(String, nullable=True) # e.g., "3.13" / "4.4.1" library_version: Mapped[str | None] = mapped_column(String, nullable=True) # e.g., "3.9.0" # Test matrix (deferred — unused by any endpoint) diff --git a/tests/unit/api/mcp/test_tools.py b/tests/unit/api/mcp/test_tools.py index 69b5131673..fe3882ba9a 100644 --- a/tests/unit/api/mcp/test_tools.py +++ b/tests/unit/api/mcp/test_tools.py @@ -53,6 +53,7 @@ def mock_spec(): mock_impl.created = None mock_impl.generated_by = "claude-opus-4" mock_impl.python_version = "3.13" + mock_impl.language_version = "3.13" mock_impl.library_version = "3.10.0" mock_impl.review_strengths = ["Clean code"] mock_impl.review_weaknesses = ["Could improve"] diff --git a/tests/unit/api/test_routers.py b/tests/unit/api/test_routers.py index 89ecb1fc4d..fd295d514f 100644 --- a/tests/unit/api/test_routers.py +++ b/tests/unit/api/test_routers.py @@ -86,6 +86,7 @@ def mock_spec(): mock_impl.updated = None mock_impl.generated_by = "claude" mock_impl.python_version = "3.13" + mock_impl.language_version = "3.13" mock_impl.library_version = "3.10.0" # Review fields (must be proper types, not MagicMock) mock_impl.review_image_description = "A scatter plot showing data points" @@ -1597,6 +1598,7 @@ def test_potd_with_db(self, client: TestClient, mock_spec) -> None: mock_impl.review_image_description = "A scatter plot" mock_impl.library_version = "3.10.0" mock_impl.python_version = "3.13.11" + mock_impl.language_version = "3.13.11" mock_impl_repo = MagicMock() mock_impl_repo.get_by_spec_and_library = AsyncMock(return_value=mock_impl) diff --git a/tests/unit/core/test_constants.py b/tests/unit/core/test_constants.py index 051b4a5cd8..4019c5adbc 100644 --- a/tests/unit/core/test_constants.py +++ b/tests/unit/core/test_constants.py @@ -29,12 +29,8 @@ class TestSupportedLibraries: """Tests for SUPPORTED_LIBRARIES constant.""" - def test_contains_all_libraries(self) -> None: - """Should contain every catalog library (8 Python + ggplot2).""" - assert len(SUPPORTED_LIBRARIES) == 10 - def test_contains_expected_libraries(self) -> None: - """Should contain all expected library IDs.""" + """Should contain exactly the expected catalog of library IDs (9 Python + ggplot2).""" expected = { "altair", "bokeh", @@ -57,10 +53,6 @@ def test_is_frozenset(self) -> None: class TestLibrariesMetadata: """Tests for LIBRARIES_METADATA constant.""" - def test_contains_all_libraries(self) -> None: - """Should contain metadata for every supported library.""" - assert len(LIBRARIES_METADATA) == len(SUPPORTED_LIBRARIES) - def test_ids_match_supported_libraries(self) -> None: """All metadata IDs should match SUPPORTED_LIBRARIES.""" metadata_ids = {lib["id"] for lib in LIBRARIES_METADATA} From 28c5a4511e1e72ec11036778a20cba0a8659de99 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 20:25:28 +0000 Subject: [PATCH 4/6] docs: align project docs + prompts + scripts for ggplot2 / multi-language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Audit pass on PR #6944. Adds R/ggplot2 coverage to the surface areas the parent commits missed. Docs (no behavioural change, only counts and wording) - README, agentic/docs/project-guide.md, docs/concepts/vision.md, docs/concepts/library-expansion.md, docs/reference/style-guide.md, docs/reference/tagging-system.md, docs/reference/repository.md, prompts/README.md: every "9 libraries" / "nine ecosystems" line either generalised to "10 libraries (Python + R)" or rewritten so the count is derived from the canonical list. - library-expansion.md: Phase 3 marked as shipped in the rollout table; current-state line now reads "9 Python + 1 R". Prompts (extend Python-only assumptions) - prompts/quality-evaluator.md: role line generalised; code path uses {ext}; static-library lists include ggplot2. - prompts/workflow-prompts/ai-quality-review.md: implementation path uses {EXT}; static-library AR-08 check includes ggplot2. - prompts/workflow-prompts/impl-similarity-claude.md: sibling-source paths are no longer hardcoded to python/.py; uses {language}/{library}{ext}. Frontend - SpecPage SEO schema.org JSON-LD: programmingLanguage now maps r -> "R" (was lowercase, which downgrades SEO). Scripts - scripts/evaluate-plot.py: get_plot_paths picks the right file extension per language; the CLI exits cleanly for ggplot2 (Python-only AST + Rscript runner are out of scope for the local evaluator — directs users to the CI workflow). --- README.md | 5 +++-- agentic/docs/project-guide.md | 15 ++++++++++----- app/src/pages/SpecPage.tsx | 2 +- docs/concepts/library-expansion.md | 19 ++++++++++--------- docs/concepts/vision.md | 2 +- docs/reference/repository.md | 2 +- docs/reference/style-guide.md | 10 +++++----- docs/reference/tagging-system.md | 4 ++-- prompts/README.md | 2 +- prompts/quality-evaluator.md | 8 ++++---- prompts/workflow-prompts/ai-quality-review.md | 4 ++-- .../impl-similarity-claude.md | 4 ++-- scripts/evaluate-plot.py | 14 +++++++++++++- 13 files changed, 55 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 0526a86964..94a0f4cb91 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ checks ensure excellence. Zero manual coding required. ## Architecture **Specification-first design**: Every plot starts as a Markdown spec (library-agnostic), then AI generates -implementations for all 9 supported libraries. +implementations for all 10 supported libraries across Python and R. ``` plots/scatter-basic/ @@ -70,7 +70,8 @@ See [docs/reference/](docs/reference/) for details. **Frontend**: React 19 • Vite • TypeScript • MUI -**Plotting**: matplotlib • seaborn • plotly • bokeh • altair • plotnine • pygal • highcharts • lets-plot +**Plotting (Python)**: matplotlib • seaborn • plotly • bokeh • altair • plotnine • pygal • highcharts • lets-plot +**Plotting (R)**: ggplot2 **Infrastructure**: Google Cloud Run • Cloud SQL • Cloud Storage diff --git a/agentic/docs/project-guide.md b/agentic/docs/project-guide.md index c8b4156218..78e533ea1d 100644 --- a/agentic/docs/project-guide.md +++ b/agentic/docs/project-guide.md @@ -6,7 +6,9 @@ This document contains comprehensive project documentation for AI agents working **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. -**Supported Libraries** (9 total): +**Supported Libraries** (10 total): + +*Python (9):* - **matplotlib** - The classic standard, maximum flexibility - **seaborn** - Statistical visualizations, beautiful defaults - **plotly** - Interactive web plots, dashboards, 3D @@ -17,6 +19,9 @@ This document contains comprehensive project documentation for AI agents working - **highcharts** - Interactive web charts, stock charts (requires license for commercial use) - **lets-plot** - ggplot2 grammar of graphics by JetBrains, interactive +*R (1):* +- **ggplot2** - The de facto standard for R; grammar of graphics, layered chart composition + **Core Principle**: Community proposes plot ideas via GitHub Issues -> AI generates code -> AI quality review -> Deployed. ## Essential Commands @@ -139,7 +144,7 @@ Example: `plots/scatter-basic/` contains everything for the basic scatter plot. **Key benefits of per-library metadata:** - No merge conflicts (each library updates only its own file) -- Partial implementations OK (6/9 done = fine) +- Partial implementations OK (e.g. 6/10 done = fine) - Each library runs independently ### Spec ID Naming Convention @@ -169,7 +174,7 @@ Example: `plots/scatter-basic/` contains everything for the basic scatter plot. - `implementations/python/{library}.py`: Library-specific implementations - **`prompts/`**: AI agent prompts for code generation, quality evaluation, and tagging - `templates/`: Spec and metadata templates - - `library/`: Library-specific generation rules (9 files) + - `library/`: Library-specific generation rules (10 files) - **`core/`**: Shared business logic (database, repositories, config, utils, images) - **`core/database/types.py`**: Custom SQLAlchemy types (PostgreSQL + SQLite compatibility) - **`core/database/repositories.py`**: Data access layer @@ -440,7 +445,7 @@ The `prompts/` directory contains AI agent prompts for code generation, quality |------|---------| | `plot-generator.md` | Base rules for all plot implementations | | `default-style-guide.md` | Default visual style rules (colors, typography, layout) | -| `library/*.md` | Library-specific rules (9 files) | +| `library/*.md` | Library-specific rules (10 files) | | `quality-criteria.md` | Definition of code/visual quality | | `quality-evaluator.md` | AI quality evaluation prompt | | `spec-id-generator.md` | Assigns unique spec IDs | @@ -685,7 +690,7 @@ uv run python -m automation.scripts.label_manager list ### Implementation Labels (on specification issue) - **`generate:{library}`** - Trigger single library generation (e.g., `generate:matplotlib`) -- **`generate:all`** - Trigger all 9 libraries via bulk-generate +- **`generate:all`** - Trigger all 10 libraries via bulk-generate - **`impl:{library}:pending`** - Generation in progress - **`impl:{library}:done`** - Implementation merged to main - **`impl:{library}:failed`** - Max retries exhausted (3 attempts) diff --git a/app/src/pages/SpecPage.tsx b/app/src/pages/SpecPage.tsx index f98a25d1c4..de50197b3f 100644 --- a/app/src/pages/SpecPage.tsx +++ b/app/src/pages/SpecPage.tsx @@ -355,7 +355,7 @@ export function SpecPage() { '@type': 'SoftwareSourceCode', name: `${specData.title} (${selectedLibrary})`, description: specData.description, - programmingLanguage: urlLanguage === 'python' ? 'Python' : urlLanguage, + programmingLanguage: { python: 'Python', r: 'R' }[urlLanguage ?? ''] ?? urlLanguage, codeSampleType: 'code snippet', codeRepository: 'https://github.com/MarkusNeusinger/anyplot', url: canonical, diff --git a/docs/concepts/library-expansion.md b/docs/concepts/library-expansion.md index fb518c3467..464be55255 100644 --- a/docs/concepts/library-expansion.md +++ b/docs/concepts/library-expansion.md @@ -14,7 +14,8 @@ priority order. ## 1. Current state -anyplot currently ships **9 Python libraries** and zero non-Python libraries. +anyplot currently ships **9 Python libraries** and **1 R library** (ggplot2 — +the first non-Python entry; landed as Phase 3 of the rollout below). | # | Library | Native | In anyplot as | Most-used variant | Notes | |---|------------|------------|---------------|-------------------|---------------------------------------------| @@ -296,14 +297,14 @@ Ranked by `reach × ease-of-integration ÷ duplication-risk`. ## 8. Suggested phased rollout -| Phase | Adds | New languages | Cumulative library count | -|-------|-------------------------------------|---------------|--------------------------| -| 0 (today) | — | Python | 9 | -| 1 | Chart.js, D3.js, ECharts | + JavaScript | 12 | -| 2 | Highcharts (replaces Python entry) | — | 12 | -| 3 | ggplot2 | + R | 13 | -| 4 | Recharts, Observable Plot | — | 15 | -| 5 | Makie.jl, ApexCharts | + Julia | 17 | +| Phase | Adds | New languages | Cumulative library count | Status | +|-------|-------------------------------------|---------------|--------------------------|----------| +| 0 | — | Python | 9 | shipped | +| 1 | Chart.js, D3.js, ECharts | + JavaScript | 12 | planned | +| 2 | Highcharts (replaces Python entry) | — | 12 | planned | +| 3 | **ggplot2** | **+ R** | **10** | **shipped** (Phase 3 was implemented before Phases 1+2; net total is 10 not 13 until JS lands) | +| 4 | Recharts, Observable Plot | — | TBD | planned | +| 5 | Makie.jl, ApexCharts | + Julia | TBD | planned | > **Why Phase 2 ≠ Tier 2 #4 from §7.** §7 ranks ggplot2 (Tier 2 #4) above > Highcharts (Tier 2 #5) by reach; §8 still ships Highcharts first because diff --git a/docs/concepts/vision.md b/docs/concepts/vision.md index f468eb4570..f8f5709392 100644 --- a/docs/concepts/vision.md +++ b/docs/concepts/vision.md @@ -42,7 +42,7 @@ anyplot is different: **AI curates and maintains, humans discover and choose.** ### What Makes It Different -- **Library Agnostic**: Compare libraries side-by-side. Nine ecosystems today (matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, lets-plot); the architecture is designed to welcome more +- **Library Agnostic**: Compare libraries side-by-side. Ten ecosystems across two languages today — Python (matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, lets-plot) and R (ggplot2); the architecture is designed to welcome more - **Always Current**: AI agents keep examples updated and optimized - **Multi-Dimensional Discovery**: Find plots by domain (finance, research), type (scatter, bar), or specific use case ( ROC curve, Sankey diagram) diff --git a/docs/reference/repository.md b/docs/reference/repository.md index f663577a3b..2d51ceba1f 100644 --- a/docs/reference/repository.md +++ b/docs/reference/repository.md @@ -67,7 +67,7 @@ anyplot/ │ ├── quality-evaluator.md # AI quality evaluation prompt │ ├── spec-id-generator.md # Assigns spec IDs │ ├── default-style-guide.md # Default visual style rules -│ ├── library/ # Library-specific rules (9 files) +│ ├── library/ # Library-specific rules (10 files: 9 Python + ggplot2) │ │ ├── matplotlib.md │ │ ├── seaborn.md │ │ └── ... diff --git a/docs/reference/style-guide.md b/docs/reference/style-guide.md index 8ba5ee9a8a..5ffd0af1cb 100644 --- a/docs/reference/style-guide.md +++ b/docs/reference/style-guide.md @@ -20,7 +20,7 @@ anyplot.ai is a considered reference work styled like a code editor over a paper |-------|------|--------| | **Brand** | Identity, voice, logo, tone of communication | Lowercase default; `any.plot()` wordmark with green dot | | **Frontend** | Website visual language: typography, layout, components | Editorial-scientific paper × terminal/code-editor overlay | -| **Plots** | Color palette for every visualization across all 9 libraries | Okabe-Ito 8-color categorical, brand `#009E73` first | +| **Plots** | Color palette for every visualization across all 10 libraries (9 Python + R/ggplot2) | Okabe-Ito 8-color categorical, brand `#009E73` first | **Aesthetic direction:** `arXiv paper` × `tmux/lazygit` rather than `SaaS dashboard` or `AI startup`. The reader should feel they're browsing a curated journal that happens to live inside a terminal — section headers carry shell prompts (`❯`, `$`, `~/plots/`), hero text types itself with a blinking cursor, action buttons read as method calls (`.copy()`, `.open()`, `.download()`). @@ -266,7 +266,7 @@ The lowercase default matches the code-forward aesthetic and keeps the site feel > Instead of: `🚀 Supercharge your data viz workflow with AI-powered plot generation! Unlock 1000+ beautifully crafted charts across multiple libraries. Ship faster, iterate smarter, and empower your team to create stunning visualizations!` > -> Write: `A catalogue of 1,000+ plotting examples across nine Python libraries. Plot ideas come from humans; AI drafts the spec, generates code for every library, and reviews each implementation. Humans approve specs and tune the rules when something repeatedly fails. Every example uses the same colorblind-safe palette, so switching libraries never breaks your color grammar.` +> Write: `A catalogue of 1,000+ plotting examples across ten libraries in two languages (Python + R). Plot ideas come from humans; AI drafts the spec, generates code for every library, and reviews each implementation. Humans approve specs and tune the rules when something repeatedly fails. Every example uses the same colorblind-safe palette, so switching libraries never breaks your color grammar.` The second version is shorter, says what's actually happening (humans submit ideas + approve + tune; AI does the drafting/generating/reviewing), and reads as if written by someone who cares about what they're building. @@ -293,13 +293,13 @@ The second version is shorter, says what's actually happening (humans submit ide Narrative hooks for talking about anyplot — adapt to context, don't recite verbatim: -**The pipeline story:** Humans submit plot ideas as GitHub issues. AI drafts the spec from each idea; humans approve it before any code is generated. AI then generates implementations for all nine libraries from the same spec and reviews each one for visual quality and spec compliance. When something doesn't pass review, AI retries — and when it keeps failing, we refine the spec or tune the AI rules. We never patch the code by hand. That makes anyplot a catalogue that maintains itself: when matplotlib ships a new release, we re-run the pipeline; when a better example pattern emerges, we update the spec and every library regenerates. Humans curate; AI executes. +**The pipeline story:** Humans submit plot ideas as GitHub issues. AI drafts the spec from each idea; humans approve it before any code is generated. AI then generates implementations across every supported library from the same spec — Python for most, R for ggplot2 — and reviews each one for visual quality and spec compliance. When something doesn't pass review, AI retries — and when it keeps failing, we refine the spec or tune the AI rules. We never patch the code by hand. That makes anyplot a catalogue that maintains itself: when matplotlib ships a new release, we re-run the pipeline; when a better example pattern emerges, we update the spec and every library regenerates. Humans curate; AI executes. **The palette story:** Every plot uses the Okabe-Ito palette, peer-reviewed for colorblind safety and designed for scientific publications in 2008. About 8% of men have some form of color vision deficiency — most plotting libraries ignore this entirely. We make it the default. **The library-agnostic story:** A "Gentoo penguin" is always blue, whether you draw it in matplotlib, plotly, or bokeh. The palette travels with you across libraries. Switching tools doesn't mean re-learning your color grammar. -**The catalogue story:** A thousand examples across nine libraries, each reproducible and copy-pasteable. No ads. No affiliate links. No "suggested tutorials you might like." Just the plots and the code that made them. +**The catalogue story:** A thousand examples across ten libraries in two languages, each reproducible and copy-pasteable. No ads. No affiliate links. No "suggested tutorials you might like." Just the plots and the code that made them. **The origin story:** It started as pyplots.ai, a small Python-only catalogue I built in a weekend. It grew when I realized people wanted the same examples across different libraries, and the same safe palette everywhere. anyplot is the grown-up version. @@ -322,7 +322,7 @@ Narrative hooks for talking about anyplot — adapt to context, don't recite ver **Examples and identifiers:** - A **slug** (URL-safe): `matplotlib-scatter-penguins-species` - A **title** (human-readable): `Scatter plot of penguin species` -- A **library**: one of the nine supported +- A **library**: one of the ten supported (Python or R) - **Tags**: chart type, data domain, complexity level Slugs are the canonical identifier. They're used in URLs, filenames, and `ap.load()` calls. diff --git a/docs/reference/tagging-system.md b/docs/reference/tagging-system.md index 4f61ec4f11..f38c8400b6 100644 --- a/docs/reference/tagging-system.md +++ b/docs/reference/tagging-system.md @@ -165,8 +165,8 @@ impl_tags: | Aspect | Spec-Level Tags | Impl-Level Tags | |--------|-----------------|-----------------| | **Describes** | WHAT is visualized | HOW it is implemented | -| **Storage Location** | `specification.yaml` | `metadata/python/{library}.yaml` | -| **Applies to** | All 9 libraries | Only this library | +| **Storage Location** | `specification.yaml` | `metadata/{language}/{library}.yaml` | +| **Applies to** | All libraries (every language) | Only this library | | **Assigned by** | `spec-create.yml` | `impl-review.yml` | | **Dimensions** | 4 (plot_type, data_type, domain, features) | 5 (dependencies, techniques, patterns, dataprep, styling) | | **Example** | "This is a scatter plot" | "This code uses twin-axes and scipy" | diff --git a/prompts/README.md b/prompts/README.md index 48377279a1..1b659447a6 100644 --- a/prompts/README.md +++ b/prompts/README.md @@ -13,7 +13,7 @@ Git history shows all changes (`git log -p prompts/plot-generator.md`). |------|-------|------| | `plot-generator.md` | Plot Generator | Base rules for all plot implementations | | `default-style-guide.md` | Plot Generator | Default visual style (colors, typography, layout) | -| `library/*.md` | Plot Generator | Library-specific rules (9 files) | +| `library/*.md` | Plot Generator | Library-specific rules (10 files: 9 Python + ggplot2) | | `quality-criteria.md` | All | Definition of what "good code" means | | `quality-evaluator.md` | Quality Checker | AI quality evaluation | | `spec-id-generator.md` | Spec ID Generator | Assigns unique spec IDs | diff --git a/prompts/quality-evaluator.md b/prompts/quality-evaluator.md index 3139c4a835..9fd2bd07c9 100644 --- a/prompts/quality-evaluator.md +++ b/prompts/quality-evaluator.md @@ -2,7 +2,7 @@ ## Role -You are a strict code reviewer for Python data visualizations. You evaluate plot implementations against `prompts/quality-criteria.md`. +You are a strict code reviewer for data visualizations. Most implementations are Python; ggplot2 is R. You evaluate plot implementations against `prompts/quality-criteria.md`. ## Two-Stage Evaluation @@ -37,7 +37,7 @@ You evaluate implementations that passed all auto-reject checks. Focus purely on ## Input 1. **Specification**: From `plots/{spec-id}/specification.md` -2. **Code**: From `plots/{spec-id}/implementations/{language}/{library}.py` +2. **Code**: From `plots/{spec-id}/implementations/{language}/{library}{ext}` — `{ext}` is `.py` for python libraries and `.R` for ggplot2 3. **Previews**: BOTH theme renders of the plot image — `plot-light.png` and `plot-dark.png`. You must inspect both. For interactive libraries, also `plot-light.html` and `plot-dark.html`. 4. **Library Rules**: From `prompts/library/{library}.md` 5. **Style Guide** (canonical palette + theme tokens): `prompts/default-style-guide.md` — consult its "Categorical Palette", "Continuous Data", "Background", and "Theme-adaptive Chrome" sections for VQ-07 scoring. @@ -161,7 +161,7 @@ You evaluate implementations that passed all auto-reject checks. Focus purely on ### Step 0: Check for Fake Functionality (AR-08) -**For static libraries (matplotlib, seaborn, plotnine) only:** +**For static libraries (matplotlib, seaborn, plotnine, ggplot2) only:** Scan the code and image for: - Simulated tooltips, hover states, or selection states @@ -310,7 +310,7 @@ These features **add significant value** in the HTML output. The PNG is just a s ## Static Libraries and Interactive Specs -**For matplotlib, seaborn, plotnine:** +**For matplotlib, seaborn, plotnine, ggplot2:** These libraries produce static PNG only. When evaluating their implementations of specs that mention interactive features: diff --git a/prompts/workflow-prompts/ai-quality-review.md b/prompts/workflow-prompts/ai-quality-review.md index d1c13025cd..f4c4c3d8d9 100644 --- a/prompts/workflow-prompts/ai-quality-review.md +++ b/prompts/workflow-prompts/ai-quality-review.md @@ -17,7 +17,7 @@ Evaluate if the **${LIBRARY}** implementation matches the specification for `${S - Note all required features ### 2. Read the Implementation -`plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}.py` +`plots/${SPEC_ID}/implementations/${LANGUAGE}/${LIBRARY}${EXT}` — `${EXT}` is `.py` for python libraries (matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, letsplot) and `.R` for ggplot2 ### 3. Read Library-Specific Rules `prompts/library/${LIBRARY}.md` @@ -67,7 +67,7 @@ A plot that's perfect in one theme but unreadable in the other still **fails** ### 6. Check for Auto-Reject (AR-08) -**For static libraries (matplotlib, seaborn, plotnine) only:** +**For static libraries (matplotlib, seaborn, plotnine, ggplot2) only:** Before scoring, check if the implementation fakes interactive features: - Simulated tooltips (annotation boxes styled as hover tooltips) diff --git a/prompts/workflow-prompts/impl-similarity-claude.md b/prompts/workflow-prompts/impl-similarity-claude.md index d6f2f837dd..6d724ac62e 100644 --- a/prompts/workflow-prompts/impl-similarity-claude.md +++ b/prompts/workflow-prompts/impl-similarity-claude.md @@ -48,9 +48,9 @@ If a candidate cluster's identical signal is *only* one of the mandated items ab ## Step 4: Inspect ambiguous clusters (optional) -If the `image_description` blobs for a candidate cluster don't conclusively show copying — e.g. you can't tell whether two libraries used the same random seed, or whether their domain is genuinely the same — you MAY use the Read tool on `plots/{SPEC_ID}/implementations/python/{library}.py` for **only those libraries inside the candidate cluster** to verify. +If the `image_description` blobs for a candidate cluster don't conclusively show copying — e.g. you can't tell whether two libraries used the same random seed, or whether their domain is genuinely the same — you MAY use the Read tool on `plots/{SPEC_ID}/implementations/{language}/{library}{ext}` for **only those libraries inside the candidate cluster** to verify. (`{language}` is `python` for the Python libraries and `r` for ggplot2; `{ext}` is `.py` or `.R` accordingly.) -**Do not read .py files for libraries that are not in a candidate cluster.** That wastes tokens and is not what this audit is for. +**Do not read sibling source files for libraries that are not in a candidate cluster.** That wastes tokens and is not what this audit is for. ## Step 5: Build the hint per cluster diff --git a/scripts/evaluate-plot.py b/scripts/evaluate-plot.py index 5ea5a3b29b..bc1843558c 100755 --- a/scripts/evaluate-plot.py +++ b/scripts/evaluate-plot.py @@ -137,9 +137,12 @@ def get_plot_paths(spec_id: str, library: str, language: str = "python") -> dict """Get all relevant paths for a plot implementation.""" plots_dir = PROJECT_ROOT / "plots" / spec_id impl_dir = plots_dir / "implementations" / language + # File extension follows the implementation language. ggplot2 is the only + # non-Python entry today; extend this when more languages join. + ext = ".R" if language == "r" else ".py" return { "spec": plots_dir / "specification.md", - "impl": impl_dir / f"{library}.py", + "impl": impl_dir / f"{library}{ext}", "metadata": plots_dir / "metadata" / language / f"{library}.yaml", "image": impl_dir / "plot.png", "image_light": impl_dir / "plot-light.png", @@ -585,6 +588,15 @@ def main(): print(f"Evaluating: {args.spec_id} / {library}") print("="*60) + # Local evaluator currently only supports Python implementations + # (AR-01 parses Python AST, AR-02 runs `python script.py`). For R + # implementations (ggplot2), use the CI workflow instead: + # gh workflow run impl-generate.yml -f specification_id= -f library=ggplot2 + if library == "ggplot2": + print("⚠ Skipping ggplot2: this local evaluator is Python-only.") + print(" Use the CI workflow for R: gh workflow run impl-generate.yml -f library=ggplot2 -f specification_id=" + args.spec_id) + continue + paths = get_plot_paths(args.spec_id, library) if not paths["impl"].exists(): From a07b6f2240457704a3040d50029bfa52ef2078d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 20:27:56 +0000 Subject: [PATCH 5/6] fix(setup-r): use real action ref instead of bogus pinned SHA The two r-lib/actions references were pinned to a SHA I fabricated. GitHub would have rejected the workflow at first run with "unable to resolve action". Switched to @v2 (the maintained moving tag for r-lib/actions). Dependabot will pin it to a real SHA on its next run, consistent with the rest of the repo's pinning policy. Also threaded RENV_CONFIG_INSTALL_HASHES=FALSE to setup-renv so the hand-written hash-less lockfile restores without renv refusing. The pinned Posit RSPM snapshot URL in renv.lock provides reproducibility; once CI's first successful run regenerates renv.lock with hashes, the flag can drop. --- .github/actions/setup-r/action.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup-r/action.yml b/.github/actions/setup-r/action.yml index 51d51a0dd1..cf254a0e1b 100644 --- a/.github/actions/setup-r/action.yml +++ b/.github/actions/setup-r/action.yml @@ -18,7 +18,9 @@ runs: using: "composite" steps: - name: Install R - uses: r-lib/actions/setup-r@e2e21ddc06d49ebc3007bf9d52aa66f8d4b1d8f3 # v2 + # 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 with: r-version: ${{ inputs.r-version }} use-public-rspm: true @@ -44,9 +46,16 @@ runs: libfribidi-dev - name: Install R packages from renv.lock - uses: r-lib/actions/setup-renv@e2e21ddc06d49ebc3007bf9d52aa66f8d4b1d8f3 # v2 + uses: r-lib/actions/setup-renv@v2 with: working-directory: ${{ inputs.working-directory }} + env: + # renv refuses to restore when per-package `Hash` fields are missing. + # The lockfile here is hand-written and intentionally omits hashes — + # reproducibility comes from the pinned Posit RSPM snapshot URL in + # renv.lock instead. Once a successful CI run populates hashes via + # renv::snapshot(), this flag can be dropped. + RENV_CONFIG_INSTALL_HASHES: "FALSE" - name: Smoke-test ggplot2 renders a PNG shell: bash From ce724e86635e0e6dfe89282c9216af3d917a596d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 16 May 2026 20:33:58 +0000 Subject: [PATCH 6/6] fix(review): address second Copilot review on PR #6944 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more review comments on commit a07b6f2: 1. PlotOfTheDay still had hardcoded `python` + `.py` in the source-link href and the displayed command string — only the runtime label rendering was language- aware. If POTD ever picks ggplot2 the GitHub link 404s and the chip lies about how to run the file. Now derives `ext` and `runner` from `data.language`, so the chip flips to `Rscript plots/.../ggplot2.R` and the GitHub URL points at the .R file. PlotOfTheDayTerminal had the same bug (it builds the same filename + GitHub URL) — fixed alongside. 2. scripts/evaluate-plot.py was duplicating the language->extension mapping with a local ternary and keyed the skip off `library == "ggplot2"` instead of the underlying language. Now imports LANGUAGE_FILE_EXTENSIONS from core.constants (single source of truth shared with sync_to_postgres) and skips any non-python library generically — automatically covers future R/JS/Julia entries without another edit. SUPPORTED_LIBRARIES is now derived from LIBRARIES_METADATA filtered by language_id=python, so it can't drift. --- app/src/components/PlotOfTheDay.tsx | 45 ++++++++++++--------- app/src/components/PlotOfTheDayTerminal.tsx | 11 +++-- scripts/evaluate-plot.py | 38 ++++++++++------- 3 files changed, 58 insertions(+), 36 deletions(-) diff --git a/app/src/components/PlotOfTheDay.tsx b/app/src/components/PlotOfTheDay.tsx index b49a89209c..86863f6a6f 100644 --- a/app/src/components/PlotOfTheDay.tsx +++ b/app/src/components/PlotOfTheDay.tsx @@ -101,24 +101,33 @@ export function PlotOfTheDay() { gap: 0.75, }}> $ - { - e.stopPropagation(); - trackEvent('nav_click', { source: 'potd_source_link', target: 'github', spec: data.spec_id, library: data.library_id }); - }} - sx={{ - fontFamily: mono, fontSize: fontSize.xxs, color: semanticColors.mutedText, - flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', - textDecoration: 'none', - '&:hover': { color: colors.primary }, - }} - > - python plots/{data.spec_id}/{data.library_id}.py - + {(() => { + // Per-language file extension + runner command. Anyplot ships Python + // for nine libraries and R for ggplot2; the chip mimics what a user + // would actually type into a shell, so the runner label flips too. + const ext = data.language === 'r' ? '.R' : '.py'; + const runner = data.language === 'r' ? 'Rscript' : 'python'; + return ( + { + e.stopPropagation(); + trackEvent('nav_click', { source: 'potd_source_link', target: 'github', spec: data.spec_id, library: data.library_id }); + }} + sx={{ + fontFamily: mono, fontSize: fontSize.xxs, color: semanticColors.mutedText, + flex: 1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', + textDecoration: 'none', + '&:hover': { color: colors.primary }, + }} + > + {runner} plots/{data.spec_id}/{data.library_id}{ext} + + ); + })()} language_id, used for path resolution and the +# Python-only skip. Falls back to "python" for legacy callers that pass a +# library not in LIBRARIES_METADATA (e.g. local test fixtures). +LIBRARY_LANGUAGE = {lib["id"]: lib["language_id"] for lib in LIBRARIES_METADATA} # Library-specific plot function patterns LIBRARY_PATTERNS = { @@ -137,9 +143,11 @@ def get_plot_paths(spec_id: str, library: str, language: str = "python") -> dict """Get all relevant paths for a plot implementation.""" plots_dir = PROJECT_ROOT / "plots" / spec_id impl_dir = plots_dir / "implementations" / language - # File extension follows the implementation language. ggplot2 is the only - # non-Python entry today; extend this when more languages join. - ext = ".R" if language == "r" else ".py" + # File extension comes from the canonical LANGUAGE_FILE_EXTENSIONS mapping + # in core.constants — the single source of truth shared with + # automation/scripts/sync_to_postgres.py. Falls back to ".py" so callers + # passing an unknown language id don't crash here. + ext = LANGUAGE_FILE_EXTENSIONS.get(language, ".py") return { "spec": plots_dir / "specification.md", "impl": impl_dir / f"{library}{ext}", @@ -589,15 +597,17 @@ def main(): print("="*60) # Local evaluator currently only supports Python implementations - # (AR-01 parses Python AST, AR-02 runs `python script.py`). For R - # implementations (ggplot2), use the CI workflow instead: - # gh workflow run impl-generate.yml -f specification_id= -f library=ggplot2 - if library == "ggplot2": - print("⚠ Skipping ggplot2: this local evaluator is Python-only.") - print(" Use the CI workflow for R: gh workflow run impl-generate.yml -f library=ggplot2 -f specification_id=" + args.spec_id) + # (AR-01 parses a Python AST, AR-02 runs `python script.py`). Skip any + # non-Python library based on its declared language in + # LIBRARIES_METADATA so this automatically covers future R/JS/Julia + # entries without another code edit. Direct users at the CI workflow. + library_language = LIBRARY_LANGUAGE.get(library, "python") + if library_language != "python": + print(f"⚠ Skipping {library}: this local evaluator is Python-only (language={library_language}).") + print(f" Use the CI workflow instead: gh workflow run impl-generate.yml -f library={library} -f specification_id={args.spec_id}") continue - paths = get_plot_paths(args.spec_id, library) + paths = get_plot_paths(args.spec_id, library, language=library_language) if not paths["impl"].exists(): print(f"⚠ Skipping: implementation not found")