rmathjaxquartokablekableextra

Why is MathJax not rendered within my kableExtra table in a Quarto document?


I'm trying to generate properly a table with variable and associated p-value with kable package in a quarto document. I use kable and kableExtra packages. My yaml header is the following one:

---
title: "Towards quantification of image quality in chest CT clinical images : part I. Extraction of imaging features for prediction of system performances and lesion detectability"
author: 
  - name: F. Gardavaud
    orcid: 0000-0001-9767-3241
    corresponding: true
    email: francois.gardavaud@aphp.fr
    roles:
      - Image database designing
      - Article writing
      - Radiomics computing
      - Data analysis
    affiliations:
      - APHP
  - name: Hugo Pasquier
    orcid: 0000-0001-9823-3563
    corresponding: false
    roles: 
    - Radiomics computing
    - Data analysis
    - Article reading
    affiliations:
      - GE HealthCare
date: last-modified
keywords:
    - Computed Tomography
    - Radiomics
    - Image quality metrics
abstract: | 
  Introduction: Task-based CT image quality is commonly assessed using large homogenous Regions Of Interests (ROI). Identifying such regions within chest images being almost impossible, the purpose of this study was to evaluate the capability to predict the system performances and the lesion detectability based on small ROIs.<br> \newline
  Material and methods:<br> \newline
  Conclusions:
plain-language-summary: Bli bli bi
key-points:
  - Image quality metrics based on gold standard and radiomics features have been performed on a standard control quality phantom,
  - Radiomics features, computed in small analysis ROI, could be used to replace image quality metrics in computed tomography examination,
  - Radiomics features could be used to assess the image quality in computed tomography clinical image.
number-sections: true
highlight-style: pygments
format:
  html: 
    code-fold: true
    html-math-method: katex
    embed-resources: true
    self-contained-math: true
    df-print: "kable"
  pdf:
    toc: false
    df-print: "kable"
    number-sections: true
    colorlinks: true
    geometry: 
      - top=30mm
      - left=30mm
  docx: default
editor: visual
bibliography: references.bib
csl: physica-medica.csl
---

Here's my code (just the corresponding chunk):

{r}
#| code-summary: "Kruskal-Wallis and Dunn post-hoc tests for the NPS, FD, TTF and d' values"
#| message: false
#| warning: false
#| echo: false
#| label: tbl-res-p-value
#| tbl-cap: p values of the Kruskal-Wallis and Dunn post-hoc comparison tests of the average spatial frequency of the Noise Power Spectrum f<sub>av</sub>, the spatial frequency at which the Task Transfer Function was reduced by 50% (TTF<sub>50</sub>) and detectability indexes (d') between the different CT units. p-value < 0.05 were considered significant. \\*, \\**, \\*** notifications indicate p < 0.05, p < 0.01, p < 0.001 respectively.
#| cache: true # pour ne pas recalculer les résultats à chaque compilation
#| results: 'asis'


# comme la normalité b'est pas respecté, on exécute le test de Kruskal-Wallis pour tester l'égalité des moyennes des variables NPS, FD, TTF et d' entre les 3 modèles de CT


# pour le NPS
kruskal_NPS <- kruskal.test(meanFreq_128 ~ CT,
  data = DF_IQ_NPS_3CT_selected_rfe
)
# pour les TTF
# pour bone
kruskal_TTF_bone <- kruskal.test(f50Bone ~ CT,
  data = DF_IQ_TTF_3CT_filtered
)
# pour air
kruskal_TTF_air <- kruskal.test(f50Air ~ CT,
  data = DF_IQ_TTF_3CT_filtered
)
# pour HEIodine
kruskal_TTF_iodine <- kruskal.test(f50HEIodine ~ CT,
  data = DF_IQ_TTF_3CT_filtered
)
# pour Phantom
kruskal_TTF_edge <- kruskal.test(f50Phantom ~ CT,
  data = DF_IQ_TTF_3CT_filtered
)
# pour le dprime
kruskal_dprime <- kruskal.test(dPrime ~ CT,
  data = DF_IQ_dprime_3CT_selected_rfe
)
# pour le dprime GGN
kruskal_dprime_GGN <- kruskal.test(dPrime ~ CT,
  data = DF_IQ_dprime_3CT_GGN
)
# pour le dprime partial
kruskal_dprime_partial <- kruskal.test(dPrime ~ CT,
  data = DF_IQ_dprime_3CT_partial
)
# pour le dprime solid
kruskal_dprime_solid <- kruskal.test(dPrime ~ CT,
  data = DF_IQ_dprime_3CT_solid
)
# les tests sont tous significatifs, on peut donc faire des tests post-hoc pour déterminer les différences entre les groupes



