Skip to content

Commit 57cc44f

Browse files
cpsievertwch
authored andcommitted
Coordmap info should retain discrete limits (#2410)
* ggplot2 input brushes should retain discrete range mapping, and be imposed in brushedPoints(), closes #1433 * simplify logic and reduce required storage * get nearPoints() working as well, cleanup * only remember scale range if ggplot is facet with a free discrete axis * Use the scale limits (before the range) since the former is specified, that's what is actually shown on the plot also, introduce within_brush() helper to consistently handle missing values produced by asNumber() * also use scale limits in older versions of ggplot2 * DRY * discrete_mapping -> discrete_limits; better comments * update test expectation * a couple unit tests * update comment to reflect new coordmap data structure * use unlink() not rm() * add some tests for specifying scale limits and labels * Use get_limits() if available * update news * better name and comment for new asNumber() argument
1 parent 4eaa9c7 commit 57cc44f

File tree

4 files changed

+227
-47
lines changed

4 files changed

+227
-47
lines changed

NEWS.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
shiny 1.3.2.9000
22
=======
33

4+
## Changes
5+
6+
* Resolved ([#1433](https://github.com/rstudio/shiny/issues/1433)): `plotOutput()`'s coordmap info now includes discrete axis limits for **ggplot2** plots. As a result, any **shinytest** tests that contain **ggplot2** plots with discrete axes (that were recorded before this change) will now report differences that can safely be updated. This new coordmap info was added to correctly infer what data points are within an input brush and/or near input click/hover in scenarios where a non-trivial discrete axis scale is involved (e.g., whenever `scale_[x/y]_discrete(limits = ...)` and/or free scales across multiple discrete axes are used). ([#2410](https://github.com/rstudio/shiny/pull/2410))
7+
48
### Improvements
59

610
* Resolved ([#2402](https://github.com/rstudio/shiny/issues/2402)): An informative warning is now thrown for mis-specified (date) strings in `dateInput()`, `updateDateInput()`, `dateRangeInput()`, and `updateDateRangeInput()`. ([#2403](https://github.com/rstudio/shiny/pull/2403))

R/image-interact.R

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,14 @@ brushedPoints <- function(df, brush, xvar = NULL, yvar = NULL,
8888
stop("brushedPoints: not able to automatically infer `xvar` from brush")
8989
if (!(xvar %in% names(df)))
9090
stop("brushedPoints: `xvar` ('", xvar ,"') not in names of input")
91-
# Extract data values from the data frame
92-
x <- asNumber(df[[xvar]])
93-
keep_rows <- keep_rows & (x >= brush$xmin & x <= brush$xmax)
91+
keep_rows <- keep_rows & within_brush(df[[xvar]], brush, "x")
9492
}
9593
if (use_y) {
9694
if (is.null(yvar))
9795
stop("brushedPoints: not able to automatically infer `yvar` from brush")
9896
if (!(yvar %in% names(df)))
9997
stop("brushedPoints: `yvar` ('", yvar ,"') not in names of input")
100-
y <- asNumber(df[[yvar]])
101-
keep_rows <- keep_rows & (y >= brush$ymin & y <= brush$ymax)
98+
keep_rows <- keep_rows & within_brush(df[[yvar]], brush, "y")
10299
}
103100

104101
# Find which rows are matches for the panel vars (if present)
@@ -281,8 +278,8 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
281278
stop("nearPoints: `yvar` ('", yvar ,"') not in names of input")
282279

283280
# Extract data values from the data frame
284-
x <- asNumber(df[[xvar]])
285-
y <- asNumber(df[[yvar]])
281+
x <- asNumber(df[[xvar]], coordinfo$domain$discrete_limits$x)
282+
y <- asNumber(df[[yvar]], coordinfo$domain$discrete_limits$y)
286283

287284
# Get the coordinates of the point (in img pixel coordinates)
288285
point_img <- coordinfo$coords_img
@@ -402,11 +399,27 @@ nearPoints <- function(df, coordinfo, xvar = NULL, yvar = NULL,
402399
# ..$ y: NULL
403400
# $ .nonce : num 0.603
404401

405-
402+
# Helper to determine if data values are within the limits of
403+
# an input brush
404+
within_brush <- function(vals, brush, var = "x") {
405+
var <- match.arg(var, c("x", "y"))
406+
vals <- asNumber(vals, brush$domain$discrete_limits[[var]])
407+
# It's possible for a non-missing data values to not
408+
# map to the axis limits, for example:
409+
# https://github.com/rstudio/shiny/pull/2410#issuecomment-488100881
410+
!is.na(vals) &
411+
vals >= brush[[paste0(var, "min")]] &
412+
vals <= brush[[paste0(var, "max")]]
413+
}
406414

407415
# Coerce various types of variables to numbers. This works for Date, POSIXt,
408416
# characters, and factors. Used because the mouse coords are numeric.
409-
asNumber <- function(x) {
417+
# The `levels` argument should be used when mapping this variable to
418+
# a known set of discrete levels, which is needed for ggplot2 since
419+
# it allows you to control ordering and possible values of a discrete
420+
# positional scale (#2410)
421+
asNumber <- function(x, levels = NULL) {
422+
if (length(levels)) return(match(x, levels))
410423
if (is.character(x)) x <- as.factor(x)
411424
if (is.factor(x)) x <- as.integer(x)
412425
as.numeric(x)

R/render-plot.R

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -353,62 +353,88 @@ custom_print.ggplot <- function(x) {
353353
# With a faceted ggplot2 plot, the outer list contains two objects, each of
354354
# which represents one panel. In this example, there is one panelvar, but there
355355
# can be up to two of them.
356-
# mtc <- mtcars
357-
# mtc$am <- factor(mtc$am)
358-
# p <- print(ggplot(mtc, aes(wt, mpg)) + geom_point() + facet_wrap(~ am))
359-
# str(getGgplotCoordmap(p, 400, 300, 72))
356+
# p <- print(ggplot(mpg) + geom_point(aes(fl, cty), alpha = 0.2) + facet_wrap(~drv, scales = "free_x"))
357+
# str(getGgplotCoordmap(p, 500, 400, 72))
360358
# List of 2
361-
# $ panels:List of 2
359+
# $ panels:List of 3
362360
# ..$ :List of 8
363361
# .. ..$ panel : num 1
364362
# .. ..$ row : int 1
365363
# .. ..$ col : int 1
366364
# .. ..$ panel_vars:List of 1
367-
# .. .. ..$ panelvar1: Factor w/ 2 levels "0","1": 1
365+
# .. .. ..$ panelvar1: chr "4"
368366
# .. ..$ log :List of 2
369367
# .. .. ..$ x: NULL
370368
# .. .. ..$ y: NULL
371-
# .. ..$ domain :List of 4
372-
# .. .. ..$ left : num 1.32
373-
# .. .. ..$ right : num 5.62
374-
# .. .. ..$ bottom: num 9.22
375-
# .. .. ..$ top : num 35.1
369+
# .. ..$ domain :List of 5
370+
# .. .. ..$ left : num 0.4
371+
# .. .. ..$ right : num 4.6
372+
# .. .. ..$ bottom : num 7.7
373+
# .. .. ..$ top : num 36.3
374+
# .. .. ..$ discrete_limits:List of 1
375+
# .. .. .. ..$ x: chr [1:4] "d" "e" "p" "r"
376376
# .. ..$ mapping :List of 3
377-
# .. .. ..$ x : chr "wt"
378-
# .. .. ..$ y : chr "mpg"
379-
# .. .. ..$ panelvar1: chr "am"
377+
# .. .. ..$ x : chr "fl"
378+
# .. .. ..$ y : chr "cty"
379+
# .. .. ..$ panelvar1: chr "drv"
380380
# .. ..$ range :List of 4
381381
# .. .. ..$ left : num 33.3
382-
# .. .. ..$ right : num 191
383-
# .. .. ..$ bottom: num 328
382+
# .. .. ..$ right : num 177
383+
# .. .. ..$ bottom: num 448
384384
# .. .. ..$ top : num 23.1
385385
# ..$ :List of 8
386386
# .. ..$ panel : num 2
387387
# .. ..$ row : int 1
388388
# .. ..$ col : int 2
389389
# .. ..$ panel_vars:List of 1
390-
# .. .. ..$ panelvar1: Factor w/ 2 levels "0","1": 2
390+
# .. .. ..$ panelvar1: chr "f"
391391
# .. ..$ log :List of 2
392392
# .. .. ..$ x: NULL
393393
# .. .. ..$ y: NULL
394-
# .. ..$ domain :List of 4
395-
# .. .. ..$ left : num 1.32
396-
# .. .. ..$ right : num 5.62
397-
# .. .. ..$ bottom: num 9.22
398-
# .. .. ..$ top : num 35.1
394+
# .. ..$ domain :List of 5
395+
# .. .. ..$ left : num 0.4
396+
# .. .. ..$ right : num 5.6
397+
# .. .. ..$ bottom : num 7.7
398+
# .. .. ..$ top : num 36.3
399+
# .. .. ..$ discrete_limits:List of 1
400+
# .. .. .. ..$ x: chr [1:5] "c" "d" "e" "p" ...
399401
# .. ..$ mapping :List of 3
400-
# .. .. ..$ x : chr "wt"
401-
# .. .. ..$ y : chr "mpg"
402-
# .. .. ..$ panelvar1: chr "am"
402+
# .. .. ..$ x : chr "fl"
403+
# .. .. ..$ y : chr "cty"
404+
# .. .. ..$ panelvar1: chr "drv"
403405
# .. ..$ range :List of 4
404-
# .. .. ..$ left : num 197
405-
# .. .. ..$ right : num 355
406-
# .. .. ..$ bottom: num 328
406+
# .. .. ..$ left : num 182
407+
# .. .. ..$ right : num 326
408+
# .. .. ..$ bottom: num 448
409+
# .. .. ..$ top : num 23.1
410+
# ..$ :List of 8
411+
# .. ..$ panel : num 3
412+
# .. ..$ row : int 1
413+
# .. ..$ col : int 3
414+
# .. ..$ panel_vars:List of 1
415+
# .. .. ..$ panelvar1: chr "r"
416+
# .. ..$ log :List of 2
417+
# .. .. ..$ x: NULL
418+
# .. .. ..$ y: NULL
419+
# .. ..$ domain :List of 5
420+
# .. .. ..$ left : num 0.4
421+
# .. .. ..$ right : num 3.6
422+
# .. .. ..$ bottom : num 7.7
423+
# .. .. ..$ top : num 36.3
424+
# .. .. ..$ discrete_limits:List of 1
425+
# .. .. .. ..$ x: chr [1:3] "e" "p" "r"
426+
# .. ..$ mapping :List of 3
427+
# .. .. ..$ x : chr "fl"
428+
# .. .. ..$ y : chr "cty"
429+
# .. .. ..$ panelvar1: chr "drv"
430+
# .. ..$ range :List of 4
431+
# .. .. ..$ left : num 331
432+
# .. .. ..$ right : num 475
433+
# .. .. ..$ bottom: num 448
407434
# .. .. ..$ top : num 23.1
408435
# $ dims :List of 2
409-
# ..$ width : num 400
410-
# ..$ height: num 300
411-
436+
# ..$ width : num 500
437+
# ..$ height: num 400
412438

413439
getCoordmap <- function(x, width, height, res) {
414440
if (inherits(x, "ggplot_build_gtable")) {
@@ -570,6 +596,9 @@ find_panel_info_api <- function(b) {
570596
domain$bottom <- -domain$bottom
571597
}
572598

599+
domain <- add_discrete_limits(domain, xscale, "x")
600+
domain <- add_discrete_limits(domain, yscale, "y")
601+
573602
domain
574603
}
575604

@@ -689,6 +718,9 @@ find_panel_info_non_api <- function(b, ggplot_format) {
689718
domain$bottom <- -domain$bottom
690719
}
691720

721+
domain <- add_discrete_limits(domain, xscale, "x")
722+
domain <- add_discrete_limits(domain, yscale, "y")
723+
692724
domain
693725
}
694726

@@ -995,3 +1027,23 @@ find_panel_ranges <- function(g, res) {
9951027
)
9961028
})
9971029
}
1030+
1031+
# Remember the x/y limits of discrete axes. This info is
1032+
# necessary to properly inverse map the numeric (i.e., trained)
1033+
# positions back to the data scale, for example:
1034+
# https://github.com/rstudio/shiny/pull/2410#issuecomment-487783828
1035+
# https://github.com/rstudio/shiny/pull/2410#issuecomment-488100881
1036+
#
1037+
# Eventually, we may want to consider storing the entire ggplot2
1038+
# object server-side and querying information from that object
1039+
# as we need it...that's the only way we'll ever be able to
1040+
# faithfully brush examples like this:
1041+
# https://github.com/rstudio/shiny/issues/2411
1042+
add_discrete_limits <- function(domain, scale, var = "x") {
1043+
var <- match.arg(var, c("x", "y"))
1044+
if (!is.function(scale$is_discrete) || !is.function(scale$get_limits)) return(domain)
1045+
if (scale$is_discrete()) {
1046+
domain$discrete_limits[[var]] <- scale$get_limits()
1047+
}
1048+
domain
1049+
}

0 commit comments

Comments
 (0)