R/datanode.R

Defines functions encode.datanode .validate.named .validate.unnamed .head.string .validate.scalar .validate.atomic .validate.string .validate.length .validate.type .validate.list.with.names .validate.data.frame.list data.frame.to.list .validate.datanode.array new.datanode.array .validate.datanode.plot new.datanode.plot new.filter.widget .validate.datanode.table new.datanode.table .validate.datanode.html new.datanode.html .validate.datanode.simple new.datanode.simple .validate.custom .validate.datanode new.datanode

Documented in data.frame.to.list encode.datanode new.datanode.array new.datanode.html new.datanode.plot new.datanode.simple new.datanode.table new.filter.widget

## not exported--- this is the "abstract" base constructor
## custom, if provided, is a named list, and gives more metadata.
## Each entry in the list is a charvec of HTML to render, and normally
## the whole thing should be rendered as more sections of the accordion
new.datanode <- function(type, name, value, label=name, description=label,
                         warnings = character(),
                         custom = NULL)  {
  
  retval <- list(type=type,
                 name=name,
                 label=label,
                 description=description,
                 value=value)
  if(length(warnings) > 0)  retval$warnings <- warnings
  if(length(custom) > 0)  retval$custom <- custom
  
  class(retval) <- "AnalysisPageDataNode"

  return(retval)
}


.validate.datanode <- function(node, prefix = "")  {
  .validate.type(node, "AnalysisPageDataNode", prefix)
  .validate.list.with.names(node, c("type", "name", "label", "description", "value"),
                            optional.names = c("warnings", "custom"),
                            prefix = paste(prefix, "data node: "))
  
  for(param in c("type", "name", "label", "description"))
    .validate.string(node[[param]], paste(prefix, "$", param, ": "))

  if(!is.null(node$warnings))  {
    .validate.type(node$warnings, is.character)
    .validate.type(node$warnings, is.vector)
  }
  
  if(!is.null(node$custom))
    .validate.custom(node$custom, paste0(prefix, "$custom: "))
  
  type <- node$type
  constructor.name <- paste(sep=".", "new.datanode", type)
  exists(constructor.name) || stop(prefix, "data node is not of a known type: ", type)

  validator.name <- paste(sep=".", ".validate.datanode", type)
  ## every constructor should have a companion validator or this is a structural issue
  stopifnot(exists(constructor.name))
  
  validator <- get(validator.name)
  validator(node, prefix)
}


.validate.custom <- function(custom, prefix)  {
  .validate.type(custom, is.list, prefix)

  
  if(length(custom) > 0)
    .validate.type(names(custom), Negate(is.null), paste0(prefix, "names: "))
    
  for(sectionName in names(custom))  {
    section <- custom[[sectionName]]
    sectionPrefix <- paste0(prefix, "$", sectionName, ": ")
    .validate.type(section, is.character, sectionPrefix)
    .validate.type(section, is.vector, sectionPrefix)
  }
}


##' Construct a new simple-type data node
##'
##' Construct a new simple-type data node. A simple data node must have either a NULL value
##' or a "scalar" value, which means length 1 atomic
##' @title new.datanode.simple
##' @param name Name of the node
##' @param value The value
##' @param ... Passed through to \code{new.datanode}, in particular \code{label} and \code{description}
##' @return AnalysisPageDataNode
##' @author Brad Friedman
##' @seealso \code{\link{is.atomic}}
##' @export
##' @examples
##' new.datanode.simple(name = "x", value = 100)
new.datanode.simple <- function(name, value, ...)  new.datanode("simple", name, value, ...)

.validate.datanode.simple <- function(node, prefix)  {
  is.null(node) && return()
  .validate.scalar(node$value, prefix)
}



