Skip to content
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

Combine multiple widgets in a single htmlwidget object. #226

Open
FrancoisGuillem opened this issue Aug 19, 2016 · 12 comments
Open

Combine multiple widgets in a single htmlwidget object. #226

FrancoisGuillem opened this issue Aug 19, 2016 · 12 comments

Comments

@FrancoisGuillem
Copy link

It would be nice to be able to combine multiple widgets in the single like one can combine multiple static plots in a single one using par(mfrow = c(...)), for instance to compare the same graphic generated on different datasets or to visualize different informations about the same data.

For now, to do so one has to create a complete shiny app/gadget or to create a rmarkdown document. But during analysis work, it would be be nice to be able to quickly combine widgets with a simple function.

I implemented a function that do this using htmltools and miniUI but it is very hacky and has problems of sizing (0px height on rmarkdown documents). Moreover, the function needs to perform a special processing for each kind of htmlwidget.

#' Combine several interactive plots
#'
#' This function combines different interactive plots in a unique view. It is
#' especially useful in the function \code{\link{manipulateWidget}} to have in
#' the same window several related plots that respond to the same set of
#' controls.
#'
#' @param ...
#'   Elements to combine. They should be htmlwidgets, but they can also be
#'   shiny tags or html object or text
#' @param nrow
#'   Number of rows of the layout.
#' @param ncol
#'   Number of columns of the layout.
#' @param title
#'   Title of the view
#' @param hflex
#'   This argument controls the relative size of each column. For instance, if
#'   the layout has two columns and \code{hflex = c(2,1)}, then the width of the
#'   first column will be twice the one of the second one. If a value is equal
#'   to NA, then the corresponding column will have its 'natural' width, and the
#'   remaining space will be shared between the other columns.
#' @param vflex
#'   Same as hflex but for the height of the rows of the layout.
#'
#' @return
#' Object of class 'combinedHtmlwidgets' which is an extension of 'shiny.tags'.
#'
#' @details
#' The function only allows table like layout : each row has the same number of
#' columns and reciprocally. But it is possible to create more complex layout
#' by nesting combined htmlwidgets. (see examples)
#'
#' @examples
#' if require(plotly) {
#'   data(iris)
#'
#'  combineWidgets(title = "The Iris dataset",
#'    plot_ly(iris, x = Sepal.Length, type = "histogram", nbinsx = 20),
#'    plot_ly(iris, x = Sepal.Width, type = "histogram", nbinsx = 20),
#'    plot_ly(iris, x = Petal.Length, type = "histogram", nbinsx = 20),
#'    plot_ly(iris, x = Petal.Width, type = "histogram", nbinsx = 20)
#'  )
#'
#'  # Create a more complex layout by nesting combinedWidgets
#'  combineWidgets(title = "The iris data set: sepals", ncol = 2, hflex = c(2,1),
#'    plot_ly(iris, x = Sepal.Length, y = Sepal.Width, mode = "markers", color = Species),
#'    combineWidgets(
#'      plot_ly(iris, x = Sepal.Length, type = "histogram", nbinsx = 20),
#'      plot_ly(iris, x = Sepal.Width, type = "histogram", nbinsx = 20)
#'    )
#'  )
#'
#' }
#'
#' @export
#'
combineWidgets <- function(..., nrow = NULL, ncol = NULL, title = NULL,
                           hflex = 1, vflex = 1) {

  widgets <- lapply(list(...), .processOutput)

  # Get Number of rows and columns
  nwidgets <- length(widgets)
  if (!is.null(nrow) && !is.null(ncol) && nrow * ncol < nwidgets) {
    stop("There are too much widgets compared to the number of rows and columns")
  } else if (is.null(nrow) && !is.null(ncol)) {
    nrow <- ceiling(nwidgets / ncol)
  } else if (!is.null(nrow) && is.null(ncol)) {
    ncol <- ceiling(nwidgets / nrow)
  } else {
    nrow <- ceiling(sqrt(nwidgets))
    ncol <- ceiling(nwidgets / nrow)
  }

  hflex <- rep(hflex, length.out = ncol)
  vflex <- rep(vflex, length.out = nrow)

  rows <- lapply(1:nrow, function(i) {
    args <- widgets[((i-1) * ncol + 1):(i * ncol)]

    # If vflex is NA for this row, then try to infer the height of the row.
    if (is.na(vflex[i])) {
      heights <- unlist(sapply(args, function(x) {
        if (!is.list(x)) return (NULL)
        if (!is.null(x$height)) return(x$height)
        if (!is.null(x$attribs)) return(x$attribs$height)
        NULL
      }))
      if (!is.null(heights)) {
        heights <- na.omit(heights)
        if (length(heights) > 0)  args$height <- heights[1]
      }
      if (is.null(args$height)) args$height <- 200
    }

    args$flex <- hflex
    do.call(fillRow, args)
  })

  # Title
  if(!is.null(title)) {
    vflex <- c(NA, vflex)
    title <- tags$div(style = "text-align: center;",
      tags$h1(title)
    )
    rows <- append(list(title), rows)
  }

  rows$flex <- vflex

  res <- do.call(fillCol, rows)
  class(res) <- append("combinedHtmlwidgets", class(res))
  res
}

