• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Displayr / flipFormat / 847

pending completion
847

push

travis-ci-com

web-flow
RS-12845: column header height can work without units (#37)

* RS-12845: allow column header height to work without units

* bump version

* Check for null col header height

4 of 4 new or added lines in 1 file covered. (100.0%)

2532 of 3301 relevant lines covered (76.7%)

19.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

52.69
/R/createcustomtable.R
1
#' Display a html table with custom formatting
2
#' @description Displays html table with custom formatting. This can be specified
3
#'  separately for each cell be specifying attributes or using CSS.
4
#' @param x Matrix or data frame of contents to show in the table
5
#' @param sig.change.fills Matrix of same dim as \code{x} used for cell fills
6
#'  (1 denotes increase/green fill, -1 denotes decrease/red fill, 0 no fill)
7
#' @param sig.change.arrows Matrix of same dim as \code{x} used for cell arrows
8
#'  (1 denotes increase/green up arrow, -1 denotes decrease/red down arrow, 0 no arrow)
9
#' @param sig.leader.circles Matrix of same dim as \code{x} used for 'leader' circles
10
#'  (2 denotes row leader, 1 denotes tied leaders, 0 denotes no circle)
11
#' @param format.type One of "Automatic", "Percentage (multiply by 100
12
#'  and add percentage sign) or "Numeric". When set to "Automatic", the format type
13
#'  will be determined by \code{attr(x, "statistic")}. Ignored if \code{x} is not numeric.
14
#' @param format.show.pct.sign Show percentage sign when \code{format.type} is "Percentage".
15
#' @param format.decimals Controls number of decimal places shown in table cells.
16
#'  Ignored if \code{x} is not numeric.
17
#' @param transpose Whether to switch rows and columns in \code{x}.
18
#' @param global.font.family Character; font family for all occurrences of any
19
#'  font attribute in the table unless specified individually.
20
#' @param global.font.color Global font color as a named color in character format
21
#'  (e.g. "black") or an a hex code.
22
#' @param font.size Global font size of all elements in the table. This is provided for
23
#'  convenience but its overriden by the font sizes of specific components.
24
#' @param font.unit One of "px" of "pt". By default all font sizes are specified in terms of
25
#'  pixels ("px"). But changing this to "pt" will mean that the font sizes will be in terms
26
#'  points ("pt"), which will be consistent with font sizes in text boxes.
27
#' @param border.color Color of all borders. Will be overriden if specific elements are set.
28
#' @param border.width Width of borders (in pixels) in all cells. Will be overriden if specific elements are set.
29
#' @param border.collapse Logical; whether the borders of adjacent cells
30
#'   should be shown as a single line or separate lines.
31
#' @param border.row.gap Numeric; the space between the borders
32
#'   separating different rows. Only used if \code{border.collapse} is false.
33
#' @param border.column.gap Numeric; the space between the borders
34
#'   separating different columns. Only used if \code{border.collapse} is false.
35
#' @param cell.prefix Character value/vector/matrix that is prepended before the cell values.
36
#' @param cell.suffix Character value/vector/matrix that is appended after the cell values.
37
#' @param cell.fill Background color of the cells in the table.
38
#' @param cell.border.width Width of border around table cells (in pixels).
39
#' @param cell.border.color Color of border around table cells,
40
#' @param cell.align.horizontal Horizontal alignment of text in table cells.
41
#' @param cell.align.vertical Vertical alignment of text in table cells.
42
#' @param cell.font.family Font family of text in table cells.
43
#' @param cell.font.color Font color of text in table cells.
44
#' @param cell.font.size Font size (in pixels) of text in table cells.
45
#' @param cell.font.weight One of "normal" or "bold".
46
#' @param cell.font.style One of "normal" or "italic".
47
#' @param cell.pad Space between text and cell border in pixels. This is only used if the
48
#'  horizontal alignment is "left" or "right".
49
#' @param show.col.headers Logical; whether to show column headers in the table.
50
#'  This will be ignored if \code{x} does not contain column names.
51
#' @param col.header.labels A vector or comma-separated labels to override the
52
#'  column names of \code{x}.
53
#' @param col.header.fill Background color of the column headers in the table.
54
#' @param col.header.border.width Width of border around table column headers (in pixels).
55
#' @param col.header.border.color Color of border around table column headers,
56
#' @param col.header.align.horizontal Horizontal alignment of text in table column headers.
57
#' @param col.header.align.vertical Vertical alignment of text in table column headers.
58
#' @param col.header.font.family Font family of text in table column headers.
59
#' @param col.header.font.color Font color of text in table column headers.
60
#' @param col.header.font.size Font size (in pixels) of text in table column headers.
61
#' @param col.header.font.weight One of "normal" or "bold".
62
#' @param col.header.font.style One of "normal" or "italic".
63
#' @param col.header.pad Space between text and cell border in pixels. This is only used if the
64
#'  horizontal alignment is "left" or "right".
65
#' @param show.row.headers Logical; whether to show row headers in the table.
66
#'  This will be ignored if \code{x} does not contain row names.
67
#' @param row.header.labels A vector or comma-separated labels to override
68
#'   the column names of \code{x}.
69
#' @param row.header.fill Background color of the row headers in the table.
70
#' @param row.header.border.width Width of border around table row headers (in pixels).
71
#' @param row.header.border.color Color of border around table row headers,
72
#' @param row.header.align.horizontal Horizontal alignment of text in table row headers.
73
#' @param row.header.align.vertical Vertical alignment of text in table row headers.
74
#' @param row.header.font.family Font family of text in table row headers.
75
#' @param row.header.font.color Font color of text in table row headers.
76
#' @param row.header.font.size Font size (in pixels) of text in table row headers.
77
#' @param row.header.font.weight One of "normal" or "bold".
78
#' @param row.header.font.style One of "normal" or "italic".
79
#' @param row.header.pad Space between text and cell border in pixels. This is only used if the
80
#'  horizontal alignment is "left" or "right".
81
#' @param row.span.fill Background color of the row.spans in the table.
82
#' @param row.span.border.width Width of border around table row.spans (in pixels).
83
#' @param row.span.border.color Color of border around table row.spans,
84
#' @param row.span.align.horizontal Horizontal alignment of text in table row.spans.
85
#' @param row.span.align.vertical Vertical alignment of text in table row.spans.
86
#' @param row.span.font.family Font family of text in table row.spans.
87
#' @param row.span.font.color Font color of text in table row.spans.
88
#' @param row.span.font.size Font size (in pixels) of text in table row.spans.
89
#' @param row.span.font.weight One of "normal" or "bold".
90
#' @param row.span.font.style One of "normal" or "italic".
91
#' @param row.span.pad Space between text and cell border in pixels. This is only used if the
92
#'  horizontal alignment is "left" or "right".
93
#' @param col.span.fill Background color of the col.spans in the table.
94
#' @param col.span.border.width Width of border around table col.spans (in pixels).
95
#' @param col.span.border.color Color of border around table col.spans,
96
#' @param col.span.align.horizontal Horizontal alignment of text in table col.spans.
97
#' @param col.span.align.vertical Vertical alignment of text in table col.spans.
98
#' @param col.span.font.family Font family of text in table col.spans.
99
#' @param col.span.font.color Font color of text in table col.spans.
100
#' @param col.span.font.size Font size (in pixels) of text in table col.spans.
101
#' @param col.span.font.weight One of "normal" or "bold".
102
#' @param col.span.font.style One of "normal" or "italic".
103
#' @param col.span.pad Space between text and cell border in pixels. This is only used if the
104
#'  horizontal alignment is "left" or "right".
105
#' @param col.header.classes CSS classes of column headers. The class definition should be added to
106
#'  \code{custom.css}. This overrides \code{col.header.fill},
107
#'  \code{col.header.border}, \code{col.header.font}, \code{col.header.align}, etc.
108
#' @param row.header.classes CSS classes of column headers. This overrides \code{row.header.fill},
109
#'  \code{row.header.border}, \code{row.header.font}, \code{row.header.align}, etc
110
#' @param col.classes any specific column classes to apply. e.g. \code{list(list(ix=3, class="bluefill"))}
111
#'  will cause column 3 to have class "bluefill".
112
#' @param row.classes any specific row classes to apply.
113
#' @param col.widths specify column widths in \% or px; Remaining width divided between remaining columns.
114
#' @param corner Contents of the corner cell, if row and column headers are used
115
#' @param corner.class Class of the corner cell, if row and column headers are used
116
#' @param corner.fill Background color of the corners in the table.
117
#' @param corner.border.width Width of border around table corners (in pixels).
118
#' @param corner.border.color Color of border around table corners,
119
#' @param corner.align.horizontal Horizontal alignment of text in table corners.
120
#' @param corner.align.vertical Vertical alignment of text in table corners.
121
#' @param corner.font.family Font family of text in table corners.
122
#' @param corner.font.color Font color of text in table corners.
123
#' @param corner.font.size Font size (in pixels) of text in table corners.
124
#' @param corner.font.weight One of "normal" or "bold".
125
#' @param corner.font.style One of "normal" or "italic".
126
#' @param corner.pad Space between text and cell border in pixels. This is only used if the
127
#'  horizontal alignment is "left" or "right".
128
#' @param footer Optional text shown as a footer below the table
129
#' @param footer.fill Background color of the footer in the table.
130
#' @param footer.height Height of the footer (ignored if no text in footer).
131
#' @param footer.lineheight Controls spacing between the lines of text in the
132
#'   footer. It can be specified in multiple ways but as a unitless number
133
#'   it is applied as a multiple to the font size.
134
#' @param footer.align.horizontal Horizontal alignment of text in table footer.
135
#' @param footer.align.vertical Vertical alignment of text in table footer.
136
#' @param footer.font.family Font family of text in table footer.
137
#' @param footer.font.color Font color of text in table footer.
138
#' @param footer.font.size Font size of text in table footer.
139
#' @param footer.font.weight One of "normal" or "bold".
140
#' @param footer.font.style One of "normal" or "italic".
141
#' @param banded.rows Whether to have banded rows
142
#' @param banded.cols Whether to have banded columns
143
#' @param banded.odd.fill Background of cells in odd rows or columns when \code{banded.rows} or \code{banded.cols}.
144
#' @param banded.even.fill Background of cells in even rows or columns when \code{banded.rows} or \code{banded.cols}.
145
#' @param sig.fills.up Cell color when \code{sig.change.fills} is used.
146
#' @param sig.fills.down Cell color when \code{sig.change.fills} is used.
147
#' @param sig.fills.nothing Cell color when \code{sig.change.nothing} is used.
148
#' @param sig.arrows.up Color of up arrows when \code{sig.change.arrows} is used.
149
#' @param sig.arrows.down Color of down arrows when \code{sig.change.arrows} is used.
150
#' @param circle.size Size of circles when \code{sig.leader.circles} is used.
151
#' @param spacer.row Indices of any blank divider rows
152
#' @param spacer.col Indices of any blank divider columns
153
#' @param row.height Height of table body rows. If \code{NULL}, then the rows are stretched to fill container.
154
#' @param num.header.rows This is the number of rows from \code{x} which always be shown at the
155
#'   top of the window (only used when \code{row.height} is specified.
156
#' @param col.header.height Height of table header rows
157
#' @param col.spans List of column spans to place above the column headers:
158
#'  list(list(width=,label=,class=), list(width=,label=,class=))
159
#' @param row.spans List of row spans to place left of the row headers: list(list(height=,label=,class=),
160
#'  list(height=,label=,class=)
161
#' @param custom.css Any custom CSS to add to the \code{<style>} header of the html
162
#'  (e.g. defining nth-child logic or custom classes not included in the CSS function).
163
#'  When this is used, the resulting widget is inclosed inside an iframe to avoid
164
#'  affecting other widgets.
165
#' @param use.predefined.css Logical; whether to include CSS definitions for classes
166
#'  \code{rh, rhclean, simpleheader, simpleheaderclean, nsline, subjourneyHeader, subjourneySubHeader
167
#'  white, spacer}. This is included for backwards compatibiliy but it is probably safer
168
#'  to omit this is not used.
169
#' @param suppress.nan whether to empty cells containing only NaN
170
#' @param suppress.na whether to empty cells containing only NA
171
#' @param overflow Determines behaviour of text that is too long to fit in the table cells. By default,
172
#'  it is set to "hidden" but change to "visible" to show overflow text.
173
#' @param resizable Allow column widths to be resizeable by dragging with mouse.
174
#' @importFrom flipU ConvertCommaSeparatedStringToVector
175
#' @examples
176
#' xx <- structure(1:24, .Dim = c(4L, 6L), .Dimnames = list(c("a", "b", "c", "d"),
177
#'          c("A", "B", "C", "D", "E", "F")))
178
#' CreateCustomTable(xx, row.spans=list(list(height=2, label="AA"),
179
#'          list(height=1, label="BB"), list(height=1, label="CC")))
180
#' @export
181
CreateCustomTable = function(x,
182
                        sig.change.fills = NULL,
183
                        sig.change.arrows = NULL,
184
                        sig.leader.circles = NULL,
185
                        format.type = "Automatic",
186
                        format.show.pct.sign = TRUE,
187
                        format.decimals = 0,
188
                        suppress.nan = TRUE,
189
                        suppress.na = TRUE,
190
                        transpose = FALSE,
191
                        col.widths = if (is.null(rownames(x))) NULL else c("25%"),
192
                        row.height = NULL,
193
                        col.header.height = "35px",
194
                        num.header.rows = 0,
195
                        global.font.family = "Arial",
196
                        global.font.color = rgb(44, 44, 44, maxColorValue = 255),
197
                        font.size = 13,
198
                        font.unit = "px",
199
                        border.color = "#FFFFFF",
200
                        border.width = 1,
201
                        border.collapse = TRUE,
202
                        border.row.gap = 2,
203
                        border.column.gap = 2,
204
                        cell.prefix = "",
205
                        cell.suffix = "",
206
                        cell.fill = "#FFFFFF",
207
                        cell.border.width = border.width,
208
                        cell.border.color = border.color,
209
                        cell.align.horizontal = "center",
210
                        cell.align.vertical = "middle",
211
                        cell.font.family = global.font.family,
212
                        cell.font.color = global.font.color,
213
                        cell.font.size = font.size,
214
                        cell.font.weight = "normal",
215
                        cell.font.style = "normal",
216
                        cell.pad = 0,
217
                        show.col.headers = TRUE,
218
                        col.header.labels = NULL,
219
                        col.header.fill = "transparent",
220
                        col.header.border.width = border.width,
221
                        col.header.border.color = border.color,
222
                        col.header.align.horizontal = "center",
223
                        col.header.align.vertical = "middle",
224
                        col.header.font.family = global.font.family,
225
                        col.header.font.color = global.font.color,
226
                        col.header.font.size = font.size,
227
                        col.header.font.weight = "bold",
228
                        col.header.font.style = "normal",
229
                        col.header.pad = 0,
230
                        show.row.headers = TRUE,
231
                        row.header.labels = NULL,
232
                        row.header.fill = "transparent",
233
                        row.header.border.width = border.width,
234
                        row.header.border.color = col.header.border.color,
235
                        row.header.align.horizontal = "left",
236
                        row.header.align.vertical = "middle",
237
                        row.header.font.family = global.font.family,
238
                        row.header.font.color = global.font.color,
239
                        row.header.font.size = font.size,
240
                        row.header.font.style = "normal",
241
                        row.header.font.weight = "bold",
242
                        row.header.pad = 0,
243
                        row.span.fill = "transparent",
244
                        row.span.border.width = row.header.border.width,
245
                        row.span.border.color = row.header.border.color,
246
                        row.span.align.horizontal = "left",
247
                        row.span.align.vertical = "middle",
248
                        row.span.font.family = global.font.family,
249
                        row.span.font.color = global.font.color,
250
                        row.span.font.size = font.size,
251
                        row.span.font.style = "normal",
252
                        row.span.font.weight = "bold",
253
                        row.span.pad = 0,
254
                        col.span.fill = "transparent",
255
                        col.span.border.width = col.header.border.width,
256
                        col.span.border.color = col.header.border.color,
257
                        col.span.align.horizontal = "center",
258
                        col.span.align.vertical = "middle",
259
                        col.span.font.family = global.font.family,
260
                        col.span.font.color = global.font.color,
261
                        col.span.font.size = font.size,
262
                        col.span.font.style = "normal",
263
                        col.span.font.weight = "bold",
264
                        col.span.pad = 0,
265
                        corner = "",
266
                        corner.class = "",
267
                        corner.fill = "transparent",
268
                        corner.border.width = col.header.border.width,
269
                        corner.border.color = col.header.border.color,
270
                        corner.align.horizontal = "center",
271
                        corner.align.vertical = "middle",
272
                        corner.font.family = global.font.family,
273
                        corner.font.color = global.font.color,
274
                        corner.font.size = font.size,
275
                        corner.font.weight = "bold",
276
                        corner.font.style = "normal",
277
                        corner.pad = 0,
278
                        footer = "",
279
                        footer.height = paste0(footer.font.size + 5, font.unit),
280
                        footer.lineheight = "normal",
281
                        footer.fill = "transparent",
282
                        footer.align.horizontal = "center",
283
                        footer.align.vertical = "bottom",
284
                        footer.font.family = global.font.family,
285
                        footer.font.color = global.font.color,
286
                        footer.font.size = 8,
287
                        footer.font.weight = "normal",
288
                        footer.font.style = "normal",
289
                        col.header.classes = "",
290
                        row.header.classes = NULL,
291
                        col.classes = list(),
292
                        row.classes = list(),
293
                        banded.rows = FALSE,
294
                        banded.cols = FALSE,
295
                        banded.odd.fill = 'rgb(250,250,250)',
296
                        banded.even.fill = 'rgb(245,245,245)',
297
                        sig.fills.up = 'rgb(195,255,199)',
298
                        sig.fills.down = 'rgb(255,213,213)',
299
                        sig.fills.nothing = 'rgb(255,255,255)',
300
                        sig.arrows.up = 'rgb(0,172,62)',
301
                        sig.arrows.down = 'rgb(192,0,0)',
302
                        circle.size = 35,
303
                        spacer.row = NULL,
304
                        spacer.col = NULL,
305
                        col.spans = NULL,
306
                        row.spans = NULL,
307
                        overflow = "hidden",
308
                        custom.css = '',
309
                        use.predefined.css = TRUE,
310
                        resizable = FALSE)
311
{
312
    # Check input
313
    x <- tidyMatrixValues(x, transpose, row.header.labels, col.header.labels)
3✔
314
    stat <- attr(x, "statistic")
3✔
315
    nrows <- nrow(x)
3✔
316
    ncols <- ncol(x)
3✔
317
    if (is.null(colnames(x)))
3✔
318
        show.col.headers <- FALSE
3✔
319
    if (is.null(rownames(x)))
3✔
320
        show.row.headers <- FALSE
×
321
    if (is.null(row.height)) # all rows are stretched to fit height of window - no scrolling
3✔
322
        num.header.rows <- 0
3✔
323
    if (num.header.rows >= nrows)
3✔
324
        num.header.rows <- nrows - 1
×
325

326
    # Format table contents
327
    if (isTRUE(grepl("%", stat)))
3✔
328
        x <- x/100
3✔
329
    if (format.type == "Automatic" && any(grepl("%)?$", stat)))
3✔
330
        format.type <- "Percentage"
3✔
331

332
    content <- if (!is.numeric(x))                   x
3✔
333
               else if (format.type == "Percentage") FormatAsPercent(x, decimals = format.decimals, show.sign = format.show.pct.sign)
3✔
334
               else                                  FormatAsReal(x, decimals = format.decimals)
3✔
335
    content <- matrix(paste0(cell.prefix, content, cell.suffix), nrows, ncols)
3✔
336
    if (suppress.nan)
3✔
337
        content[which(is.nan(x))] <- "<br>"
3✔
338
    if (suppress.na)
3✔
339
        content[which(is.na(x) & !is.nan(x))] <- "<br>"
3✔
340
    ind.empty <- which(!nzchar(content))
3✔
341
    if (any(ind.empty))
3✔
342
        content[ind.empty] <- "<br>"
×
343
    if (is.character(x))
3✔
344
    {
345
        # check image tags and remove and warn for invalid urls
346
        # wrap images in a div to preserve alignment
347
        ind <- grep("<img", x, fixed = TRUE)
×
348
        for (ii in ind)
×
349
            content[ii] <- checkImageTag(content[ii])
×
350
    }
351

352
    # Significance testing arrows/circles/fills
353
    if (!is.null(sig.change.arrows))
3✔
354
    {
355
        content[which(sig.change.arrows ==  1)] <- paste0(content[which(sig.change.arrows ==  1)],
×
356
                    "<font style='color:", sig.arrows.up, "'>&#x2191;</font>")
×
357
        content[which(sig.change.arrows == -1)] <- paste0(content[which(sig.change.arrows == -1)],
×
358
                    "<font style='color:", sig.arrows.down, "'>&#x2193;</font>")
×
359
    }
360
    circle.css <- ""
3✔
361
    if (!is.null(sig.leader.circles))
3✔
362
    {
363
        metric.leader.border = '2px solid rgb(120,120,120)'
×
364
        metric.tie.border = '1px solid rgb(150,150,150)'
×
365
        circle.fmt <- paste0('display: inline-block; line-height:', circle.size, 'px; border-radius:',
×
366
                            circle.size, 'px; height: ', circle.size, 'px; width:', circle.size, 'px;')
×
367

368
        # Unfilled leader circles
369
        circle.css <- paste0('.circle2 {  border: ', metric.leader.border, ';', circle.fmt, '}\n',
×
370
                             '.circle1 {  border: ', metric.tie.border, ';', circle.fmt, '}\n',
×
371
                             '.circle0 {  border: 0px solid rgb(0,0,0);', circle.fmt, '}\n')
×
372

373
        # CSS generation for filled leader circles
374
        circle.types = paste0(rep(c(2, 1, 0), 3), rep(c(1,0,-1), each=3))
×
375
        circle.colors = rep(c(sig.fills.up, sig.fills.nothing, sig.fills.down), each=3)
×
376
        circle.border = rep(c(metric.leader.border, metric.tie.border, '0px solid rgb(0,0,0)'), 3)
×
377
        filled.circle.styles = paste0('.circle', circle.types,' { border: ', circle.border,';
×
378
                               background-color:',circle.colors,';', circle.fmt, '}', collapse='  ')
×
379
        circle.css <- paste0(circle.css, filled.circle.styles)
×
380
        sig.leader.circles[!which(sig.leader.circles == 1 | sig.leader.circles == 2)] <- 0
×
381
        content <- matrix(sprintf('<div class="circle%s">%s</div>', sig.leader.circles, content), nrows, ncols)
×
382
    }
383
    if (!banded.rows && !banded.cols)
3✔
384
        cell.fill <- matrix(paste("background:", cell.fill, ";"), nrows, ncols)
3✔
385
    else
386
        cell.fill <- matrix("", nrows, ncols)
×
387

388
    # Significance coloring takes precedence over cell.fill or class definitions
389
    # At the moment only sig.change.fills affects cell.inline.style
390
    cell.inline.style <- matrix("", nrows, ncols)
3✔
391
    if (!is.null(sig.change.fills))
3✔
392
    {
393
        cell.inline.style[which(sig.change.fills ==  1)] <- paste0(" style='background:", sig.fills.up, "'")
×
394
        cell.inline.style[which(sig.change.fills == -1)] <- paste0(" style='background:", sig.fills.down, "'")
×
395
    }
396
    if (show.row.headers)
3✔
397
        cell.inline.style <- cbind("", cell.inline.style)
3✔
398
    if (show.col.headers)
3✔
399
        cell.inline.styl <- rbind("", cell.inline.style)
×
400
    override.borders <- grepl("border", custom.css, fixed = TRUE) && grepl("nth-child", custom.css, fixed = TRUE)
3✔
401

402
    # Setup html file
403
    tfile <- createTempFile()
3✔
404
    cata <- createCata(tfile)
3✔
405
    # Create unique class name for parent div container
406
    container.name <- paste0("custom-table-container-", generateRandomString())
3✔
407
    container.selector.name <- paste0(".", container.name)
3✔
408
    cata("<style>\n")
3✔
409
    if (is.numeric(border.row.gap))
3✔
410
        border.row.gap <- paste0(border.row.gap, "px")
3✔
411
    if (is.numeric(border.column.gap))
3✔
412
        border.column.gap <- paste0(border.column.gap, "px")
3✔
413
    cata(container.selector.name, "{ table-layout: fixed; border-collapse: ",
3✔
414
         if (border.collapse) "collapse; " else "separate; ",
3✔
415
         "border-spacing: ", border.column.gap, border.row.gap, ";",
3✔
416
         "position: relative; width: 100%; ",
3✔
417
         "font-family: ", global.font.family, "; color: ", global.font.color, "; ",
3✔
418
         "cellspacing:'0'; cellpadding:'0'; ",
3✔
419
         "white-space: normal; line-height: normal; }\n")
3✔
420

421
    # Sticky only applies to <th> elements inside <thead> - i.e. column headers not row headers
422
    # Both the height and position are defined inside cell.styles/row.header.styles
423
    # to allow for multiple sticky rows
424
    cata(container.selector.name, "th { position: -webkit-sticky; position: sticky; top: 0px; overflow: ", overflow, "; ")
3✔
425
    if (resizable)
3✔
426
        cata("resize: both; ")
×
427
    cata("}\n")
3✔
428
    cata(container.selector.name, "td { overflow: ", overflow, "; ")
3✔
429
    if (sum(nchar(row.height)) > 0)
3✔
430
        cata("height:", row.height, "; ")
×
431
    cata("}\n")
3✔
432

433
    # supply units if none given (default px); however other units such as pt, em still valid
434
    if (!show.col.headers)
3✔
435
        col.header.height <- "0px"
3✔
436
    if (length(col.header.height) > 0 && !is.na(suppressWarnings(as.numeric(col.header.height))))
3✔
437
        col.header.height <- paste0(col.header.height, "px")
×
438

439
    # initialize positions for sticky header with scrollable table
440
    top.position <- NULL
3✔
441
    if (!is.null(row.height) && num.header.rows > 0)
3✔
442
    {
443
        top.position <- sprintf("%s + %.0fpx", col.header.height, col.header.border.width)
×
444
        if (num.header.rows > 1)
×
445
        {
446
            join.str <- sprintf(" + %.0fpx + ", cell.border.width)
×
447
            hh <- c(top.position, rep(row.height, num.header.rows - 1))
×
448
            top.position <- paste0("calc(", sapply(1:num.header.rows,
×
449
                function(i) paste(rep(hh, length = i), collapse = join.str)), ")")
×
450
        }
451
    }
452

453
    # Set up styles for each cell
454
    ncells <- nrows * ncols
3✔
455
    cell.styles <- addCSSclass(cata, "celldefault",
3✔
456
        rep(paste0(cell.fill, "; ", if (sum(nchar(row.height)) > 0) paste0("height: ", row.height, "; ") else "",
3✔
457
        if (override.borders) "" else paste0("border: ", cell.border.width, "px solid ", cell.border.color),
3✔
458
        ";", getPaddingCSS(tolower(cell.align.horizontal), cell.pad),
3✔
459
        "; font-size: ", cell.font.size, font.unit, "; font-style: ", cell.font.style,
3✔
460
        "; font-weight: ", cell.font.weight, "; font-family: ", cell.font.family,
3✔
461
        "; color:", cell.font.color, "; text-align: ", cell.align.horizontal,
3✔
462
        "; vertical-align: ", cell.align.vertical, ";"), length=ncells), nrows, ncols,
3✔
463
        position = top.position, parent.stem = container.name)
3✔
464

465
    # Row/column classes overrides other attributes (except coloring based on significance)
466
    for (cc in row.classes)
3✔
467
        cell.styles[cc[[1]],] = paste(cell.styles[cc[[1]],], cc[[2]])
×
468

469
    for (cc in col.classes)
3✔
470
        cell.styles[,cc[[1]]] <- paste(cell.styles[,cc[[1]]], cc[[2]])
×
471

472
    # Row headers
473
    row.header.class.css <- NULL
3✔
474
    if (show.row.headers)
3✔
475
    {
476
        row.header.styles <- addCSSclass(cata, "rowheaderdefault", paste0("background: ", row.header.fill,
3✔
477
            if (override.borders) "" else paste0("; border: ", row.header.border.width, "px solid ", row.header.border.color),
3✔
478
            ";", getPaddingCSS(tolower(row.header.align.horizontal), row.header.pad),
3✔
479
            "; font-size: ", row.header.font.size, font.unit, "; font-style: ", row.header.font.style,
3✔
480
            "; font-weight: ", row.header.font.weight, "; font-family: ", row.header.font.family,
3✔
481
            "; color:", row.header.font.color, "; text-align: ", row.header.align.horizontal,
3✔
482
            "; vertical-align: ", row.header.align.vertical, ";"), nrows, position = top.position,
3✔
483
            parent.stem = container.name)
3✔
484
        if (!is.null(row.header.classes))
3✔
485
            row.header.styles <- paste(row.header.styles, row.header.classes)
×
486
        content <- cbind(rownames(x), content)
3✔
487
        cell.styles <- cbind(row.header.styles, cell.styles)
3✔
488
    } else { corner = NULL; corner.class = NULL; }
×
489

490
    # Row spans
491
    row.span.class.css <- NULL
3✔
492
    if (!is.null(row.spans))
3✔
493
    {
494
        if (!is.null(top.position))
×
495
        {
496
            j <- 1
×
497
            rm.index <- c()
×
498
            for (i in 1:length(row.spans))
×
499
            {
500
                offset <- row.spans[[i]]$height - 1
×
501
                if (offset >= 1)
×
502
                    rm.index <- c(rm.index, j + (1:offset))
×
503
                j <- j + offset + 1
×
504
            }
505
            top.position <- top.position[-rm.index]
×
506

507
        }
508

509
        row.span.lengths <- sapply(row.spans, function(x) x[['height']])
×
510
        row.span.styles <- addCSSclass(cata, "rowspandefault", paste0("background: ", row.span.fill,
×
511
            if (override.borders) "" else paste0("; border: ", row.span.border.width,
×
512
            "px solid ", row.span.border.color),
×
513
            ";", getPaddingCSS(tolower(row.span.align.horizontal), row.span.pad),
×
514
            "; font-size: ", row.span.font.size, font.unit, "; font-style: ", row.span.font.style,
×
515
            "; font-weight: ", row.span.font.weight, "; font-family: ", row.span.font.family,
×
516
            "; color:", row.span.font.color, "; text-align: ", row.span.align.horizontal,
×
517
            "; vertical-align: ", row.span.align.vertical, ";"), nrows, position = top.position,
×
518
            parent.stem = container.name)
×
519
        for (i in 1:length(row.spans))
×
520
            if (!is.null(row.spans[[i]]$class))
×
521
                row.span.styles[i] <- paste(row.span.styles[i], row.spans[[i]]$class)
×
522

523
        row.spans <- sapply(1:length(row.spans), function(i) sprintf('<td rowspan="%s" class="%s">%s</td>',
×
524
                            row.spans[[i]][['height']], row.span.styles[i], row.spans[[i]][['label']]))
×
525
        row.span.html <- rep("", nrows)
×
526
        j <- 1
×
527
        for (i in 1:length(row.spans))
×
528
        {
529
            row.span.html[j] <- row.spans[i]
×
530
            j <- j + row.span.lengths[i]
×
531
        }
532

533
    } else
534
        row.span.html <- ''
3✔
535

536
    # Column headers
537
    col.header.class.css <- NULL
3✔
538
    if (show.col.headers)
3✔
539
    {
540
        col.header.styles <- addCSSclass(cata, "colheaderdefault", paste0("background: ", col.header.fill,
×
541
            "; ", if (sum(nchar(col.header.height)) > 0) paste0("height: ", col.header.height, "; ") else "",
×
542
            if (override.borders) "" else paste0("; border: ", col.header.border.width,
×
543
            "px solid ", col.header.border.color),
×
544
            ";", getPaddingCSS(tolower(col.header.align.horizontal), col.header.pad),
×
545
            "; font-size: ", col.header.font.size, font.unit, "; font-style: ", col.header.font.style,
×
546
            "; font-weight: ", col.header.font.weight, "; font-family: ", col.header.font.family,
×
547
            "; color:", col.header.font.color, "; text-align: ", col.header.align.horizontal,
×
548
            "; vertical-align: ", col.header.align.vertical, ";"), ncols,
×
549
            parent.stem = container.name)
×
550
        if (!is.null(col.header.classes))
×
551
            col.header.styles <- paste(col.header.styles, col.header.classes)
×
552
        col.labels <- colnames(x)
×
553

554
        if (show.row.headers)
×
555
        {
556
            corner.styles <- addCSSclass(cata, "cornerdefault",
×
557
                paste0("background: ", corner.fill,
×
558
                if (override.borders) "" else paste0("; border: ", corner.border.width, "px solid ", corner.border.color),
×
559
                ";", getPaddingCSS(tolower(corner.align.horizontal), corner.pad),
×
560
                "; font-size: ", corner.font.size, font.unit, "; font-style: ", corner.font.style,
×
561
                "; font-weight: ", corner.font.weight, "; font-family: ", corner.font.family,
×
562
                "; color:", corner.font.color, "; text-align: ", corner.align.horizontal,
×
563
                "; vertical-align: ", corner.align.vertical, ";"),
×
564
                parent.stem = container.name)
×
565
            if (sum(nchar(corner.class)) > 0)
×
566
                corner.styles <- paste(corner.styles, corner.class)
×
567
            col.header.styles <- c(corner.styles[1], col.header.styles)
×
568
            col.labels <- c(corner, col.labels)
×
569
        }
570

571
        if (!is.null(row.spans))
×
572
        {
573
            col.header.styles <- c(corner.styles[1], col.header.styles)
×
574
            col.labels <- c("", col.labels)
×
575
        }
576
        if (!is.null(spacer.col))
×
577
            col.header.styles[spacer.col] <- "spacer"
×
578
        if (!is.null(spacer.row))
×
579
            spacer.row <-  spacer.row + 1
×
580
        header.html <- paste0(c('<tr>', sprintf('<th class="%s">%s</th>', col.header.styles, col.labels),
×
581
                                '</tr>'), collapse='')
×
582
    } else
583
        header.html <- ''
3✔
584

585
    # Column spans
586
    if (!is.null(col.spans))
3✔
587
    {
588
        col.span.lengths <- sapply(col.spans, function(x) x[['width']])
×
589
        col.span.styles <- addCSSclass(cata, "colspandefault", paste0("background: ", col.span.fill,
×
590
            if (override.borders) "" else paste0("; border: ", col.span.border.width,
×
591
            "px solid ", col.span.border.color),
×
592
            ";", getPaddingCSS(tolower(col.span.align.horizontal), col.span.pad),
×
593
            "; font-size: ", col.span.font.size, font.unit, "; font-style: ", col.span.font.style,
×
594
            "; font-weight: ", col.span.font.weight, "; font-family: ", col.span.font.family,
×
595
            "; color:", col.span.font.color, "; text-align: ", col.span.align.horizontal,
×
596
            "; vertical-align: ", col.span.align.vertical, ";"), ncols, position = top.position,
×
597
            parent.stem = container.name)
×
598
        for (i in 1:length(col.spans))
×
599
            if (!is.null(col.spans[[i]]$class))
×
600
                col.span.styles[i] <- paste(col.span.styles[i], col.spans[[i]]$class)
×
601

602
        col.spans <- sapply(1:length(col.spans), function(i) sprintf('<th colspan="%s" class="%s">%s</th>',
×
603
                            col.spans[[i]][['width']], col.span.styles[i], col.spans[[i]][['label']]))
×
604
        col.span.html <- paste0('<tr>', paste0(col.spans, collapse=''),'</tr>')
×
605
    } else
606
        col.span.html <- ''
3✔
607

608

609
    # Row/Column banding
610
    if (banded.rows)
3✔
611
        cata(container.selector.name, 'tbody tr:nth-child(odd){background-color:', banded.odd.fill,
×
612
                ';} tr:nth-child(even){background-color:', banded.even.fill, ';}')
×
613
    if (banded.cols)
3✔
614
        cata(container.selector.name, 'tbody td:nth-child(2n+3){background-color:', banded.odd.fill,
×
615
             ';} td:nth-child(even){background-color:', banded.even.fill, ';}')
×
616

617
    # Other CSS
618
    if (use.predefined.css)
3✔
619
        cata("\n", predefinedCSS(container.selector.name), "\n")
3✔
620
    cata("\n", circle.css, "\n")
3✔
621
    cata("\n", custom.css, "\n")
3✔
622
    cata("</style>\n\n")
3✔
623

624
    # Wrap table inside a div to allow scrolling (overflow=auto)
625
    # when the number of rows is large and row-height is fixed.
626
    # But for automatically sized rows we remove div firefox does not like nested tables
627
    if (!is.null(row.height))
3✔
628
        cata("<div style='overflow-y:auto; height: 100%;'>")
×
629
    table.height <- if (sum(nchar(row.height)) != 0) ""
3✔
630
                    else paste0("; height:calc(100% - ", rev(cell.border.width)[1], "px)")
3✔
631
    cata(sprintf("<table class = '%s' style = 'width:calc(%s - %dpx)%s'>\n",
3✔
632
        container.name, "100%", max(0, max(cell.border.width)), table.height))
3✔
633
    if (sum(nchar(col.widths)) > 0)
3✔
634
    {
635
        col.widths <- ConvertCommaSeparatedStringToVector(col.widths)
3✔
636
        cata(paste(paste("<col width='", col.widths, "'>\n"), collapse = ""))
3✔
637
    }
638
    cata('<thead>', col.span.html, header.html)
3✔
639

640
    # Build table
641
    cell.html <- matrix(sprintf('<td class="%s"%s>%s</td>', cell.styles, cell.inline.style, content),
3✔
642
                        nrow = nrows)
3✔
643
    cell.html <- cbind(row.span.html, cell.html)
3✔
644

645
    if (num.header.rows > 0) # additional rows that float at the top
3✔
646
    {
647
        extra.header.html <- paste0(sprintf('<tr>%s</tr>\n',
×
648
                                apply(cell.html[1:num.header.rows,,drop = FALSE], 1,
×
649
                                paste0, collapse = '')), collapse='')
×
650
        extra.header.html <- gsub("<td ", "<th ", extra.header.html, fixed = TRUE)
×
651
        extra.header.html <- gsub("</td>", "</th>", extra.header.html, fixed = TRUE)
×
652
        cata(extra.header.html)
×
653
        cell.html <- cell.html[-(1:num.header.rows),,drop = FALSE]
×
654
    }
655
    cata('</thead>')
3✔
656
    body.html <- paste0(sprintf('<tr>%s</tr>\n',
3✔
657
                    apply(cell.html, 1, paste0, collapse = '')), collapse='')
3✔
658
    cata(body.html)
3✔
659

660
    # Optional footer
661
    if (nchar(footer) > 0)
3✔
662
    {
663
        tot.columns <- (ncols + show.row.headers + !is.null(row.spans))
×
664
        cata(paste0('<tr><th colspan="', tot.columns, '" style="',
×
665
            'height:', footer.height,
×
666
            '; line-height:', footer.lineheight,
×
667
            '; background-color:', footer.fill,
×
668
            '; font-family:', footer.font.family,
×
669
            '; color:', footer.font.color,
×
670
            '; font-size:', footer.font.size, font.unit,
×
671
            '; font-style:', footer.font.style,
×
672
            '; font-weight:', footer.font.weight,
×
673
            '; text-align:', footer.align.horizontal,
×
674
            '; vertical-align:', footer.align.vertical,
×
675
            '">', footer, '</th></tr>\n'))
×
676
    }
677
    cata("</table>\n")
3✔
678
    if (!is.null(row.height))
3✔
679
        cata("</div>\n")
×
680
    html <- paste(readLines(tfile), collapse = "\n")
3✔
681
    if (!any(nzchar(custom.css)))
3✔
682
        out <- boxIframeless(html, text.as.html = TRUE,
2✔
683
                         font.family = "Circular, Arial, sans-serif",
2✔
684
                         font.size = 8)
2✔
685
    else
686
        out <- Box(html, text.as.html = TRUE,
1✔
687
                         font.family = "Circular, Arial, sans-serif",
1✔
688
                         font.size = 8)
1✔
689
    class(out) <- c(class(out), "visualization-selector")
3✔
690
    attr(out, "ChartData") <- prepareForExport(x, format.type)
3✔
691
    return(out)
3✔
692
}
693

694
prepareForExport <- function(x, format.type)
695
{
696
    if (format.type == "Percentage")
3✔
697
    {
698
        x <- x * 100
3✔
699
        attr(x, "statistic") <- "%"
3✔
700
        return(x)
3✔
701
    }
702
    else if (is.numeric(x))
×
703
        return(x)
×
704
    else
705
        return(clean_html(x))
×
706

707
}
708

709

710
#' @importFrom xml2 xml_text read_xml
711
clean_html <- function(x)
712
{
713
    if (!is.character(x))
×
714
        return(x)
×
715

716
    .strip_html <- function(x) if (!nzchar(trimws(x))) x else xml_text(read_xml(charToRaw(x), as_html = TRUE))
×
717
    if (is.matrix(x))
×
718
        return(apply(x, c(1, 2), .strip_html))
×
719
    else
720
        return(sapply(x, .strip_html))
×
721
}
722

723
tidyMatrixValues <- function(x, transpose, row.header.labels, col.header.labels)
724
{
725
    stat <- attr(x, "statistic")
3✔
726
    ndim <- length(dim(x))
3✔
727

728
    # extract primary statistic from higher dimensions if x is a QTable
729
    if (is.null(stat) && all(c("questions", "name") %in% names(attributes)))
3✔
730
        stat <- dimnames(x)[[ndim]][1]
×
731
    if (ndim == 3)
3✔
732
        x <- x[,,1]
×
733
    if (ndim == 4)
3✔
734
        x <- x[,,1,1]
×
735

736
    x <- as.matrix(x)
3✔
737
    if (transpose)
3✔
738
        x <- t(x)
×
739

740
    if (length(row.header.labels) < nrow(x))
3✔
741
        row.header.labels <- ConvertCommaSeparatedStringToVector(row.header.labels)
3✔
742
    if (sum(nchar(row.header.labels)) > 0)
3✔
743
    {
744
        new.labels <- paste0(rownames(x), rep("", nrow(x))) # in case rownames is NULL
×
745
        tmp.len <- min(length(row.header.labels), length(new.labels))
×
746
        new.labels[1:tmp.len] <- row.header.labels[1:tmp.len]
×
747
        rownames(x) <- new.labels
×
748
    }
749
    if (length(col.header.labels) < ncol(x))
3✔
750
        col.header.labels <- ConvertCommaSeparatedStringToVector(col.header.labels)
3✔
751
    if (sum(nchar(col.header.labels)) > 0)
3✔
752
    {
753
        new.labels <- paste0(colnames(x), rep("", ncol(x)))
×
754
        tmp.len <- min(length(col.header.labels), length(new.labels))
×
755
        new.labels[1:tmp.len] <- col.header.labels[1:tmp.len]
×
756
        colnames(x) <- new.labels
×
757
    }
758
    attr(x, "statistic") <- stat
3✔
759
    return(x)
3✔
760
}
761

762
getPaddingCSS <- function(align, pad)
763
{
764
    if (length(align) < length(pad))
6✔
765
        align <- rep(align, length = length(pad))
×
766
    ind <- which(align %in% c("left", "right"))
6✔
767

768
    # Center alignment does not use padding
769
    if (length(ind) == 0)
6✔
770
        return("")
3✔
771

772
    res <- rep("", length = length(align))
3✔
773
    res[ind] <- paste0("padding-", align[ind], ":", pad, "px")
3✔
774
    return(res)
3✔
775
}
776

777
addCSSclass <- function(cata, class.stem, class.css, nrow = 1, ncol = 1, position = NULL, parent.stem = NULL)
778
{
779
    if (length(class.css) < 1)
6✔
780
        return(NULL)
×
781
    if (!is.null(position))
6✔
782
    {
783
        class.css <- matrix(class.css, nrow, ncol)
×
784
        for (i in 1:length(position))
×
785
            class.css[i,] <- paste0("position: sticky; top: ", position[i], "; ", class.css[i,])
×
786
    }
787
    n <- length(class.css)
6✔
788

789
    # The number of classes created is the length of class.css
790
    # recycling occurs if needed inside CreateCustomTable
791
    class.names <- paste0(class.stem, 1:n)
6✔
792
    css.selectors <- if (!is.null(parent.stem)) paste0(".", parent.stem, " .", class.names) else paste0(".", class.names)
6✔
793
    tmp.css <- paste0(css.selectors, "{ ", class.css, " }")
6✔
794

795
    # Add class definition to CSS file
796
    cata(paste(tmp.css, collapse = "\n"))
6✔
797

798
    # Return class names - otherwise the main function does not know
799
    # how many classes were created
800
    if (ncol == 1)
6✔
801
        return(rep(class.names, length = nrow))
6✔
802
    else
803
        return(matrix(class.names, nrow, ncol))
×
804
}
805

806

807

808
predefinedCSS <- function(container.selector.name)
809
{
810
    do.call(sprintf, as.list(c("
3✔
811
    %s .rh {
3✔
812
        text-align:left;
3✔
813
        font-weight: bold;
3✔
814
        }
815
    %s .rhclean {
3✔
816
        text-align:left;
3✔
817
        }
818
    %s .simpleheader {
3✔
819
        background: #DCDCDC;
3✔
820
        font-weight: bold;
3✔
821
        }
822
    %s .simpleheaderclean {
3✔
823
        background: #DCDCDC;
3✔
824
        font-weight: normal;
3✔
825
        }
826
    %s .nsline {
3✔
827
        font-style: italic;
3✔
828
        font-size: 9pt;
3✔
829
        white-space:nowrap;
3✔
830
        display: block;}
3✔
831
    %s .subjourneyHeader{
3✔
832
        font-style: bold;
3✔
833
        border-bottom: 1px black solid;
3✔
834
        }
835
    %s .subjourneySubHeader{
3✔
836
        font-style: bold;
3✔
837
        border-top: 1px grey solid;
3✔
838
        border-bottom: 1px grey solid;
3✔
839
        }
840
    %s .white {background-color:white;}
3✔
841
    %s .spacer {background: white;color: white;border: none;overflow:hidden;}
3✔
842
", rep(container.selector.name, 9))))
3✔
843

844
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc