Skip to content

Commit 6cb034d

Browse files
Copilotyihui
andauthored
Implement gglite R package wrapping AntV G2 (#2)
* Initial plan * Implement gglite R package wrapping AntV G2 library - DESCRIPTION with xfun as sole Import, knitr/testit as Suggests - Core API: g2(), encode(), mark_*(), scale_of(), coordinate(), interact() - HTML generation: chart_html(), preview(), knit_print.g2(), render_shiny() - Shiny JS handlers: inst/www/g2-handler.js - Roxygen2 markdown documentation with generated man pages - testit tests in tests/testit/test-gglite.R - R CMD check passes with 0 errors, 0 warnings Co-authored-by: yihui <163582+yihui@users.noreply.github.com> * Address all review feedback: fix chart rendering, code style, tests - Fix G2 rendering: container/width/height passed to G2.Chart() constructor, spec (data/marks/scales) passed to chart.options() - Use `=` for assignment, single quotes for strings throughout - CDN URL customizable via options(gglite.g2_cdn = '...') - Use `<script type="module">` and `const` in generated JS - Use `defer` on CDN `<script>` tags - Use arrow functions (`=>`) in all JS code - Remove unused `+.g2` operator overloading - Fix tests: all conditions wrapped in `()`, removed default-testing - Verified charts render correctly in headless browser (Puppeteer) ![Bar chart rendered](https://github.com/user-attachments/assets/a787ceca-a5cb-4380-9262-8131a638016d) Co-authored-by: yihui <163582+yihui@users.noreply.github.com> * Fix code review findings: improve CDN docs and test option cleanup Co-authored-by: yihui <163582+yihui@users.noreply.github.com> * Fix coordinate docs/example, roxygenize, add R-CMD-check workflow - Fix coordinate() to use valid G2 v5 types: 'polar', 'theta', 'radial', 'radar', 'helix', 'parallel', 'cartesian' - Change example from coordinate('transpose') to coordinate('theta') - Run roxygenize() to regenerate all man pages - Add .github/workflows/R-CMD-check.yaml for CI - Add node_modules to .Rbuildignore - All examples verified in headless browser (Puppeteer) - R CMD check passes with 0 errors, 0 warnings, 0 notes Co-authored-by: yihui <163582+yihui@users.noreply.github.com> * author and maintainer will be auto-generated * Major refactoring: remove NSE, add all marks/transforms/faceting/animation/components - Remove NSE: variables now character strings (g2(mtcars, x = 'mpg')) - Column-major data via annotate_df() + g2-column.min.js CDN - Split into separate R files: mark.R, scale.R, coordinate.R, interact.R, theme.R, transform.R, facet.R, animate.R, component.R, render.R - Add 35 mark types (interval, rect, line, point, text, cell, area, path, polygon, image, link, lineX, lineY, range, rangeX, rangeY, connector, box, boxplot, density, heatmap, vector, node, edge, sankey, chord, treemap, pack, forceGraph, tree, wordCloud, gauge, liquid, shape, partition) - Add coord_transpose() for coord_flip equivalent - Add transform_of() with all G2 transforms (stackY, dodgeX, normalizeY, jitterX/Y, bin/binX, groupX/Y, sortX/Y, symmetryY, diffY, etc.) - Add facet_rect() and facet_circle() for faceting - Add animate() for enter/update/exit animations - Add component config: axis_of(), legend_of(), title_of(), tooltip_of(), labels_of(), style_mark(), slider_of(), scrollbar_of() - Add theme_of() with all G2 themes - Update tests for new API - R CMD check: 0 errors, 0 warnings, 0 notes Co-authored-by: yihui <163582+yihui@users.noreply.github.com> * Fix heatmap/gauge examples, verify 40/41 examples pass in headless browser Co-authored-by: yihui <163582+yihui@users.noreply.github.com> * Fix corrupted character in word cloud example, remove temp test file - Fix corrupted UTF-8 character in mark_word_cloud example - Remove accidentally committed test-all.mjs - Add test-all.mjs to .gitignore - Re-run roxygenize Co-authored-by: yihui <163582+yihui@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: yihui <163582+yihui@users.noreply.github.com> Co-authored-by: Yihui Xie <xie@yihui.name>
1 parent 728e469 commit 6cb034d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

84 files changed

+3134
-0
lines changed

.Rbuildignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
^\.Rproj\.user$
2+
^.*\.Rproj$
3+
^LICENSE\.md$
4+
^README\.md$
5+
^\.github$
6+
^node_modules$
7+
^package\.json$
8+
^package-lock\.json$

.github/workflows/R-CMD-check.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
on:
2+
push:
3+
branches: [main, master]
4+
pull_request:
5+
branches: [main, master]
6+
7+
name: R-CMD-check
8+
9+
permissions: read-all
10+
11+
jobs:
12+
R-CMD-check:
13+
runs-on: ${{ matrix.config.os }}
14+
15+
name: ${{ matrix.config.os }} (${{ matrix.config.r }})
16+
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
config:
21+
- {os: macos-latest, r: 'release'}
22+
- {os: windows-latest, r: 'release'}
23+
- {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
24+
- {os: ubuntu-latest, r: 'release'}
25+
- {os: ubuntu-latest, r: 'oldrel-1'}
26+
27+
env:
28+
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
29+
R_KEEP_PKG_SOURCE: yes
30+
31+
steps:
32+
- uses: actions/checkout@v4
33+
34+
- uses: r-lib/actions/setup-r@v2
35+
with:
36+
r-version: ${{ matrix.config.r }}
37+
http-user-agent: ${{ matrix.config.http-user-agent }}
38+
use-public-rspm: true
39+
40+
- uses: r-lib/actions/setup-r-dependencies@v2
41+
with:
42+
extra-packages: any::rcmdcheck
43+
needs: check
44+
45+
- uses: r-lib/actions/check-r-package@v2
46+
with:
47+
upload-snapshots: true
48+
build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
package.json
3+
package-lock.json
4+
*.Rcheck/
5+
test-all.mjs

DESCRIPTION

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Package: gglite
2+
Title: Lightweight Data Visualization via the Grammar of Graphics
3+
Version: 0.0.1
4+
Authors@R: person("Yihui", "Xie", role = c("aut", "cre"), email = "xie@yihui.name",
5+
comment = c(ORCID = "0000-0003-0645-5666"))
6+
Description: A lightweight R interface to the AntV G2 JavaScript visualization
7+
library with a ggplot2-style API. Supports rendering in R Markdown, Shiny,
8+
and standalone HTML previews. Depends only on 'xfun' for JSON serialization.
9+
License: MIT + file LICENSE
10+
URL: https://github.com/yihui/gglite
11+
BugReports: https://github.com/yihui/gglite/issues
12+
Imports:
13+
xfun
14+
Suggests:
15+
knitr,
16+
testit
17+
Encoding: UTF-8
18+
Roxygen: list(markdown = TRUE)
19+
RoxygenNote: 7.3.3

LICENSE

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
YEAR: 2026
2+
COPYRIGHT HOLDER: Yihui Xie

NAMESPACE

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Generated by roxygen2: do not edit by hand
2+
3+
S3method(print,g2)
4+
export(animate)
5+
export(axis_of)
6+
export(chart_html)
7+
export(coord_transpose)
8+
export(coordinate)
9+
export(encode)
10+
export(facet_circle)
11+
export(facet_rect)
12+
export(g2)
13+
export(interact)
14+
export(labels_of)
15+
export(legend_of)
16+
export(mark_area)
17+
export(mark_box)
18+
export(mark_boxplot)
19+
export(mark_cell)
20+
export(mark_chord)
21+
export(mark_connector)
22+
export(mark_density)
23+
export(mark_edge)
24+
export(mark_force_graph)
25+
export(mark_gauge)
26+
export(mark_heatmap)
27+
export(mark_image)
28+
export(mark_interval)
29+
export(mark_line)
30+
export(mark_line_x)
31+
export(mark_line_y)
32+
export(mark_link)
33+
export(mark_liquid)
34+
export(mark_node)
35+
export(mark_pack)
36+
export(mark_partition)
37+
export(mark_path)
38+
export(mark_point)
39+
export(mark_polygon)
40+
export(mark_range)
41+
export(mark_range_x)
42+
export(mark_range_y)
43+
export(mark_rect)
44+
export(mark_sankey)
45+
export(mark_shape)
46+
export(mark_text)
47+
export(mark_tree)
48+
export(mark_treemap)
49+
export(mark_vector)
50+
export(mark_word_cloud)
51+
export(preview)
52+
export(render_shiny)
53+
export(scale_of)
54+
export(scrollbar_of)
55+
export(slider_of)
56+
export(style_mark)
57+
export(theme_of)
58+
export(title_of)
59+
export(tooltip_of)
60+
export(transform_of)
61+
importFrom(utils,modifyList)

R/animate.R

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#' Set Animation Options
2+
#'
3+
#' Configure animation for the most recently added mark. G2 supports `enter`,
4+
#' `update`, and `exit` animations.
5+
#'
6+
#' @param chart A `g2` object.
7+
#' @param ... Animation configuration as named lists. Use `enter`, `update`,
8+
#' and `exit` to control each phase. Set to `FALSE` to disable animation
9+
#' entirely.
10+
#' @return The modified `g2` object.
11+
#' @export
12+
#' @examples
13+
#' # Fade-in animation on bars
14+
#' g2(data.frame(x = c('A', 'B', 'C'), y = c(3, 7, 2)), x = 'x', y = 'y') |>
15+
#' mark_interval() |>
16+
#' animate(enter = list(type = 'fadeIn', duration = 1000))
17+
#'
18+
#' # Wave-in animation
19+
#' g2(data.frame(x = c('A', 'B', 'C'), y = c(3, 7, 2)), x = 'x', y = 'y') |>
20+
#' mark_interval() |>
21+
#' animate(enter = list(type = 'waveIn', duration = 800))
22+
#'
23+
#' # Disable animation
24+
#' g2(mtcars, x = 'mpg', y = 'hp') |>
25+
#' mark_point() |>
26+
#' animate(FALSE)
27+
animate = function(chart, ...) {
28+
n = length(chart$layers)
29+
if (n == 0) stop('add a mark before setting animation')
30+
args = list(...)
31+
if (length(args) == 1 && is.logical(args[[1]])) {
32+
chart$layers[[n]]$animate = args[[1]]
33+
} else {
34+
chart$layers[[n]]$animate = args
35+
}
36+
chart
37+
}

R/component.R

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#' Configure an Axis
2+
#'
3+
#' Customise the axis for a positional channel (`'x'` or `'y'`). Set to
4+
#' `FALSE` to hide the axis.
5+
#'
6+
#' @param chart A `g2` object.
7+
#' @param channel Positional channel: `'x'` or `'y'`.
8+
#' @param ... Axis options such as `title`, `labelFormatter`, `tickCount`,
9+
#' `grid`, etc., or `FALSE` to hide.
10+
#' @return The modified `g2` object.
11+
#' @export
12+
#' @examples
13+
#' g2(mtcars, x = 'mpg', y = 'hp') |>
14+
#' mark_point() |>
15+
#' axis_of('x', title = 'Miles per Gallon') |>
16+
#' axis_of('y', title = 'Horsepower')
17+
axis_of = function(chart, channel, ...) {
18+
args = list(...)
19+
if (length(args) == 1 && is.logical(args[[1]])) {
20+
chart$axes[[channel]] = args[[1]]
21+
} else {
22+
chart$axes[[channel]] = args
23+
}
24+
chart
25+
}
26+
27+
#' Configure a Legend
28+
#'
29+
#' Customise the legend for a visual channel (`'color'`, `'size'`, `'shape'`,
30+
#' `'opacity'`). Set to `FALSE` to hide.
31+
#'
32+
#' @param chart A `g2` object.
33+
#' @param channel Visual channel name.
34+
#' @param ... Legend options such as `position` (`'top'`, `'bottom'`, `'left'`,
35+
#' `'right'`), `layout`, `title`, etc.
36+
#' @return The modified `g2` object.
37+
#' @export
38+
#' @examples
39+
#' g2(iris, x = 'Sepal.Width', y = 'Sepal.Length', color = 'Species') |>
40+
#' mark_point() |>
41+
#' legend_of('color', position = 'right')
42+
legend_of = function(chart, channel, ...) {
43+
args = list(...)
44+
if (length(args) == 1 && is.logical(args[[1]])) {
45+
chart$legends[[channel]] = args[[1]]
46+
} else {
47+
chart$legends[[channel]] = args
48+
}
49+
chart
50+
}
51+
52+
#' Set the Chart Title
53+
#'
54+
#' @param chart A `g2` object.
55+
#' @param text Title text string.
56+
#' @param ... Additional title options such as `subtitle`, `align`, `style`.
57+
#' @return The modified `g2` object.
58+
#' @export
59+
#' @examples
60+
#' g2(mtcars, x = 'mpg', y = 'hp') |>
61+
#' mark_point() |>
62+
#' title_of('Motor Trend Cars', subtitle = 'mpg vs hp')
63+
title_of = function(chart, text, ...) {
64+
dots = list(...)
65+
if (length(dots)) {
66+
chart$chart_title = c(list(title = text), dots)
67+
} else {
68+
chart$chart_title = text
69+
}
70+
chart
71+
}
72+
73+
#' Configure the Tooltip
74+
#'
75+
#' Set chart-level tooltip options. Mark-level tooltips can be passed via `...`
76+
#' in `mark_*()` functions.
77+
#'
78+
#' @param chart A `g2` object.
79+
#' @param ... Tooltip options such as `shared`, `crosshairs`, `marker`, or
80+
#' `FALSE` to disable.
81+
#' @return The modified `g2` object.
82+
#' @export
83+
#' @examples
84+
#' g2(mtcars, x = 'mpg', y = 'hp') |>
85+
#' mark_point() |>
86+
#' tooltip_of(crosshairs = TRUE)
87+
tooltip_of = function(chart, ...) {
88+
args = list(...)
89+
if (length(args) == 1 && is.logical(args[[1]])) {
90+
chart$tooltip_config = args[[1]]
91+
} else {
92+
chart$tooltip_config = args
93+
}
94+
chart
95+
}
96+
97+
#' Add Labels to the Last Mark
98+
#'
99+
#' Append a label configuration to the most recently added mark. Can be called
100+
#' multiple times to add several label layers.
101+
#'
102+
#' @param chart A `g2` object.
103+
#' @param ... Label options such as `text` (channel name), `position`,
104+
#' `formatter`, `style`.
105+
#' @return The modified `g2` object.
106+
#' @export
107+
#' @examples
108+
#' df = data.frame(x = c('A', 'B', 'C'), y = c(3, 7, 2))
109+
#' g2(df, x = 'x', y = 'y') |>
110+
#' mark_interval() |>
111+
#' labels_of(text = 'y', position = 'inside')
112+
labels_of = function(chart, ...) {
113+
n = length(chart$layers)
114+
if (n == 0) stop('add a mark before setting labels')
115+
chart$layers[[n]]$labels = c(chart$layers[[n]]$labels, list(list(...)))
116+
chart
117+
}
118+
119+
#' Set Style on the Last Mark
120+
#'
121+
#' @param chart A `g2` object.
122+
#' @param ... Style options such as `fill`, `stroke`, `lineWidth`,
123+
#' `fillOpacity`, `strokeOpacity`.
124+
#' @return The modified `g2` object.
125+
#' @export
126+
#' @examples
127+
#' g2(mtcars, x = 'mpg', y = 'hp') |>
128+
#' mark_point() |>
129+
#' style_mark(fill = 'steelblue', stroke = 'white', lineWidth = 1)
130+
style_mark = function(chart, ...) {
131+
n = length(chart$layers)
132+
if (n == 0) stop('add a mark before setting style')
133+
chart$layers[[n]]$style = list(...)
134+
chart
135+
}
136+
137+
#' Add a Slider
138+
#'
139+
#' Add a range slider to a positional channel for zooming/panning.
140+
#'
141+
#' @param chart A `g2` object.
142+
#' @param channel Positional channel: `'x'` or `'y'`.
143+
#' @param ... Slider options.
144+
#' @return The modified `g2` object.
145+
#' @export
146+
#' @examples
147+
#' g2(mtcars, x = 'mpg', y = 'hp') |>
148+
#' mark_point() |>
149+
#' slider_of('x')
150+
slider_of = function(chart, channel, ...) {
151+
if (is.null(chart$sliders)) chart$sliders = list()
152+
args = list(...)
153+
chart$sliders[[channel]] = if (length(args)) args else TRUE
154+
chart
155+
}
156+
157+
#' Add a Scrollbar
158+
#'
159+
#' @param chart A `g2` object.
160+
#' @param channel Positional channel: `'x'` or `'y'`.
161+
#' @param ... Scrollbar options.
162+
#' @return The modified `g2` object.
163+
#' @export
164+
#' @examples
165+
#' df = data.frame(x = 1:100, y = cumsum(rnorm(100)))
166+
#' g2(df, x = 'x', y = 'y') |>
167+
#' mark_line() |>
168+
#' scrollbar_of('x')
169+
scrollbar_of = function(chart, channel, ...) {
170+
if (is.null(chart$scrollbars)) chart$scrollbars = list()
171+
args = list(...)
172+
chart$scrollbars[[channel]] = if (length(args)) args else TRUE
173+
chart
174+
}

0 commit comments

Comments
 (0)