#' @export
print.combinedHtmlwidgets <- function(x, ...) {
  htmltools:::html_print(miniPage(x))
}

#' Prepare widgets and other types of objects to be displayed in the shiny
#' gadget
#' @noRd
.processOutput <- function(x) {

  if (is(x, "plotly_hash")) {
    if(requireNamespace("plotly")) {
      x <- plotly::as.widget(x)
      x$width <- x$height <- "100%"
      return(x)
    }
    else return("Package plotly is missing")
  }

  if (is(x, "datatables")) {
    # How to set size ?
    return(tags$div(htmltools::as.tags(x),
                    style = "width:100%;max-height:100%;overflow:auto"))
  }

  if (is(x, "htmlwidget")) {
    x$width <- x$height <- "100%"
    return(x)
  }

  x
}
@cpsievert
Copy link
Collaborator

Great stuff @cuche27! I find arranging multiple htmlwidgets to be painful as well, and would love an official solution. BTW, in the dev version of plotly, plotly object are htmlwidget objects, so we won't be needing this in .processOutput:

if (is(x, "plotly_hash")) {
    if(requireNamespace("plotly")) {
      x <- plotly::as.widget(x)
      x$width <- x$height <- "100%"
      return(x)
    }
    else return("Package plotly is missing")
  }

@FrancoisGuillem
Copy link
Author

Hello and happy new year,

I wanted to inform you that I have finally included my function combineWidgets in another package I have developed for a client in order to make it more sustainable: https://github.com/rte-antares-rpackage/manipulateWidget

The purpose of this package is to help users to create easily and quickly graphical interface in order to change the data or the graphical parameters of an htmlwidget. It uses shiny gadget, but the user does not have to know anything about shiny, HTML or CSS.

I have submitted it to CRAN today and I hope it will be published soon.

@ramnathv
Copy link
Owner

ramnathv commented Jan 4, 2017

@FrancoisGuillem This looks really awesome! I think it would be nice to start a section on the htmlwidgets site where we can point users to packages like manipulateWidget that enhance the feature set of htmlwidgets @jjallaire @jcheng5 @timelyportfolio @yihui what do you think?

@jjallaire
Copy link
Collaborator

jjallaire commented Jan 4, 2017 via email

@jjallaire
Copy link
Collaborator

@ramnathv I agree that there are a few things long overdue for the website:

  1. Link to flexdashboard and manipulateWidget

  2. Link to crosstalk

@FrancoisGuillem
Copy link
Author

@jjallaire this is a very nice idea. What is the name of the method I need to write to implement it ? Is there a way to know that we are in "runtime: shiny" mode ?

@jjallaire
Copy link
Collaborator

I just added prominent navigational and home page links to the flexdashboard and crosstalk packages (I gave them special consideration because they are core add-ons to the htmlwidgets package created by the same authors). Next we need to figure out a way to link prominently to "third-party" add-ons (it may be that the Gallery can already accommodate this, but it seems like distinguishing between layout oriented widget aggregators and actual widgets would be useful)

@jjallaire
Copy link
Collaborator

jjallaire commented Jan 4, 2017 via email

@jjallaire
Copy link
Collaborator

@FrancoisGuillem Here is a diff that shows what is required to support printing manipulateWidget in Rmd documents with runtime: shiny:

jjallaire/manipulateWidget@3386099?w=1

There is also a simple example which I put into inst/examples.

Note that I would have given this to you as a PR but my changes for some reason created a diff with every line changed (just whitespace) and I didn't think you'd want to merge a change with so much diff noise.

@FrancoisGuillem
Copy link
Author

@jjallaire Wow that was fast ! Thank you :). Can you add yourself as a contributor in the DESCRIPTION file ?

@jjallaire
Copy link
Collaborator

jjallaire commented Jan 4, 2017 via email

@FrancoisGuillem
Copy link
Author

@jjallaire Ok, thank you :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants