Open danielleoneil opened 4 years ago
Hi,
This isn't possible right now due to how grouping works in the React Table library. You can at least display values for single-row groups using a custom aggregate function:
library(reactable)
df <- data.frame(
'Col1' = c(0,1,1,1,1,3,2,2,2,3,7,9,9),
'Col2' = c(0,1,17,18,19,3,2,20,21,22,7,9,15),
'Values' = c(3303,50575,39373,30303,760096,40484,50505,20202,8374,4837636,287375,133434,35083)
)
reactable(
df,
groupBy = c('Col1', 'Col2'),
defaultColDef = colDef(
# Default aggregate function: display the value if there's only one row in the group
aggregate = JS("function(values) {
if (values.length === 1) return values
}")
),
columns = list(
Values = colDef(aggregate = 'sum')
)
)
The first and fifth rows now show their Col2 values, but the groups are still there and the rows are still expandable. Unfortunately, it'll be tricky to ungroup single-row groups. I've looked into it before because I wanted to do the same, but found that it required too many hacks to get working properly.
For now, I'd recommend using conditional row details + nested tables if possible -- see example in https://github.com/glin/reactable/issues/33#issuecomment-596106592. It's more work to do the grouping manually, but at least you'd be able to control the row expansion.
In the future, this may also be possible through a feature like tree tables, where you'd have full control over the parent and child rows. You'd still have to do the grouping manually with a tree table, but it would probably be the best solution to this problem.
Ah, makes sense - thank you so much for the thoughtful response. I'll try my hand at the using conditional row details + nested tables approach. Thanks again and thanks for the package - it's made my Shiny apps the envy of my team.
Hello, would it be possible to have the click event listen on the entire group row, so that users don't have to click on the arrow to open up the grouped row?
@amanthapar You can use an onClick = "expand"
click action to do that: https://glin.github.io/reactable/articles/examples.html#expand-on-click
Hi, I ran into this issue, too and I think I found a workaround for it. Disclaimer: I'm not sure how robust this is..
The trick is to
aggregate
and grouped
arguments in colDef to make sure, all the info from the single child row is shown in the "parent" rowHere's a reprex:
library(MASS)
library(dplyr)
library(reactable)
# Mock data, 3 levels of grouping, 2 numerical non-grouping Variables per row
group_vars <- c("Manufacturer", "Type", "DriveTrain")
data <- MASS::Cars93[, c(group_vars, "Price", "MPG.city")]
# custom `rowClass` function -
# Rows without subrows get class `block-expandable` which we later use to
# hide the expander-button (the little triangle) and disable the event listener
# Expandable rows get the class `allow-expandable` instead. We usi it for
# styling.
row_class_fun <- JS("function(rowInfo) { return( rowInfo.subRows.length <= 1 ? 'block-expandable' : 'allow-expandable') }")
# Custom `aggregate` funtion for group columns -
# For groups with only one row, show that row's value in the parent row
aggregate_group_col <- JS("function(x) { return(x.length == 1 ? x : '') }")
# Optional: Custom `grouped` function for group columns -
# Suppress the `(n)` after the group name.
# Without it, it will not be uniform across the expandable and not-expandable
# rows and across group cols, i.e. some will have `(n)` and others wont,
# so you probbly want some version of this.
grouped_group_col <- JS("function(cellInfo) { return cellInfo.value }")
# Completely optional: Trigger group expansion regardless of which column the
# user clicks (...or filter "click-to-expand"-columns via`column`)
on_click_fun <- JS("function(rowInfo, column) { if (rowInfo.subRows.length > 1) rowInfo.toggleRowExpanded() }")
# ColDef for the 3 group cols -
# use custom group and aggregate functions, add class for styling.
group_col_def <- colDef(
grouped = grouped_group_col,
aggregate = aggregate_group_col,
class = "group-col"
)
# The table
rt <- reactable(
data,
highlight = TRUE,
groupBy = group_vars,
rowClass = row_class_fun,
onClick = on_click_fun,
columns = list(
Manufacturer = group_col_def,
Type = group_col_def,
DriveTrain = group_col_def,
# per-row variables.
# Don't forget to define aggregate functions for value when grouped
Price = colDef("Max. Price", aggregate = "max"),
MPG.city = colDef("Avg. MPG", aggregate = "mean", format = colFormat(digits = 1))
),
)
# Custom CSS:
# A lot of this is pure styling.
# - hide the expand button in non-expandable rows (this is the only non-optional change)
# - change the cursor on non-expandable rows from pointer to 'regular'
# - color the expandable rows blue and make text bold
# - color the expander Button red
# - add some left padding to the table header and group cols to align all text,
# regardless of whether there is an expander-button
custom_css <- "
.rt-tr.block-expandable button.rt-expander-button {
display: none;
pointer-events: none;
}
.rt-tr.block-expandable .rt-td-expandable {
cursor: auto;
}
.rt-tr.allow-expandable .rt-td-inner {
color: steelblue;
font-weight: bold;
}
.rt-td-expandable .rt-expander::after {
border-top-color: tomato;
}
.rt-th-inner .rt-text-content {
padding-left: 20px;
}
.rt-tr.block-expandable .group-col {
padding-left: 20px
}
"
# Custom JS:
# click events on the table are first registered by the `rt-td-inner` elements
# We add an event listener that intercepts these events and checks if any
# ancestor element (this would be the row it is in) has the class 'block-expandable'.
# (<https://developer.mozilla.org/en-US/docs/Web/API/Element/closest>)
# If so, we stop propagation of the click event.
# One could also check just the parent and grandparent element, as this should
# cover all "candidates", i.e.
# ```
# e.target.parentElement.classList.contains('block-expandable') ||
# e.target.parentElement.parentElement.classList.contains('block-expandable')
# ````
# (also note the ID `my-table` that is used to select the wrapping DOM element.)
custom_js <- "
document.addEventListener('DOMContentLoaded', function() {
let table = document.getElementById('my-table');
table.addEventListener('click', function(e) {
if ( e.target.closest('div.rt-tr.block-expandable') !== null) {
e.stopImmediatePropagation()
}
},
useCapture = true // ensures that listener fires before the regular reactable listeners
)
});
"
# Put it all together in a "browsable()"
# In a Shiny app the custom CSS and JS code would be added to the head
# somewhere else.
htmltools::tagList(
htmltools::tags$head(
htmltools::tags$style(custom_css),
htmltools::tags$script(custom_js)
),
htmltools::tags$div(id = "my-table", rt)
) |>
htmltools::browsable()
(This might also be a workaround for #168)
For example, in the below, since Col1 and Col2 for the first row both only have one value, I'd like them to appear on one line, rather than having the drop-downs to show the nested rows.
For the second row, the drop down for Col1 is perfect, but once you click into the nested rows, I wouldn't want to see the drop down for the Col2 values as there is only one value and having that nested row doesn't add more information.
df <- data.frame('Col1' = c(0,1,1,1,1,3,2,2,2,3,7,9,9), 'Col2' = c(0,1,17,18,19,3,2,20,21,22,7,9,15), 'Values' = c(3303,50575,39373,30303,760096,40484,50505,20202,8374,4837636,287375,133434,35083)) reactable(df, groupBy = c('Col1', 'Col2'), columns = list(Values = colDef(aggregate = 'sum')))