Set y axis limit in {reactablefmtr} `react_sparkline` in each row separately

309 views Asked by At

I have a reactable which contains react_sparklines from the {reactablefmtr} package which in turn is based on the {dataui} package (at least the sparklines).

I want to set the y axis in each row to a different value and it is not working (see below for what is and what is not working).

Is there a way to set the y axis in each row to a different value? I would welcome approaches that either ...

  1. make it work with the existing react_sparklines function or * (see Update below)
  2. change the underlying function to make it work or
  3. rely on a different package (not {dataui}) to create the sparklines (however they should be equally nice looking).

Update: The maintainer of {reactablefmtr} told me on Twitter that react_sparklines does not allow different y axis limits across rows in the current version.

I still would be interested in options 2. & 3. above.

library(dplyr)
library(reactable)
library(reactablefmtr)
library(dataui) # this is needed for {reactablefmtr}'s sparklines

# the input data
set.seed(123)

mydat <- tibble(id = c("A", "B", "C"),
                data = list(sample(c(1:20), 20),
                            sample(seq(5, 100, by = 5), 20),
                            sample(seq(2.5, 50, by = 2.5), 20)
                            )
)


# (1) y axis range not specified
# Each sparkline has its own limit on the y axis equalling the max of each data set
# works
reactable(mydat,
  columns = list(
    data = colDef(
      cell = react_sparkline(
        mydat,
        height = 80,
        show_area = TRUE,
        tooltip_type = 2
        )
      )
    )
  )
)


# (2) y axis specified to 100
# Each sparkline has the same max limit on the y axis: 100
# works
reactable(mydat,
          columns = list(
            data = colDef(
              cell = react_sparkline(
                mydat,
                height = 80,
                show_area = TRUE,
                max_value = 100,
                tooltip_type = 2
              )
            )
          )
)
)


# (3) y axis specified to 50,100,50 for each graph respectively
# Each sparkline should have its own limit set to: 50, 100, 50 respectively 
# does not work!! y axis limit is same as one (1) the maximum of each data set
reactable(mydat,
          columns = list(
            data = colDef(
              cell = react_sparkline(
                mydat,
                height = 80,
                show_area = TRUE,
                max_value = c(50, 100, 50),
                tooltip_type = 2
              )
            )
          )
)
)
1

There are 1 answers

0
TimTeaFan On

I found a way to do it by changing the underlying function react_sparkline. At the end of the post I show the full function mySparkline and highlight in the comments which part I added.

Now two new options are possible:

First, we can add a vector of length > 1 containing the maximum value for each data set:

# (A) we can now use a vector of length > 1 in max_value
# works
reactable(mydat,
          columns = list(
            data = colDef(
              cell = mySparkline(
                mydat,
                height = 80,
                show_area = TRUE,
                max_value = c(50,100,50),
                tooltip_type = 2
              )
            )
          )
)

Second, we can use a function instead which is applied to the maximum value of each data set:

# (B) we can now use a function in max_value which will be applied to the maximum value of each
# data set 
# works
reactable(mydat,
          columns = list(
            data = colDef(
              cell = mySparkline(
                mydat,
                height = 80,
                show_area = TRUE,
                max_value = function(x) ceiling(x/50)*50,
                tooltip_type = 2
              )
            )
          )
)

Below is the new function mySparkline. Look for the comments saying ## new code.