##' Construct a new HTML-type data node
##'
##' Construct a new HTML-type data node. An HTML-type data node is like a simple data node but
##' it has an HTML string or character vector as its value. It should be rendered as-is, but
##' with activated analysis-page-data-set containers.
##' @title new.datanode.html
##' @param name Name of the node
##' @param value The value, an HTML string or charvec.
##' @param ... Passed through to \code{new.datanode}, in particular \code{label} and \code{description}
##' @return AnalysisPageDataNode
##' @author Brad Friedman
##' @seealso \code{\link{is.atomic}}
##' @export
##' @examples
##' new.datanode.html(name = "shakespeare",
##'                   value = "<i>Shall I compute thee to a summer's data?</i>")
new.datanode.html <- function(name, value, ...)  new.datanode("html", name, value, ...)

.validate.datanode.html <- function(node, prefix)  {
  is.null(node) && return()
  .validate.type(node$value, "character", prefix)
}



##' Construct a new table-type data node
##'
##' Construct a new table-type data node either from an \code{AnnotatedDataFrame}.
##' @title new.datanode.table
##' @param name Name of the node
##' @param data An \code{AnnotatedDataFrame}. Unless "label" is already available,
##' "labelDescription" in the \code{varMetadata} will be
##' changed to "label" to agree with the syntax we use in the rest of the AnalysisPage interface.
##' @param caption Caption for the table. Default: ""
##' @param ... Passed through to \code{new.datanode}, in particular \code{label} and \code{description}
##' @return AnalysisPageDataNode
##' @author Brad Friedman
##' @note Captions for plots are included implicitly in the table component of their responses.
##' @export
new.datanode.table <- function(name, data, caption="", ...) {
  stopifnot(is(data, "AnnotatedDataFrame"))

  ## change "labelDescription" to "label" in meta-data (unless both are present)
  vmd <- varMetadata(data) 
  if(!"label" %in% names(vmd) && "labelDescription" %in% names(vmd))
    names(vmd)[names(vmd) == "labelDescription"] <- "label"

  
  new.datanode("table", name,
               list(data = data.frame.to.list(pData(data)),
                    meta = data.frame.to.list(vmd),
                    caption = caption),
               ...)
}

.validate.datanode.table <- function(node, prefix)  {
  .validate.list.with.names(node$value, c("data", "meta", "caption"),
                            prefix = paste(prefix, "table datanode value: "))
  
  .validate.data.frame.list(node$value$data)
  .validate.data.frame.list(node$value$meta)
  .validate.type(node$value$caption, "character", paste(sep="$", "caption"))

  if(length(node$value$data) >= 1)  {
    colnames.from.data <- names(node$value$data[[1]])
    colnames.from.meta <- names(node$value$meta)
    identical(colnames.from.data, colnames.from.meta) || stop(prefix, " colnames from meta and data don't agree:\n  meta: ",
                                                              paste(collapse = " ", colnames.from.meta), "\n  data: ",
                                                              paste(collapse = " ", colnames.from.data))
  }
}