# pour le NPS
dunn_NPS <- dunnTest(meanFreq_128 ~ CT,
  data = DF_IQ_NPS_3CT_selected_rfe,
  method = "bon"
)
# pour les TTF
# pour bone
dunn_TTF_bone <- dunnTest(f50Bone ~ CT,
  data = DF_IQ_TTF_3CT_filtered,
  method = "holm"
)
# pour air
dunn_TTF_air <- dunnTest(f50Air ~ CT,
  data = DF_IQ_TTF_3CT_filtered,
  method = "holm"
)
# pour iodine
dunn_TTF_iodine <- dunnTest(f50HEIodine ~ CT,
  data = DF_IQ_TTF_3CT_filtered,
  method = "holm"
)
# pour phantom
dunn_TTF_edge <- dunnTest(f50Phantom ~ CT,
  data = DF_IQ_TTF_3CT_filtered,
  method = "holm"
)
# pour le dprime
dunn_dprime <- dunnTest(dPrime ~ CT,
  data = DF_IQ_dprime_3CT_selected_rfe,
  method = "holm"
)
# pour le dprime GGN
dunn_dprime_GGN <- dunnTest(dPrime ~ CT,
  data = DF_IQ_dprime_3CT_GGN,
  method = "holm"
)
# pour le dprime partial
dunn_dprime_partial <- dunnTest(dPrime ~ CT,
  data = DF_IQ_dprime_3CT_partial,
  method = "holm"
)
# pour le dprime solid
dunn_dprime_solid <- dunnTest(dPrime ~ CT,
  data = DF_IQ_dprime_3CT_solid,
  method = "holm"
)

