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
79 changes: 79 additions & 0 deletions .github/actions/setup-julia/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: "Setup Julia + Makie.jl"
description: >-
Installs Julia, the system libraries needed for headless plot rendering with
CairoMakie, and the package set used by anyplot's Julia implementations
(Makie + CairoMakie + DataFrames + dataset packages). Packages are restored
from the committed top-level Project.toml / Manifest.toml for reproducibility.
inputs:
julia-version:
description: "Julia version to install"
required: false
default: "1.11"

runs:
using: "composite"
steps:
- name: Install Julia
# julia-actions/setup-julia ships every sub-action from one repo and
# tags them together; `@v2` is the maintained moving tag. Dependabot
# will pin to a SHA on first run.
uses: julia-actions/setup-julia@v2
with:
version: ${{ inputs.julia-version }}

# CairoMakie bundles Cairo via Cairo_jll, so apt installs are not strictly
# required; keep the fallback in place so the action is robust against
# future runner-image changes that affect font rendering.
- name: Install system dependencies for CairoMakie
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libfontconfig1-dev \
libfreetype6-dev \
libpng-dev

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

- name: Install Julia packages
# `Pkg.instantiate()` restores the exact versions pinned in
# Manifest.toml. If Manifest.toml is absent (first run before the
# lockfile is committed), `Pkg.add(...)` falls back to fresh resolution
# of the packages we need so impl runs don't hard-fail. The fallback
# list intentionally excludes Julia stdlibs (`Random`, `Statistics`):
# they ship with Julia itself, are not in the General registry, and
# `Pkg.add("Random")` would error before precompile. Stdlibs stay in
# Project.toml `[deps]` for the post-Manifest path.
shell: bash
run: |
julia --project=. -e '
using Pkg
if isfile("Manifest.toml")
Pkg.instantiate()
else
Pkg.add([
"CairoMakie", "Makie", "DataFrames", "CSV",
"Colors", "ColorSchemes", "RDatasets",
"PalmerPenguins"
])
Comment on lines +52 to +61
Pkg.precompile()
end
'

- name: Smoke-test CairoMakie renders a PNG
shell: bash
run: |
julia --project=. -e '
using CairoMakie
tmp = tempname() * ".png"
fig = Figure(resolution = (400, 300))
ax = Axis(fig[1, 1])
scatter!(ax, 1:10, rand(10))
save(tmp, fig)
@assert isfile(tmp) "smoke-test PNG was not created"
@assert filesize(tmp) > 0 "smoke-test PNG is empty"
println("ok: ", tmp, " (", filesize(tmp), " bytes)")
'
3 changes: 2 additions & 1 deletion .github/workflows/bulk-generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ on:
- highcharts
- letsplot
- ggplot2
- makie
dry_run:
description: "List what would be generated without executing"
type: boolean
Expand All @@ -53,7 +54,7 @@ on:
default: '{}'

env:
ALL_LIBRARIES: "matplotlib seaborn plotly bokeh altair plotnine pygal highcharts letsplot ggplot2"
ALL_LIBRARIES: "matplotlib seaborn plotly bokeh altair plotnine pygal highcharts letsplot ggplot2 makie"

# Serialise bulk-generate runs. Each run paces its own dispatches with
# `pace_seconds`; letting two runs overlap would interleave their
Expand Down
47 changes: 45 additions & 2 deletions .github/workflows/impl-generate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ on:
- highcharts
- letsplot
- ggplot2
- makie
issue_number:
description: "Issue number (optional, for tracking)"
required: false
Expand Down Expand Up @@ -119,6 +120,10 @@ jobs:
LANGUAGE="r"
EXT=".R"
;;
makie)
LANGUAGE="julia"
EXT=".jl"
;;
*)
LANGUAGE="python"
EXT=".py"
Expand Down Expand Up @@ -246,6 +251,10 @@ jobs:
if: steps.inputs.outputs.language == 'r'
uses: ./.github/actions/setup-r

- name: Setup Julia + Makie
if: steps.inputs.outputs.language == 'julia'
uses: ./.github/actions/setup-julia

# ========================================================================
# Generate: Create implementation branch and code
# ========================================================================
Expand Down Expand Up @@ -511,13 +520,20 @@ jobs:
# Rscript exits non-zero after partial stdout.
LANGUAGE_VERSION=$(RENV_CONFIG_STARTUP_QUIET=TRUE Rscript -e 'cat(as.character(getRversion()))' 2>/dev/null | tail -n1)
is_version "$LANGUAGE_VERSION" || LANGUAGE_VERSION="unknown"
elif [ "$LANGUAGE" = "julia" ]; then
# `julia -e 'print(VERSION)'` prints `1.11.2` (or similar) on stdout.
# tail -n1 + is_version reject anything else that might leak in or land
# if julia exits non-zero after partial stdout.
LANGUAGE_VERSION=$(julia -e 'print(VERSION)' 2>/dev/null | tail -n1)
is_version "$LANGUAGE_VERSION" || LANGUAGE_VERSION="unknown"
else
LANGUAGE_VERSION="$PYTHON_VERSION"
fi

