Using datatable
s in shiny
, I came across following problem regarding formatting of numeric columns and ordering of the values.
I want numeric values to be shown according to a given formatting, e.g.
X.000
are confusingUsing some formatting to manipulate the numeric values, the ordering in the table is destroyed, since values are compared as strings instead of numeric values. I found out that given language options of datatable will tell it how to handle columns according to given speicifcations, but this does not seem to work.
Moreover, DT
provides the formatRound
method that works in the sense that the numbers are formatted and the ordering works, but e.g. a fixed number of digits is always shown, which is confusing for integers. But integers should also be formatted, since they can be big and marks like "," will be good to grasp the number.
Following App illustrates the problem
library(shiny)
markerDecimal <- ","
markerThousand <- "."
df <- data.frame(
"numCol" = c(1900,2.111,0.9),
"someStrings" = c("A","BB","CCC")
)
ui <- fluidPage(
h4("Original: No format, numeric sorting works, but numbers are not displayed like I want them to be displayed"),
DT::dataTableOutput(outputId = "dtNoFormat"),
h4("Format using base::prettyNum destroying 'numeric behaviour' because of conversion to string"),
DT::dataTableOutput(outputId = "dtFormat1"),
h4("Format using DT::formatRound keeping ordering but showing too much decimals that are not required"),
DT::dataTableOutput(outputId = "dtFormat2")
)
server <- function(input, output, session) {
.formatNumber <- function(x) {
base::prettyNum(x = x,
big.mark = markerThousand, big.interval = 3,
decimal.mark = markerDecimal,
small.mark = "", small.interval = 3)
}
.formatDfNums <- function(df, nams = names(df)) {
numNames <- names(df)[sapply(df, is.numeric)]
nams2 <- intersect(numNames, nams)
if(length(nams2)) df[nams2] <- lapply(df[nams2], .formatNumber)
return(df)
}
defaultOps <- list(pageLength = 20,
bPaginate = FALSE, bSearch = FALSE)
output$dtNoFormat <- DT::renderDataTable({
DT::datatable(df, options = defaultOps)
}, server = FALSE)
output$dtFormat1 <- DT::renderDataTable({
DT::datatable(
df |> .formatDfNums(),
options = c(defaultOps,
list(language.thousands = markerThousand,
language.decimal = markerDecimal,
columnDefs = list(list(targets = "numCol",
type = "num-fmt")))))
}, server = FALSE)
output$dtFormat2 <- DT::renderDataTable({
DT::datatable(df, options = defaultOps) |>
DT::formatRound(
columns = names(df)[sapply(df, is.numeric)],
digits = 2,
interval = 3,
mark = markerThousand,
dec.mark = markerDecimal
)
}, server = FALSE)
}
shinyApp(ui, server, options = list(launch.browser = TRUE))
How can I update the app, such that numbers are displayed using the given formatting, e.g. custom thousands and decimals, and the ordering is preserved as a numeric ordering?
You could use DT::formatSignif
which does a better job. However, it may use scientific notation, which you may want to avoid as well.
Your best bet then is to provide a custom formatter. If you look into the source code of DT::formatSignif
you will see that under the hood a custom JavaScript formatting function is called (here's the source code of this formatter).
I would consider this a hack, but you can piggyback on that and do the following:
DTWidget
formatSignif2
which is basically a clone of DT::formatSignif
but calls your custom formatter instead of the pre-defined one.formatSignif2
in your rendering.library(shiny)
library(jsonlite)
library(htmlwidgets)
markerDecimal <- ","
markerThousand <- "."
js <- HTML("
$(function() {
DTWidget.formatSignif2 = function (data, digits, interval, mark, decMark, zeroPrint) {
const value = parseFloat(data);
if (isNaN(value)) {
return '';
}
if (zeroPrint !== null && value === 0.0) {
return zeroPrint;
}
// Round to significant digits
let roundedValue = Number(value).toPrecision(digits);
// Convert scientific notation
if (roundedValue.includes('e')) {
const [base, exponent] = roundedValue.split('e').map(Number);
const mantissa = base * Math.pow(10, exponent);
const base10 = Math.log10(Math.abs(mantissa));
roundedValue = mantissa.toFixed(Math.max(0, digits - Math.floor(base10) + 1));
}
// Remove trailing zeros by parsing back to a number
roundedValue = parseFloat(roundedValue).toString();
// Replace decimal marker with own marker
let [integerPart, fractionalPart] = roundedValue.split('.');
// Add thousand separators to the integer part
const re = new RegExp('\\\\B(?=(\\\\d{' + interval + '})+(?!\\\\d))', 'g');
integerPart = integerPart.replace(re, mark);
return fractionalPart ? `${integerPart}${decMark}${fractionalPart}` : integerPart;
}
})
")
formatSignif2 <- function(table, columns, digits = 2, interval = 3, mark = ",",
dec.mark = getOption("OutDec"), zero.print = NULL,
rows = NULL) {
tplSignif2 <- function(digits, interval, mark, dec.mark, zero.print, ...) {
sprintf("DTWidget.formatSignif2(data, %d, %d, %s, %s, %s);",
digits, interval, toJSON(unbox(mark)),
toJSON(unbox(dec.mark)),
if (is.null(zero.print)) "null" else toJSON(unbox(zero.print)))
}
DT:::formatColumns(table, columns, tplSignif2, digits, interval,
mark, dec.mark, zero.print, rows = rows)
}
df <- data.frame(
"numCol" = c(1900,2.111,0.9),
"someStrings" = c("A","BB","CCC")
)
ui <- fluidPage(
tags$head(tags$script(js)),
h1("Default formatSignif"),
DT::dataTableOutput(outputId = "dtFormat1"),
h1("Custom formatSignif"),
DT::dataTableOutput(outputId = "dtFormat2")
)
server <- function(input, output, session) {
defaultOps <- list(pageLength = 20,
bPaginate = FALSE, bSearch = FALSE)
output$dtFormat1 <- DT::renderDataTable({
DT::datatable(df, options = defaultOps) |>
DT::formatSignif(
columns = names(df)[sapply(df, is.numeric)],
digits = 2,
interval = 3,
mark = markerThousand,
dec.mark = markerDecimal
)
}, server = FALSE)
output$dtFormat2 <- DT::renderDataTable({
DT::datatable(df, options = defaultOps) |>
formatSignif2(
columns = names(df)[sapply(df, is.numeric)],
digits = 2,
interval = 3,
mark = markerThousand,
dec.mark = markerDecimal
)
}, server = FALSE)
}
shinyApp(ui, server, options = list(launch.browser = TRUE))
Both solution maintain the intrinsic order of the numbers and teh custom function avoids scientific notation but uses internal DT
functions, which must be used with care.
You may want to further adapt the custom formatter to your needs (think especially about the amount of digits), but this should get you going.