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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .github/actions/setup-r/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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
# 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
Comment on lines +20 to +26

# 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@v2
with:
working-directory: ${{ inputs.working-directory }}
env:
Comment on lines +48 to +52
# 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
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 = "")
'
5 changes: 3 additions & 2 deletions .github/workflows/bulk-generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ on:
- pygal
- highcharts
- letsplot
- ggplot2
dry_run:
description: "List what would be generated without executing"
type: boolean
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/daily-regen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}" \
Expand Down
119 changes: 87 additions & 32 deletions .github/workflows/impl-generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ on:
- pygal
- highcharts
- letsplot
- ggplot2
issue_number:
description: "Issue number (optional, for tracking)"
required: false
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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: |
Expand All @@ -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@2e1d749697dd1612b833dba4a722266286fbefcd # 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
# ========================================================================
Expand All @@ -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
Expand Down Expand Up @@ -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 }}

Expand All @@ -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 }}

Expand All @@ -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 }}
Expand Down Expand Up @@ -424,39 +455,60 @@ jobs:

mkdir -p "$METADATA_DIR"

# Get Python version (e.g., "3.13.1" from "Python 3.13.1")
# 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
LANGUAGE_VERSION=$(Rscript -e 'cat(as.character(getRversion()))' 2>/dev/null || echo "unknown")
else
LANGUAGE_VERSION="$PYTHON_VERSION"
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 $LANGUAGE_VERSION, pipeline python $PYTHON_VERSION)"

# Interactive libraries additionally produce HTML previews (one per theme)
HAS_HTML="false"
Expand All @@ -475,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'
Expand Down Expand Up @@ -508,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',
Expand All @@ -527,7 +581,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
Expand Down Expand Up @@ -625,6 +679,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: |
Expand All @@ -643,7 +698,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}
Expand Down
21 changes: 17 additions & 4 deletions .github/workflows/impl-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down
Loading