mySparkline <- function (data, height = 22, show_line = TRUE, line_color = "slategray",
          line_color_ref = NULL, line_width = 1, line_curve = "cardinal",
          highlight_points = NULL, point_size = 1.1, labels = "none",
          label_size = "0.8em", decimals = 0, min_value = NULL, max_value = NULL,
          show_area = FALSE, area_color = NULL, area_color_ref = NULL,
          area_opacity = 0.1, statline = NULL, statline_color = "red",
          statline_label_size = "0.8em", bandline = NULL, bandline_color = "red",
          bandline_opacity = 0.2, tooltip = TRUE, tooltip_type = 1,
          tooltip_color = NULL, tooltip_size = "1.1em", margin = NULL) {
  cell <- function(value, index, name) {
    if (!requireNamespace("dataui", quietly = TRUE)) {
      stop("The `dataui` package is required to use `react_sparkline()`.",
           call. = FALSE)
    }

    else {
      if (!is.null(margin) && length(margin) < 4) {
        stop("please provide margin dimensions within `margin()`. Ex. margin = margin(t=10)")
      }
      if (!is.null(highlight_points) && length(highlight_points) <
          5) {
        stop("please provide point color assignments within `highlight_points()`. Ex. highlight_points = highlight_points(max='red')")
      }
      if (is.null(highlight_points)) {
        highlight_points <- highlight_points(all = "transparent",
                                             first = "transparent", last = "transparent",
                                             min = "transparent", max = "transparent")
      }
      else {
        highlight_points <- highlight_points
      }
      if (!is.logical(show_line)) {
        stop("`show_line` must either be TRUE or FALSE.")
      }
      if (!is.logical(tooltip)) {
        stop("`tooltip` must either be TRUE or FALSE.")
      }
      if (!is.null(tooltip_type) && !any(tooltip_type %in%
                                         c(1, 2))) {
        stop("`tooltip_type` must be either 1 or 2")
      }
      if (!is.logical(show_area)) {
        stop("`show_area` must either be TRUE or FALSE.")
      }
      if (!is.null(labels) && !any(labels %in% c("none",
                                                 "first", "last", "min", "max", "all"))) {
        stop("`labels` must be either first, last, min, max, all, or none")
      }
      if (!is.null(line_curve) && !any(line_curve %in%
                                       c("cardinal", "linear", "basis", "monotoneX"))) {
        stop("`line_curve` must be either cardinal, linear, basis, or monotoneX")
      }
      if (!is.null(bandline) && !any(bandline %in% c("innerquartiles",
                                                     "range"))) {
        stop("`bandline` must be either innerquartiles or range")
      }
      if (!is.null(statline) && !any(statline %in% c("mean",
                                                     "median", "min", "max"))) {
        stop("`statline` must be either mean, median, min, or max")
      }
      last_index <- lapply(data[[name]], function(x) length(x) -
                             1)
      value_max <- lapply(data[[name]], function(x) x[which.max(abs(x))])
      value_min <- lapply(data[[name]], function(x) x[which.min(abs(x))])
      value_mean <- lapply(data[[name]], mean)

      ## new code added from here ##
      ### make max_value work with vectors of length > 1 and functions
      if(is.function(max_value)) {
        max_value <- lapply(value_max, max_value)
      }

      if (length(max_value) > 1) {
        if (length(max_value) != length(value_max)) {
          stop(paste0("`max_value` must either be a numeric vector of length 1, ",
                      "a numeric vector of length equal to the number of rows or a function."))
        }
        max_value <- max_value[[index]]
      }
      ## new code up to here ##
      
      if (!is.null(statline) && statline %in% c("mean",
                                                "median", "min", "max")) {
        statline <- dataui::dui_sparkhorizontalrefline(stroke = statline_color,
                                                       strokeDasharray = "2, 2", strokeWidth = 1,
                                                       strokeOpacity = 0.75, reference = statline,
                                                       renderLabel = htmlwidgets::JS(htmltools::HTML(paste0("(d) => React.createElement('tspan', {fill: '",
                                                                                                            statline_color, "', fontWeight: 'bold', fontSize: '",
                                                                                                            statline_label_size, "', stroke: 'transparent'}, d.toFixed(",
                                                                                                            decimals, "))"))), labelPosition = "right",
                                                       labelOffset = 5)
        if (any(labels %in% "none") && is.null(margin)) {
          margin <- margin(t = 4, r = 28, b = 3, l = 13)
        }
        else if (any(labels %in% c("first", "last")) &&
                 (!any(stringr::str_detect(labels, "min")) &&
                  !any(stringr::str_detect(labels, "max")) &&
                  !any(stringr::str_detect(labels, "all"))) &&
                 is.null(margin)) {
          margin <- margin(t = 5, r = 28, b = 3, l = 24)
        }
        else if (is.null(margin)) {
          margin <- margin(t = 14, r = 28, b = 10, l = 13)
          if (height == 22) {
            height <- 28
          }
          else {
            height <- height
          }
        }
      }
      else {
        statline <- dataui::dui_sparkhorizontalrefline(stroke = "transparent")
      }
      if (any(labels %in% "none") && is.null(margin)) {
        margin <- margin(t = 3, r = 13, b = 2, l = 13)
      }
      else if (any(labels %in% c("first", "last")) &&
               (!any(stringr::str_detect(labels, "min")) &&
                !any(stringr::str_detect(labels, "max")) &&
                !any(stringr::str_detect(labels, "all"))) &&
               is.null(margin)) {
        margin <- margin(t = 5, r = 24, b = 3, l = 24)
      }
      else if (is.null(margin)) {
        margin <- margin(t = 14, r = 13, b = 10, l = 13)
        if (height == 22) {
          height <- 30
        }
        else {
          height <- height
        }
      }
      if (any(labels %in% c("first", "last")) && (!any(stringr::str_detect(labels,
                                                                           "max")) && !any(stringr::str_detect(labels,
                                                                                                               "min")) && !any(stringr::str_detect(labels,
                                                                                                                                                   "all")))) {
        label_position <- htmlwidgets::JS(paste0("{(d, i) => ((i === 0) ? 'left'\n           : (i === ",
                                                 last_index[index], ") ? 'right'\n           : 'top')}"))
        label_offset <- 6
      }
      else {
        label_position <- "auto"
        label_offset <- 7
      }
      if (!is.null(bandline) && bandline == "innerquartiles") {
        bandline_pattern <- dataui::dui_sparkpatternlines(id = "pattern",
                                                          height = 4, width = 4, stroke = bandline_color,
                                                          strokeWidth = 1, orientation = list("diagonal"))
        bandline <- dataui::dui_sparkbandline(band = "innerquartiles",
                                              fill = "url(#pattern)", fillOpacity = bandline_opacity)
      }
      else if (!is.null(bandline) && bandline == "range") {
        bandline_pattern <- dataui::dui_sparkpatternlines(id = "pattern",
                                                          height = 4, width = 4, stroke = bandline_color,
                                                          strokeWidth = 1, orientation = list("diagonal"))
        bandline <- dataui::dui_sparkbandline(band = list(from = list(y = min(value)),
                                                          to = list(y = max(value))), fill = "url(#pattern)",
                                              fillOpacity = bandline_opacity)
      }
      else {
        bandline_pattern <- dataui::dui_sparkpatternlines(id = "NA",
                                                          stroke = "transparent")
        bandline <- dataui::dui_sparkbandline(fill = "transparent")
      }
      if (!is.null(line_color_ref) && is.character(line_color_ref)) {
        if (all(line_color_ref %in% names(which(sapply(data,
                                                       is.character))))) {
          if (is.character(line_color_ref)) {
            line_color_ref <- which(names(data) %in%
                                      line_color_ref)
          }
          line_color <- data[[line_color_ref]][index]
        }
        else {
          stop("Attempted to select non-existing column or non-character column with line_color_ref")
        }
      }
      if (is.null(line_color_ref)) {
        line_color <- line_color
      }
      if (is.null(area_color)) {
        area_color <- line_color
      }
      else {
        area_color <- area_color
      }
      if (!is.null(area_color_ref) && is.character(area_color_ref)) {
        if (all(area_color_ref %in% names(which(sapply(data,
                                                       is.character))))) {
          if (is.character(area_color_ref)) {
            area_color_ref <- which(names(data) %in%
                                      area_color_ref)
          }
          area_color <- data[[area_color_ref]][index]
        }
        else {
          stop("Attempted to select non-existing column or non-character column with area_color_ref")
        }
      }
      if (is.null(area_color_ref)) {
        area_color <- area_color
      }
      tooltip_position <- htmlwidgets::JS(paste0("{(yVal, i) => ((yVal > ",
                                                 value_mean[index], ") ? 'bottom'\n       : 'top')}"))
      tooltip_offset <- 5
      if (is.null(tooltip_color)) {
        tooltip_color <- line_color
      }
      else {
        tooltip_color <- tooltip_color
      }
      if (tooltip == TRUE) {
        if (tooltip_type == 1) {
          tooltip_1 <- dataui::dui_tooltip(components = list(dataui::dui_sparkpointseries(size = 0,
                                                                                          renderLabel = htmlwidgets::JS(htmltools::HTML(paste0("(d) => React.createElement('tspan', {fill: '",
                                                                                                                                               tooltip_color, "', fontSize: '", tooltip_size,
                                                                                                                                               "', fontWeight: 'bold', stroke: 'white'}, d.toFixed(",
                                                                                                                                               decimals, "))"))), labelPosition = tooltip_position,
                                                                                          labelOffset = tooltip_offset)))
          tooltip_2 <- NULL
        }
        else {
          tooltip_1 <- dataui::dui_tooltip(components = list(dataui::dui_sparkpointseries(size = 0)))
          tooltip_2 <- htmlwidgets::JS(htmltools::HTML(paste0("\n            function (_ref) {\n              var datum = _ref.datum;\n              return React.createElement(\n                    'tspan',\n                    {style: {fontSize: '",
                                                              tooltip_size, "', color: '", tooltip_color,
                                                              "', fontWeight: 'bold', stroke: 'transparent'}},\n                    datum.y ? datum.y.toLocaleString(undefined, {maximumFractionDigits: ",
                                                              decimals, "}) : \"--\"\n                  )\n            }\n        ")))
        }
      }
      else {
        tooltip_1 <- dataui::dui_tooltip(components = list(dataui::dui_sparkpointseries(size = 0)))
        tooltip_2 <- NULL
      }
      dataui::dui_sparkline(data = value, height = height,
                            max = max_value, min = min_value, margin = list(top = margin[[1]],
                                                                            right = margin[[2]], bottom = margin[[3]],
                                                                            left = margin[[4]]), renderTooltip = tooltip_2,
                            components = list(dataui::dui_sparklineseries(curve = line_curve,
                                                                          showLine = show_line, stroke = line_color,
                                                                          strokeWidth = line_width, fill = area_color,
                                                                          fillOpacity = area_opacity, showArea = show_area),
                                              dataui::dui_sparkpointseries(points = as.list("all"),
                                                                           stroke = highlight_points[[1]], fill = highlight_points[[1]],
                                                                           size = point_size), dataui::dui_sparkpointseries(points = as.list("first"),
                                                                                                                            stroke = highlight_points[[2]], fill = highlight_points[[2]],
                                                                                                                            size = point_size), dataui::dui_sparkpointseries(points = as.list("last"),
                                                                                                                                                                             stroke = highlight_points[[3]], fill = highlight_points[[3]],
                                                                                                                                                                             size = point_size), dataui::dui_sparkpointseries(points = as.list("min"),
                                                                                                                                                                                                                              stroke = highlight_points[[4]], fill = highlight_points[[4]],
                                                                                                                                                                                                                              size = point_size), dataui::dui_sparkpointseries(points = as.list("max"),
                                                                                                                                                                                                                                                                               stroke = highlight_points[[5]], fill = highlight_points[[5]],
                                                                                                                                                                                                                                                                               size = point_size), dataui::dui_sparkpointseries(points = as.list(labels),
                                                                                                                                                                                                                                                                                                                                fill = "transparent", stroke = "transparent",
                                                                                                                                                                                                                                                                                                                                renderLabel = htmlwidgets::JS(htmltools::HTML(paste0("(d) => React.createElement('tspan', {fill: '",
                                                                                                                                                                                                                                                                                                                                                                                     line_color, "', fontSize: '", label_size,
                                                                                                                                                                                                                                                                                                                                                                                     "', stroke: 'transparent'}, d.toFixed(",
                                                                                                                                                                                                                                                                                                                                                                                     decimals, "))"))), labelPosition = label_position,
                                                                                                                                                                                                                                                                                                                                labelOffset = label_offset), statline, bandline_pattern,
                                              bandline, tooltip_1))
    }
  }
}