##' AnalysisPageFilterWidget object and system
##'
##' The AnalysisPageFilterWidget object specifies a "filter widget" to be displayed
##' to the user. This is a grid of colored squares, each of which controls the filtering
##' of a subset of the samples based on the values of a pheno field.
##' This object specifies the dimension of the grid,
##' the colors of the squares, rollovers to appear for each square, and the subset
##' of samples that each square filters.
##'
##' \code{new.filter.widget} is the constructor for this object. This should be used
##' when constructing an \code{AnalysisPageReponse} explicitly, with the return
##' value then passed to \code{\link{new.datanode.plot}}.
##' @param data.field Name of table field which should be used for filtering.
##' @param color Named character vector. Names should be values (or possible values)
##' that the data field could take on. Values are string specifying colors. These are
##' passed through directly to javascript so they should be valid colors there, whatever
##' that means.
##' @param cells Character matrix. This gives the layout of the filter grid. The values
##' should all be either taken from \code{names(color)} or else be NA values. The NA
##' values will be inactive (no rollover or click listeners).
##' @param inactive.color Color for inactive cells (that are the positions with
##' \code{is.na(cells)}). Default: "gray"
##' @param type Filter widget type. The only currently supported type is "filter_grid".
##' @return \code{new.filter.widget} returns an \code{AnalysisPageFilterWidget}
##' @author Brad Friedman
##' @export
##' @rdname filterWidget
new.filter.widget <- function(data.field,
                              color,
                              cells,
                              inactive.color = "gray",
                              type = "filter_grid")  {
  stopifnot(is.character(data.field) && length(data.field) == 1 && is.vector(data.field))
  stopifnot(is.character(color) && !is.null(names(color)) && is.vector(color))
  data.values <- names(color)
  stopifnot(is.matrix(cells) && is.character(cells))
  stopifnot(all(is.na(cells) | cells %in% data.values))
  stopifnot(is.character(inactive.color) &&
            length(inactive.color) == 1 &&
            is.vector(inactive.color))


  ## rjson encodes a matrix just as a vector, but we want it as a nested array.
  ## So we'll explicitly cast it to a list of (row)-vectors.
  cellsAsList <- lapply(1:nrow(cells), function(i) cells[i,])
  
  obj <- list(type = "filter_grid",
              data_field = data.field,
              color = color,
              cells = cellsAsList,
              inactive_color = inactive.color)

  class(obj) <- "AnalysisPageFilterWidget"
  return(obj)
}




##' Construct a new plot-type data node
##'
##' Construct a new plot-type data node from a plot.file and an AnnotatedDataFrame.
##' Note: caption is included implicitly in the table object.
##' @title new.datanode.plot
##' @param name Name of the node
##' @param plot.file Path to plot file, relative to server tempdir.
##' @param table A table-type \code{AnalysisPageDataNode}
##' @param warnings Character vector of warnings, to be passed through to
##' \code{new.datanode}, possibly after appending the warning described above for
##' \code{filter.widget}. Default: \code{character()}
##' @param filter.widget If provided, then an AnalysisPageFilterWidget. See
##' \code{\link{new.filter.widget}} and \code{\link{setFilterWidget}}.
##' A check is made that \code{filter.widget$data_field} is an actual field
##' from the data table. If not, then the filter widget is omitted, with a warning.
##' NULL (default) means to not include this.
##' @param ... Passed through to \code{new.datanode}, in particular \code{label} and \code{description}
##' @return AnalysisPageDataNode
##' @author Brad Friedman
##' @export
new.datanode.plot <- function(name, plot.file, table, 
                              warnings = character(),
                              filter.widget = NULL,
                              ...)  {
  stopifnot(is.character(plot.file) && length(plot.file) == 1)
  stopifnot(is(table, "AnalysisPageDataNode") && table$type == "table")
  value <- list(plot = plot.file,
                table = table)
  if(!is.null(filter.widget))  {
    stopifnot(is(filter.widget, "AnalysisPageFilterWidget"))
    
    tableData <- table$value$data

    if(length(tableData) > 0)  {
      ## If the table is non-empty, check that filter.widget$data_field
      ## is one of the table fields
      tableFields <- names(tableData[[1]])
      if(filter.widget$data_field %in% tableFields)  {
        value$filter_widget <- filter.widget
      }  else  {
        missingDataFieldWarning <- paste0("No data field '",
                                          filter.widget$data_field,
                                          "' in table. Not rendering filter widget")
        warnings <- c(warnings, missingDataFieldWarning)
      }
    }
    ## If the table is empty then we can't do the check. But on the
    ## other hand there are no active points, so let's not bother
    ## even rendering the widget, or throwing a warning about it.
  }

  new.datanode("plot",
               name,
               value = value,
               warnings = warnings,
               ...)
}

  
.validate.datanode.plot <- function(node, prefix)  {
  .validate.list.with.names(node$value,
                            expected.names = c("plot", "table"),
                            optional.names = "filter_widget",
                            prefix = paste(prefix, "plot datanode value: "))

  .validate.string(node$value$plot, paste(prefix, "$value$plot: "))

  .validate.datanode.table(node$value$table, paste(prefix, "$value$table: "))
}
  


