@@ -905,83 +905,69 @@ gg2list <- function(p, width = NULL, height = NULL,
905905 # will there be a legend?
906906 gglayout $ showlegend <- sum(unlist(lapply(traces , " [[" , " showlegend" ))) > = 1
907907
908- # legend styling
909- gglayout $ legend <- list (
910- bgcolor = toRGB(theme $ legend.background $ fill ),
911- bordercolor = toRGB(theme $ legend.background $ colour ),
912- borderwidth = unitConvert(theme $ legend.background $ size , " pixels" , " width" ),
913- font = text2font(theme $ legend.text )
914- )
915-
916908 # if theme(legend.position = "none") is used, don't show a legend _or_ guide
917909 if (npscales $ n() == 0 || identical(theme $ legend.position , " none" )) {
918910 gglayout $ showlegend <- FALSE
919911 } else {
920- # by default, guide boxes are vertically aligned
921- theme $ legend.box <- theme $ legend.box %|| % " vertical"
922912
923- # size of key (also used for bar in colorbar guide)
913+ # ------------------------------------------------------------------
914+ # Copied from body of ggplot2:::guides_build().
924915 theme $ legend.key.width <- theme $ legend.key.width %|| % theme $ legend.key.size
925916 theme $ legend.key.height <- theme $ legend.key.height %|| % theme $ legend.key.size
926-
927- # legend direction must be vertical
928- theme $ legend.direction <- theme $ legend.direction %|| % " vertical"
929- if (! identical(theme $ legend.direction , " vertical" )) {
930- warning(
931- " plotly.js does not (yet) support horizontal legend items \n " ,
932- " You can track progress here: \n " ,
933- " https://github.com/plotly/plotly.js/issues/53 \n " ,
934- call. = FALSE
935- )
936- theme $ legend.direction <- " vertical"
917+ # Layout of legends depends on their overall location
918+ position <- ggfun(" legend_position" )(theme $ legend.position %|| % " right" )
919+ if (position == " inside" ) {
920+ theme $ legend.box <- theme $ legend.box %|| % " vertical"
921+ theme $ legend.direction <- theme $ legend.direction %|| % " vertical"
922+ theme $ legend.box.just <- theme $ legend.box.just %|| % c(" center" , " center" )
923+ } else if (position == " vertical" ) {
924+ theme $ legend.box <- theme $ legend.box %|| % " vertical"
925+ theme $ legend.direction <- theme $ legend.direction %|| % " vertical"
926+ theme $ legend.box.just <- theme $ legend.box.just %|| % c(" left" , " top" )
927+ } else if (position == " horizontal" ) {
928+ theme $ legend.box <- theme $ legend.box %|| % " horizontal"
929+ theme $ legend.direction <- theme $ legend.direction %|| % " horizontal"
930+ theme $ legend.box.just <- theme $ legend.box.just %|| % c(" center" , " top" )
937931 }
938932
939- # justification of legend boxes
940- theme $ legend.box.just <- theme $ legend.box.just %|| % c(" center" , " center" )
941- # scales -> data for guides
942933 gdefs <- ggfun(" guides_train" )(scales , theme , plot $ guides , plot $ labels )
943934 if (length(gdefs ) > 0 ) {
944935 gdefs <- ggfun(" guides_merge" )(gdefs )
945936 gdefs <- ggfun(" guides_geom" )(gdefs , layers , plot $ mapping )
946937 }
938+ # ------------------------------------------------------------------
947939
948- # colourbar -> plotly.js colorbar
949- colorbar <- compact(lapply(gdefs , gdef2trace , theme , gglayout ))
950- nguides <- length(colorbar ) + gglayout $ showlegend
951- # If we have 2 or more guides, set x/y positions accordingly
952- if (nguides > = 2 ) {
953- # place legend at the bottom
954- gglayout $ legend $ y <- 1 / nguides
955- gglayout $ legend $ yanchor <- " top"
956- # adjust colorbar position(s)
957- for (i in seq_along(colorbar )) {
958- colorbar [[i ]]$ marker $ colorbar $ yanchor <- " top"
959- colorbar [[i ]]$ marker $ colorbar $ len <- 1 / nguides
960- colorbar [[i ]]$ marker $ colorbar $ y <- 1 - (i - 1 ) * (1 / nguides )
961- }
962- }
963- traces <- c(traces , colorbar )
940+ # Until plotly.js has multiple legend support, we're stuck with smashing
941+ # all legends into one...
942+ legendTitle <- paste(
943+ compact(lapply(gdefs , function (g ) if (inherits(g , " legend" )) g $ title else NULL )),
944+ collapse = br()
945+ )
946+
947+ # Discard everything but the first legend and colourbar(s)
948+ is_legend <- vapply(gdefs , is_guide_legend , logical (1 ))
949+ is_colorbar <- vapply(gdefs , is_guide_colorbar , logical (1 ))
950+ gdefs <- c(
951+ gdefs [is_colorbar ],
952+ if (gglayout $ showlegend ) gdefs [which(is_legend )[1 ]]
953+ )
964954
965- # legend title annotation - https://github.com/plotly/plotly.js/issues/276
966- if (isTRUE(gglayout $ showlegend )) {
967- legendTitles <- compact(lapply(gdefs , function (g ) if (inherits(g , " legend" )) g $ title else NULL ))
968- legendTitle <- paste(legendTitles , collapse = br())
969- titleAnnotation <- make_label(
970- legendTitle ,
971- x = gglayout $ legend $ x %|| % 1.02 ,
972- y = gglayout $ legend $ y %|| % 1 ,
973- theme $ legend.title ,
974- xanchor = " left" ,
975- yanchor = " bottom" ,
976- # just so the R client knows this is a title
977- legendTitle = TRUE
955+ # Get plotly.js positioning and orientation of all the guides at once
956+ positions <- plotly_guide_positions(gdefs , theme )
957+
958+ # Convert the legend
959+ is_legend <- vapply(gdefs , is_guide_legend , logical (1 ))
960+ if (sum(is_legend ) == 1 ) {
961+ idx <- which(is_legend )
962+ gglayout $ legend <- plotly_guide_legend(
963+ gdefs [[idx ]], theme ,
964+ positions [[idx ]], legendTitle
978965 )
979- gglayout $ annotations <- c(gglayout $ annotations , titleAnnotation )
980- # adjust the height of the legend to accomodate for the title
981- # this assumes the legend always appears below colorbars
982- gglayout $ legend $ y <- (gglayout $ legend $ y %|| % 1 ) -
983- length(legendTitles ) * unitConvert(theme $ legend.title $ size , " npc" , " height" )
984966 }
967+
968+ # Convert the colorbars
969+ is_colorbar <- vapply(gdefs , is_guide_colorbar , logical (1 ))
970+ traces <- c(traces , plotly_guide_colorbars(gdefs [is_colorbar ], theme , positions [is_colorbar ], gglayout ))
985971 }
986972
987973 # flip x/y in traces for flipped coordinates
@@ -1324,14 +1310,109 @@ ggtype <- function(x, y = "geom") {
13241310 sub(y , " " , tolower(class(x [[y ]])[1 ]))
13251311}
13261312
1327- # colourbar -> plotly.js colorbar
1328- gdef2trace <- function (gdef , theme , gglayout ) {
1329- if (inherits(gdef , " colorbar" )) {
1330- # sometimes the key has missing values, which we can ignore
1313+
1314+ plotly_guide_positions <- function (gdefs , theme ) {
1315+ length <- 1 / length(gdefs )
1316+ isTop <- " top" %in% theme $ legend.position
1317+ isLeft <- " left" %in% theme $ legend.position
1318+
1319+ lapply(seq_along(gdefs ), function (i ) {
1320+ position <- (i / length(gdefs )) - (0.5 * length )
1321+ orientation <- substr(gdefs [[i ]]$ direction , 1 , 1 )
1322+ if (theme $ legend.position %in% c(" top" , " bottom" )) {
1323+ list (
1324+ xanchor = " center" ,
1325+ x = position ,
1326+ len = length ,
1327+ orientation = orientation ,
1328+ yanchor = if (isTop ) " bottom" else " top" ,
1329+ # bottom needs some additional space to dodge x-axis
1330+ # TODO: can we measure size of axis in npc?
1331+ y = if (isTop ) 1 else - 0.25
1332+ )
1333+ } else if (theme $ legend.position %in% c(" left" , " right" )) {
1334+ list (
1335+ yanchor = " middle" ,
1336+ y = position ,
1337+ len = length ,
1338+ orientation = orientation ,
1339+ xanchor = if (isLeft ) " right" else " left" ,
1340+ # left needs some additional space to dodge y-axis
1341+ # TODO: can we measure size of axis in npc?
1342+ x = if (isLeft ) - 0.25 else 1
1343+ )
1344+ } else if (is.numeric(theme $ legend.position )) {
1345+ list (
1346+ x = theme $ legend.position [1 ],
1347+ xanchor = " center" ,
1348+ y = theme $ legend.position [2 ],
1349+ yanchor = " middle" ,
1350+ orientation = orientation
1351+ )
1352+ } else {
1353+ stop(" Unrecognized legend positioning" , call. = FALSE )
1354+ }
1355+ })
1356+ }
1357+
1358+
1359+ plotly_guide_legend <- function (gdef , theme , position , title ) {
1360+ if (! is_guide_legend(gdef )) stop(" gdef must be a legend" , call. = FALSE )
1361+ legend <- list (
1362+ title = list (
1363+ # TODO: is it worth mapping to side?
1364+ text = title ,
1365+ font = text2font(gdef $ title.theme %|| % theme $ legend.text )
1366+ ),
1367+ bgcolor = toRGB(theme $ legend.background $ fill ),
1368+ bordercolor = toRGB(theme $ legend.background $ colour ),
1369+ borderwidth = unitConvert(
1370+ theme $ legend.background $ size , " pixels" , " width"
1371+ ),
1372+ font = text2font(gdef $ label.theme %|| % theme $ legend.text )
1373+ )
1374+ modifyList(legend , position )
1375+ }
1376+
1377+
1378+ # Colourbar(s) are implemented as an additional (hidden) trace(s)
1379+ # (Note these can't yet be displayed horizontally https://github.com/plotly/plotly.js/issues/1244)
1380+ plotly_guide_colorbars <- function (gdefs , theme , positions , gglayout ) {
1381+ Map(function (gdef , position ) {
1382+ if (! is_guide_colorbar(gdef )) stop(" gdef must be a colourbar" , call. = FALSE )
1383+
13311384 gdef $ key <- gdef $ key [! is.na(gdef $ key $ .value ), ]
13321385 rng <- range(gdef $ bar $ value )
13331386 gdef $ bar $ value <- scales :: rescale(gdef $ bar $ value , from = rng )
13341387 gdef $ key $ .value <- scales :: rescale(gdef $ key $ .value , from = rng )
1388+
1389+ colorbar <- list (
1390+ bgcolor = toRGB(theme $ legend.background $ fill ),
1391+ bordercolor = toRGB(theme $ legend.background $ colour ),
1392+ borderwidth = unitConvert(
1393+ theme $ legend.background $ size , " pixels" , " width"
1394+ ),
1395+ thickness = unitConvert(
1396+ theme $ legend.key.width , " pixels" , " width"
1397+ ),
1398+ title = gdef $ title ,
1399+ titlefont = text2font(gdef $ title.theme %|| % theme $ legend.title ),
1400+ tickmode = " array" ,
1401+ ticktext = gdef $ key $ .label ,
1402+ tickvals = gdef $ key $ .value ,
1403+ tickfont = text2font(gdef $ label.theme %|| % theme $ legend.text ),
1404+ ticklen = 2
1405+ )
1406+
1407+ colorbar <- modifyList(position , colorbar )
1408+ if (identical(colorbar $ orientation , " h" )) {
1409+ warning(
1410+ " plotly.js colorbars cannot (yet) be displayed horizontally " ,
1411+ " https://github.com/plotly/plotly.js/issues/1244" ,
1412+ call. = FALSE
1413+ )
1414+ }
1415+
13351416 list (
13361417 x = with(gglayout $ xaxis , if (identical(tickmode , " auto" )) ticktext else tickvals )[[1 ]],
13371418 y = with(gglayout $ yaxis , if (identical(tickmode , " auto" )) ticktext else tickvals )[[1 ]],
@@ -1346,29 +1427,16 @@ gdef2trace <- function(gdef, theme, gglayout) {
13461427 marker = list (
13471428 color = c(0 , 1 ),
13481429 colorscale = setNames(gdef $ bar [c(" value" , " colour" )], NULL ),
1349- colorbar = list (
1350- bgcolor = toRGB(theme $ legend.background $ fill ),
1351- bordercolor = toRGB(theme $ legend.background $ colour ),
1352- borderwidth = unitConvert(
1353- theme $ legend.background $ size , " pixels" , " width"
1354- ),
1355- thickness = unitConvert(
1356- theme $ legend.key.width , " pixels" , " width"
1357- ),
1358- title = gdef $ title ,
1359- titlefont = text2font(gdef $ title.theme %|| % theme $ legend.title ),
1360- tickmode = " array" ,
1361- ticktext = gdef $ key $ .label ,
1362- tickvals = gdef $ key $ .value ,
1363- tickfont = text2font(gdef $ label.theme %|| % theme $ legend.text ),
1364- ticklen = 2 ,
1365- len = 1 / 2
1366- )
1430+ colorbar = colorbar
13671431 )
13681432 )
1369- } else {
1370- # if plotly.js gets better support for multiple legends,
1371- # that conversion should go here
1372- NULL
1373- }
1433+ }, gdefs , positions )
1434+ }
1435+
1436+ is_guide_colorbar <- function (x ) {
1437+ inherits(x , " guide" ) && inherits(x , " colorbar" )
1438+ }
1439+
1440+ is_guide_legend <- function (x ) {
1441+ inherits(x , " guide" ) && inherits(x , " legend" )
13741442}
0 commit comments