{"id":50539120,"url":"https://github.com/clement-lee/ggspec","last_synced_at":"2026-06-03T19:01:01.486Z","repository":{"id":355890241,"uuid":"1210623596","full_name":"clement-lee/ggspec","owner":"clement-lee","description":"Extract and Compare ggplot2 Plot Specifications as Tidy Data Frames","archived":false,"fork":false,"pushed_at":"2026-05-21T10:14:30.000Z","size":245,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-28T19:39:32.258Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"R","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/clement-lee.png","metadata":{"files":{"readme":"README.Rmd","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-14T15:41:57.000Z","updated_at":"2026-05-21T10:14:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/clement-lee/ggspec","commit_stats":null,"previous_names":["clement-lee/ggspec"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/clement-lee/ggspec","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clement-lee%2Fggspec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clement-lee%2Fggspec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clement-lee%2Fggspec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clement-lee%2Fggspec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/clement-lee","download_url":"https://codeload.github.com/clement-lee/ggspec/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/clement-lee%2Fggspec/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33876333,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-03T19:01:00.296Z","updated_at":"2026-06-03T19:01:01.478Z","avatar_url":"https://github.com/clement-lee.png","language":"R","funding_links":[],"categories":["Data and models"],"sub_categories":[],"readme":"---\noutput: github_document\n---\n\n\u003c!-- README.md is generated from README.Rmd. Please edit that file --\u003e\n\n```{r setup, include = FALSE}\nknitr::opts_chunk$set(\n  collapse  = TRUE,\n  comment   = \"#\u003e\",\n  fig.path  = \"man/figures/README-\",\n  out.width = \"100%\"\n)\n```\n\n# ggspec\n\n\u003c!-- badges: start --\u003e\n[![Lifecycle: experimental](https://img.shields.io/badge/lifecycle-experimental-orange.svg)](https://lifecycle.r-lib.org/articles/stages.html#experimental)\n[![R-CMD-check](https://github.com/clement-lee/ggspec/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/clement-lee/ggspec/actions/workflows/R-CMD-check.yaml)\n[![Codecov test coverage](https://codecov.io/gh/clement-lee/ggspec/graph/badge.svg)](https://app.codecov.io/gh/clement-lee/ggspec)\n[![CRAN status](https://www.r-pkg.org/badges/version/ggspec)](https://CRAN.R-project.org/package=ggspec)\n\u003c!-- badges: end --\u003e\n\n`ggspec` extracts the full declarative specification of a `ggplot2` object —\nlayers, aesthetic mappings, scales, facets, coordinate system, and labels — as\ntidy data frames. A second tier of functions enables structural comparison of\ntwo ggplot objects, supporting automated plot testing, auditing, and\nframework-agnostic grading workflows.\n\n## Motivation\n\nDifferent large-language models and AI coding assistants generate syntactically\ndifferent code for the same visualisation task.  One AI might write\n`geom_bar(aes(x = species))` on raw data; another might write\n`count(species) |\u003e ... geom_col(aes(x = species, y = n))`.  Both produce the\nsame chart, but naive string or AST comparison would flag them as different.\n`ggspec` provides a principled hierarchy of equivalence checks — from strict\nspec equality through structural canonicalisation to rendered-output comparison\n— so that equivalent plots are recognised as equivalent regardless of which\nsyntactic path an AI (or human student) took to produce them.\n\n## Installation\n\nInstall the development version from GitHub:\n\n```r\n# install.packages(\"remotes\")\nremotes::install_github(\"clement-lee/ggspec\")\n```\n\n## Usage\n\n### Extracting a spec\n\n```{r usage-spec}\nlibrary(ggspec)\nlibrary(ggplot2)\n\np \u003c- ggplot(mpg, aes(displ, hwy)) +\n  geom_point(aes(colour = class)) +\n  geom_smooth(method = \"lm\", se = FALSE) +\n  facet_wrap(~drv) +\n  labs(title = \"Engine displacement vs highway MPG\")\n\nspec_layers(p)\nspec_aes(p)\n```\n\n### Comparing two plots\n\n```{r usage-equiv}\nref \u003c- ggplot(mpg, aes(displ, hwy)) +\n  geom_point(aes(colour = class)) +\n  facet_wrap(~drv)\n\nobs_correct \u003c- ggplot(mpg, aes(displ, hwy)) +\n  geom_point(aes(colour = class)) +\n  facet_wrap(~drv)\n\nobs_wrong \u003c- ggplot(mpg, aes(displ, hwy)) +\n  geom_smooth() +\n  facet_wrap(~cyl)\n\nequiv_plot(ref, obs_correct)\nequiv_plot(ref, obs_wrong)\n```\n\n## Comparison modes\n\n`ggspec` recognises four levels of plot equivalence or similarity, ordered from\nmost to least restrictive.\n\n### Strict equivalence\n\nTwo plots are **strictly equivalent** when their specifications are identical\nwith no canonicalisation applied: same layer order, same data/mapping\nplacement, same geom names, same random seed for stochastic elements.\n\n```{r mode-strict, eval=FALSE}\ncompare_plots(p1, p2, mode = \"strict\")\n```\n\n### Structural equivalence\n\nTwo plots are **structurally equivalent** when their specifications are\nidentical after canonicalisation via `canon()`.  The canonical form is\ncomputed by a term rewriting system (TRS) that applies a fixed set of\nconfluent rewrite rules:\n\n- **fold\\_global**: resolves the ambiguity of placing data/mapping at the\n  global level vs per-layer — both forms normalise to the same spec.\n- **geom\\_col\\_to\\_bar**: `geom_col()` is a shorthand for\n  `geom_bar(stat = \"identity\")`; the canonical form always uses the latter.\n- **layer\\_order**: non-spanning geoms are sorted alphabetically, so layer\n  order does not affect structural equivalence.\n\n```{r mode-structural, eval=FALSE}\ncompare_plots(p1, p2, mode = \"structural\")  # default\n```\n\n### Visual equivalence\n\nTwo plots are **visually equivalent** when they produce identical rendered\noutput.  This pathway uses `ggplot_build()` to evaluate plots semantically\nrather than comparing their specs, so it can detect equivalences that\nstructural comparison cannot:\n\n- `geom_bar()` on raw data vs `geom_col()` on pre-counted data (same bars,\n  different specs).\n- `coord_flip()` vs swapped aesthetics (same visual output, different\n  coordinate systems).\n- `scale_fill_*(name = \"v\")` vs `labs(fill = \"v\")` (same legend label,\n  different spec location).\n\n```{r mode-visual, eval=FALSE}\ncompare_plots(p1, p2, mode = \"visual\")\n```\n\n**Design constraint**: visual equivalence calls `ggplot_build()` on both plots.\nThis means (a) both plots must be buildable with their data accessible in the\nsession, (b) it is slower than structural comparison, and (c) it is\noutput-based — it does not verify that the plots were derived from the same\nsource data.  Two plots backed by different datasets that happen to produce the\nsame rendered output will pass visual equivalence.  Use structural mode when\ndata provenance must be verified.\n\n### Conceptual similarity\n\nTwo plots are **conceptually similar** when they communicate the same\ninformation using potentially different visual encodings.  Unlike the\nequivalence modes above, conceptual similarity is not a strict mathematical\nequivalence relation; each claim is qualified by a WHEN condition:\n\n| Claim | WHEN |\n|---|---|\n| boxplot, violin, jitter all similar| 1 continuous + 1 discrete variable |\n| density, histogram, freqpoly, dotplot all similar | 1 continuous variable |\n| `geom_count`, `geom_point(aes(size = n))` similar | 2 discrete variables, joint counts |\n\n```{r mode-conceptual, eval=FALSE}\ncompare_plots(p1, p2, mode = \"conceptual\")\n```\n\n### Enriching a spec with build-derived defaults\n\n`enrich_spec()` uses `ggplot_build()` to identify which parameters and\naesthetics were explicitly set by the user versus filled in by ggplot2:\n\n```{r usage-enrich}\nes \u003c- enrich_spec(p)\n\n# Non-aesthetic parameters with explicit flag\nes$params_tbl[[1]]\n\n# Aesthetics resolved by ggplot2, with explicit flag\nes$built_aes[[1]]\n```\n\n## Key functions\n\n| Tier | Function | What it returns |\n|---|---|---|\n| Extraction | `spec_layers()` | One row per layer |\n| Extraction | `spec_aes()` | One row per layer × aesthetic |\n| Extraction | `spec_scales()` | One row per scale |\n| Extraction | `spec_facets()` | Facet type and variables |\n| Extraction | `spec_labels()` | One row per label |\n| Extraction | `spec_coord()` | Coordinate system |\n| Extraction | `enrich_spec()` | spec_layers + default/explicit flags |\n| Comparison | `equiv_plot()` | All checks in one call (strict) |\n| Comparison | `equiv_layers()` | Geom and stat per layer |\n| Comparison | `equiv_aes()` | Aesthetic mappings |\n| Comparison | `compare_plots()` | Four-mode comparison entry point |\n| Comparison | `compare_visual()` | Visual equivalence via `ggplot_build()` |\n| Comparison | `compare_conceptual()` | Conceptual similarity detectors |\n| Comparison | `equiv_rendered()` | Rendered layer data comparison |\n| Check | `check_plot()` | Framework-agnostic assertion |\n| Check | `expect_equiv_plot()` | testthat expectation |\n\n## Related packages\n\n- **[ggcheck](https://github.com/rstudio/ggcheck)** — designed for\n  `learnr`/`gradethis` pipelines; returns ad-hoc objects.\n  `ggspec` returns rectangular, pipeable tibbles and has no grading framework\n  dependency.\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclement-lee%2Fggspec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclement-lee%2Fggspec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclement-lee%2Fggspec/lists"}