File: plotlyM.r

package info (click to toggle)
hmisc 5.2-5-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 4,044 kB
  • sloc: asm: 28,907; f90: 590; ansic: 415; xml: 160; fortran: 75; makefile: 2
file content (363 lines) | stat: -rw-r--r-- 16,018 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
#' plotly Multiple
#'
#' Generates multiple plotly graphics, driven by specs in a data frame
#'
#' Generates multiple \code{plotly} traces and combines them with \code{plotly::subplot}.  The traces are controlled by specifications in data frame \code{data} plus various arguments.  \code{data} must contain these variables: \code{x}, \code{y}, and \code{tracename} (if \code{color} is not an "AsIs" color such as \code{~ I('black')}), and can contain these optional variables: \code{xhi}, \code{yhi} (rows containing \code{NA} for both \code{xhi} and \code{yhi} represent points, and those with non-\code{NA} \code{xhi} or \code{yhi} represent segments, \code{connect} (set to \code{TRUE} for rows for points, to connect the symbols), \code{legendgroup} (see \code{plotly} documentation), and \code{htext} (hovertext).  If the \code{color} argument is given and it is not an "AsIs" color, the variable named in the \code{color} formula must also be in \code{data}.  Likewise for \code{size}.  If the \code{multplot} is given, the variable given in the formula must be in \code{data}.  If \code{strata} is present, another level of separate plots is generated by levels of \code{strata}, within levels of \code{multplot}.
#'
#' If \code{fitter} is specified, x,y coordinates for an individual plot are
#' run through \code{fitter}, and a line plot is made instead of showing data points.  Alternatively you can specify \code{fitter='ecdf'} to compute and plot emirical cumulative distribution functions.
#'
#' @param data input data frame
#' @param x formula specifying the x-axis variable
#' @param y formula for y-axis variable
#' @param xhi formula for upper x variable limits (\code{x} taken to be lower value)
#' @param yhi formula for upper y variable limit (\code{y} taken to be lower value)
#' @param htext formula for hovertext variable
#' @param multplot formula specifying a variable in \code{data} that when stratified on produces a separate plot
#' @param strata formula specifying an optional stratification variable
#' @param fitter a fitting such as \code{loess} that comes with a \code{predict} method.  Alternatively specify \code{fitter='ecdf'} to use an internal function for computing and displaying ECDFs, which moves the analysis variable from the y-axis to the x-axis
#' @param color \code{plotly} formula specifying a color variable or e.g. \code{~ I('black')}.  To keep colors constant over multiple plots you will need to specify an AsIs color when you don't have a variable representing color groups.
#' @param size \code{plotly} formula specifying a symbol size variable or AsIs
#' @param showpts if \code{fitter} is given, set to \code{TRUE} to show raw data points in addition to smooth fits
#' @param rotate set to \code{TRUE} to reverse the roles of \code{x} and \code{y}, for example to get horizontal dot charts with error bars
#' @param xlab x-axis label.  May contain html.
#' @param ylab a named vector of y-axis labels, possibly containing html (see example below).  The names of the vector must correspond to levels of the \code{multplot} variable.  \code{ylab} can be unnamed if \code{multplot} is not used.
#' @param ylabpos position of y-axis labels.  Default is on top left of plot.  Specify \code{ylabpos='y'} for usual y-axis placement.
#' @param xlim 2-vector of x-axis limits, optional
#' @param ylim 2-vector of y-axis limits, optional
#' @param shareX specifies whether x-axes should be shared when they align vertically over multiple plots
#' @param shareY specifies whether y-axes should be shared when they align horizontally over multiple plots
#' @param nrows the number of rows to produce using \code{subplot}
#' @param ncols the number of columns to produce using \code{subplot} (specify at most one of \code{nrows,ncols})
#' @param height height of the combined image in pixels
#' @param width width of the combined image in pixels
#' @param colors the color palette.  Leave unspecified to use the default \code{plotly} palette
#' @param alphaSegments alpha transparency for line segments (when \code{xhi} or \code{yhi} is not \code{NA})
#' @param alphaCline alpha transparency for lines used to connect points
#' @param digits number of significant digits to use in constructing hovertext
#' @param zeroline set to \code{FALSE} to suppress vertical line at x=0
#'
#' @return \code{plotly} object produced by \code{subplot}
#' @author Frank Harrell
#' @examples
#' \dontrun{
#' set.seed(1)
#' pts     <- expand.grid(v=c('y1', 'y2', 'y3'), x=1:4, g=c('a', 'b'), yhi=NA,
#'                        tracename='mean', legendgroup='mean',
#'                        connect=TRUE, size=4)
#'
#' pts$y   <- round(runif(nrow(pts)), 2)
#'
#' segs     <- expand.grid(v=c('y1', 'y2', 'y3'), x=1:4, g=c('a', 'b'),
#'                         tracename='limits', legendgroup='limits',
#'                         connect=NA, size=6)
#' segs$y   <- runif(nrow(pts))
#' segs$yhi <- segs$y + runif(nrow(pts), .05, .15)
#'
#' z <- rbind(pts, segs)
#'
#' xlab <- labelPlotmath('X<sub>12</sub>', 'm/sec<sup>2</sup>', html=TRUE)
#' ylab <- c(y1=labelPlotmath('Y1', 'cm', html=TRUE),
#'           y2='Y2',
#'           y3=labelPlotmath('Y3', 'mm', html=TRUE))
#'
#' W=plotlyM(z, multplot=~v, color=~g, xlab=xlab, ylab=ylab, ncols=2,
#'           colors=c('black', 'blue'))
#'
#' W2=plotlyM(z, multplot=~v, color=~I('black'), xlab=xlab, ylab=ylab,
#'            colors=c('black', 'blue'))
#' 
#' }
#' @export
plotlyM <- function(data, x=~x, y=~y, xhi=~xhi, yhi=~yhi, htext=NULL,
                    multplot=NULL, strata=NULL, fitter=NULL,
                    color=NULL, size=NULL,
                    showpts=! length(fitter),
                    rotate=FALSE, xlab=NULL, ylab=NULL,
                    ylabpos=c('top', 'y'),
                    xlim=NULL, ylim=NULL,
                    shareX=TRUE, shareY=FALSE, height=NULL, width=NULL,
                    nrows=NULL, ncols=NULL,
                    colors=NULL, alphaSegments=1, alphaCline=0.3, digits=4,
                    zeroline=TRUE) {
  
  if (!requireNamespace("plotly"))
    stop("This function requires the 'plotly' package.")
  auto <- .Options$plotlyauto
  if(length(auto) && auto) height <- width <- NULL
  
  ylabpos <- match.arg(ylabpos)

  if(rotate) {
    xf  <- y   #~ y
    yf  <- x   #~ x
    xfe <- yhi #~ yhi
    yfe <- xhi #~ xhi
    
  } else {
    xf  <- x    #~ x
    yf  <- y    #~ y
    xfe <- xhi  #~ xhi
    yfe <- yhi  #~ yhi
  }

  xn   <- all.vars(xf) #x)
  yn   <- all.vars(yf) #y)
  xhin <- all.vars(xfe) #xhi)
  yhin <- all.vars(yfe) #yhi)

  n <- nrow(data)
  if(! length(multplot)) {
    multplot <- ~ .v.
    data$.v. <- rep(' ', n)
  } else data$.v. <- data[[all.vars(multplot)]]

  vlevs  <- levels(as.factor(data$.v.))
  lastv  <- vlevs[length(vlevs)]

  strpres <- length(strata) > 0
  strata <- if(strpres) as.factor(data[[all.vars(strata)]])
            else
              as.factor(rep('', nrow(data)))
  stlevs <- levels(strata)
  lasts  <- stlevs[length(stlevs)]
  if(! length(nrows) && ! length(ncols) && strpres)
    ncols <- length(stlevs)

  if(length(ylab) && ! length(names(ylab))) names(ylab) <- vlevs
  if(! length(ylab)) ylab <- structure(vlevs, names=vlevs)

  fmt <- function(x) htmlSN(x, digits=digits)

  nam <- names(data)
  if(xhin  %nin% nam)         data[[xhin]]     <- rep(NA, n)
  if(yhin  %nin% nam)         data[[yhin]]     <- rep(NA, n)
  if('connect' %nin% nam)     data$connect     <- rep(FALSE, n)
  if('tracename' %in% nam && 'legendgroup' %nin% nam)
    data$legendgroup <- data$tracename

  if(length(color)) {
    ## ~ I('black') will not show inherits('AsIs') but all.vars is char(0)
    colasis         <- ! length(all.vars(color))
    traceform       <- if(colasis) ~ tracename
    legendgroupform <- if(colasis) ~ legendgroup
    colvar          <- if(! colasis) all.vars(color)
  }
  else if(length(size)) {
    sizeasis        <- ! length(all.vars(color))
    traceform       <- if(sizeasis) ~ tracename
    legendgroupform <- if(sizeasis) ~ legendgroup
    sizevar         <- if(! sizeasis) all.vars(size)
    }
  else {
    traceform       <- if('tracename'   %in% nam) ~ tracename
    legendgroupform <- if('legendgroup' %in% nam) ~ legendgroup
    colasis         <- FALSE
    colvar          <- NULL
    sizeasis        <- FALSE
    sizevar         <- NULL
    }
  
  if(length(color)) legendgroupform <- color

  usualfitter <- length(fitter) && is.function(fitter)
  is.ecdf     <- length(fitter) && is.character(fitter) && fitter == 'ecdf'
  xpresent    <- ! is.ecdf
  
  runfit <- if(usualfitter) function() {
    xv <- all.vars(xf)
    yv <- all.vars(yf)
    x  <- pt[[xv]]
    y  <- pt[[yv]]
    g  <- if(length(colvar))
            pt[[colvar]] else rep('', nrow(pt))
    g  <- as.factor(g)
    d  <- data.frame(x, y, g)
    Dp <- NULL
    xgrid <- seq(min(x, na.rm=TRUE), max(x, na.rm=TRUE), length=150)
    dx <- data.frame(x = xgrid)
    for(gv in levels(g)) {
      f  <- fitter(y ~ x, data=subset(d, g == gv))
      y  <- predict(f, newdata=dx)
      dp <- cbind(dx, y, g=gv)
      Dp <- rbind(Dp, dp)
    }
    names(Dp) <- c(xv, yv, if(length(colvar)) colvar else 'g')
    Dp
  }
  else
    if(is.ecdf) function() {
      yv <- all.vars(xf)
      y  <- pt[[yv]]
      g  <- if(length(colvar))
              pt[[colvar]] else rep('', nrow(pt))
      g  <- as.factor(g)
      Dp <- NULL
      rng <- range(y, na.rm=TRUE)
      for(gv in levels(g)) {
        j    <- g == gv & ! is.na(y)
        yg   <- sort(y[j])
        n    <- length(yg)
        vals <- unique(yg)   # see stats::ecdf
        a <- approx(vals, cumsum(tabulate(match(yg, vals))) / n,
                    method='constant', yleft=0, yright=1, f=0,
                    ties='ordered', xout=vals)
        delta <- diff(rng) * 0.025
        a$x   <- c(min(a$x) - delta, a$x, max(a$x) + delta)
        a$y   <- c(0, a$y, 1)
        dp <- data.frame(x = a$x, y = a$y, g=gv)
        Dp <- rbind(Dp, dp)
      }
      names(Dp) <- c(yv, 'ecdf', if(length(colvar)) colvar else 'g')
      Dp
    }

  xlabc <- if(length(xlab)) paste0(xlab, ': ')
  llab <- ifelse('tracename' %in% nam,
                 as.character(data$tracename), 'Limits')
  wl <- function(n, hin)
    paste0(xlabc, fmt(data[[hin]]),
           '<br>', llab, ':[',
           fmt(data[[n]]), ', ',
           fmt(data[[hin]]), ']')
  
    if(! length(htext)) { 
      nhi <- is.na(data[[xhin]]) + is.na(data[[yhin]])
      whi <- ifelse(nhi == 2, 'xy',       ## which vars missing hi?
                    ifelse(nhi == 0, '',
                           ifelse(is.na(data[[xhin]]), 'x', 'y')))
    data$htxt <- ifelse(whi == 'xy',
                        paste0(xlabc, fmt(data[[xn]]),
                               '<br>',
                               ylab[data$.v.], ':', fmt(data[[yn]])),
                 ifelse(whi == 'x', wl(yn, yhin),
                 ifelse(whi == 'y', wl(xn, xhin),
                        paste0(xlabc, fmt(data[[xn]]),
                               '<br>', xn, ' ', llab, ': [',
                               fmt(data[[xn]]), ', ', fmt(data[[xhin]]), ']',
                               '<br>', yn, ' ', llab, ': [',
                               fmt(data[[yn]]), ', ', fmt(data[[yhin]]), ']'))))
    htext <- ~ htxt
    }
  p <- plotly::plot_ly(height=height, width=width, colors=colors)
  ## For some reason colors doesn't always take in add_*
  P <- list()
  iv <- 0
#  axislab <- character(0)
#  axn1 <- if(rotate) 'yaxis' else 'xaxis'
#  axn2 <- if(rotate) 'xaxis' else 'yaxis'

  for(vn in vlevs) {
    for(sn in stlevs) {
      iv <- iv + 1
      whichaxis <- if(iv == 1) '' else iv
      if(is.ecdf) {
        ax1 <- ylab[vn]
        ax2 <- 'Cumulative Probability'
        xn  <- yn
        xf  <- yf
        yf  <- ~ ecdf
      } else {
        ax1 <- if(rotate) ylab[vn] else xlab
        ax2 <- if(rotate) xlab else ylab[vn]
      }

      w  <- subset(data, .v. == vn & strata == sn)
      wxn <- w[[xn]]  # if(xpresent) w[[xn]] else 1 : nrow(w)
      j <- if(length(colvar)) order(w[[colvar]], wxn)
           else
             if(length(sizevar)) order(w[[sizevar]], wxn)
           else order(wxn)
      w <- w[j, ]
      r <- p
      ipt <- is.na(w[[yhin]]) & is.na(w[[xhin]])
      pt  <- w[ipt, ]
      conct <- is.logical(pt$connect) && pt$connect[1]
      if(nrow(pt)) {
        if(length(fitter)) {
          Dp <- runfit()
          r <- plotly::add_lines(r, data=Dp, x=xf, y=yf,
                                 name=traceform, legendgroup=legendgroupform,
                                 showlegend=vn==lastv & sn==lasts,
                                 color=color, size=size,
                                 colors=colors,
                                 line=if(is.ecdf) list(shape='hv'))
        }
        if(showpts) {
          r <- plotly::add_markers(r, data=pt, x=xf, y=yf,
                                   name=traceform, legendgroup=legendgroupform,
                                   showlegend=vn==lastv & sn==lasts,
                                   color=color, size=size,
                                   text=htext, hoverinfo='text', colors=colors)
          if(conct)
            r <- plotly::add_lines(r, data=pt, x=xf, y=yf,
                                   name=traceform, legendgroup=legendgroupform,
                                   showlegend=FALSE, color=color,
                                   size=I(1),
                                   hoverinfo='none', colors=colors, alpha=alphaCline)
        }
      }
      
      s <- w[! ipt, ]
        
      if(nrow(s)) {
        ## If only one of xhi and yhi is missing, need to copy non-NA
        ## value from x/y.  Must go to extra trouble to preserve factors
        m <- is.na(s[[xhin]])
        if(any(m)) {
          a <- s[[xn]]
          a[! m] <- s[! m, xhin]
          s[[xhin]] <- a
        }
        m <- is.na(s[[yhin]])
        if(any(m)) {
          a <- s[[yn]]
          a[! m] <- s[! m, yhin]
          s[[yhin]] <- a
          }

        r <-
          plotly::add_segments(r, data=s, x=xf, y=yf, xend=xfe, yend=yfe,
                               name=traceform, legendgroup=legendgroupform,
                               showlegend=vn==lastv & sn==lasts,
                               color=color, size=size,
                               colors=colors, alpha=alphaSegments,
                               text=htext, hoverinfo='text')
        }
      ## rdocumentation.org/packages/plotly/versions/4.7.1/topics/add_annotations
      ## https://plot.ly/r/text-and-annotations/
			## plot.ly/r/text-and-annotations/#set-annotation-coordinate-references      
      firstst <- length(stlevs) > 1 && vn == vlevs[1]
      if(firstst || ylabpos == 'top') {
        lab <- ax2
        if(firstst) lab <- paste0(lab, '<br>', sn)
        r <- plotly::add_annotations(r,  x=0, y=1,
                                     xref='paper', xanchor='left',
                                     yref='paper', yanchor='bottom',
                                     text=paste0('<b>', lab, '</b>'),
                                     showarrow=FALSE,
                                     font=list(color='rgba(25, 25, 112, 1.0)',
                                               size=14))
        ## midnight blue
        }
      r <- plotly::layout(r, xaxis=list(title=ax1, range=xlim,
                                        zeroline=zeroline),
                          yaxis=list(title=if(ylabpos == 'y') ax2 else '',
                                     range=ylim))
      P[[iv]] <- r
    }
  }
  if(length(ncols)) nrows <- ceil(iv / ncols)
  if(length(stlevs) > 1) shareY <- TRUE
  if(length(P) == 1) P <- P[[1]]
  else {
    P <- if(length(nrows))
           plotly::subplot(P, shareX=shareX, shareY=shareY,
                           titleX=TRUE, titleY=TRUE, nrows=nrows)
         else
           plotly::subplot(P, shareX=shareX, shareY=shareY,
                           titleX=TRUE, titleY=TRUE)
    }
  P
}
utils::globalVariables('.v.')