-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Enable geom_sf to automatically determine the legend type #3646
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Enable geom_sf to automatically determine the legend type #3646
Conversation
Clean up theme addition (tidyverse#3570)
update to tidyverse edition 20191118
update to tidyverse edition 20191129
Some simple examples:
|
Please consider this pr. @yutannihilation @clauswilke |
Just a quick comment. It's not always accurate to guess what the legend should be at the For per-layer legend feature, I'd recommend you to create it as an extension package, not as a PR; I'm afraid your PR would be unlikely to be accepted. I believe it's not impossible, but, IMHO, it would be only after we implement per-layer scales, which doesn't seem to happen in any near future. I don't want you to waste your time. |
Thanks for your reply~ You raised two problems, but I think I can properly response to them. |
Reply for the first problem. Essentially, your problem is that my pr is not automatical enough when layer_sf$stat is replaced by another Stat* which will change the geometry type of sf data. Two reasons:
For the above two reasons, I think my pr would be an efficient and harmless approach. At last, @clauswilke created a way to automatically determine the name of the geometry column: Lines 29 to 37 in 6424808
It also dose not know the dynamical sf geometry type. If @clauswilke add support to the dynamical name of sfc column, I will improve my pr in the same way. |
Reply for the second problem. The main point of the second problem is the lack of a geometry scale. By now, the code is completed, it can work! |
I think it's fine to guess the correct legend type in
|
Per-layer legend glyphs is a separate issue that should be addressed for all of ggplot2, not just for |
@clauswilke |
OK, then let's move this forward. I'll add a few more comments. |
R/layer-sf.R
Outdated
if (sf_geometry_type(data) %in% c("POINT", "MULTIPOINT")) | ||
self$geom_params$legend <- "point" | ||
else if (sf_geometry_type(data) %in% c("LINESTRING", "MULTILINESTRING", | ||
"CIRCULARSTRING", "COMPOUNDCURVE", | ||
"CURVE", "MULTICURVE")) | ||
self$geom_params$legend <- "line" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably you can use sf_types
instead of a raw list of sf types.
Lines 300 to 306 in 6424808
sf_types <- c(GEOMETRY = "other", POINT = "point", LINESTRING = "line", | |
POLYGON = "other", MULTIPOINT = "point", MULTILINESTRING = "line", | |
MULTIPOLYGON = "other", GEOMETRYCOLLECTION = "collection", | |
CIRCULARSTRING = "line", COMPOUNDCURVE = "other", CURVEPOLYGON = "other", | |
MULTICURVE = "other", MULTISURFACE = "other", CURVE = "other", | |
SURFACE = "other", POLYHEDRALSURFACE = "other", TIN = "other", | |
TRIANGLE = "other") |
R/layer-sf.R
Outdated
@@ -35,6 +35,18 @@ LayerSf <- ggproto("LayerSf", Layer, | |||
self$mapping$geometry <- as.name(geometry_col) | |||
} | |||
} | |||
|
|||
# automatically determine the legend type | |||
if (is.na(self$show.legend) || self$show.legend == TRUE) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not assume show.legend
is a object of length 1. The conditions here should be wrapped all()
, any()
or isTRUE()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Writing code of the form if (x == TRUE) {...}
is almost always bad in R, because x
can be unexpectedly of length > 1 (even if nonsensical). Please get into the habit of writing if (isTRUE(x)) {...}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it~ Thanks for your kind instruction!
@yutannihilation Reply for the first one: The main difference between sf_types and my determine rule is that my line group has three more geometry types: "COMPOUNDCURVE", "CURVE" and "MULTICURVE". Reply for the second one: I am afraid that the length of show.legend has to be 1.
You can see that the legend dose not show, even if you tell ggplot2 twice that the show.legend is set to be TRUE. |
Oh, good catch. But, if so, it should be fixed on the
|
@yutannihilation |
@microly You don't need to reply just to notify us that you read the comment. No worries, I understand it takes you time to write in English and we don't mind if you are not super-responsive. |
R/layer-sf.R
Outdated
if (is_sf(data)) { | ||
if (sf_geometry_type(data) %in% c("POINT", "MULTIPOINT")) | ||
self$geom_params$legend <- "point" | ||
else if (sf_geometry_type(data) %in% c("LINESTRING", "MULTILINESTRING", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're calling sf_geometry_type()
twice. Better to call it once, assign the result to a temp variable, and use that in the if
statements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am sorry that I can not express all my thanks in English! I feel that I am a young pupil and you are my teacher who teaches me hand by hand. Thanks!
R/layer-sf.R
Outdated
# helper function to determine the geometry type of sf object | ||
sf_geometry_type <- function(sf) { | ||
geometry_type <- unique(as.vector(sf::st_geometry_type(sf))) | ||
if (length(geometry_type) != 1) geometry_type <- "blend" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still don't like "blend"
. It is introduced here without documentation and not used anywhere else. I'd be fine with "other"
, since that is used elsewhere in the code.
R/layer-sf.R
Outdated
@@ -62,3 +74,9 @@ is_sf <- function(data) { | |||
#' @export | |||
scale_type.sfc <- function(x) "identity" | |||
|
|||
# helper function to determine the geometry type of sf object | |||
sf_geometry_type <- function(sf) { | |||
geometry_type <- unique(as.vector(sf::st_geometry_type(sf))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why as.vector()
? Why not as.character()
?
@clauswilke Issue 1, 2 and 4 have been solved. Issue 3: Line 200 in 6424808
Because of it, show.legend will never be a character in the stage of setup_layer(). I think that a new params of layer_sf (e.g. show.legend_origin), may be needed to solve this problem. The code of this pr:
will be changed to something like:
I think that this solution can work, but it looks ugly. |
I think the solution is simple: In line 200, just write Line 204 in 6424808
Then move all the logic to You'll have to do the same for Lines 31 to 35 in 6424808
But please make sure this works as expected for |
test code:
|
@yutannihilation |
R/layer-sf.R
Outdated
sf_geometry_type <- function(sf) { | ||
geometry_type <- unique(as.character(sf::st_geometry_type(sf))) | ||
if (length(geometry_type) != 1) geometry_type <- "GEOMETRY" | ||
geometry_type |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this function should return sf_types[geometry_type]
here, to simplify the code further up. I would also propose renaming it, since sf_geometry_type()
sounds so similar to sf::st_geometry_type()
but does somewhat different things. I recommend the name detect_geometry_type()
.
The test results look good to me. A subset of these should be added to the unit test suite. I would propose code similar to the below (but again, without library(tidyverse)
library(sf)
#> Linking to GEOS 3.7.2, GDAL 2.4.2, PROJ 5.2.0
library(testthat)
#>
#> Attaching package: 'testthat'
#> The following object is masked from 'package:dplyr':
#>
#> matches
#> The following object is masked from 'package:purrr':
#>
#> is_null
#> The following object is masked from 'package:tidyr':
#>
#> matches
p <- rbind(c(3.2,4), c(3,4.6), c(3.8,4.4), c(3.5,3.8), c(3.4,3.6), c(3.9,4.5))
mp <- st_multipoint(p) %>% st_sfc() %>% st_sf() %>% mutate(v = "a")
fun_geom_sf <- function(sf, show.legend) {
p <- ggplot() +
geom_sf(
aes(colour = v),
data = sf, show.legend = show.legend
)
ggplot_build(p)
}
b <- fun_geom_sf(mp, "point")
expect_identical(b$plot$layers[[1]]$geom_params$legend, "point")
expect_identical(b$plot$layers[[1]]$show.legend, TRUE) Created on 2019-12-03 by the reprex package (v0.3.0) |
R/geom-sf.R
Outdated
@@ -197,11 +197,12 @@ geom_sf <- function(mapping = aes(), data = NULL, stat = "sf", | |||
mapping = mapping, | |||
stat = stat, | |||
position = position, | |||
show.legend = if (is.character(show.legend)) TRUE else show.legend, | |||
#show.legend = if (is.character(show.legend)) TRUE else show.legend, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please delete.
R/geom-sf.R
Outdated
inherit.aes = inherit.aes, | ||
params = list( | ||
na.rm = na.rm, | ||
legend = if (is.character(show.legend)) show.legend else "polygon", | ||
#legend = if (is.character(show.legend)) show.legend else "polygon", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please delete.
R/stat-sf.R
Outdated
@@ -28,11 +28,12 @@ stat_sf <- function(mapping = NULL, data = NULL, geom = "rect", | |||
mapping = mapping, | |||
geom = geom, | |||
position = position, | |||
show.legend = if (is.character(show.legend)) TRUE else show.legend, | |||
#show.legend = if (is.character(show.legend)) TRUE else show.legend, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please delete.
R/stat-sf.R
Outdated
inherit.aes = inherit.aes, | ||
params = list( | ||
na.rm = na.rm, | ||
legend = if (is.character(show.legend)) show.legend else "polygon", | ||
#legend = if (is.character(show.legend)) show.legend else "polygon", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please delete.
R/layer.r
Outdated
if (!inherits(layer_class, "LayerSf")) { | ||
warning("`show.legend` must be a logical vector.", call. = FALSE) | ||
show.legend <- FALSE | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not particularly excited by this special casing here. Can we just move the warning to the point in the code where the decision about drawing a legend is actually made?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, looks like that will be even uglier. Maybe we can just eliminate this warning, since it's not entirely true? @yutannihilation Any opinion?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have an opinion yet here, but I was thinking it was a design mistake to let show.legend
to be used as the specification of the type of legend instead of creating a new parameter. Let me think a bit more...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, agreed. However, I think we're stuck with it, see e.g. here. At the same time, I don't think it's big enough of an issue to worry much about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The source of this inconvenience is that the sf geom is special, it can be point, line, polygon...
We have two choice: adding a new parameter, or allowing a chaos in show.legend.
I think the warning 'warning("show.legend
must be a logical vector.", call. = FALSE)' is still needed to keep the API safe and consistance.
After all, layer_sf is the only exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a simpler proposition, just needs a big of refactoring in the guide code. Will try to do a PR in the next few hours or tomorrow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Honestly, I'm not familiar with the codes around legends/guide, so I'll wait for your PR.
@microly Sorry for confusing you with my random comments. Let's wait for a while...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've submitted my PR (#3652). With it, we can just delete these lines and not think about them anymore here. :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@microly I think you can move this PR forward by addressing all the other comments (deleting unneeded code instead of commenting it out, adding unit tests) and leaving this issue as is for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have merged my refactored code, so this code block can now be eliminated.
@microly Please merge the master branch into your PR and then delete this code block.
sure:)
…---Original---
From: "Claus Wilke"<notifications@github.com>
Date: Thu, Dec 5, 2019 00:39 AM
To: "tidyverse/ggplot2"<ggplot2@noreply.github.com>;
Cc: "microly"<microly@126.com>;"Mention"<mention@noreply.github.com>;
Subject: Re: [tidyverse/ggplot2] Enable geom_sf to automatically determine the legend type (#3646)
@clauswilke commented on this pull request.
In R/layer.r:
> @@ -81,8 +81,12 @@ layer <- function(geom = NULL, stat = NULL, params$show_guide <- NULL } if (!is.logical(show.legend)) { - warning("`show.legend` must be a logical vector.", call. = FALSE) - show.legend <- FALSE + # a charater vector have to be allowed for LayerSf$show.legend + # in order to set its legend type. + if (!inherits(layer_class, "LayerSf")) { + warning("`show.legend` must be a logical vector.", call. = FALSE) + show.legend <- FALSE + }
@microly I think you can move this PR forward by addressing all the other comments (deleting unneeded code instead of commenting it out, adding unit tests) and leaving this issue as is for now.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or unsubscribe.
|
I have updated this PR. |
Don't worry about the codecov check. The tests look good, but could you also add one test to verify that the automatic choice can be overridden manually? For example, test that you can set the legend type to "point" when the automatic choice would be "line". |
hi, @clauswilke , I add more tests to confirm that automatic choices of legend type can be overridden manually. Please check it, thanks~ |
The tests look good to me. Please also add a brief (1-2 sentences) news item to |
update to tidyverse edition 20191206
There is still something wrong about codecov check :( |
…/microly/ggplot2 into layer-sf_patch1_auto-legend-type
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me.
@yutannihilation Anything else from your side?
R/layer-sf.R
Outdated
if (sf_type == "point") | ||
self$geom_params$legend <- "point" | ||
else if (sf_type == "line") | ||
self$geom_params$legend <- "line" | ||
else self$geom_params$legend <- "polygon" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor comment. According to the tidyverse style guide,
It’s ok to drop the curly braces for very simple statements that fit on one line, as long as they don’t have side-effects.
so you need {}
here since they are multiple lines.
R/layer-sf.R
Outdated
else self$geom_params$legend <- "polygon" | ||
} | ||
} else if (is.character(self$show.legend)) { | ||
self$geom_params$legend = self$show.legend |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self$geom_params$legend = self$show.legend | |
self$geom_params$legend <- self$show.legend |
R/layer-sf.R
Outdated
} | ||
} else if (is.character(self$show.legend)) { | ||
self$geom_params$legend = self$show.legend | ||
self$show.legend = TRUE |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self$show.legend = TRUE | |
self$show.legend <- TRUE |
R/layer-sf.R
Outdated
else if (sf_type == "line") { | ||
self$geom_params$legend <- "line" | ||
} | ||
else self$geom_params$legend <- "polygon" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably, should be this?
if (sf_type == "point") {
self$geom_params$legend <- "point"
} else if (sf_type == "line") {
self$geom_params$legend <- "line"
} else {
self$geom_params$legend <- "polygon"
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good!
Thanks for merging! |
fix #3572 and relate to #3636.
This pr works in LayerSf$setup_layer().
The determination rule is:
"CIRCULARSTRING", "COMPOUNDCURVE", "CURVE", "MULTICURVE"), the legend type of geom_sf will be set to line, which indicates that the layer is composed of line(s) only.
In short, point is point, line is line, polygon is polygon, any mixture of more than one type is also polygon. (please forgive my poor english~)
Moreover, after this pr, users still have full control of the lengend type by setting show.legend to "point", "line" or "polygon"; and users also can turn off the legend by setting show.legend to FALSE.