##' Construct a new array-type data node
##'
##' Construct a new array-type data node from a list of \code{AnalysisPageDataNode}s
##' @title new.datanode.array
##' @param name Name of the node
##' @param children List of \code{AnalysisPageDataNode}s
##' @param ... Passed through to \code{new.datanode}, in particular \code{label} and \code{description}
##' @return AnalysisPageDataNode
##' @author Brad Friedman
##' @export
##' @examples
##' html.node <- example(new.datanode.html)$value
##' simple.node <- example(new.datanode.simple)$value
##' new.datanode.array(name = "arr", children = list(html.node, simple.node))
new.datanode.array <- function(name, children, ...)  {
  stopifnot(is.list(children))
  stopifnot(length(children) == 0 || sapply(children, is, "AnalysisPageDataNode"))
  new.datanode("array", name, value=children, ...)
}

.validate.datanode.array <- function(node, prefix)  {
  .validate.type(node$value, "list", paste(prefix, "$value: "))

  length(node$value) == 0 && return()
  
  for(i in 1:length(node$value))  {
    child <- node$value[[i]]
    .validate.datanode(child, paste(prefix, "$value[", i, "]: "))
  }
}



##' Create a list representation of a data.frame
##'
##' We represent a data.frame as an hash of hashes.
##' Factors are first coerced into characters.
##'
##' The outer hash is keyed by the rownames of your data.frame
##' The inner hash is keyed by the colnames of your data.frame
##' @title data.frame.to.json
##' @param df data.frame to represent as a list
##' @return list
##' @author Brad Friedman
##' @examples
##' df <- data.frame(A=1:3, B=3:1, C=factor(c("foo","bar","foo")), row.names = c("one", "two", "three"))
##' ## Should give the following
##' ## list(one=list(A=1, B=3, C="foo"),
##' ##      two=list(A=2, B=2, C="bar"),
##' ##      three=list(A=3, B=1, C="foo"))
##' data.frame.to.list(df)
##' @export
##' @examples
##' data.frame.to.list(head(cars))
data.frame.to.list <- function(df)  {

  if(nrow(df) == 0)  {
    ## kind of hard to make an empty (length 0) named list.
    empty.named.list <- list()
    names(empty.named.list) <- character(0)
    return(empty.named.list)
  }

    
  ## We return an ARRAY of HASHES

  ## First cast factors to characters
  if(ncol(df) > 0)  for(i in 1:ncol(df))  if(is.factor(df[[i]]))  df[[i]] <- as.character(df[[i]])

  ## drop = FALSE is important here because otherwise in the case where there is just
  ## 1 column the row will be demoted to a scalar thing without the column name
  list.of.rows <- lapply(1:nrow(df), function(i) as.list(df[i,,drop=FALSE]))
  names(list.of.rows) <- rownames(df)
  return(list.of.rows)
}


.validate.data.frame.list <- function(dfl, prefix="")  {
  is.list(dfl) || stop(prefix, "data.frame.list must be a list: ", paste(collapse=" ", is(dfl)))
  is.null(names(dfl)) && stop(prefix, "data.frame.list must have row names")
  length(dfl) == 0 && return()  # empty list---nothing else to validate

  non.list <- !sapply(dfl, is.list)
  sum(non.list) == 0 || stop(prefix, "entries in data.frame.list are not lists: ", paste(collapse=" ", which(non.list)))
  
  unnamed <- sapply(dfl, function(x) is.null(names(x)))
  sum(unnamed) == 0 || stop(prefix, "entries in data.frame.list are unnamed: ", paste(collapse=" ", which(non.list)))

  is.list.of.scalars <- sapply(dfl, function(x) all(sapply(x, length) == 1) && all(sapply(x, is.atomic)))
  sum(!is.list.of.scalars) == 0 || stop(prefix, "entries in data.frame.list are not lists of scalars: ", paste(collapse=" ", which(!is.list.of.scalars)))
  
  if(length(dfl) > 1)  {
    cn <- names(dfl[[1]])
    for(i in 2:length(dfl))  {
      identical(cn, names(dfl[[i]])) || stop(prefix, "names of entry ", i, " do not agree with those of entry 1")
    }
  }
}  


.validate.list.with.names <- function(l, expected.names, optional.names = character(), prefix="")  {
  .validate.type(l, is.list, prefix)
  n.names <- length(expected.names)
  identical(names(l)[1:n.names], expected.names) || stop(prefix, " names differ from expected.\n",
                                                         "  expected: ", paste(collapse=" ", expected.names), "\n",
                                                         "       got: ", paste(collapse=" ", names(l)))
  unknown.names <- setdiff(names(l), c(expected.names, optional.names))
  length(unknown.names) == 0 || stop(prefix, " unknown names: ", paste(collapse = " ", unknown.names))
}

## type can be character then do is(obj, type) or it can be a function like is.list then do type(obj).
## turns out that is(obj,"list") is slightly different than is.list(obj)
.validate.type <- function(obj, type, prefix="")  {
  if(is.function(type))  {
    isname <- deparse(substitute(type))
    type(obj) || stop(prefix, " object fails ", isname, "': ", paste(collapse=" ", is(obj)))
  }  else  {
    is(obj, type) || stop(prefix, "object is not '", type, "': ", paste(collapse=" ", is(obj)))
  }
}

.validate.length <- function(obj, explen, prefix="")  {
  length(obj) == explen || stop(prefix, "object is not length ", explen, ": ", length(obj))
}

## string is a character of length 1
.validate.string <- function(obj, prefix="")  {
  .validate.type(obj, "character", prefix)
  .validate.length(obj, 1, prefix)
}

.validate.atomic <- function(obj, prefix="")  {
  is.atomic(obj) || stop(prefix, "is not atomic: ", paste(collase=" ", is(obj)))
}

## a scalar is an atomic of length 1
.validate.scalar <- function(obj, prefix="")  {
  .validate.atomic(obj, prefix)
  .validate.length(obj, 1, prefix)
}


## turns LETTERS into "A B C ..." and anything n or shorter just concatenate
.head.string <- function(vec, n=3)  {
  first.few <- if(length(vec) > n)  {
    c(head(vec, n), "...")
  }  else  {
    head(vec, n)
  }

  paste(collapse=" ", first.few)
}
  
.validate.unnamed <- function(obj, prefix="")  {
  length(obj) == 0 || is.null(names(obj)) || stop(prefix, "has length > 0 but has names: ", .head.string(obj))
}


.validate.named <- function(obj, prefix="")  {
  is.null(names(obj)) && stop(prefix, "has no names")
}

##' JSON-Encode an AnalysisPageDataNode for the front end
##'
##' JSON-Encode an AnalysisPageDataNode for the front end.
##' This just calls \code{toJSON}, but before doing so
##' it makes sure that \code{$warnings} will be sent
##' as an array.
##'
##' Mostly this function is only called once, from
##' the \code{$analysis} method of an \code{AnalysisPageRAapacheApp}.
##' @param datanode AnalysisPageDataNode or other object
##' @return JSON-encoded string
##' @author Brad Friedman
encode.datanode <- function(datanode)  {
  ## Make sure this is sent as an array, not a scalar
  if(is(datanode, "AnalysisPageDataNode") && !is.null(datanode$warnings))
    datanode$warnings <- as.list(datanode$warnings)

  toJSON(datanode)
}

Try the AnalysisPageServer package in your browser

Any scripts or data that you put into this service are public.

AnalysisPageServer documentation built on April 28, 2020, 6:32 p.m.