Skip to content

Commit 60be3da

Browse files
Copilotyihui
andauthored
fix mark_ctx dual-axis bug, simplify tooltip_ API, update docs/tests
Agent-Logs-Url: https://github.com/yihui/gglite/sessions/10dfbf63-dbab-42e9-8579-8d0d66f06c0a Co-authored-by: yihui <163582+yihui@users.noreply.github.com>
1 parent f89a31c commit 60be3da

12 files changed

Lines changed: 148 additions & 143 deletions

File tree

R/component.R

Lines changed: 34 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
#' Customise the axis for a positional channel (`'x'` or `'y'`). Set to
44
#' `FALSE` to hide the axis. When called immediately after a `mark_*()`
55
#' function (or `style_mark()`, `labels_()`, etc.), the axis is applied to
6-
#' that mark only, enabling per-mark axis customization (e.g., a right-side
7-
#' y-axis for a dual-axis chart). Otherwise it applies at the chart level.
6+
#' that mark only, enabling per-mark axis customization for dual-axis charts.
7+
#' Otherwise it applies at the chart level.
88
#'
99
#' @param chart A `g2` object.
1010
#' @param channel Positional channel: `'x'` or `'y'`.
@@ -18,13 +18,16 @@
1818
#' axis_('x', title = 'Miles per Gallon') |>
1919
#' axis_('y', title = 'Horsepower')
2020
#'
21-
#' # Mark-level axis for dual-axis chart
22-
#' df = data.frame(x = 1:5, a = c(1, 4, 2, 5, 3), b = c(100, 200, 150, 300, 250))
23-
#' g2(df, ~ x) |>
24-
#' mark_interval(encode = list(y = 'a')) |>
25-
#' mark_line(encode = list(y = 'b')) |>
21+
#' # Dual-axis chart: each mark gets its own axis immediately after mark_*()
22+
#' air = aggregate(cbind(Temp, Wind) ~ Month, data = airquality, FUN = mean)
23+
#' air$Month = month.abb[air$Month]
24+
#' g2(air, x = 'Month') |>
25+
#' mark_interval(encode = list(y = 'Temp')) |>
2626
#' scale_y(independent = TRUE) |>
27-
#' axis_y(position = 'right', grid = FALSE)
27+
#' axis_y(title = 'Temperature (°F)') |>
28+
#' mark_line(encode = list(y = 'Wind')) |>
29+
#' scale_y(independent = TRUE) |>
30+
#' axis_y(position = 'right', grid = FALSE, title = 'Wind Speed (mph)')
2831
axis_ = function(chart = NULL, channel, ...) {
2932
mod = check_chart(axis_, chart, c(if (!missing(channel)) list(channel), list(...)))
3033
if (!is.null(mod)) return(mod)
@@ -144,41 +147,36 @@ title_ = function(chart = NULL, text, ...) {
144147
chart
145148
}
146149

147-
# Keys that configure the tooltip interaction (e.g., crosshairs, shared) rather
148-
# than the tooltip data items (e.g., channel, valueFormatter). Used by tooltip_()
149-
# to route args to the correct G2 spec location.
150-
.tooltip_interact_keys = c(
151-
'shared', 'series', 'facet', 'body', 'crosshairs', 'marker',
152-
'groupName', 'disableNative', 'disableAutoHide', 'offset',
153-
'position', 'bounding', 'mount', 'css', 'enterable', 'sort',
154-
'filter', 'render'
155-
)
156-
157150
#' Configure the Tooltip
158151
#'
159-
#' Configure tooltip behavior and data display. Behavior options such as
160-
#' `crosshairs` and `shared` are applied to the tooltip interaction
161-
#' (`interaction.tooltip` in G2). Data display options such as `channel` and
162-
#' `valueFormatter` are applied to the last mark's tooltip (call after adding
163-
#' marks). Pass `FALSE` to disable the tooltip entirely.
152+
#' Configure tooltip interaction behavior. All options are applied to
153+
#' `interaction.tooltip` in the G2 spec: pass `FALSE` to disable the tooltip,
154+
#' or pass named options such as `crosshairs`, `shared`, `marker`, and any
155+
#' `crosshairs*`/`marker*` style properties. To configure the data displayed in
156+
#' a tooltip for a specific mark (e.g., `channel`, `valueFormatter`, `items`),
157+
#' pass a `tooltip` list argument directly to the mark function instead, e.g.,
158+
#' `mark_line(tooltip = list(channel = 'y', valueFormatter = '.0%'))`.
164159
#'
165160
#' @param chart A `g2` object.
166-
#' @param ... Tooltip options. Behavior options: `shared`, `crosshairs`,
167-
#' `marker`, `series`, `facet`, `groupName`, and all `crosshairs*`/`marker*`
168-
#' style props. Data options (applied to the last mark): `channel`,
169-
#' `valueFormatter`, `title`, `items`, `name`, `color`. Or `FALSE` to
170-
#' disable.
161+
#' @param ... Tooltip interaction options such as `shared`, `crosshairs`,
162+
#' `marker`, `series`, `crosshairsStroke`, or `FALSE` to disable the tooltip.
171163
#' @return The modified `g2` object.
172164
#' @export
173165
#' @examples
174-
#' # Enable crosshairs
175-
#' g2(mtcars, hp ~ mpg) |>
166+
#' # Enable crosshairs (works best with line/area marks which use series tooltip)
167+
#' df = data.frame(x = 1:6, y = c(3, 1, 4, 1, 5, 2))
168+
#' g2(df, y ~ x) |>
169+
#' mark_line() |>
176170
#' tooltip_(crosshairs = TRUE)
177171
#'
178-
#' # Format y-axis values in tooltip (call after mark)
179-
#' g2(mtcars, hp ~ mpg) |>
180-
#' mark_point() |>
181-
#' tooltip_(channel = 'y', valueFormatter = '.0f')
172+
#' # Shared tooltip for multi-series line chart
173+
#' df2 = data.frame(
174+
#' x = rep(1:5, 2), y = c(3, 1, 4, 1, 5, 2, 7, 1, 8, 3),
175+
#' group = rep(c('A', 'B'), each = 5)
176+
#' )
177+
#' g2(df2, y ~ x, color = ~ group) |>
178+
#' mark_line() |>
179+
#' tooltip_(shared = TRUE)
182180
#'
183181
#' # Disable tooltip
184182
#' g2(mtcars, hp ~ mpg) |>
@@ -191,27 +189,8 @@ tooltip_ = function(chart = NULL, ...) {
191189
chart$interactions[['tooltip']] = args[[1]]
192190
return(chart)
193191
}
194-
keys = names(args)
195-
# grepl prefix match covers crosshairs* and marker* style props (e.g.
196-
# crosshairsStroke, markerFill) which are too numerous to enumerate
197-
is_int = keys %in% .tooltip_interact_keys | grepl('^crosshairs|^marker', keys)
198-
if (any(is_int)) {
199-
cur = if (is.list(chart$interactions[['tooltip']])) chart$interactions[['tooltip']] else list()
200-
chart$interactions[['tooltip']] = modifyList(cur, args[is_int])
201-
}
202-
data_args = args[!is_int]
203-
if (length(data_args)) {
204-
if (length(chart$layers)) {
205-
n = length(chart$layers)
206-
cur = if (is.list(chart$layers[[n]]$tooltip)) chart$layers[[n]]$tooltip else list()
207-
chart$layers[[n]]$tooltip = modifyList(cur, data_args)
208-
} else {
209-
# No marks yet: store at chart level as a fallback. This is serialized
210-
# as the view-level tooltip component, but may not be processed by G2
211-
# for channel/valueFormatter — prefer calling tooltip_() after marks.
212-
chart$tooltip_config = modifyList(as.list(chart$tooltip_config), data_args)
213-
}
214-
}
192+
cur = if (is.list(chart$interactions[['tooltip']])) chart$interactions[['tooltip']] else list()
193+
chart$interactions[['tooltip']] = modifyList(cur, args)
215194
chart
216195
}
217196

R/gglite.R

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ g2_patches_cdn = 'https://cdn.jsdelivr.net/npm/@xiee/utils@v1.14.32/js/g2-patche
2525
xfun::js
2626

2727
# Returns TRUE when scale_/axis_ should target the last mark rather than the
28-
# chart: only when the user just added a mark AND there are multiple marks
29-
# (so single-mark charts always use chart-level scale/axis for compatibility).
30-
mark_ctx = function(chart) isTRUE(chart$last_op == 'mark') && length(chart$layers) > 1
28+
# chart: any time the user just added a mark (last_op == 'mark'), regardless
29+
# of how many marks exist. This means scale/axis always stay with their mark,
30+
# enabling correct dual-axis charts even when only one mark has been added.
31+
mark_ctx = function(chart) isTRUE(chart$last_op == 'mark')
3132

3233
#' Create a Deferred Chart Modifier
3334
#'

R/scale.R

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
#' immediately after a `mark_*()` function (or after `style_mark()`,
55
#' `labels_()`, etc. that target the last mark), the scale is applied to that
66
#' mark only. Otherwise it is applied at the chart level and affects all marks.
7-
#' This context-sensitivity enables dual-axis charts: add marks and pipe
8-
#' `scale_y(independent = TRUE)` to give one mark its own y scale.
7+
#' This context-sensitivity enables dual-axis charts: pipe `scale_y()` right
8+
#' after each mark to give it its own independent y scale.
99
#'
1010
#' G2 scale types: `'linear'`, `'ordinal'`, `'band'`, `'point'`, `'time'`,
1111
#' `'log'`, `'pow'`, `'sqrt'`, `'threshold'`, `'quantize'`, `'quantile'`,
@@ -30,11 +30,13 @@
3030
#' g2(iris, Sepal.Length ~ Sepal.Width, color = ~ Species) |>
3131
#' scale_('color', palette = 'category10')
3232
#'
33-
#' # Mark-level independent y scale for dual-axis charts
34-
#' df = data.frame(x = 1:5, a = c(1, 4, 2, 5, 3), b = c(100, 200, 150, 300, 250))
35-
#' g2(df, ~ x) |>
36-
#' mark_interval(encode = list(y = 'a')) |>
37-
#' mark_line(encode = list(y = 'b')) |>
33+
#' # Dual-axis: pipe scale_y() right after each mark
34+
#' air = aggregate(cbind(Temp, Wind) ~ Month, data = airquality, FUN = mean)
35+
#' air$Month = month.abb[air$Month]
36+
#' g2(air, x = 'Month') |>
37+
#' mark_interval(encode = list(y = 'Temp')) |>
38+
#' scale_y(independent = TRUE) |>
39+
#' mark_line(encode = list(y = 'Wind')) |>
3840
#' scale_y(independent = TRUE) |>
3941
#' axis_y(position = 'right', grid = FALSE)
4042
scale_ = function(chart = NULL, field, ...) {

examples/axes-legends.Rmd

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,8 @@ p2 |> legend_color(position = 'right', title = 'Iris Species')
9292
## Dual Y-Axis
9393

9494
Two marks can share the same x-axis but each have their own independent y-axis.
95-
When `scale_y()` or `axis_y()` follows the second (or later) mark in a chain,
96-
they automatically apply to that mark rather than the whole chart. Place the
97-
first mark's axis call before adding the second mark to target the chart-level
98-
axis; then configure the second mark's axis right after adding it.
95+
Call `scale_y(independent = TRUE)` and `axis_y()` immediately after each mark
96+
to configure its y-axis independently.
9997

10098
```{r}
10199
# Monthly averages of temperature (°F) and wind speed (mph) from airquality
@@ -105,6 +103,7 @@ air$Month = month.abb[air$Month]
105103
g2(air, ~ Month) |>
106104
mark_interval(encode = list(y = 'Temp')) |>
107105
style_mark(fill = '#85C5A6', fillOpacity = 0.7) |>
106+
scale_y(independent = TRUE) |>
108107
axis_y(title = 'Temperature (°F)', titleFill = '#85C5A6') |>
109108
mark_line(encode = list(y = 'Wind')) |>
110109
style_mark(stroke = 'steelblue', lineWidth = 2) |>

examples/titles-tooltips.Rmd

Lines changed: 28 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,26 @@ g2(df, y ~ x) |>
4747
### Enable crosshairs
4848

4949
Crosshairs are lines that track the cursor position. Pass `crosshairs = TRUE`
50-
to enable them.
50+
to `tooltip_()`. Crosshairs work with line and area charts (which use series
51+
tooltip by default).
5152

5253
```{r}
53-
g2(mtcars, hp ~ mpg) |>
54+
air = aggregate(Temp ~ Month, data = airquality, FUN = mean)
55+
air$Month = month.abb[air$Month]
56+
g2(air, Temp ~ Month) |>
57+
mark_line() |>
5458
mark_point() |>
5559
tooltip_(crosshairs = TRUE)
5660
```
5761

62+
### Crosshairs with custom style
63+
64+
```{r}
65+
g2(air, Temp ~ Month) |>
66+
mark_line() |>
67+
tooltip_(crosshairs = TRUE, crosshairsStroke = '#aaa', crosshairsLineWidth = 1)
68+
```
69+
5870
### Shared tooltip (useful for multi-series)
5971

6072
A shared tooltip shows values from all series at the current x position.
@@ -69,43 +81,36 @@ g2(df, y ~ x, color = ~ group) |>
6981
tooltip_(shared = TRUE)
7082
```
7183

72-
### Crosshairs with custom style
73-
74-
```{r}
75-
g2(mtcars, hp ~ mpg) |>
76-
mark_point() |>
77-
tooltip_(crosshairs = TRUE, crosshairsStroke = '#aaa', crosshairsLineWidth = 1)
78-
```
79-
8084
### Format tooltip values
8185

82-
Use `valueFormatter` with a
83-
[d3-format](https://d3js.org/d3-format) string to format the displayed value.
84-
Call `tooltip_()` **after** the mark.
86+
Pass a `tooltip` list to the mark function to format values using a
87+
[d3-format](https://d3js.org/d3-format) string. This keeps the tooltip data
88+
configuration with the mark that owns it.
8589

8690
```{r}
87-
g2(mtcars, hp ~ mpg) |>
88-
mark_point() |>
89-
tooltip_(channel = 'y', valueFormatter = '.0f')
91+
g2(airquality, Temp ~ Wind) |>
92+
mark_point(tooltip = list(channel = 'y', valueFormatter = '.1f'))
9093
```
9194

9295
### Format tooltip values in a normalized stacked bar chart
9396

97+
After `transform_('normalizeY')`, y values are on a 0–1 scale. Use
98+
`valueFormatter = '.0%'` to display them as percentages.
99+
94100
```{r}
95-
data(diamonds, package = 'ggplot2')
96-
aggregate(data = diamonds, price ~ cut + clarity, FUN = mean) |>
97-
g2(price ~ clarity, color = ~ cut) |>
98-
mark_interval() |>
101+
wb = aggregate(breaks ~ wool + tension, data = warpbreaks, FUN = mean)
102+
g2(wb, breaks ~ tension, color = ~ wool) |>
103+
mark_interval(tooltip = list(channel = 'y', valueFormatter = '.0%')) |>
99104
transform_('stackY') |>
100105
transform_('normalizeY') |>
101-
scale_('color', palette = 'puRd') |>
102106
axis_y(labelFormatter = '.0%') |>
103-
tooltip_(channel = 'y', valueFormatter = '.1%') |>
104-
legend_color(position = 'right')
107+
legend_color(position = 'right') |>
108+
title_('Warp Breaks by Tension and Wool Type')
105109
```
106110

107111
### Disable tooltip
108112

109113
```{r}
110114
p |> tooltip_(FALSE)
111115
```
116+

man/axis_.Rd

Lines changed: 11 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/scale_.Rd

Lines changed: 9 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/tooltip_.Rd

Lines changed: 21 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)