# 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.
# packageVersion() inside Rscript; Julia libs via Pkg API.
# 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}'
}
Expand All @@ -526,9 +542,36 @@ jobs:
v=$(RENV_CONFIG_STARTUP_QUIET=TRUE Rscript -e "cat(as.character(packageVersion('$1')))" 2>/dev/null | tail -n1)
is_version "$v" && printf '%s' "$v"
}
get_julia_version() {
# Pkg.dependencies() returns a Dict{UUID, PackageInfo}; locate the
# entry by name and print its `.version`. Any version-parse failure
# gets rejected by the is_version check downstream.
local v
v=$(julia --project=. -e "
using Pkg
for (_, info) in Pkg.dependencies()
if info.name == \"$1\"
print(info.version)
break
end
end
" 2>/dev/null | tail -n1)
is_version "$v" && printf '%s' "$v"
}

if [ "$LANGUAGE" = "r" ]; then
LIBRARY_VERSION=$(get_r_version "$LIBRARY")
elif [ "$LANGUAGE" = "julia" ]; then
# Catalogue id `makie` maps to the Julia package `Makie` (CairoMakie
# bundles the same version; we report the user-facing Makie version).
case "$LIBRARY" in
makie)
LIBRARY_VERSION=$(get_julia_version "Makie")
;;
*)
LIBRARY_VERSION=$(get_julia_version "$LIBRARY")
;;
esac
else
case "$LIBRARY" in
letsplot)
Expand Down
20 changes: 13 additions & 7 deletions .github/workflows/impl-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ jobs:
LANGUAGE="r"
EXT=".R"
;;
makie)
LANGUAGE="julia"
EXT=".jl"
;;
*)
LANGUAGE="python"
EXT=".py"
Expand Down Expand Up @@ -331,8 +335,9 @@ jobs:
ISSUE: ${{ steps.issue.outputs.number }}
SPEC_ID: ${{ steps.extract.outputs.specification_id }}
run: |
# All 9 supported libraries
LIBRARIES="matplotlib seaborn plotly bokeh altair plotnine pygal highcharts letsplot"
# All supported libraries (mirrors core/constants.py SUPPORTED_LIBRARIES).
# ggplot2 (R) and makie (Julia) are non-Python entries; everything else is Python.
LIBRARIES="matplotlib seaborn plotly bokeh altair plotnine pygal highcharts letsplot ggplot2 makie"

# Get current labels on the issue
LABELS=$(gh issue view "$ISSUE" --json labels -q '.labels[].name' 2>/dev/null || echo "")
Expand All @@ -353,11 +358,12 @@ jobs:
fi
done

TOTAL_LIBS=$(echo "$LIBRARIES" | wc -w | tr -d ' ')
TOTAL=$((DONE_COUNT + FAILED_COUNT))
echo "::notice::Libraries: $DONE_COUNT done, $FAILED_COUNT failed, $TOTAL/9 total"
echo "::notice::Libraries: $DONE_COUNT done, $FAILED_COUNT failed, $TOTAL/$TOTAL_LIBS total"

# Close issue if all 9 libraries are done OR done+failed=9
if [ "$TOTAL" -eq 9 ]; then
# Close issue if all supported libraries are done OR done+failed=TOTAL_LIBS
if [ "$TOTAL" -eq "$TOTAL_LIBS" ]; then
# Build status table
TABLE="| Library | Status |\n|---------|--------|"
for lib in $LIBRARIES; do
Expand All @@ -370,10 +376,10 @@ jobs:

if [ "$FAILED_COUNT" -eq 0 ]; then
TITLE=":tada: All Implementations Complete!"
SUMMARY="All 9 library implementations for \`${SPEC_ID}\` have been successfully merged."
SUMMARY="All ${TOTAL_LIBS} library implementations for \`${SPEC_ID}\` have been successfully merged."
else
TITLE=":white_check_mark: Implementations Complete"
SUMMARY="${DONE_COUNT}/9 implementations merged, ${FAILED_COUNT} libraries could not implement this plot type."
SUMMARY="${DONE_COUNT}/${TOTAL_LIBS} implementations merged, ${FAILED_COUNT} libraries could not implement this plot type."
fi

gh issue comment "$ISSUE" --body "## ${TITLE}
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/impl-repair.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ jobs:
LANGUAGE="r"
EXT=".R"
;;
makie)
LANGUAGE="julia"
EXT=".jl"
;;
*)
LANGUAGE="python"
EXT=".py"
Expand Down Expand Up @@ -119,6 +123,10 @@ jobs:
if: steps.lang.outputs.language == 'r'
uses: ./.github/actions/setup-r

- name: Setup Julia + Makie
if: steps.lang.outputs.language == 'julia'
uses: ./.github/actions/setup-julia