# on créé un dataframe pour stocker les résultats des tests de Kruskal-Wallis et des tests post-hoc
# pour ce faire la première colone correspond aux variables, la seconde aux p-values de Kruskal-Wallis. La troisième, quatrième est cinquième colonne correspondent aux p-values des tests post-hoc de Dunn avec en titre de colonne la comparaison entre les groupes
DF_kruskal_dunn <- data.frame(
  # Metric = c("$\\mathrm{f_{av}}$", "$\\mathrm{TTF_{50\\_Bone}}$", "$\\mathrm{TTF_{50\\_Air}}$", "$\\mathrm{TTF_{50\\_Iodine}}$", "$\\mathrm{TTF_{50\\_Edge}}$", "$\\mathrm{d'_{Solid}}$", "$\\mathrm{d'_{Partial}}$", "$\\mathrm{d'_{GGN}}$"),
  # Metric = c("x","a","b","c","d","e","f","g"),
  Metric = c("$\\mathrm{f_{av}}$", "$\\mathrm{TTF_{50\\_Bone}}$", "$\\mathrm{TTF_{50\\_Air}}$", "$\\mathrm{TTF_{50\\_Iodine}}$", "$\\mathrm{TTF_{50\\_Edge}}$", "$\\mathrm{d'_{Solid}}$", "$\\mathrm{d'_{Partial}}$", "$\\mathrm{d'_{GGN}}$"),
  `Kruskal.Wallis` = c(kruskal_NPS$p.value, kruskal_TTF_bone$p.value, kruskal_TTF_air$p.value, kruskal_TTF_iodine$p.value, kruskal_TTF_edge$p.value, kruskal_dprime_solid$p.value, kruskal_dprime_partial$p.value, kruskal_dprime_GGN$p.value),
  Canon_vs_GE = c(dunn_NPS$res$P.adj[1], dunn_TTF_bone$res$P.adj[1], dunn_TTF_air$res$P.adj[1], dunn_TTF_iodine$res$P.adj[1], dunn_TTF_edge$res$P.adj[1], dunn_dprime_solid$res$P.adj[1], dunn_dprime_partial$res$P.adj[1], dunn_dprime_GGN$res$P.adj[1]),
  Canon_vs_Siemens = c(dunn_NPS$res$P.adj[2], dunn_TTF_bone$res$P.adj[2], dunn_TTF_air$res$P.adj[2], dunn_TTF_iodine$res$P.adj[2], dunn_TTF_edge$res$P.adj[2], dunn_dprime_solid$res$P.adj[2], dunn_dprime_partial$res$P.adj[2], dunn_dprime_GGN$res$P.adj[2]),
  GE_vs_Siemens = c(dunn_NPS$res$P.adj[3], dunn_TTF_bone$res$P.adj[3], dunn_TTF_air$res$P.adj[3], dunn_TTF_iodine$res$P.adj[3], dunn_TTF_edge$res$P.adj[3], dunn_dprime_solid$res$P.adj[3], dunn_dprime_partial$res$P.adj[3], dunn_dprime_GGN$res$P.adj[3])
)

# on met les valeurs en format décimal fixe pour les valeurs de p > 0.001 et on ajoute des * pour les p-values < 0.05, ** pour les p-values < 0.01 et *** pour les p-values < 0.001
DF_kruskal_dunn <- DF_kruskal_dunn %>%
  mutate(across(c(Kruskal.Wallis, Canon_vs_GE, Canon_vs_Siemens, GE_vs_Siemens), ~ ifelse(. < 0.001, paste0(formatC(., format = "f", digits = 3), "***"), ifelse(. < 0.01, paste0(formatC(., format = "f", digits = 3), "**"), ifelse(. < 0.05, paste0(formatC(., format = "f", digits = 3), "*"), formatC(., format = "f", digits = 3))))))

# pour le colonnes p,  Canon_vs_GE, Canon_vs_Siemens et GE_vs_Siemens on passe du format puissance au format numérique et on remplace les valeurs inférieures à 0.001 par "< 0.001"
DF_kruskal_dunn <- DF_kruskal_dunn %>%
  mutate(across(c(Kruskal.Wallis, Canon_vs_GE, Canon_vs_Siemens, GE_vs_Siemens), ~ ifelse(. < 0.001, "< 0.001 ***", formatC(., format = "f", digits = 3))))

# Renommer la colonne Kruskal.Wallis en Kruskal Wallis
DF_kruskal_dunn <- DF_kruskal_dunn %>%
  rename(`Kruskal Wallis` = Kruskal.Wallis)

# on affiche le dataframe
kable(
  DF_kruskal_dunn,
  escape = FALSE,
  booktabs = TRUE
 )
  # kable_styling(full_width = TRUE)
  # kable_styling(latex_options = c("striped", "scale_down"))
# %>%
   #add_header_above(c(" " = 2, "Dunn post-hoc" = 3), italic = TRUE)

In this case the table is generated correctly but if I'm trying to uncomment add_header_above(c(" " = 2, "Dunn post-hoc" = 3), italic = TRUE)the first column which contains Latex caracters is displayed with all the characters, i.e. $\\mathrm{f_{av}}$ for the first value instead of the right format.

For test purpose here's a reprex :

