Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export(animate)
export(axis_)
export(axis_x)
export(axis_y)
export(canvas)
export(chart_html)
export(coord_)
export(coord_helix)
Expand Down
122 changes: 106 additions & 16 deletions R/gglite.R
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,6 @@ check_chart = function(fn, chart, args) {
#' inside inline JavaScript functions that cannot be statically detected.
#' @param ... Aesthetic mappings as `name = ~column` formulas or a positional
#' formula for `x`/`y`. Character strings are also accepted.
#' @param width,height Width and height of the chart in pixels.
#' @param padding,margin,inset Layout spacing in pixels. Each can be a scalar
#' (applied to all sides) or a length-4 vector `c(top, right, bottom, left)`;
#' use `NA` to skip individual sides. `NULL` (the default) leaves the value
#' unset.
#' @param title Chart title string, a convenient alternative to piping into
#' [title_()] separately.
#' @param subtitle Chart subtitle string.
Expand All @@ -142,11 +137,7 @@ check_chart = function(fn, chart, args) {
#'
#' # Title and subtitle
#' g2(mtcars, hp ~ mpg, title = 'Motor Trend Cars', subtitle = 'mpg vs hp')
g2 = function(
data = NULL, ..., width = NULL, height = 480,
padding = NULL, margin = NULL, inset = NULL,
title = NULL, subtitle = NULL
) {
g2 = function(data = NULL, ..., title = NULL, subtitle = NULL) {
dots = list(...)
# A positional (unnamed) formula like `hp ~ mpg` or `~ mpg` as the first arg
has_formula = length(dots) &&
Expand All @@ -171,7 +162,7 @@ g2 = function(
}
chart = structure(list(
data = data,
options = list(width = width, height = height, autoFit = if (is.null(width)) TRUE),
options = list(height = 480L, autoFit = TRUE),
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

g2() should not have this options at all; these default options should go to build_config() or chart_html()

layers = list(),
scales = list(),
coords = NULL,
Expand All @@ -184,16 +175,115 @@ g2 = function(
legends = list(),
chart_title = dropNulls(list(title = title, subtitle = subtitle)),
facet = facet_from_formula,
layout = c(
process_layout('padding', padding),
process_layout('margin', margin),
process_layout('inset', inset)
)
layout = list()
), class = 'g2')
if (length(dots)) chart$aesthetics = modifyList(chart$aesthetics, dots)
chart
}

#' Configure Canvas Options
#'
#' Set chart dimensions, layout spacing, and renderer for a G2 chart. Pipe
#' this after [g2()] to customize the canvas before rendering.
#'
#' @section Renderer:
#' The `renderer` argument controls which rendering backend G2 uses:
#' \describe{
#' \item{`"Canvas"` (default)}{Uses the Canvas 2D API via the full
#' `g2.min.js` bundle (~1.1 MB). Fast and appropriate for most charts.}
#' \item{`"SVG"`}{Uses SVG rendering. Requires loading the G2 lite bundle
#' plus `@antv/g` and `@antv/g-svg` (~1.5 MB total). SVG output can be
#' inspected in browser DevTools and is useful for debugging.}
#' \item{`"WebGL"`}{Uses WebGL for GPU-accelerated rendering. Requires
#' loading the G2 lite bundle plus `@antv/g` and `@antv/g-webgl` (~1.9 MB
#' total). Best suited for charts with very large numbers of data points.}
#' }
#'
#' **Important caveat for multi-chart documents:** `g2.min.js` and
#' `g2.lite.min.js` cannot coexist on the same page. If you want to use SVG or
#' WebGL rendering in a document that contains multiple charts (e.g., R
#' Markdown, Quarto, Jupyter), you must declare a global renderer option at the
#' top of the document:
#'
#' ```r
#' options(gglite.renderer = 'svg') # or 'webgl'
#' ```
#'
#' When this option is set, **all** charts in the document switch to the
#' `g2.lite.min.js` CDN. Individual charts can still override the renderer via
#' `canvas(renderer = ...)` — for example, when `options(gglite.renderer =
#' 'svg')` is set globally, a specific chart can use
#' `g2(...) |> canvas(renderer = 'webgl')`.
#'
#' For **standalone plots** previewed in the browser (via [print.g2()]), no
#' global option is needed because each plot is a separate HTML page.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't document the details here; instead, point users to https://pkg.yihui.org/examples/canvas.html

#'
#' @param chart A `g2` object, or `NULL` to create a deferred modifier.
#' @param width Width of the chart in pixels. `NULL` (default) enables
#' auto-fit to the container width.
#' @param height Height of the chart in pixels. Default is `480`.
#' @param padding,margin,inset Layout spacing in pixels. Each can be a scalar
#' (applied to all sides) or a length-4 vector `c(top, right, bottom, left)`;
#' use `NA` to skip individual sides. `NULL` (the default) leaves the value
#' unset.
#' @param renderer The rendering backend: `"Canvas"` (default), `"SVG"`, or
#' `"WebGL"` (case-insensitive). See the **Renderer** section for details.
#' @param ... Additional top-level chart options passed to `chart.options()` in
#' JavaScript (e.g., `clip = TRUE`, `depth = 400`).
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is depth = 400 a sensible value? make sure you didn't make up a meaningless value

#' @return The modified `g2` object (or a `g2_mod` when `chart` is `NULL`).
#' @export
#' @examples
#' # Set chart dimensions
#' g2(mtcars, hp ~ mpg) |> canvas(width = 600, height = 400)
#'
#' # Add padding
#' g2(mtcars, hp ~ mpg) |> canvas(padding = 30)
#'
#' # SVG renderer (standalone; no global option needed)
#' g2(mtcars, hp ~ mpg) |> canvas(renderer = 'svg')
canvas = function(
chart = NULL, width = NULL, height = 480,
padding = NULL, margin = NULL, inset = NULL,
renderer = NULL, ...
) {
args = list(
width = width, height = height,
padding = padding, margin = margin, inset = inset,
renderer = renderer, ...
)
mod = check_chart(canvas, chart, args)
if (!is.null(mod)) return(mod)

# Dimensions
chart$options = dropNulls(list(
width = width,
height = height,
autoFit = if (is.null(width)) TRUE
))

# Layout spacing
chart$layout = c(
process_layout('padding', padding),
process_layout('margin', margin),
process_layout('inset', inset)
)

# Renderer
if (!is.null(renderer)) {
r = tolower(renderer)
r = match.arg(r, c('canvas', 'svg', 'webgl'))
chart$renderer = r
}

# Extra top-level chart.options() args (e.g., clip, depth)
extra = list(...)
if (length(extra)) chart$canvas_extra = modifyList(
as.list(chart$canvas_extra), extra
)

chart
}

#' Set Aesthetic Mappings
#'
#' Map data columns to visual channels (x, y, color, size, shape, etc.).
Expand Down
79 changes: 60 additions & 19 deletions R/render.R
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ build_config = function(chart) {
config$slider = chart$sliders
config$scrollbar = chart$scrollbars
if (length(chart$layout)) config = modifyList(config, chart$layout)
if (length(chart$canvas_extra)) config = modifyList(config, chart$canvas_extra)

# Theme: merge global option with per-chart theme
theme = modifyList(as.list(getOption('gglite.theme')), as.list(chart$theme))
Expand All @@ -225,6 +226,51 @@ build_config = function(chart) {

# ---- HTML generation ----

# Returns the effective renderer string for a chart ('canvas', 'svg', 'webgl').
# Per-chart setting (chart$renderer) takes precedence over the global option.
effective_renderer = function(chart) {
chart$renderer %||% tolower(getOption('gglite.renderer') %||% 'canvas')
}

# Returns TRUE when the page should use g2.lite (non-canvas global option set,
# or per-chart renderer is svg/webgl).
needs_lite = function(chart) {
global_r = tolower(getOption('gglite.renderer') %||% 'canvas')
global_r != 'canvas' || isTRUE(chart$renderer %in% c('svg', 'webgl'))
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lite is needed if gglite.renderer is not null (even if it's set to 'canvas', we should still use lite)

Suggested change
global_r = tolower(getOption('gglite.renderer') %||% 'canvas')
global_r != 'canvas' || isTRUE(chart$renderer %in% c('svg', 'webgl'))
!is.null(getOption('gglite.renderer')) || (chart$renderer %in% c('svg', 'webgl'))

}

# CDN URLs for a given renderer mode ('canvas' uses g2.min.js; 'svg'/'webgl'
# use g2.lite.min.js + @antv/g + renderer-specific package).
g2_cdn_urls = function(renderer = 'canvas') {
if (renderer == 'canvas')
return(c(g2_cdn(), g2_patches_cdn))
r_url = switch(renderer,
svg = 'https://unpkg.com/@antv/g-svg',
webgl = 'https://unpkg.com/@antv/g-webgl'
)
c(
'https://unpkg.com/@antv/g',
r_url,
'https://unpkg.com/@antv/g2@5/dist/g2.lite.min.js',
g2_patches_cdn
)
}

cdn_scripts = function(renderer = 'canvas') {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function should receive a g2() object instead, and pass it to g2_cdn() instead; inside g2_cdn(), you test need_lite(); if true, return g2.min.js; otherwise return g2.lite urls according to the effective renderer of the g2() object

sprintf('<script src="%s" defer></script>', g2_cdn_urls(renderer))
}

g2_html_page = function(body, renderer = 'canvas') {
paste(c(
'<!DOCTYPE html>', '<html>', '<head>',
'<meta charset="utf-8">',
cdn_scripts(renderer),
'</head>', '<body>',
body,
'</body>', '</html>'
), collapse = '\n')
}

#' Generate Chart HTML
#'
#' Create an HTML string containing a container `<div>` and a `<script>` block
Expand Down Expand Up @@ -284,6 +330,14 @@ chart_html = function(chart, id = NULL, width = NULL, height = NULL) {

if (nzchar(style)) style = paste0(' style="', style, '"')

# Renderer setup: when using g2.lite (non-canvas renderer or global option
# set), pass a raw JS renderer instantiation directly in the ctor.
if (needs_lite(chart)) {
r = effective_renderer(chart)
r_ns = switch(r, svg = 'window.G.SVG', webgl = 'window.G.WebGL', canvas = 'window.G.Canvas2D')
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure it's window.G.Canvas2D instead of window.G.Canvas? download https://unpkg.com/@antv/g-canvas and double check

ctor$renderer = js(paste0('new ', r_ns, '.Renderer()'))
}

if (is.null(id)) {
div = paste0('<div data-gglite-container', style, '></div>\n')
ctor$container = js('el')
Expand All @@ -301,21 +355,6 @@ chart_html = function(chart, id = NULL, width = NULL, height = NULL) {
paste0(div, '<script type="module">\n', spec_js, ctor_js, options_js, render_js, '</script>')
}

cdn_scripts = function() {
sprintf('<script src="%s" defer></script>', c(g2_cdn(), g2_patches_cdn))
}

g2_html_page = function(body) {
paste(c(
'<!DOCTYPE html>', '<html>', '<head>',
'<meta charset="utf-8">',
cdn_scripts(),
'</head>', '<body>',
body,
'</body>', '</html>'
), collapse = '\n')
}

#' Preview a Chart in the Viewer or Browser
#'
#' @param x A `g2` object.
Expand All @@ -324,7 +363,7 @@ g2_html_page = function(body) {
#' @export
print.g2 = function(x, ...) {
#TODO: xfun >= 0.57.3 no longer needs paste()
xfun::html_view(g2_html_page(chart_html(x, ...)))
xfun::html_view(g2_html_page(chart_html(x, ...), renderer = effective_renderer(x)))
invisible(x)
}

Expand All @@ -343,7 +382,9 @@ knit_print.g2 = function(x, ...) {
html = chart_html(x)
if (!isTRUE(knitr::opts_knit$get(.knitr.flag))) {
knitr::opts_knit$set(setNames(list(TRUE), .knitr.flag))
html = paste(c(cdn_scripts(), html), collapse = '\n')
# CDN choice is driven by global option so all charts use consistent scripts
global_r = tolower(getOption('gglite.renderer') %||% 'canvas')
html = paste(c(cdn_scripts(global_r), html), collapse = '\n')
}
structure(html, class = c('knit_asis', 'html'))
}
Expand All @@ -358,7 +399,7 @@ knit_print.g2 = function(x, ...) {
#' @param ... Ignored.
#' @return A character string of complete HTML.
#' @noRd
repr_html.g2 = function(obj, ...) g2_html_page(chart_html(obj))
repr_html.g2 = function(obj, ...) g2_html_page(chart_html(obj), renderer = effective_renderer(obj))

#' Text Representation for Jupyter Notebooks
#'
Expand All @@ -381,7 +422,7 @@ repr_text.g2 = function(obj, ...) {
#' @importFrom xfun record_print
#' @export
record_print.g2 = function(x, ...) {
xfun::new_record(c(cdn_scripts(), chart_html(x, ...), ''), 'asis')
xfun::new_record(c(cdn_scripts(effective_renderer(x)), chart_html(x, ...), ''), 'asis')
}

register_methods = function(pkgs, generics) {
Expand Down
4 changes: 4 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ ts_to_df = function(x) {
#' @noRd
dropNulls = function(x) x[!vapply(x, is.null, logical(1))]

#' Null-coalescing operator
#' @noRd
`%||%` = function(x, y) if (is.null(x)) y else x

#' Process a Layout Argument (padding, margin, or inset)
#'
#' Convert a scalar or length-4 vector into named G2 layout options.
Expand Down
Loading
Loading