Skip to content

Toby cookbook lines #196

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

Merged
merged 26 commits into from
May 7, 2015
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e47ca93
expect_shape for hline
tdhock Mar 31, 2015
3ef9be5
test scatter facet lines
tdhock Mar 31, 2015
b60d935
only copy global mapping to layer if inherit.aes
tdhock Mar 31, 2015
1bea709
compute ranges for factors
tdhock Apr 1, 2015
8e68d8a
also use ranges for date and datetime variables
tdhock Apr 1, 2015
305ad6c
tests fail
tdhock Apr 1, 2015
7308760
simplify factor handling
tdhock Apr 1, 2015
6c72262
handle dodged factors with aes.name
tdhock Apr 1, 2015
84ab366
special cases
tdhock Apr 1, 2015
6318787
markLegends + boxplot = markSplit
tdhock Apr 1, 2015
4a2dc57
no need for workaround
tdhock Apr 1, 2015
a4d0059
testing showlegend=FALSE for each trace is sufficient
tdhock Apr 1, 2015
4a7f713
set showlegend=FALSE for all but the first trace with a given name
tdhock Apr 2, 2015
56ede93
merge conflict
tdhock Apr 2, 2015
3105143
use more complicated legend merging from master
tdhock Apr 2, 2015
bd8c3f6
test for no legends
tdhock Apr 8, 2015
5fa2434
hide boxplot legends
tdhock Apr 14, 2015
96f0d40
clean up comments
tdhock Apr 14, 2015
f0eb05e
version update
tdhock Apr 15, 2015
8f937e0
Use ||, not |, for logical vector of length 1
cpsievert May 2, 2015
9ab52e1
Resolve merge conflicts with master
cpsievert May 2, 2015
f90928b
merge conflicts with master
cpsievert May 5, 2015
d142570
test fixes for cookbook-lines (due to changes in gg2list())
cpsievert May 5, 2015
ab4ca1c
Fix style (notably, RStudio indentation)
mkcor May 5, 2015
498455d
Update version and docs
mkcor May 5, 2015
4cfedf9
Fix merge conflicts with master
mkcor May 5, 2015
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
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Package: plotly
Type: Package
Title: Interactive, publication-quality graphs online.
Version: 0.5.29
Version: 0.5.30
Authors@R: c(person("Chris", "Parmer", role = c("aut", "cre"),
email = "[email protected]"),
person("Scott", "Chamberlain", role = "aut",
Expand Down
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
0.5.30 -- 5 May 2015

Add test-cookbook-lines.R and fix bugs that showed up in those tests.

0.5.29 -- 16 April 2015

geom_density() as filled area chart #202
Expand Down
176 changes: 98 additions & 78 deletions R/ggplotly.R
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,12 @@ markLegends <-
errorbarh=c("colour", "linetype"),
area=c("colour", "fill"),
step=c("linetype", "size", "colour"),
boxplot=c("x"),
text=c("colour"))

markUnique <- as.character(unique(unlist(markLegends)))

markSplit <- c(markLegends,list(boxplot=c("x")))

#' Convert a ggplot to a list.
#' @import ggplot2
#' @param p ggplot2 plot.
Expand Down Expand Up @@ -97,29 +98,91 @@ gg2list <- function(p){
# worry about combining global and layer-specific aes/data later.
for(layer.i in seq_along(p$layers)) {
layer.aes <- p$layers[[layer.i]]$mapping
to.copy <- names(p$mapping)[!names(p$mapping) %in% names(layer.aes)]
layer.aes[to.copy] <- p$mapping[to.copy]
mark.names <- markUnique[markUnique %in% names(layer.aes)]
if(p$layers[[layer.i]]$inherit.aes){
to.copy <- names(p$mapping)[!names(p$mapping) %in% names(layer.aes)]
layer.aes[to.copy] <- p$mapping[to.copy]
}
mark.names <- names(layer.aes) # make aes.name for all aes.
name.names <- sprintf("%s.name", mark.names)
layer.aes[name.names] <- layer.aes[mark.names]
p$layers[[layer.i]]$mapping <- layer.aes
if(!is.data.frame(p$layers[[layer.i]]$data)){
p$layers[[layer.i]]$data <- p$data
}
}

# Test fill and color to see if they encode a quantitative
# variable. This may be useful for several reasons: (1) it is
# sometimes possible to plot several different colors in the same
# trace (e.g. points), and that is faster for large numbers of
# data points and colors; (2) factors on x or y axes should be
# sent to plotly as characters, not as numeric data (which is
# what ggplot_build gives us).
misc <- list()
for(a in c("fill", "colour", "x", "y", "size")){
for(data.type in c("continuous", "date", "datetime", "discrete")){
fun.name <- sprintf("scale_%s_%s", a, data.type)
misc.name <- paste0("is.", data.type)
misc[[misc.name]][[a]] <- tryCatch({
fun <- get(fun.name)
suppressMessages({
with.scale <- original.p + fun()
})
ggplot_build(with.scale)
TRUE
}, error=function(e){
FALSE
})
}
}

## scales are needed for legend ordering.
misc$breaks <- list()
for(sc in p$scales$scales){
a.vec <- sc$aesthetics
default.breaks <- inherits(sc$breaks, "waiver")
if (length(a.vec) == 1 && (!default.breaks) ) {
## TODO: generalize for x/y scales too.
br <- sc$breaks
ranks <- seq_along(br)
names(ranks) <- br
misc$breaks[[a.vec]] <- ranks
}
## store if this is a reverse scale so we can undo that later.
if(is.character(sc$trans$name)){
misc$trans[sc$aesthetics] <- sc$trans$name
}
}
reverse.aes <- names(misc$trans)[misc$trans=="reverse"]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this whole block which creates misc was inside the for(layer.i) block below, which is potentially confusing, since misc contains plot-specific, not layer-specific, information.


# Extract data from built ggplots
built <- ggplot_build2(p)
# Get global x-range now because we need some of its info in layer2traces
ggranges <- built$panel$ranges
# Extract x.range
xrange <- sapply(ggranges, `[[`, "x.range", simplify=FALSE, USE.NAMES=FALSE)
ggxmin <- min(sapply(xrange, min))
ggxmax <- max(sapply(xrange, max))
# Extract y.range
yrange <- sapply(ggranges, `[[`, "y.range", simplify=FALSE, USE.NAMES=FALSE)
ggymin <- min(sapply(yrange, min))
ggymax <- max(sapply(yrange, max))
# Get global ranges now because we need some of its info in layer2traces
ranges.list <- list()
for(xy in c("x", "y")){
use.ranges <-
misc$is.continuous[[xy]] ||
misc$is.date[[xy]] ||
misc$is.datetime[[xy]]
range.values <- if(use.ranges){
range.name <- paste0(xy, ".range")
sapply(built$panel$ranges, "[[", range.name)
}else{
## for categorical variables on the axes, panel$ranges info is
## meaningless.
name.name <- paste0(xy, ".name")
sapply(built$data, function(df){
if(name.name %in% names(df)){
## usually for discrete data there is a .name column.
paste(df[[name.name]])
}else{
## for heatmaps there may not be.
df[[xy]]
}
})
}
ranges.list[[xy]] <- range(range.values)
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

getting global ranges is more complicated now that we respect inherit.aes


# Get global size range because we need some of its info in layer2traces
if ("size.name" %in% name.names) {
Expand All @@ -135,51 +198,7 @@ gg2list <- function(p){

# for each layer, there is a correpsonding data.frame which
# evaluates the aesthetic mapping.
df <- built$data[[i]]

# Test fill and color to see if they encode a quantitative
# variable. This may be useful for several reasons: (1) it is
# sometimes possible to plot several different colors in the same
# trace (e.g. points), and that is faster for large numbers of
# data points and colors; (2) factors on x or y axes should be
# sent to plotly as characters, not as numeric data (which is
# what ggplot_build gives us).
misc <- list()
for(a in c("fill", "colour", "x", "y", "size")){
for(data.type in c("continuous", "date", "datetime", "discrete")){
fun.name <- sprintf("scale_%s_%s", a, data.type)
misc.name <- paste0("is.", data.type)
misc[[misc.name]][[a]] <- tryCatch({
fun <- get(fun.name)
suppressMessages({
with.scale <- original.p + fun()
})
ggplot_build(with.scale)
TRUE
}, error=function(e){
FALSE
})
}
}

# scales are needed for legend ordering.
misc$breaks <- list()
for(sc in p$scales$scales){
a.vec <- sc$aesthetics
default.breaks <- inherits(sc$breaks, "waiver")
if (length(a.vec) == 1 && (!default.breaks) ) {
# TODO: generalize for x/y scales too.
br <- sc$breaks
ranks <- seq_along(br)
names(ranks) <- br
misc$breaks[[a.vec]] <- ranks
}
## store if this is a reverse scale so we can undo that later.
if(is.character(sc$trans$name)){
misc$trans[sc$aesthetics] <- sc$trans$name
}
}
reverse.aes <- names(misc$trans)[misc$trans=="reverse"]
df <- built$data[[i]]

# get gglayout now because we need some of its info in layer2traces
gglayout <- built$panel$layout
Expand All @@ -203,21 +222,24 @@ gg2list <- function(p){
for (a in replace.aes) {
prestats[[a]] <- -1 * prestats[[a]]
}
misc$prestats.data <-
L$prestats.data <-
Copy link
Contributor Author

Choose a reason for hiding this comment

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

prestats.data is layer-specific (misc is plot-specific)

merge(prestats,
gglayout[, c("PANEL", "plotly.row", "COL")])

# Add global x-range info
misc$prestats.data$globxmin <- ggxmin
misc$prestats.data$globxmax <- ggxmax
# Add global y-range info
misc$prestats.data$globymin <- ggymin
misc$prestats.data$globymax <- ggymax

# Add global range info.
for(xy in names(ranges.list)){
range.vec <- ranges.list[[xy]]
names(range.vec) <- c("min", "max")
for(range.name in names(range.vec)){
glob.name <- paste0("glob", xy, range.name)
L$prestats.data[[glob.name]] <- range.vec[[range.name]]
}
}

# Add global size info if relevant
if ("size.name" %in% name.names) {
misc$prestats.data$globsizemin <- ggsizemin
misc$prestats.data$globsizemax <- ggsizemax
L$prestats.data$globsizemin <- ggsizemin
L$prestats.data$globsizemax <- ggsizemax
}

# This extracts essential info for this geom/layer.
Expand Down Expand Up @@ -415,7 +437,7 @@ gg2list <- function(p){
sc$limits
}else{
if(misc$is.continuous[[xy]]){
ggranges[[1]][[s("%s.range")]] #TODO: facets!
built$panel$ranges[[1]][[s("%s.range")]] #TODO: facets!
}else{ # for a discrete scale, range should be NULL.
NULL
}
Expand Down Expand Up @@ -594,9 +616,6 @@ gg2list <- function(p){
layout$annotations <- annotations
}

# Remove legend if theme has no legend position
layout$showlegend <- !(theme.pars$legend.position=="none")

# Main plot title.
layout$title <- built$plot$labels$title

Expand All @@ -612,10 +631,6 @@ gg2list <- function(p){
layout$legend <- list(bordercolor="transparent",
x=1.05, y=1/2,
xanchor="center", yanchor="top")
# Workaround for removing unnecessary legends.
# [markUnique != "x"] is for boxplot's particular case.
if (any(names(layer.aes) %in% markUnique[markUnique != "x"]) == FALSE)
layout$showlegend <- FALSE

## Legend hiding when guides(fill="none").
legends.present <- unique(unlist(layer.legends))
Expand All @@ -628,12 +643,17 @@ gg2list <- function(p){
is.hidden <- function(x){
is.false(x) || is.none(x)
}
layout$showlegend <- if(length(legends.present) == 0) FALSE else TRUE
for(a in legends.present){
if(is.hidden(p$guides[[a]])){
layout$showlegend <- FALSE
}
}

# Legend hiding from theme.
if(theme.pars$legend.position=="none"){
Copy link
Contributor

Choose a reason for hiding this comment

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

Spaces at least around == and between ) {.

layout$showlegend <- FALSE
}

# Only show a legend title if there is at least 1 trace with
# showlegend=TRUE.
trace.showlegend <- sapply(trace.list, "[[", "showlegend")
Expand Down
33 changes: 19 additions & 14 deletions R/trace_generation.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#' Convert a layer to a list of traces. Called from gg2list()
#' @param l one layer of the ggplot object
#' @param d one layer of calculated data from ggplot2::ggplot_build(p)
#' @param misc named list.
#' @param misc named list of plot info, independent of layer.
#' @return list representing a layer, with corresponding aesthetics, ranges, and groups.
#' @export
layer2traces <- function(l, d, misc) {
Expand All @@ -12,7 +12,7 @@ layer2traces <- function(l, d, misc) {
}
g <- list(geom=l$geom$objname,
data=not.na(d),
prestats.data=not.na(misc$prestats.data))
prestats.data=not.na(l$prestats.data))

# needed for when group, etc. is an expression.
g$aes <- sapply(l$mapping, function(k) as.character(as.expression(k)))
Expand Down Expand Up @@ -46,21 +46,22 @@ layer2traces <- function(l, d, misc) {
g$geom <- "bar"
bargap <- 0
}

Copy link
Contributor

Choose a reason for hiding this comment

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

No, I'm pretty sure this indentation is from RStudio.

# For non-numeric data on the axes, we should take the values from
# the original data.
for (axis.name in c("x", "y")) {
if (!misc$is.continuous[[axis.name]]) {
aes.names <- paste0(axis.name, c("", "end", "min", "max"))
aes.used <- aes.names[aes.names %in% names(g$aes)]
for(a in aes.used) {
a.name <- paste0(a, ".name")
col.name <- g$aes[aes.used]
dtemp <- l$data[[col.name]]
if (is.null(dtemp)) {
if (!inherits(g$data[[paste0(a, ".name")]], "NULL")) {
if (!is.null(g$data[[a.name]])) {
# Handle the case where as.Date() is passed in aes argument.
if (class(g$data[[a]]) != class(g$data[[paste0(a, ".name")]])) {
g$data[[a]] <- g$data[[paste0(a, ".name")]]
if (class(g$data[[a]]) != class(g$data[[a.name]])) {
g$data[[a]] <- g$data[[a.name]]
data.vec <- g$data[[a]]
}
}
Expand All @@ -86,14 +87,18 @@ layer2traces <- function(l, d, misc) {
} else if (inherits(data.vec, "factor")) {
# Re-order data so that Plotly gets it right from ggplot2.
g$data <- g$data[order(g$data[[a]]), ]
data.vec <- data.vec[match(g$data[[a]], as.numeric(data.vec))]
vec.i <- match(g$data[[a]], as.numeric(data.vec))
if(anyNA(vec.i)){
vec.i <- match(g$data[[a.name]], data.vec)
}
data.vec <- data.vec[vec.i]
g$prestats.data <- g$prestats.data[order(g$prestats.data[[a]]), ]
pdata.vec <- pdata.vec[match(g$prestats.data[[a]],
as.numeric(pdata.vec))]
pvec.i <- match(g$prestats.data[[a]], as.numeric(pdata.vec))
pdata.vec <- pdata.vec[pvec.i]
if (length(pdata.vec) == length(data.vec))
pdata.vec <- data.vec
if (!is.factor(pdata.vec))
pdata.vec <- g$prestats.data[[paste0(a, ".name")]]
pdata.vec <- g$prestats.data[[a.name]]
}
g$data[[a]] <- data.vec
g$prestats.data[[a]] <- pdata.vec
Expand Down Expand Up @@ -131,7 +136,7 @@ layer2traces <- function(l, d, misc) {

# symbol=circle,square,diamond,cross,x,
# triangle-up,triangle-down,triangle-left,triangle-right

Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

# First convert to a "basic" geom, e.g. segments become lines.
convert <- toBasic[[g$geom]]
basic <- if(is.null(convert)){
Expand All @@ -141,8 +146,8 @@ layer2traces <- function(l, d, misc) {
}
# Then split on visual characteristics that will get different
# legend entries.
data.list <- if (basic$geom %in% names(markLegends)) {
mark.names <- markLegends[[basic$geom]]
data.list <- if (basic$geom %in% names(markSplit)) {
mark.names <- markSplit[[basic$geom]]
# However, continuously colored points are an exception: they do
# not need a legend entry, and they can be efficiently rendered
# using just 1 trace.
Expand Down Expand Up @@ -173,7 +178,7 @@ layer2traces <- function(l, d, misc) {
}
# Split hline and vline when multiple panels or intercepts:
# Need multiple traces accordingly.
if (g$geom == "hline" || g$geom == "vline") {
if (g$geom %in% c("hline", "vline")) {
intercept <- paste0(ifelse(g$geom == "hline", "y", "x"), "intercept")
vec.list <- basic$data[c("PANEL", intercept)]
df.list <- split(basic$data, vec.list, drop=TRUE)
Expand Down
Loading