DF_kruskal_dunn <- data.frame(
  Metric = c("$\\mathrm{f_{av}}$", "$\\mathrm{TTF_{50\\_Bone}}$", "$\\mathrm{TTF_{50\\_Air}}$", "$\\mathrm{TTF_{50\\_Iodine}}$", "$\\mathrm{TTF_{50\\_Edge}}$", "$\\mathrm{d'_{Solid}}$", "$\\mathrm{d'_{Partial}}$", "$\\mathrm{d'_{GGN}}$"),
  `Kruskal.Wallis` = c(0.4, 0.0002, 0.3, 0.005, 0.3, 0.0001, 0.0001, 0.0001),
  Canon_vs_GE = c(0.4, 0.0002, 0.3, 0.5, 0.003, 0.0001, 0.0001, 0.0001),
  Canon_vs_Siemens = c(0.4, 0.0002, 0.3, 0.5, 0.3, 0.0001, 0.00003, 0.000001),
  GE_vs_Siemens = c(0.4, 0.0002, 0.3, 0.5, 0.3, 0.0001, 0.0002, 0.00001)
)

# we put the values in fixed decimal format for p-values > 0.001 and add * for p-values < 0.05, ** for p-values < 0.01 and *** for p-values < 0.001.
DF_kruskal_dunn <- DF_kruskal_dunn %>%
  mutate(across(c(Kruskal.Wallis, Canon_vs_GE, Canon_vs_Siemens, GE_vs_Siemens), ~ ifelse(. < 0.001, paste0(formatC(., format = "f", digits = 3), "***"), ifelse(. < 0.01, paste0(formatC(., format = "f", digits = 3), "**"), ifelse(. < 0.05, paste0(formatC(., format = "f", digits = 3), "*"), formatC(., format = "f", digits = 3))))))

# for the columns p, Canon_vs_GE, Canon_vs_Siemens and GE_vs_Siemens we switch from power format to numeric format and replace values less than 0.001 by “< 0.001”.DF_kruskal_dunn <- DF_kruskal_dunn %>%
DF_kruskal_dunn <- DF_kruskal_dunn %>%
mutate(across(c(Kruskal.Wallis, Canon_vs_GE, Canon_vs_Siemens, GE_vs_Siemens), ~ ifelse(. < 0.001, "< 0.001 ***", formatC(., format = "f", digits = 3))))

  # Rename columns with spaces for greater clarity
DF_kruskal_dunn <- DF_kruskal_dunn %>%
  rename(`Kruskal Wallis` = Kruskal.Wallis)
DF_kruskal_dunn <- DF_kruskal_dunn %>%
  rename(`Canon vs GE` = Canon_vs_GE)
DF_kruskal_dunn <- DF_kruskal_dunn %>%
  rename(`Canon vs Siemens` = Canon_vs_Siemens)
DF_kruskal_dunn <- DF_kruskal_dunn %>%
  rename(`GE vs Siemens` = GE_vs_Siemens)

# dataframe diasplay
kable(
  DF_kruskal_dunn,
  escape = FALSE,
  booktabs = TRUE
)
# kable_styling(full_width = TRUE)
# kable_styling(latex_options = c("striped", "scale_down"))
# %>%
# add_header_above(c(" " = 2, "Dunn post-hoc" = 3), italic = TRUE)

Could you help me to solve this problem?


