Skip to content

Commit 19f40f3

Browse files
authored
Merge pull request #886 from ropensci/feature/sf
add support for geom_sf()
2 parents 8c26c2f + 72a2727 commit 19f40f3

File tree

14 files changed

+390
-34
lines changed

14 files changed

+390
-34
lines changed

.github/ISSUE_TEMPLATE.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
Thanks for your interest in plotly!
1+
Please briefly describe your problem and what output you expect. If you have a question, please don't use this form, but instead ask on the community forum <http://community.plot.ly/c/api/r> or stackoverflow <http://stackoverflow.com>.
22

3-
Before opening an issue, please search for existing and closed issues. If your problem or idea is not addressed yet, [please open a new issue](https://github.com/ropensci/plotly/issues/new).
3+
Please include a minimal reprex. The goal of a reprex is to make it as easy as possible for me to recreate your problem so that I can fix it. If you've never heard of a reprex before, start by reading <https://github.com/jennybc/reprex#what-is-a-reprex>, and follow the advice further down the page. Do NOT include session info unless it's explicitly asked for, or you've used `reprex::reprex(..., si = TRUE)` to hide it away.
44

5-
Bug reports must be accompanied with a [reproducible example](http://stackoverflow.com/questions/5963269/how-to-make-a-great-r-reproducible-example).
5+
Delete these instructions once you have read them.
66

7-
Note that GitHub issues are reserved for bug reports and feature requests only. Implementation questions should be asked on community.plot.ly (tagged [`R`](http://community.plot.ly/c/api/r)) or on Stack Overflow (tagged [`plotly`](https://stackoverflow.com/questions/tagged/plotly)).
7+
---
88

9-
Comments on GitHub issues or pull requests should add content to the discussions. Approbation comments such as *+1* or *I would like this feature to be implemented as well* will be deleted by the maintainers. Please use [GitHub reactions](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments) instead.
9+
Brief description of the problem
10+
11+
```r
12+
# insert reprex here
13+
```

.travis.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ before_script:
4040
- echo "Sys.setenv('plotly_username' = 'cpsievert')" > ~/.Rprofile
4141
- git clone https://github.com/cpsievert/plotly-test-table.git ../plotly-test-table
4242
- cd ..; rm -f *.tar.gz; R CMD build $R_PKG
43-
- R CMD INSTALL ${R_PKG}_*.tar.gz
4443

4544
script:
4645
# run R CMD check on the non-pull request build

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Suggests:
6060
webshot,
6161
listviewer,
6262
dendextend,
63+
sf,
6364
RSelenium,
6465
png,
6566
IRdisplay

NAMESPACE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ S3method(geom2trace,GeomPolygon)
2020
S3method(geom2trace,GeomText)
2121
S3method(geom2trace,GeomTile)
2222
S3method(geom2trace,default)
23+
S3method(get_l,LINESTRING)
24+
S3method(get_l,MULTILINESTRING)
25+
S3method(get_l,MULTIPOINT)
26+
S3method(get_l,MULTIPOLYGON)
27+
S3method(get_l,POINT)
28+
S3method(get_l,POLYGON)
29+
S3method(get_x,LINESTRING)
30+
S3method(get_x,MULTILINESTRING)
31+
S3method(get_x,MULTIPOINT)
32+
S3method(get_x,MULTIPOLYGON)
33+
S3method(get_x,POINT)
34+
S3method(get_x,POLYGON)
35+
S3method(get_y,LINESTRING)
36+
S3method(get_y,MULTILINESTRING)
37+
S3method(get_y,MULTIPOINT)
38+
S3method(get_y,MULTIPOLYGON)
39+
S3method(get_y,POINT)
40+
S3method(get_y,POLYGON)
2341
S3method(ggplot,plotly)
2442
S3method(ggplotly,ggmatrix)
2543
S3method(ggplotly,ggplot)
@@ -67,6 +85,7 @@ S3method(to_basic,GeomRect)
6785
S3method(to_basic,GeomRibbon)
6886
S3method(to_basic,GeomRug)
6987
S3method(to_basic,GeomSegment)
88+
S3method(to_basic,GeomSf)
7089
S3method(to_basic,GeomSmooth)
7190
S3method(to_basic,GeomSpoke)
7291
S3method(to_basic,GeomStep)
@@ -126,6 +145,9 @@ export(filter)
126145
export(filter_)
127146
export(geom2trace)
128147
export(get_figure)
148+
export(get_l)
149+
export(get_x)
150+
export(get_y)
129151
export(gg2list)
130152
export(ggplotly)
131153
export(group_by)

R/ggplotly.R

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -483,10 +483,10 @@ gg2list <- function(p, width = NULL, height = NULL,
483483
layout$layout$xanchor <- paste0("y", sub("^1$", "", layout$layout$xanchor))
484484
layout$layout$yanchor <- paste0("x", sub("^1$", "", layout$layout$yanchor))
485485
# for some layers2traces computations, we need the range of each panel
486-
layout$layout$x_min <- sapply(layout$panel_params, function(z) min(z$x.range))
487-
layout$layout$x_max <- sapply(layout$panel_params, function(z) max(z$x.range))
488-
layout$layout$y_min <- sapply(layout$panel_params, function(z) min(z$y.range))
489-
layout$layout$y_max <- sapply(layout$panel_params, function(z) max(z$y.range))
486+
layout$layout$x_min <- sapply(layout$panel_params, function(z) min(z$x.range %||% z$x_range))
487+
layout$layout$x_max <- sapply(layout$panel_params, function(z) max(z$x.range %||% z$x_range))
488+
layout$layout$y_min <- sapply(layout$panel_params, function(z) min(z$y.range %||% z$y_range))
489+
layout$layout$y_max <- sapply(layout$panel_params, function(z) max(z$y.range %||% z$y_range))
490490

491491
# layers -> plotly.js traces
492492
plot$tooltip <- tooltip
@@ -496,7 +496,7 @@ gg2list <- function(p, width = NULL, height = NULL,
496496

497497
# reattach crosstalk key-set attribute
498498
data <- Map(function(x, y) structure(x, set = y), data, sets)
499-
traces <- layers2traces(data, prestats_data, layout$layout, plot)
499+
traces <- layers2traces(data, prestats_data, layout, plot)
500500

501501
gglayout <- layers2layout(gglayout, layers, layout$layout)
502502

@@ -584,6 +584,60 @@ gg2list <- function(p, width = NULL, height = NULL,
584584
axisName <- lay[, paste0(xy, "axis")]
585585
anchor <- lay[, paste0(xy, "anchor")]
586586
rng <- layout$panel_params[[i]]
587+
588+
# panel_params is quite different for "CoordSf"
589+
if ("CoordSf" %in% class(p$coordinates)) {
590+
# see CoordSf$render_axis_v
591+
direction <- if (xy == "x") "E" else "N"
592+
idx <- rng$graticule$type == direction & !is.na(rng$graticule$degree_label)
593+
tickData <- rng$graticule[idx, ]
594+
# TODO: how to convert a language object to unicode character string?
595+
rng[[paste0(xy, ".labels")]] <- as.character(tickData[["degree_label"]])
596+
rng[[paste0(xy, ".major")]] <- tickData[[paste0(xy, "_start")]]
597+
598+
# If it doesn't already exist (for this panel),
599+
# generate graticule (as done in, CoordSf$render_bg)
600+
isGrill <- vapply(traces, function(tr) {
601+
identical(tr$xaxis, lay$xaxis) &&
602+
identical(tr$yaxis, lay$yaxis) &&
603+
isTRUE(tr$`_isGraticule`)
604+
}, logical(1))
605+
606+
if (sum(isGrill) == 0) {
607+
d <- expand(rng$graticule)
608+
d$x <- scales::rescale(d$x, rng$x_range, from = c(0, 1))
609+
d$y <- scales::rescale(d$y, rng$y_range, from = c(0, 1))
610+
params <- list(
611+
colour = theme$panel.grid.major$colour,
612+
size = theme$panel.grid.major$size,
613+
linetype = theme$panel.grid.major$linetype
614+
)
615+
grill <- geom2trace.GeomPath(d, params)
616+
grill$hoverinfo <- "none"
617+
grill$showlegend <- FALSE
618+
grill$`_isGraticule` <- TRUE
619+
grill$xaxis <- lay$xaxis
620+
grill$yaxis <- lay$yaxis
621+
622+
traces <- c(list(grill), traces)
623+
}
624+
625+
# if labels are empty, don't show axis ticks
626+
emptyTicks <- all(with(
627+
rng$graticule, sapply(degree_label, is.na) | sapply(degree_label, nchar) == 0
628+
))
629+
if (emptyTicks) {
630+
theme$axis.ticks.length <- 0
631+
} else{
632+
# convert the special *degree expression in plotmath to HTML entity
633+
# TODO: can this be done more generally for all ?
634+
rng[[paste0(xy, ".labels")]] <- sub(
635+
"\\*\\s+degree\\s+\\*", "&#176;", rng[[paste0(xy, ".labels")]]
636+
)
637+
}
638+
639+
}
640+
587641
# stuff like layout$panel_params is already flipped, but scales aren't
588642
sc <- if (inherits(plot$coordinates, "CoordFlip")) {
589643
scales$get_scales(setdiff(c("x", "y"), xy))
@@ -615,7 +669,7 @@ gg2list <- function(p, width = NULL, height = NULL,
615669
# TODO: log type?
616670
type = if (isDateType) "date" else if (isDiscreteType) "category" else "linear",
617671
autorange = isDynamic,
618-
range = rng[[paste0(xy, ".range")]],
672+
range = rng[[paste0(xy, ".range")]] %||% rng[[paste0(xy, "_range")]],
619673
tickmode = if (isDynamic) "auto" else "array",
620674
ticktext = rng[[paste0(xy, ".labels")]],
621675
tickvals = rng[[paste0(xy, ".major")]],
@@ -632,7 +686,8 @@ gg2list <- function(p, width = NULL, height = NULL,
632686
showline = !is_blank(axisLine),
633687
linecolor = toRGB(axisLine$colour),
634688
linewidth = unitConvert(axisLine, "pixels", type),
635-
showgrid = !is_blank(panelGrid),
689+
# TODO: always `showgrid=FALSE` and implement our own using traces
690+
showgrid = !is_blank(panelGrid) && !"CoordSf" %in% class(p$coordinates),
636691
domain = sort(as.numeric(doms[i, paste0(xy, c("start", "end"))])),
637692
gridcolor = toRGB(panelGrid$colour),
638693
gridwidth = unitConvert(panelGrid, "pixels", type),
@@ -642,6 +697,13 @@ gg2list <- function(p, width = NULL, height = NULL,
642697
titlefont = text2font(axisTitle)
643698
)
644699

700+
# set scaleanchor/scaleratio if these are fixed coordinates
701+
fixed_coords <- c("CoordSf", "CoordFixed", "CoordMap", "CoordQuickmap")
702+
if (inherits(p$coordinates, fixed_coords) && xy == "y") {
703+
axisObj$scaleanchor <- anchor
704+
axisObj$scaleratio <- p$coordinates$ratio
705+
}
706+
645707
# tickvals are currently on 0-1 scale, but we want them on data scale
646708
axisObj$tickvals <- scales::rescale(
647709
axisObj$tickvals, to = axisObj$range, from = c(0, 1)
@@ -656,8 +718,6 @@ gg2list <- function(p, width = NULL, height = NULL,
656718
}
657719
}
658720

659-
660-
661721
if (isDateType) {
662722
axisObj$range <- invert_date(axisObj$range, sc)
663723
traces <- lapply(traces, function(tr) {
@@ -785,6 +845,7 @@ gg2list <- function(p, width = NULL, height = NULL,
785845
}
786846
} # end of panel loop
787847

848+
788849
# ------------------------------------------------------------------------
789850
# guide conversion
790851
# Strategy: Obtain and translate the output of ggplot2:::guides_train().

R/layers2traces.R

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ layers2traces <- function(data, prestats_data, layout, p) {
99
y[["geom_params"]], y[["stat_params"]], y[["aes_params"]],
1010
position = ggtype(y, "position")
1111
)
12+
1213
# by default, show all user-specified and generated aesthetics in hovertext
1314
stat_aes <- y$stat$default_aes
1415
map <- c(y$mapping, stat_aes[grepl("^\\.\\.", as.character(stat_aes))])
@@ -27,6 +28,10 @@ layers2traces <- function(data, prestats_data, layout, p) {
2728
if (identical("fills", hover_on(x))) {
2829
map <- map[!names(map) %in% c("x", "xmin", "xmax", "y", "ymin", "ymax")]
2930
}
31+
# disregard geometry mapping in hovertext for GeomSf
32+
if ("GeomSf" %in% class(y$geom)) {
33+
map <- map[!names(map) %in% "geometry"]
34+
}
3035
param[["hoverTextAes"]] <- map
3136
param
3237
}, data, p$layers)
@@ -132,8 +137,8 @@ layers2traces <- function(data, prestats_data, layout, p) {
132137
# each trace is with respect to which axis?
133138
for (j in seq_along(trs)) {
134139
panel <- unique(dl[[j]]$PANEL)
135-
trs[[j]]$xaxis <- sub("axis", "", layout[panel, "xaxis"])
136-
trs[[j]]$yaxis <- sub("axis", "", layout[panel, "yaxis"])
140+
trs[[j]]$xaxis <- sub("axis", "", layout$layout[panel, "xaxis"])
141+
trs[[j]]$yaxis <- sub("axis", "", layout$layout[panel, "yaxis"])
137142
}
138143
trace.list <- c(trace.list, trs)
139144
}
@@ -261,6 +266,32 @@ to_basic.GeomRect <- function(data, prestats_data, layout, params, p, ...) {
261266
prefix_class(dat, c("GeomPolygon", "GeomRect"))
262267
}
263268

269+
#' @export
270+
to_basic.GeomSf <- function(data, prestats_data, layout, params, p, ...) {
271+
272+
data <- expand(data)
273+
274+
# determine the type of simple feature for each row
275+
# recode the simple feature with the type of geometry used to render it
276+
data[[".plotlySfType"]] <- sapply(data$geometry, function(x) class(x)[2])
277+
dat <- dplyr::mutate(
278+
data, .plotlySfType = dplyr::recode(.plotlySfType,
279+
MULTIPOLYGON = "GeomPolygon",
280+
MULTILINESTRING = "GeomLine",
281+
MULTIPOINT = "GeomPoint",
282+
POLYGON = "GeomPolygon",
283+
LINESTRING = "GeomLine",
284+
POINT = "GeomPoint"
285+
))
286+
287+
# return a list of data frames...one for every geometry (a la, GeomSmooth)
288+
d <- split(dat, dat[[".plotlySfType"]])
289+
for (i in seq_along(d)) {
290+
d[[i]] <- prefix_class(d[[i]], names(d)[[i]])
291+
}
292+
if (length(d) == 1) d[[1]] else d
293+
}
294+
264295
#' @export
265296
to_basic.GeomMap <- function(data, prestats_data, layout, params, p, ...) {
266297
common <- intersect(data$map_id, params$map$id)
@@ -354,7 +385,7 @@ to_basic.GeomAbline <- function(data, prestats_data, layout, params, p, ...) {
354385
data$group <- interaction(
355386
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
356387
)
357-
lay <- tidyr::gather_(layout, "variable", "x", c("x_min", "x_max"))
388+
lay <- tidyr::gather_(layout$layout, "variable", "x", c("x_min", "x_max"))
358389
data <- merge(lay[c("PANEL", "x")], data, by = "PANEL")
359390
data[["y"]] <- with(data, intercept + slope * x)
360391
prefix_class(data, c("GeomHline", "GeomPath"))
@@ -366,7 +397,7 @@ to_basic.GeomHline <- function(data, prestats_data, layout, params, p, ...) {
366397
data$group <- do.call(paste,
367398
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
368399
)
369-
lay <- tidyr::gather_(layout, "variable", "x", c("x_min", "x_max"))
400+
lay <- tidyr::gather_(layout$layout, "variable", "x", c("x_min", "x_max"))
370401
data <- merge(lay[c("PANEL", "x")], data, by = "PANEL")
371402
data[["y"]] <- data$yintercept
372403
prefix_class(data, c("GeomHline", "GeomPath"))
@@ -378,7 +409,7 @@ to_basic.GeomVline <- function(data, prestats_data, layout, params, p, ...) {
378409
data$group <- do.call(paste,
379410
data[!grepl("group", names(data)) & !vapply(data, anyNA, logical(1))]
380411
)
381-
lay <- tidyr::gather_(layout, "variable", "y", c("y_min", "y_max"))
412+
lay <- tidyr::gather_(layout$layout, "variable", "y", c("y_min", "y_max"))
382413
data <- merge(lay[c("PANEL", "y")], data, by = "PANEL")
383414
data[["x"]] <- data$xintercept
384415
prefix_class(data, c("GeomVline", "GeomPath"))
@@ -394,7 +425,7 @@ to_basic.GeomJitter <- function(data, prestats_data, layout, params, p, ...) {
394425
to_basic.GeomErrorbar <- function(data, prestats_data, layout, params, p, ...) {
395426
# width for ggplot2 means size of the entire bar, on the data scale
396427
# (plotly.js wants half, in pixels)
397-
data <- merge(data, layout, by = "PANEL", sort = FALSE)
428+
data <- merge(data, layout$layout, by = "PANEL", sort = FALSE)
398429
data$width <- (data[["xmax"]] - data[["x"]]) /(data[["x_max"]] - data[["x_min"]])
399430
data$fill <- NULL
400431
prefix_class(data, "GeomErrorbar")
@@ -404,7 +435,7 @@ to_basic.GeomErrorbar <- function(data, prestats_data, layout, params, p, ...) {
404435
to_basic.GeomErrorbarh <- function(data, prestats_data, layout, params, p, ...) {
405436
# height for ggplot2 means size of the entire bar, on the data scale
406437
# (plotly.js wants half, in pixels)
407-
data <- merge(data, layout, by = "PANEL", sort = FALSE)
438+
data <- merge(data, layout$layout, by = "PANEL", sort = FALSE)
408439
data$width <- (data[["ymax"]] - data[["y"]]) / (data[["y_max"]] - data[["y_min"]])
409440
data$fill <- NULL
410441
prefix_class(data, "GeomErrorbarh")
@@ -443,11 +474,11 @@ to_basic.GeomPointrange <- function(data, prestats_data, layout, params, p, ...)
443474
#' @export
444475
to_basic.GeomDotplot <- function(data, prestats_data, layout, params, p, ...) {
445476
if (identical(params$binaxis, "y")) {
446-
dotdia <- params$dotsize * data$binwidth[1]/(layout$y_max - layout$y_min)
477+
dotdia <- params$dotsize * data$binwidth[1]/(layout$layout$y_max - layout$layout$y_min)
447478
data$size <- as.numeric(grid::convertHeight(grid::unit(dotdia, "npc"), "mm")) / 2
448479
data$x <- (data$countidx - 0.5) * (as.numeric(dotdia) * 6)
449480
} else {
450-
dotdia <- params$dotsize * data$binwidth[1]/(layout$x_max - layout$x_min)
481+
dotdia <- params$dotsize * data$binwidth[1]/(layout$layout$x_max - layout$layout$x_min)
451482
data$size <- as.numeric(grid::convertWidth(grid::unit(dotdia, "npc"), "mm")) / 2
452483
# TODO: why times 6?!?!
453484
data$y <- (data$countidx - 0.5) * (as.numeric(dotdia) * 6)
@@ -482,6 +513,7 @@ utils::globalVariables(c("xmin", "xmax", "y", "size"))
482513
#' @export
483514
to_basic.GeomRug <- function(data, prestats_data, layout, params, p, ...) {
484515
# allow the tick length to vary across panels
516+
layout <- layout$layout
485517
layout$tickval_y <- 0.03 * abs(layout$y_max - layout$y_min)
486518
layout$tickval_x <- 0.03 * abs(layout$x_max - layout$x_min)
487519
data <- merge(data, layout[c("PANEL", "x_min", "x_max", "y_min", "y_max", "tickval_y", "tickval_x")])
@@ -687,7 +719,9 @@ geom2trace.GeomBar <- function(data, params, p) {
687719

688720
#' @export
689721
geom2trace.GeomPolygon <- function(data, params, p) {
722+
690723
data <- group2NA(data)
724+
691725
L <- list(
692726
x = data[["x"]],
693727
y = data[["y"]],

0 commit comments

Comments
 (0)