Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calculate width of barchart with JS custom rendering function #88

Closed
algo-se opened this issue Oct 21, 2020 · 2 comments
Closed

Calculate width of barchart with JS custom rendering function #88

algo-se opened this issue Oct 21, 2020 · 2 comments

Comments

@algo-se
Copy link

algo-se commented Oct 21, 2020

Is it possible to calculate the width of a barchart inside a JavaScript function taking into account the rest of the values in that column?

For example, in the Twitter followers example, the width of the column is already defined by cellInfo.value. However, what if we want to calculate the width as (I´m just going to invent the code) cellInfo.value / max(colInfo.rows) * 100, like in the cookbook barchart example?

I´ve been playing with the example where a JavaScript render function is used to put a unit in a fixed-width container, preceded by the cell value. I tried to render a barchart inside that container, with not much success.

data <- MASS::Cars93[40:44, c("Make", "Length", "Luggage.room")]

reactable(data, class = "car-specs", columns = list(
  # Align values using a fixed-width container for units
  Luggage.room = colDef(
    name = "Luggage Room",
    cell = JS("function(cellInfo) {
      var units = cellInfo.viewIndex === 0 ? ' ft³' : ''

      return cellInfo.value + '<div class=\"units\">' + units + '</div>'

    }"),
    html = TRUE
  )
))

Changing "units" for a barchart:

reactable(data, columns = list(
  # Align values using a fixed-width container for units
  Luggage.room = colDef(
    name = "Luggage Room",
    cell = JS("function(cellInfo) {
      
      return cellInfo.value + '<div class=\"units\">' + 
      
      // Changing units for barchart
        '<div style=\"display: flex; align-items: center;\">' +
          '<div style=\"flex-grow: 1; margin-left: 6px; height: 14px; background-color: #e1e1e1\">' +
            '<div style=\"height: 100%; width: ' + cellInfo.value + 'rem' + '; background-color: #fc5185\"></div>' +
          '</div>' +
        '</div>' + 
        
      '</div>'
    }"),
    html = TRUE
  )
))

image
So, is it possible to access the rest of the values in the column to calculate the width of the barchart?
Also, how would be the correct way of replacing "units" with a barchart in this example so the charts are aligned?

Best regards,
Alejandro.

@glin
Copy link
Owner

glin commented Oct 24, 2020

Hi, it's not currently possible to get the rest of the values in a column from a JS cell renderer. However, it will likely be possible in the next release, similar to how header and footer render functions get the table data: https://glin.github.io/reactable/articles/custom-rendering.html#javascript-render-function-1

For now, calculations that depend on the entire column can be precalculated in R, and passed to the JS render function. For example:

  1. Calculate the bar chart width in R, and store it in a hidden column:
data <- MASS::Cars93[40:44, c("Make", "Length", "Luggage.room")]

# Put the bar chart width in a "Luggage.room_width" column
data$Luggage.room_width <- paste0(data$Luggage.room / max(data$Luggage.room) * 100, "%")

reactable(data, columns = list(
  # Align values using a fixed-width container for units
  Luggage.room = colDef(
    name = "Luggage Room",
    cell = JS("function(cellInfo) {
      // Get the precalculated bar chart width
      var width = cellInfo.row['Luggage.room_width']
      
      return cellInfo.value + '<div class=\"units\">' + 
      // Changing units for barchart
        '<div style=\"display: flex; align-items: center;\">' +
          '<div style=\"flex-grow: 1; margin-left: 6px; height: 14px; background-color: #e1e1e1\">' +
            '<div style=\"height: 100%; width: ' + width + '; background-color: #fc5185\"></div>' +
          '</div>' +
        '</div>' + 
      '</div>'
    }"),
    html = TRUE
  ),
  Luggage.room_width = colDef(show = FALSE)
))
  1. Calculate the max value in R, and embed it in the JS function string:
data <- MASS::Cars93[40:44, c("Make", "Length", "Luggage.room")]

reactable(data, columns = list(
  # Align values using a fixed-width container for units
  Luggage.room = colDef(
    name = "Luggage Room",
   # When using sprintf, make sure to escape percent characters like "%%"
    cell = JS(sprintf("function(cellInfo) {
      var width = cellInfo.value / %s * 100 + '%%'

      return cellInfo.value + '<div class=\"units\">' + 
      // Changing units for barchart
        '<div style=\"display: flex; align-items: center;\">' +
          '<div style=\"flex-grow: 1; margin-left: 6px; height: 14px; background-color: #e1e1e1\">' +
            '<div style=\"height: 100%%; width: ' + width + '; background-color: #fc5185\"></div>' +
          '</div>' +
        '</div>' + 
      '</div>'
    }", max(data$Luggage.room))),
    html = TRUE
  )
))

Neither of these workarounds are great, and I've intentionally avoided creating any examples that do this.

To align labels with bar charts, the Twitter Followers tutorial should be more helpful: https://glin.github.io/reactable/articles/building-twitter-followers.html#add-bar-charts. There's also an example further down that renders a bar chart with aligned labels in a JS function: https://glin.github.io/reactable/articles/building-twitter-followers.html#dynamic-formatting. This is probably the closest example to what you'd like to do.

Adapting those examples for this table, the code might look like this:

data <- MASS::Cars93[40:44, c("Make", "Length", "Luggage.room")]
data$Luggage.room_width <- paste0(data$Luggage.room / max(data$Luggage.room) * 100, "%")

reactable(data, columns = list(
  Luggage.room = colDef(
    name = "Luggage Room",
    cell = JS("function(cellInfo) {
      var width = cellInfo.row['Luggage.room_width']
      // Create a fixed-width label of 2 characters
      var label = '<div>' + cellInfo.value.toString().padStart(2) + '</div>'
      return (
        '<div style=\"display: flex; align-items: center;\">' +
          label +
          '<div style=\"flex-grow: 1; margin-left: 6px; height: 14px; background-color: #e1e1e1\">' +
            '<div style=\"height: 100%; width: ' + width + '; background-color: #fc5185\"></div>' +
          '</div>' +
        '</div>'
      )
    }"),
    # Styles for label alignment
    style = list(fontFamily = "monospace", whiteSpace = "pre"),
    html = TRUE
  ),
  Luggage.room_width = colDef(show = FALSE)
))

reactable output

@algo-se
Copy link
Author

algo-se commented Oct 26, 2020

Thank you a lot for the detailed answer Greg. It´s been very enlightening.

Reading documentation I came across the highlighting sorted columns example where there are several arguments passed to JS, though I understand cellInfo is not an option yet.

  defaultColDef = colDef(
    style = JS("function(rowInfo, colInfo, state) {
      // Highlight sorted columns
      for (var i = 0; i < state.sorted.length; i++) {
        if (state.sorted[i].id === colInfo.id) {
          return { background: 'rgba(0, 0, 0, 0.03)' }
        }
      }
    }")
  )

Also, in case it is useful for someone, for me using monospaced fonts caused the table to be vertically misaligned. I used the style below to correct this:

reactable(data
          theme = reactableTheme(
            cellStyle = list(display = "flex", flexDirection = "column", justifyContent = "center")
          )
)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants