print colored correlation values from print.summary.lm in a shiny app

Hello,

I'm trying to customize the summary output of a lm() fit on my shiny app to add color to the correlation values shown on the UI.
My guess is that this should be done with html/css tags.
So I have taken the print.summary.lm function to add in some html to the correlation matrix near the end when printing correlations.
I have renamed this function as customPrint in the script below.

My problem is that I'm not able to get the html tag to work.
I'm using renderPrint, but also tested with renderText and renderUI.
I have also tried with the HTML() function around the modified correlation matrix.

Any help is appreciated, even an informed "this is not possible".

library(shiny)

options(shiny.autoreload = T)

ui <- fluidPage(
  # textOutput("summary")
  # htmlOutput("summary")
  verbatimTextOutput(outputId = "summary", placeholder = F)
)

server <- function(input, output) {
  
  # output$summary <- renderText({
  # output$summary <- renderUI({
  output$summary <- renderPrint({
    n <- 1000
    x <- seq(n)
    y <- x + rnorm(n = n, mean = 2, sd = 2)
    fit <- lm(y ~ x)
    customPrint(summary.lm(object = fit, correlation = T))
  })
  
  customPrint <- function(x, digits = max(3L, getOption("digits") - 3L), symbolic.cor = x$symbolic.cor, signif.stars = getOption("show.signif.stars"), ...) {
    cat("\nCall:", "\n", paste(deparse(x$call), sep = "\n", collapse = "\n"), "\n\n", sep = "")
    resid <- x$residuals
    df <- x$df
    rdf <- df[2L]
    cat(if (!is.null(x$weights) && diff(range(x$weights))) "Weighted ", "Residuals:\n", sep = "")
    if (rdf > 5L) {
      nam <- c("Min", "1Q", "Median", "3Q", "Max")
      rq <- if (length(dim(resid)) == 2L)
        structure(apply(t(resid), 1L, quantile), dimnames = list(nam,
                                                                 dimnames(resid)[[2L]]))
      else {
        zz <- zapsmall(quantile(resid), digits + 1L)
        structure(zz, names = nam)
      }
      print(rq, digits = digits, ...)
    } else if (rdf > 0L) {
      print(resid, digits = digits, ...)
    } else {
      cat("ALL", df[1L], "residuals are 0: no residual degrees of freedom!")
      cat("\n")
    }
    if (length(x$aliased) == 0L) {
      cat("\nNo Coefficients\n")
    } else {
      if (nsingular <- df[3L] - df[1L]) {
        cat("\nCoefficients: (", nsingular, " not defined because of singularities)\n", sep = "")
      } else { 
        cat("\n")
        cat("Coefficients:\n")
      }
      coefs <- x$coefficients
      if (!is.null(aliased <- x$aliased) && any(aliased)) {
        cn <- names(aliased)
        coefs <- matrix(NA, length(aliased), 4, dimnames = list(cn, colnames(coefs)))
        coefs[!aliased, ] <- x$coefficients
      }
      printCoefmat(coefs, digits = digits, signif.stars = signif.stars, signif.legend = T, na.print = "NA", eps.Pvalue = .Machine$double.eps, ...)
    }
    cat("\nResidual standard error:", format(signif(x$sigma, digits)), "on", rdf, "degrees of freedom")
    cat("\n")
    if (nzchar(mess <- naprint(x$na.action))) {
      cat("  (", mess, ")\n", sep = "")
    }
    if (!is.null(x$fstatistic)) {
      cat("Multiple R-squared: ", formatC(x$r.squared, digits = digits))
      cat(",\tAdjusted R-squared: ", formatC(x$adj.r.squared, digits = digits),
          "\nF-statistic:", formatC(x$fstatistic[1L], digits = digits),
          "on", x$fstatistic[2L],
          "and", x$fstatistic[3L],
          "DF,  p-value:", format.pval(pf(x$fstatistic[1L], x$fstatistic[2L], x$fstatistic[3L], lower.tail = FALSE), digits = digits, .Machine$double.eps))
      cat("\n")
    }
    correl <- x$correlation
    if (!is.null(correl)) {
      p <- NCOL(correl)
      if (p > 1L) {
        cat("\nCorrelation of Coefficients:\n")
        if (is.logical(symbolic.cor) && symbolic.cor) {
          print(symnum(correl, abbr.colnames = NULL))
        } else {
          correl <- format(round(correl, 2), nsmall = 2, digits = digits)
          # Adding correlation colors here
          correlColor <- apply(correl, c(1, 2), function(x) {
            if (abs(as.numeric(x)) > 0.9) {
              sprintf('<span style="color: red;">%s</span>', x)
            } else {
              sprintf('<span style="color: green;">%s</span>', x)
            }
          })
          correl[!lower.tri(correl)] <- ""
          # print(HTML(correlColor[-1, -p, drop = F]), quote = F)
          print(correlColor[-1, -p, drop = F], quote = F)
        }
      }
    }
    cat("\n")
    invisible(x)
  }
  
}

shinyApp(ui = ui, server = server)
1 Like

I may have identified my problem coming from the verbatimTextOutput()shiny function.

This function creates an html <pre> tag in a way that additional html tags inside are not interpreted by the browser.

I have taken the html <pre> code generated by shiny and inserted it directly on the UI with the HTML() function.
In that case it does what I want, but not in the case the <pre> tag is created by the verbatimTextOutput() function.
You can see this in the updated code below.

So the thing is how to pass the output of summary() inside a <pre> to HTML()?

library(shiny)

options(shiny.autoreload = T, width = 280)

ui <- fluidPage(
  # textOutput("summary")
  # htmlOutput("summary")
  verbatimTextOutput(outputId = "summary", placeholder = F),
  HTML(
    "<pre class='shiny-text-output noplaceholder shiny-bound-output' aria-live='polite'>
    
Call:
lm(formula = y ~ x + x2)
    
Residuals:
  Min      1Q  Median      3Q     Max 
-8.0397 -1.3573 -0.0167  1.4897  5.7287 
    
Coefficients:
              Estimate Std. Error  t value Pr(>|t|)    
(Intercept)  1.658e+00  1.987e-01    8.346 2.34e-16 ***
x            1.001e+00  9.169e-04 1091.773  < 2e-16 ***
x2          -4.912e-07  8.869e-07   -0.554     0.58    
---
Signif. codes:  0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1
    
Residual standard error: 2.091 on 997 degrees of freedom
Multiple R-squared:  0.9999,	Adjusted R-squared:  0.9999 
F-statistic: 9.544e+06 on 2 and 997 DF,  p-value: < 2.2e-16
  
Correlation of Coefficients:
  (Intercept) x                                     
x  <span style='color: black;'>-0.87</span>                                       
x2 <span style='color: black;'> 0.75</span> <span style='color: red;'>-0.97</span>
</pre>")
)

server <- function(input, output) {
  
  cat("START\n")
  # output$summary <- renderText({
  # output$summary <- renderUI({
  output$summary <- renderPrint({
    n <- 1000
    x <- seq(n)
    x2 <- x^2
    y <- x + rnorm(n = n, mean = 2, sd = 2)
    fit <- lm(y ~ x + x2)
    customPrint(summary.lm(object = fit, correlation = T))
  })
  
  customPrint <- function(x, digits = max(3L, getOption("digits") - 3L), symbolic.cor = x$symbolic.cor, signif.stars = getOption("show.signif.stars"), ...) {
    cat("\nCall:", "\n", paste(deparse(x$call), sep = "\n", collapse = "\n"), "\n\n", sep = "")
    resid <- x$residuals
    df <- x$df
    rdf <- df[2L]
    cat(if (!is.null(x$weights) && diff(range(x$weights))) "Weighted ", "Residuals:\n", sep = "")
    if (rdf > 5L) {
      nam <- c("Min", "1Q", "Median", "3Q", "Max")
      rq <- if (length(dim(resid)) == 2L)
        structure(apply(t(resid), 1L, quantile), dimnames = list(nam, dimnames(resid)[[2L]]))
      else {
        zz <- zapsmall(quantile(resid), digits + 1L)
        structure(zz, names = nam)
      }
      print(rq, digits = digits, ...)
    } else if (rdf > 0L) {
      print(resid, digits = digits, ...)
    } else {
      cat("ALL", df[1L], "residuals are 0: no residual degrees of freedom!")
      cat("\n")
    }
    if (length(x$aliased) == 0L) {
      cat("\nNo Coefficients\n")
    } else {
      if (nsingular <- df[3L] - df[1L]) {
        cat("\nCoefficients: (", nsingular, " not defined because of singularities)\n", sep = "")
      } else { 
        cat("\n")
        cat("Coefficients:\n")
      }
      coefs <- x$coefficients
      if (!is.null(aliased <- x$aliased) && any(aliased)) {
        cn <- names(aliased)
        coefs <- matrix(NA, length(aliased), 4, dimnames = list(cn, colnames(coefs)))
        coefs[!aliased, ] <- x$coefficients
      }
      printCoefmat(coefs, digits = digits, signif.stars = signif.stars, signif.legend = T, na.print = "NA", eps.Pvalue = .Machine$double.eps, ...)
    }
    cat("\nResidual standard error:", format(signif(x$sigma, digits)), "on", rdf, "degrees of freedom")
    cat("\n")
    if (nzchar(mess <- naprint(x$na.action))) {
      cat("  (", mess, ")\n", sep = "")
    }
    if (!is.null(x$fstatistic)) {
      cat("Multiple R-squared: ", formatC(x$r.squared, digits = digits))
      cat(",\tAdjusted R-squared: ", formatC(x$adj.r.squared, digits = digits),
          "\nF-statistic:", formatC(x$fstatistic[1L], digits = digits),
          "on", x$fstatistic[2L],
          "and", x$fstatistic[3L],
          "DF,  p-value:", format.pval(pf(x$fstatistic[1L], x$fstatistic[2L], x$fstatistic[3L], lower.tail = FALSE), digits = digits, .Machine$double.eps))
      cat("\n")
    }
    correl <- x$correlation
    if (!is.null(correl)) {
      p <- NCOL(correl)
      if (p > 1L) {
        if (is.logical(symbolic.cor) && symbolic.cor) {
          print(symnum(correl, abbr.colnames = NULL))
        } else {
          correl <- format(round(correl, 2), nsmall = 2, digits = digits)

          ### Adding correlation colors here with HTLM (escaped or not, it changes nothing)
          correlColor <- apply(correl, c(1, 2), function(x) {
            if (abs(as.numeric(x)) > 0.9) {
              paste0("&lt;span style=&#39;color: red;&#39;&gt;",x,"&lt;/span&gt;")
            } else {
              span(x, style = 'color: black;')
            }
          })
          ###

          correlColor[!lower.tri(correlColor)] <- ""
          cat("\nCorrelation of Coefficients:\n")
          print(correlColor[-1, -p, drop = T], quote = F)
        }
      }
    }
    cat("\n")
    invisible(x)
  }
  
}

shinyApp(ui = ui, server = server)

I'd suggest to have your customPrint function return a tagList and use the result to feed renderUI / htmlOutput. This btw. also enables you to apply tagQuery on the resulting list.

Please find a simplified example below (of course this will need tuning, however it is a viable solution with regard to further changes to the tags):

library(shiny)

options(shiny.autoreload = T)

ui <- fluidPage(
  htmlOutput("summary")
)

server <- function(input, output) {
  output$summary <- renderUI({
    n <- 1000
    x <- seq(n)
    y <- x + rnorm(n = n, mean = 2, sd = 2)
    fit <- lm(y ~ x)
    customPrint(summary.lm(object = fit, correlation = T))
  })
  
  customPrint <- function(x){
    summary_output <- as.list(capture.output(summary.lm(object = fit, correlation = T)))
    summary_output <- lapply(summary_output, function(x){p(x, style = "line-height: 50%")})
    summary_output[[22L]] <- span(summary_output[[22L]], style = 'color: red;')
    tagList(summary_output)
  }
}

shinyApp(ui = ui, server = server)

image

1 Like

Many thanks @ismirsehregal, that is just the direction I needed.

Some modifications on your code to improve the output format:

library(shiny)

options(shiny.autoreload = T, width = 280)

ui <- fluidPage(
  htmlOutput("summary")
)

server <- function(input, output) {
  
  cat("START\n")
  output$summary <- renderUI({
    n <- 1000
    x <- seq(n)
    x2 <- x^2
    y <- x + rnorm(n = n, mean = 2, sd = 2)
    fit <- lm(y ~ x + x2)
    pre(customPrint(summary.lm(object = fit, correlation = T)))
  })

  customPrint <- function(x) {
    summary_output <- paste(as.list(capture.output(x)), "")
    summary_output <- lapply(summary_output, function(x) {
      span(x)
    })
    for (i in (which(grepl(pattern = "Correlation of Coefficients:", x = summary_output, ignore.case = F, fixed = T)) + 2):length(summary_output)) {
      correl_values <- strsplit(as.character(summary_output[[i]]), "(?<=\\s)|(?=\\s)", perl = T)[[1]]
      for (j in which(abs(suppressWarnings(as.numeric(correl_values))) > 0.9)) {
        correl_values[j] <- HTML(as.character(span(correl_values[j], style = "color: red;")))
      }
      summary_output[[i]] <- HTML(paste(correl_values, collapse = ""))
    }
    tagList(summary_output)
  }
  
}

shinyApp(ui = ui, server = server)