Solution

  • The issue is that kableExtra outputs an HTML table which is not processed by MathJax, see e.g. quarto-dev/quarto-cli#555 and haozhu233/kableExtra#746 for more details.

    Since these issues are currently still open, one needs a workaround which makes the Quarto processing work. One example is given in quarto-dev/quarto-r#178 and could be used in an example similar to yours like this: Defined is a helper function make_data_qmd which wraps the content into suitable HTML, and additionally the table needs escape = FALSE everywhere. This is shown in the example below.

    The second issue is that you currently mutate a column such that it's value is sometimes "< 0.001 ***" and that the "<" conflicts with kableExtra's HTML output ("<" is a reserved character). You should replace it with the HTML entity "&lt;".

    ---
    format: html
    ---
    
    ```{r}
    #| label: tbl-example
    #| tbl-cap: "Example"
    
    library(knitr)
    library(kableExtra)
    library(dplyr)
    
    DF_kruskal_dunn <- data.frame(
      Metric = c("$\\mathrm{f_{av}}$", "$\\mathrm{TTF_{50\\_Bone}}$", "$\\mathrm{TTF_{50\\_Air}}$", "$\\mathrm{TTF_{50\\_Iodine}}$", "$\\mathrm{TTF_{50\\_Edge}}$", "$\\mathrm{d'_{Solid}}$", "$\\mathrm{d'_{Partial}}$", "$\\mathrm{d'_{GGN}}$"),
      `Kruskal.Wallis` = c(0.4, 0.0002, 0.3, 0.005, 0.3, 0.0001, 0.0001, 0.0001),
      Canon_vs_GE = c(0.4, 0.0002, 0.3, 0.5, 0.003, 0.0001, 0.0001, 0.0001),
      Canon_vs_Siemens = c(0.4, 0.0002, 0.3, 0.5, 0.3, 0.0001, 0.00003, 0.000001),
      GE_vs_Siemens = c(0.4, 0.0002, 0.3, 0.5, 0.3, 0.0001, 0.0002, 0.00001)
    )
    
    # we put the values in fixed decimal format for p-values > 0.001 and add * for p-values < 0.05, ** for p-values < 0.01 and *** for p-values < 0.001.
    DF_kruskal_dunn <- DF_kruskal_dunn %>%
      mutate(across(c(Kruskal.Wallis, Canon_vs_GE, Canon_vs_Siemens, GE_vs_Siemens), ~ ifelse(. < 0.001, paste0(formatC(., format = "f", digits = 3), "***"), ifelse(. < 0.01, paste0(formatC(., format = "f", digits = 3), "**"), ifelse(. < 0.05, paste0(formatC(., format = "f", digits = 3), "*"), formatC(., format = "f", digits = 3))))))
    
    # for the columns p, Canon_vs_GE, Canon_vs_Siemens and GE_vs_Siemens we switch from power format to numeric format and replace values less than 0.001 by “< 0.001”.DF_kruskal_dunn <- DF_kruskal_dunn %>%
    DF_kruskal_dunn <- DF_kruskal_dunn %>%
    mutate(across(c(Kruskal.Wallis, Canon_vs_GE, Canon_vs_Siemens, GE_vs_Siemens), ~ ifelse(. < 0.001, "&lt; 0.001 ***", formatC(., format = "f", digits = 3))))
    
      # Rename columns with spaces for greater clarity
    DF_kruskal_dunn <- DF_kruskal_dunn %>%
      rename(`Kruskal Wallis` = Kruskal.Wallis)
    DF_kruskal_dunn <- DF_kruskal_dunn %>%
      rename(`Canon vs GE` = Canon_vs_GE)
    DF_kruskal_dunn <- DF_kruskal_dunn %>%
      rename(`Canon vs Siemens` = Canon_vs_Siemens)
    DF_kruskal_dunn <- DF_kruskal_dunn %>%
      rename(`GE vs Siemens` = GE_vs_Siemens)
    
    
    make_data_qmd <- function(content) {
      sprintf('<span data-qmd="%s">%s</span>', content, content)
    }
    
    DF_kruskal_dunn$Metric <- make_data_qmd(DF_kruskal_dunn$Metric)
    
    # dataframe diasplay
    kable(
      DF_kruskal_dunn,
      format = "html",
      escape = FALSE,
      booktabs = TRUE
    ) %>%
      kable_styling(full_width = TRUE) %>%
      kable_styling(latex_options = c("striped", "scale_down")) %>%
      add_header_above(c(" " = 2, "Dunn post-hoc" = 3), italic = TRUE, escape = FALSE)
    ```
    

    ![enter image description here