- name: Extract AI feedback from PR
id: feedback
env:
Expand Down
34 changes: 33 additions & 1 deletion .github/workflows/impl-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ jobs:
LANGUAGE="r"
EXT=".R"
;;
makie)
LANGUAGE="julia"
EXT=".jl"
;;
*)
LANGUAGE="python"
EXT=".py"
Expand Down Expand Up @@ -263,6 +267,7 @@ jobs:
"plotnine": "Check `ggsave(width=…, height=…, units='in', dpi=400)` and `theme(figure_size=…)`; do not pass `bbox_inches='tight'`.",
"letsplot": "Check `ggsize(W, H)` and `ggsave(..., scale=4)` pair — only the two canonical pairs land on target.",
"ggplot2": "Check `ggsave(width=…, height=…, units='in', dpi=400)` with `ragg::agg_png`.",
"makie": "Check `Figure(resolution=(1600, 900))` + `save(..., px_per_unit=2)` (landscape) or `resolution=(1200, 1200)` + `px_per_unit=2` (square) — those are the only two canonical pairs.",
}
cause = causes.get(LIBRARY, f"Review `prompts/library/{LIBRARY}.md` 'Canvas — hard rule' section.")

Expand Down Expand Up @@ -640,7 +645,7 @@ jobs:

# 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())
RUNTIME_LABEL = {"python": "Python", "r": "R", "julia": "Julia"}.get(language, language.capitalize())

with open(impl_file, "r") as f:
content = f.read()
Expand All @@ -662,6 +667,33 @@ jobs:
new_content = re.sub(pattern, new_header + "\n", content, count=1)
else:
new_content = new_header + "\n" + content
elif language == "julia":
# Julia has no docstring syntax; anyplot uses a leading `#` comment
# block, mirroring the Python/R conventions. Newlines in the title
# would break the comment block, so sanitize them.
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 `#`-prefixed Julia line-comments (the
# existing header). Anchored to start of file. If no header
# exists we fall back to prepending one. The character class
# after `#` is intentionally narrow: either `[ \t]` (a space or
# tab — the canonical header style `# anyplot.ai`) or empty
# (i.e. a bare `#` immediately followed by `\n`). This excludes:
# `#=` (block-comment openers, which would otherwise swallow the
# block-comment body) and `#!` (shebangs — Julia ignores them
# but they're not part of the header we own). If either ever
# appears as the first non-blank line of a file, the regex
# falls through and the canonical header is prepended above it.
pattern = r"\A(?:#(?:[ \t][^\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('"""', '\\"\\"\\"')
Expand Down
38 changes: 38 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name = "anyplot"
uuid = "5f9c7e1d-8b3d-4c1a-9f6e-1a2b3c4d5e6f"
authors = ["anyplot contributors"]
version = "0.1.0"

# Julia runtime stack for the CairoMakie implementations.
#
# Reproducibility model — sibling of `renv.lock` for R:
# - Project.toml lists the packages we depend on (this file).
# - Manifest.toml pins the exact resolved versions across the entire
# dependency tree (committed alongside this file once Julia has run
# `Pkg.instantiate()` for the first time).
# - The setup-julia action restores from Manifest.toml when present;
# otherwise it falls back to `Pkg.add(...)` so first-time CI doesn't
# hard-fail before the lockfile lands.

[deps]
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
Colors = "5ae59095-9a9b-59fe-a467-6f913c188581"
ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
PalmerPenguins = "8b842266-38fa-440a-9b57-31493939ab85"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
RDatasets = "ce6b1742-4840-55fa-b093-852dadbb1d8b"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"

[compat]
julia = "1.11"
CairoMakie = "0.12, 0.13"
Makie = "0.21, 0.22"
DataFrames = "1"
CSV = "0.10"
Colors = "0.12, 0.13"
ColorSchemes = "3"
RDatasets = "0.7"
PalmerPenguins = "0.1"
9 changes: 6 additions & 3 deletions agentic/docs/project-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ This document contains comprehensive project documentation for AI agents working
*R (1):*
- **ggplot2** - The de facto standard for R; grammar of graphics, layered chart composition

*Julia (1):*
- **Makie.jl** - High-performance Julia visualization. CairoMakie ships publication-quality static PNG via a pure-Cairo backend.

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

## Essential Commands
Expand Down Expand Up @@ -327,13 +330,13 @@ gs://anyplot-images/
3. `impl-merge.yml` promotes staging -> production when PR merges to main

**Interactive libraries** (generate `.html`): plotly, bokeh, altair, highcharts, pygal, letsplot
**PNG only**: matplotlib, seaborn, plotnine
**PNG only**: matplotlib, seaborn, plotnine, ggplot2, makie

## Tech Stack

- **Backend**: FastAPI, SQLAlchemy (async), PostgreSQL, Python 3.13+
- **Frontend**: React 19, Vite 8, TypeScript 6, MUI 9
- **Plotting**: matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, lets-plot
- **Plotting**: matplotlib, seaborn, plotly, bokeh, altair, plotnine, pygal, highcharts, lets-plot, ggplot2, Makie.jl
- **Package Manager**: uv (fast Python installer)
- **Infrastructure**: Google Cloud Run, Cloud SQL, Cloud Storage
- **Automation**: GitHub Actions
Expand Down Expand Up @@ -690,7 +693,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 10 libraries via bulk-generate
- **`generate:all`** - Trigger all 11 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)
Expand Down
Loading