Skip to content

Commit 71e29b4

Browse files
committed
explicit bar positioning and proper inverse transforming of dates via dynamicTicks
fixes #560, #874, #901, #831
1 parent 9907691 commit 71e29b4

9 files changed

+111
-99
lines changed

NEWS.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@
2727

2828
## BUG FIXES
2929

30+
31+
* Placement of bars (in all cases, even when representing a negative count) should now be correct (applies to `geom_bar()`, `geom_histogram()`, `geom_col()`). Fixes #560, #874, #901, #831.
32+
* Fix for hoverinfo displaying the heights of bars in the translation `geom_bar()` via `ggplotly()`. Fixes #557 and #662.
3033
* `embed_notebook()` now works in *nteract* notebooks (see #768).
3134
* Axis categories are no longer reordered for matrices (see #863).
32-
* Fix for hoverinfo displaying the heights of bars in the translation `geom_bar()` via `ggplotly()`. Fixes #557 and #662.
33-
* Fix for hoverinfo displaying values after scale transformations. Fixes #804.
34-
* Font faces for axis titles are now translated in `ggplotly()`. Fixes #861.
35+
* Fix for hoverinfo displaying values after scale transformations (in `ggplotly()`). Fixes #804.
36+
* Font faces for axis titles are now translated (in `ggplotly()`). Fixes #861.
3537

3638
# 4.5.6
3739

R/ggplotly.R

+24-49
Original file line numberDiff line numberDiff line change
@@ -640,32 +640,37 @@ gg2list <- function(p, width = NULL, height = NULL,
640640
titlefont = text2font(axisTitle)
641641
)
642642

643-
# ensure dates/datetimes are put on the same millisecond scale
644-
# (necessary regardless of whether axis is dynamic)
645-
# TODO: inverse transform to the original dates?!
646-
# https://github.com/plotly/plotly.js/issues/420
647-
if (isDate) {
648-
# convert days (date) / seconds (datetime) to milliseconds
649-
# (86400000 = 24 * 60 * 60 * 1000)
650-
constant <- if ("date" %in% sc$scale_name) 86400000 else 1000
651-
axisObj$range <- axisObj$range * constant
652-
if (i == 1) {
653-
traces <- lapply(traces, function(z) { z[[xy]] <- z[[xy]] * constant; z })
654-
}
655-
}
656-
657643
# tickvals are currently on 0-1 scale, but we want them on data scale
658644
axisObj$tickvals <- scales::rescale(
659645
axisObj$tickvals, to = axisObj$range, from = c(0, 1)
660646
)
661647

648+
# inverse transform date data based on tickvals/ticktext
649+
if (isDateType) {
650+
as_date <- function(x) as.POSIXct(x, origin="1970-01-01", tz=sc$timezone)
651+
axisObj$range <- as_date(axisObj$range)
652+
traces <- lapply(traces, function(tr) {
653+
tr[[xy]] <- as_date(tr[[xy]])
654+
# TODO: are there other similar cases we need to handle?
655+
if (identical("bar", tr$type)) {
656+
tr[["width"]] <- as_date(tr[["width"]])
657+
tr[["base"]] <- as_date(tr[["base"]])
658+
}
659+
tr
660+
})
661+
}
662+
662663
# inverse transform categorical data based on tickvals/ticktext
663664
if (isDiscreteType) {
664-
tickMap <- with(axisObj, setNames(ticktext, tickvals))
665665
traces <- lapply(traces, function(tr) {
666-
tr[[xy]] <- tickMap[[as.character(tr[[xy]])]]
666+
# map x/y trace data back to the 'closest' ticktext label
667+
# http://r.789695.n4.nabble.com/check-for-nearest-value-in-a-vector-td4369339.html
668+
tr[[xy]]<- vapply(tr[[xy]], function(val) {
669+
with(axisObj, ticktext[[which.min(abs(tickvals - val))]])
670+
}, character(1))
667671
tr
668672
})
673+
if ("dodge" %in% sapply(layers, ggtype, "position")) gglayout$barmode <- "dodge"
669674
}
670675

671676
# attach axis object to the layout
@@ -857,44 +862,13 @@ gg2list <- function(p, width = NULL, height = NULL,
857862
}
858863
}
859864

860-
# geom_bar() hacks
861-
geoms <- sapply(layers, ggtype, "geom")
862-
if (any(idx <- geoms %in% c("bar", "col"))) {
863-
# traces were reversed in layers2traces()
864-
gglayout$legend$traceorder <- "reversed"
865-
# since `layout.barmode` is plot-specific, we can't support multiple bar
866-
# geoms with different positions
867-
positions <- sapply(layers, ggtype, "position")
868-
position <- unique(positions[idx])
869-
if (length(position) > 1) {
870-
warning("plotly doesn't support multiple positions\n",
871-
"across geom_bar() layers", call. = FALSE)
872-
position <- position[1]
873-
}
874-
# hacks for position_identity()
875-
if ("identity" %in% position) {
876-
gglayout$barmode <- "overlay"
877-
} else {
878-
# yes, this should work even for position_dodge()
879-
gglayout$barmode <- "stack"
880-
}
881-
# note: ggplot2 doesn't flip x/y scales when the coord is flipped
882-
# (i.e., at this point, y should be the count/density)
883-
is_hist <- inherits(plot$scales$get_scales("x"), "ScaleContinuous")
884-
# TODO: get rid of this and use explicit width for bars
885-
# https://github.com/plotly/plotly.js/issues/80
886-
if (position == "dodge" || is_hist) {
887-
gglayout$bargap <- 0
888-
}
889-
}
890-
891865
# flip x/y in traces for flipped coordinates
892866
# (we've already done appropriate flipping for axis objects)
893867
if (inherits(plot$coordinates, "CoordFlip")) {
894868
for (i in seq_along(traces)) {
895869
tr <- traces[[i]]
896-
traces[[i]][c("x", "y")] <- tr[c("y", "x")]
897-
if (tr$type %in% c("bar", "box")) traces[[i]]$orientation <- "h"
870+
# TODO: move this to the layer2trace definition...
871+
if (tr$type %in% "box") traces[[i]]$orientation <- "h"
898872
if (tr$type == "box") traces[[i]]$hoverinfo <- "x"
899873
names(traces[[i]])[grepl("^error_y$", names(tr))] <- "error_x"
900874
names(traces[[i]])[grepl("^error_x$", names(tr))] <- "error_y"
@@ -950,6 +924,7 @@ gg2list <- function(p, width = NULL, height = NULL,
950924

951925
gglayout$width <- width
952926
gglayout$height <- height
927+
gglayout$barmode <- gglayout$barmode %||% "relative"
953928

954929
l <- list(
955930
data = setNames(traces, NULL),

R/layers2traces.R

+23-5
Original file line numberDiff line numberDiff line change
@@ -641,12 +641,30 @@ geom2trace.GeomPoint <- function(data, params, p) {
641641

642642
#' @export
643643
geom2trace.GeomBar <- function(data, params, p) {
644-
data[["y"]] <- data[["ymax"]] - data[["ymin"]]
645-
# TODO: use xmin/xmax once plotly.js allows explicit bar widths
646-
# https://github.com/plotly/plotly.js/issues/80
644+
# TODO: does position play a role here?
645+
#pos <- params$position %||% "stack"
646+
flip <- inherits(p$coordinates, "CoordFlip")
647+
648+
if (!flip) {
649+
width <- with(data, xmax - xmin)
650+
# TODO: does this cause rounding issues when inverse transforming for dynamicTicks?
651+
x <- with(data, (xmax + xmin) / 2)
652+
base <- data[["ymin"]]
653+
y <- with(data, ymax - ymin)
654+
} else {
655+
width <- with(data, ymax - ymin)
656+
# TODO: does this cause rounding issues when inverse transforming for dynamicTicks?
657+
y <- with(data, (ymax + ymin) / 2)
658+
base <- data[["xmin"]]
659+
x <- with(data, xmax - xmin)
660+
}
661+
647662
compact(list(
648-
x = data[["x"]],
649-
y = data[["y"]],
663+
orientation = if (flip) "h" else "v",
664+
width = width,
665+
base = base,
666+
x = x,
667+
y = y,
650668
text = uniq(data[["hovertext"]]),
651669
key = data[["key"]],
652670
frame = data[["frame"]],

tests/testthat/test-ggplot-bar.R

+38-12
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,35 @@ researchers <- data.frame(
2121

2222
gg <- ggplot(researchers, aes(country, papers, fill = field))
2323

24-
test_that("position_dodge is translated to barmode=stack", {
24+
test_that("position_dodge()", {
2525
gg.dodge <- gg + geom_bar(stat = "identity", position = "dodge")
2626
info <- expect_traces(gg.dodge, 2, "dodge")
27-
expect_identical(info$layout$barmode, "stack")
27+
expect_identical(info$layout$barmode, "relative")
28+
29+
l <- ggplotly(gg.dodge, dynamicTicks = "x")$x
30+
expect_identical(l$layout$barmode, "dodge")
31+
expect_equal(l$data[[1]]$x, c("Canada", "USA"))
32+
expect_equal(l$data[[1]]$name, "Math")
33+
expect_equal(l$data[[2]]$x, c("Canada", "Germany"))
34+
expect_equal(l$data[[2]]$name, "Bio")
2835
})
2936

30-
test_that("position_stack is translated to barmode=stack", {
37+
test_that("position_stack()", {
3138
gg.stack <- gg + geom_bar(stat = "identity", position = "stack")
3239
info <- expect_traces(gg.stack, 2, "stack")
33-
expect_identical(info$layout$barmode, "stack")
40+
expect_identical(info$layout$barmode, "relative")
41+
42+
l <- ggplotly(gg.stack, dynamicTicks = T)$x
43+
expect_identical(l$layout$barmode, "relative")
3444
})
3545

36-
test_that("position_identity is translated to barmode=overlay", {
46+
test_that("position_identity()", {
3747
gg.identity <- gg + geom_bar(stat = "identity", position = "identity")
3848
info <- expect_traces(gg.identity, 2, "identity")
39-
expect_identical(info$layout$barmode, "overlay")
49+
expect_identical(info$layout$barmode, "relative")
50+
51+
l <- ggplotly(gg.identity, dynamicTicks = T)$x
52+
expect_identical(l$layout$barmode, "relative")
4053
})
4154

4255
test_that("dates work well with bar charts", {
@@ -45,10 +58,23 @@ test_that("dates work well with bar charts", {
4558
gd <- ggplot(researchers, aes(month, papers, fill = field)) +
4659
geom_bar(stat = "identity")
4760
info <- expect_traces(gd, 2, "dates")
48-
trs <- info$data
49-
# plotly likes time in milliseconds
50-
t <- as.numeric(unique(researchers$month)) * 24 * 60 * 60 * 1000
51-
expect_equal(trs[[1]]$x, t)
61+
62+
# by default, date axes are linear...
63+
expect_equal(info$layout$xaxis$type, "linear")
64+
expect_equal(
65+
info$data[[1]]$x,
66+
as.numeric(unique(researchers$month))
67+
)
68+
69+
# different story for dynamicTicks...
70+
l <- ggplotly(gd, dynamicTicks = TRUE)$x
71+
expect_equal(l$layout$xaxis$type, "date")
72+
expect_equal(l$layout$xaxis$tickmode, "auto")
73+
expect_is(l$layout$xaxis$range, "POSIXct")
74+
for (attr in c("x", "base", "width")) {
75+
expect_is(l$data[[1]][[attr]], "POSIXct")
76+
}
77+
5278
})
5379

5480
## http://www.cookbook-r.com/Graphs/Bar_and_line_graphs_%28ggplot2%29/
@@ -117,7 +143,7 @@ base <- ggplot(mtcars, aes(factor(vs), fill = factor(cyl)))
117143

118144
test_that("geom_bar() stacks counts", {
119145
info <- expect_traces(base + geom_bar(), 3, "position-stack")
120-
expect_identical(info$layout$barmode, "stack")
146+
expect_identical(info$layout$barmode, "relative")
121147
trs <- info$data
122148
# sum of y values for each trace
123149
test <- as.numeric(sort(sapply(trs, function(x) sum(x$y))))
@@ -127,7 +153,7 @@ test_that("geom_bar() stacks counts", {
127153

128154
test_that("geom_bar(position = 'fill') stacks proportions", {
129155
info <- expect_traces(base + geom_bar(position = "fill"), 3, "position-fill")
130-
expect_identical(info$layout$barmode, "stack")
156+
expect_identical(info$layout$barmode, "relative")
131157
trs <- info$data
132158
# sum of y-values *conditioned* on a x-value
133159
prop <- sum(sapply(sapply(trs, "[[", "y"), "[", 1))

tests/testthat/test-ggplot-boxplot.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ test_that("you can make a boxplot for a distribution of datetimes", {
3636

3737
expect_equal(length(L$data), 1) # 1 trace
3838
expect_identical(L$data[[1]]$type, "box")
39-
expect_identical(L$data[[1]]$y, as.numeric(df$y) * 1000)
39+
expect_identical(L$data[[1]]$y, as.numeric(df$y))
4040
})
4141

4242
# check legend shows up when each box-and-whiskers has a fill

tests/testthat/test-ggplot-date.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ test_that("datetimes are converted to e.g. 2013-01-02 05:00:00", {
1111
info <- save_outputs(gg, "date-strings")
1212
expect_equal(length(info$data), 2)
1313
for(trace in info$data[1:2]){
14-
expect_equal(as.numeric(time.obj) * 1000, trace$x)
14+
expect_equal(as.numeric(time.obj), trace$x)
1515
}
1616
})
1717

tests/testthat/test-ggplot-histogram.R

+11-14
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@ base <- ggplot(mtcars, aes(wt))
1616

1717
test_that("geom_histogram() is a bar chart of counts with no bargap", {
1818
info <- expect_traces(base + geom_histogram(), 1, "counts")
19-
expect_identical(info$layout$bargap, 0)
2019
tr <- info$data[[1]]
2120
expect_identical(tr$type, "bar")
2221
expect_equal(sum(tr$y), nrow(mtcars))
22+
expect_equal(info$layout$barmode, "relative")
2323
})
2424

2525
test_that("geom_histogram(aes(y = ..density..)) displays a density", {
2626
info <- expect_traces(base + geom_histogram(aes(y=..density..)), 1, "density")
27-
expect_identical(info$layout$bargap, 0)
2827
tr <- info$data[[1]]
2928
expect_identical(tr$type, "bar")
3029
#default binwidth
@@ -65,24 +64,23 @@ test_that("Specify histogram binwidth", {
6564
test_that("geom_histogram(aes(fill = factor(...))) is a stacked by default", {
6665
gg <- base + geom_histogram(aes(fill = factor(vs)))
6766
info <- expect_traces(gg, 2, "fill-factor")
68-
expect_equal(info$layout$bargap, 0)
69-
expect_equal(info$layout$barmode, "stack")
67+
expect_equal(info$layout$barmode, "relative")
7068
})
7169

7270
test_that("geom_histogram(aes(fill = factor(...))) respects position_identity()", {
73-
gg <- base + geom_histogram(aes(fill = factor(vs)), alpha = 0.3,
74-
position = "identity")
71+
gg <- base + geom_histogram(
72+
aes(fill = factor(vs)), alpha = 0.3, position = "identity"
73+
)
7574
info <- expect_traces(gg, 2, "fill-factor-identity")
76-
expect_equal(info$layout$bargap, 0)
77-
expect_equal(info$layout$barmode, "overlay")
75+
expect_equal(info$layout$barmode, "relative")
7876
})
7977

8078
test_that("geom_histogram(aes(fill = factor(...))) respects position_dodge()", {
81-
gg <- base + geom_histogram(aes(fill = factor(vs)), alpha = 0.3,
82-
position = "dodge")
79+
gg <- base + geom_histogram(
80+
aes(fill = factor(vs)), alpha = 0.3, position = "dodge"
81+
)
8382
info <- expect_traces(gg, 2, "fill-factor-dodge")
84-
expect_equal(info$layout$bargap, 0)
85-
expect_equal(info$layout$barmode, "stack")
83+
expect_equal(info$layout$barmode, "relative")
8684
})
8785

8886
test_that("geom_histogram() with facets", {
@@ -94,8 +92,7 @@ test_that("geom_histogram() with facets", {
9492
gap <- unique(sapply(trs, "[[", "bargap"))
9593
barmode <- unique(sapply(trs, "[[", "barmode"))
9694
expect_identical(type, "bar")
97-
expect_equal(info$layout$bargap, 0)
98-
expect_equal(info$layout$barmode, "stack")
95+
expect_equal(info$layout$barmode, "relative")
9996
})
10097

10198
test_that("vline overlaid histogram", {

tests/testthat/test-ggplot-lines.R

+7-13
Original file line numberDiff line numberDiff line change
@@ -58,33 +58,27 @@ test_that("Translates both dates and datetimes (with dynamic ticks) correctly",
5858

5959
p <- ggplot(d, aes(date, value)) + geom_line()
6060
l <- plotly_build(ggplotly(p, dynamicTicks = TRUE))$x
61-
62-
milliseconds <- as.numeric(d$date) * 86400000
63-
61+
6462
d2 <- data.frame(
6563
value = rnorm(100),
6664
date = as.POSIXct(dates)
6765
)
6866

69-
milliseconds2 <- as.numeric(d2$date) * 1000
7067
p2 <- ggplot(d2, aes(date, value)) + geom_line()
7168
l2 <- plotly_build(ggplotly(p2, dynamicTicks = TRUE))$x
7269

73-
# data is all on millisecond level
74-
expect_equal(milliseconds, milliseconds2)
75-
expect_equal(milliseconds, l$data[[1]]$x)
76-
expect_equal(milliseconds, l2$data[[1]]$x)
77-
78-
# same with range
79-
expect_equal(grDevices::extendrange(milliseconds), l$layout$xaxis$range)
80-
expect_equal(grDevices::extendrange(milliseconds), l2$layout$xaxis$range)
81-
8270
# since these are dynamic ticks, let plotly.js generate the ticks
8371
axisType <- with(l$layout$xaxis, list(type, tickmode, autorange))
8472
expect_equal(axisType, list("date", "auto", TRUE))
8573
axisType2 <- with(l2$layout$xaxis, list(type, tickmode, autorange))
8674
expect_equal(axisType2, list("date", "auto", TRUE))
8775

76+
# range and data have been reverse transformed
77+
expect_is(l$layout$xaxis$range, "POSIXct")
78+
expect_is(l$data[[1]]$x, "POSIXct")
79+
expect_is(l2$layout$xaxis$range, "POSIXct")
80+
expect_is(l2$data[[1]]$x, "POSIXct")
81+
8882
# check the hovertext
8983
dates1 <- sapply(strsplit(l$data[[1]]$text, br()), "[[", 1)
9084
dates2 <- sapply(strsplit(l2$data[[1]]$text, br()), "[[", 1)

tests/testthat/test-ggplot-point.R

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,5 +55,5 @@ test_that("can plot on sub-second time scale", {
5555
)
5656
g <- ggplot(d, aes(x, y)) + geom_point()
5757
info <- save_outputs(g, "point-size-alpha2")
58-
expect_equivalent(info$data[[1]]$x, as.numeric(d$x) * 1000)
58+
expect_equivalent(info$data[[1]]$x, as.numeric(d$x))
5959
})

0 commit comments

Comments
 (0)