glin / reactable

Interactive data tables for R
https://glin.github.io/reactable
Other
633 stars 80 forks source link

Range filtering #9

Open nistara opened 4 years ago

nistara commented 4 years ago

Thanks a lot for this great package! Is it possible to implement filtering by a range of values? For e.g. filtering rows with prices ranging from 15 to 30, instead of a single value filter. I'm not quite sure if there's another option I could use for this in addition to filterable = TRUE. Thanks again!

image

glin commented 4 years ago

Only text filtering is supported for now, but I've always wanted different filter types like a range input or dropdown list in the table. I don't know when I'll get to it, but it's definitely high on the to-do list! Thanks for the feedback.

timelyportfolio commented 4 years ago

Found this after Twitter discussion. Lines disallow any custom filterMethod. I changed to

    if(!col.hasOwnProperty("filterMethod")) {
      col.filterMethod = (filter, rows) => {
        const id = filter.id
        const match = col.createMatcher(filter.value)
        return rows.filter(row => {
          const value = row[id]
          if (value === undefined) {
            return true
          }
          // Don't filter on aggregated cells
          if (row._subRows) {
            return true
          }
          return match(value)
        })
      }
    }

and assuming user is very JS literate we can then define a custom filter with the code below

library(reactable)
library(htmltools)

rt <- reactable(
  iris,
  filterable = TRUE
)

rt$x$tag$attribs$columns[[2]]$filterMethod <- htmlwidgets::JS("filterLessThan")
rt$x$tag$attribs$columns[[2]]$Filter <- htmlwidgets::JS("inputFilter")

browsable(
  tagList(
    tags$script(HTML("
function filterLessThan(filter, rows) {
  debugger
  return rows.filter(function(row) {
    return row[filter.id] <= +filter.value;
  })
}

function inputFilter(filter) {
  var _onChange = filter.onChange;
  return React.createElement(
    React.Fragment,
    null,
    [
      React.createElement(
        'span',
        {
          style: {fontSize: '20px'}
        },
        '<='
      ),
      React.createElement(
        'input',
        {
          type: 'number',
          min: 0,
          max: 5,
          style: {width: '70%'},
          onChange: function onChange(event) {return _onChange(event.target.value)}
        }
      )
    ]
  )
}
    ")),
    rt
  )
)

reactable_customfilter

timelyportfolio commented 4 years ago

@nistara I added an example of a range slider using material-ui.

# remotes::install_github("timelyportfolio/reactable")

library(reactable)
library(htmltools)

# not good practice since big dependency and all we want is slider
#   but for demonstration purposes do it this way
material_dep <- htmlDependency(
  name = "material-ui",
  version = "4.6.1",
  src = c(href = "https://unpkg.com/@material-ui/core/umd/"),
  script = "material-ui.production.min.js"
)

rt <- reactable(
  iris,
  filterable = TRUE
)

rt$x$tag$attribs$columns[[2]]$filterMethod <- htmlwidgets::JS("filterRange")
rt$x$tag$attribs$columns[[2]]$Filter <- htmlwidgets::JS("inputFilter")

browsable(
  tagList(
    reactR::html_dependency_react(),
    reactR::html_dependency_reacttools(),
    htmlwidgets::getDependency("reactable","reactable"),
    material_dep,
    tags$style("
.rt-thead.-filters .rt-tr {align-items: flex-end; height: 60px;}
.rt-thead.-filters .rt-th {overflow: visible;}
    "),
    tags$script(HTML("
function filterRange(filter, rows) {
  return rows.filter(function(row) {
    // Don't filter on aggregated cells
    if (row._subRows) {
      return true
    }
    return row[filter.id] >= filter.value[0] && row[filter.id] <= filter.value[1];
  })
}

function inputFilter(filter) {
  var _onChange = filter.onChange;
  return React.createElement(
    'div',
    null,
    React.createElement(
      MaterialUI.Slider,
      {
        defaultValue: [0,5],
        min: 0,
        max: 5,
        step: 0.5,
        valueLabelDisplay: 'auto',
        onChange: function onChange(event, newValue) {return _onChange(newValue)}
      }
    )
  )
}
    ")),
    rt
  )
)

reactable_customfilter_range

nistara commented 4 years ago

Thanks @glin, much appreciated!!!

@timelyportfolio Thanks a lot for showing the examples above! I'm going to try to get this working with my data. This slider looks nicer than the one from the DT package, though I really like that I can enter numbers manually with DT (helpful when the range is large and I want to subset a really small bit of it). I'll try to reproduce it and get back to you :)

glin commented 4 years ago

@timelyportfolio Nice examples! Looks like exposing filterMethod and Filter would be a quick way to at least enable custom filter implementations.

leungi commented 4 years ago

Hope there's future capability to do multi-entity filtering on character columns too 🤩

liberrenaud commented 4 years ago

@glin - Thanks for the amazing package. Really nice output! Love it!

@timelyportfolio & @glin - Do you believe that the approach that @timelyportfolio took for the value filter could be used for factors?

But this time doing the selection via drop down. Possible example here : https://material-ui.com/components/selects/

Do you believe that it could be possible the approach of @timelyportfolio and do you believe it could work? I would love your view on the feasibility:)

tylerlittlefield commented 4 years ago

Just wanted to say that this would be a great addition. If could set filterable = TRUE then filterType = "dropdown" it would simplify some of the apps I have developed that instead use shiny::selectInputs to filter the table.

shahreyar-abeer commented 4 years ago

Hey @tyluRp

Can I see the implementation of the dropdown with shiny::selectInputs?

tylerlittlefield commented 4 years ago

@shahreyar-abeer To clarify, I use selectInput outside of the reactable, unlike the slider shown in @timelyportfolio‘s example (which is what I think you’re looking for).

shahreyar-abeer commented 4 years ago

Oh, yeah I am looking for a dropdown inside the reactable. Looks like it isn't getting much attention here! Thanks for the quick reply though. Appreciate it.

timelyportfolio commented 4 years ago

I hope to get back to this, but unfortunately have not had the time or a project that requires it.

jlfitz commented 4 years ago

@glin This package is indeed amazing.
Maybe I am rehashing the past but I see that the CRAN radiant package has the filters folks here are working on.

mihirp161 commented 3 years ago

+1 for this feature request. I like using this package (thanks @glin ) because it doesn't have those slider filters like we have in DT. Problem there was if you had a wide table with a horizontal scroller, then whenever you filter a column, those range-sliders would push the scroller back to table's first column...very annoying. Please I know you're thinking of implementing this, but beware of that issue.

timelyportfolio commented 3 years ago

Just played a little with flat-ui. and the filters are very nice, but reactable far more powerful. Perhaps we could borrow the filters components https://github.com/githubocto/flat-ui/tree/main/src/components/filters.

MichaelSchatz commented 3 years ago

I'd like to +1 this feature request. A new reactable adopter and loving it, but this keeps me from making the switch from DT in a lot of places.

gaguilar2015 commented 3 years ago

+1 for the dropdown filter. This is also preventing me from switching from DT

timelyportfolio commented 2 years ago

Far from perfect, but I updated the example to work with newest reactable to add a slider for hp column in mtcars. As before we need to make a slight change in the source code to allow for custom filtering https://github.com/timelyportfolio/reactable/commit/fae87a5d525a555b2f18383a02b726e3b81f00a7#diff-4735897a0722c2357dfd440bf89a02e8e13d008249940a4218a3cad6317f63fa.

#remotes::install_github("timelyportfolio/reactable@filters")

library(htmltools)
library(magrittr)
library(reactable)

mui <- htmlDependency(
  name = "mui",
  version = "5.2.7",
  src = c(href = "https://unpkg.com/@mui/material@5.2.7/umd/"),
  script = "material-ui.development.js"
)

rt <- reactable(
  mtcars,
  columns = list(
    hp = colDef(filterable = TRUE) %>%
      {
        .$filterFun = JS('filterRange')
        .$Filter = JS('inputFilter')
        .
      }
  )
)

browsable(
  tagList(
    reactR::html_dependency_react(),
    reactR::html_dependency_reacttools(),
    htmlwidgets::getDependency("reactable","reactable"),
    tags$style(
"
.rt-td-filter {
  align-items: flex-end;
  height: 80px;
}
.rt-td-filter .rt-td-inner {
  overflow: visible;
}
"      
    ),
    mui,
    rt,
    tags$script(HTML(
      sprintf(
"
const inputFilter = ({ value, setValue, className, ...props }) => {
  const range = %s;
  return React.createElement(
    'div',
    {style: {margin: '0 5px'}},
    [
      React.createElement(
        'div',
        null,
        JSON.stringify(value ? value : range)
      ),
      React.createElement(
        MaterialUI.Slider,
        {
          defaultValue: range,
          min: range[0],
          max: range[1],
          step: 10,
          valueLabelDisplay: 'auto',
          onChange: (e, val) => {setValue(val)}
        }
      )
    ]
  )
}
const filterRange = (rng) => {
  return value => (value >= rng[0] && value <= rng[1])
}
",
        jsonlite::toJSON(c(0, max(mtcars$hp)), auto_unbox = TRUE)
      )
    ))
  )
)
januz commented 2 years ago

Just wanted to express my wish for allowing different column-based filter methods based on the column type (e.g., dropdown for character/factor, range for numeric). Would make this already invaluable package even more perfect!

yogat3ch commented 2 years ago

+1 on this! slider range filters on continuous data as in DT would be a much welcomed feature!

glin commented 2 years ago

Thanks all for the feedback and @timelyportfolio for those examples. Custom filtering is now first-class supported in the development version. See the Custom Filtering article for usage and a bunch of examples. There are a few examples about range filtering specifically:

There's still no built-in range filter though, so I'm keeping this issue open. That might be added some day, but it's not in my short-term priorities because of the required effort and lack of time. Unfortunately, the native <input type="range"> is probably too limited to be useful enough for most cases, and most users will want a multi-range slider that can filter both min and max values. That'll probably have to come from an external library because of how complicated multi-range sliders are, especially if you want them to be accessible and usable from a table cell with limited space.

MxNl commented 2 years ago

Hey, I am currently switching from DT to reactable due to some issues with DT. I am really impressed by reactable so far. I wanted to ask if you could add an example for a range slider for a date column. Thanks a lot

timelyportfolio commented 2 years ago

@glin this is amazing, and I really appreciate all the efforts that you have so generously expended on reactable. I thought what if we use the filter slot for something else. Here is a little example using a dataui sparkline. I think with a little more hacking we could connect the sparkline to the filter if desired.

reactable_filter_sparkline

library(htmltools)
library(reactable)
library(dataui) # remotes::install_github("timelyportfolio/dataui")

js <- tags$script(HTML(
"
// Custom range filter with value label
function rangeFilter(column, state) {
  // Get min and max values from raw table data
  const range = React.useMemo(() => {
    let min = Infinity
    let max = -Infinity
    state.data.forEach(row => {
      const value = row[column.id]
      if (value < min) {
        min = Math.floor(value)
      } else if (value > max) {
        max = Math.ceil(value)
      }
    })
    return [min, max]
  }, [state.data])

  const {pageRows} = state

  const data = {
    data: pageRows.map( d => ({
      x: { },
      y: d[column.id] 
    }))
  }

  const sparklineProps = {
    ariaLabel: 'sparkline bar plot of price',
    height: 100,
    margin: {left:20, top: 10, right: 40, bottom: 10}, 
    min: range[0],
    max: range[1]
  };

  // use hydrate from R reactR js tools to convert JSON object to React element
  //   for a more friendly R experience we could use dataui functions and sprintf
  const spk_hydrate = window.reactR.hydrate(
    dataui,
    {
      name: 'SparklineResponsive',
      attribs: {...sparklineProps, ...data},
      children: [
        {name: 'SparklineBarSeries', attribs: {fill: '#eebefa'}, children: []},
        {
          name: 'TooltipComponent',
          attribs: {},
          children: [
            {
              name: 'HorizontalReferenceLine',
              attribs: {
                'stroke': '#9c36b5',
                'strokeWidth': 1,
                'strokeDasharray': '3,3',
                'labelPosition': 'right',
                'labelOffset': 12,
                'renderLabel': d => d.toFixed(1)
              },
              children: []
            }
          ]
        }
      ]
    }
  )

  return spk_hydrate
}

// Filter method that filters numeric columns by minimum value
function filterMinValue(rows, columnId, filterValue) {
  // return all since not using filter cell for filtering
  return rows

  /*  old filter mechanism that we leave for legacy but will not use
  return rows.filter(function(row) {
    return row.values[columnId] >= filterValue
  })
  */
}
"
))

data <- MASS::Cars93[, c("Manufacturer", "Model", "Type", "Price")]

rt <- reactable(
  data,
  filterable = TRUE,
  columns = list(
    # we will use the filter cell for a sparkline
    Price = colDef(
      filterMethod = JS("filterMinValue"),
      filterInput = JS("rangeFilter")
    )
  ),
  defaultPageSize = 20
)

rt$dependencies <- list(html_dependency_dataui())

browsable(
  tagList(
    js,
    rt
  )
)
glin commented 2 years ago

@timelyportfolio Nice example. And yeah, using the filter slot for just arbitrary custom rendering is totally valid, and that had occurred to me as well. A better named feature might be a separate row of "sub headers", where you can render whatever you want just below the table headers. I could imagine wanting to show sparklines there while also keeping the default filter inputs.

Fluke95 commented 8 months ago

I like code provided by @timelyportfolio, however, I've encountered an issue with it. In some cases, filter range is invalid - instead of real max value from the provided data, it shows Infinity or some other (lower) value from the column. Here's an example: reactable-filterrange-issue

library(reactable)
library(htmltools)
example_data <- read.csv2("data.csv", sep = ",")

material_ui_range_filter_dependency_function <- function() {
  list(
    # Material UI requires React
    reactR::html_dependency_react(),
    # Material UI dependency
    htmltools::htmlDependency(
      name = "mui",
      version = "5.6.3",
      src = c(href = "https://unpkg.com/@mui/material@5.6.3/umd/"),
      script = "material-ui.production.min.js"
    ),
    # filter functions written in javascript
    htmltools::htmlDependency(
      name = "material_ui_range_filter",
      version = "0.1.0",
      src = c(file = here::here("inst/material_ui_range_filter")),
      script = "material-ui-range-filter.js",
      all_files = TRUE
    )
  )
}

browsable(
  tagList(
    material_ui_range_filter_dependency_function(),
    reactable(
      example_data,
      columns = list(
        Visibility.Score = colDef(
          filterable = TRUE,
          filterMethod = JS("filterRange"),
          filterInput = JS("muiRangeFilter")
        )
      ),
      defaultPageSize = 5,
      minRows = 5
    )
  ))

example_data$Visibility.Score |> summary()
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
    0.0    10.0    40.0   511.1   187.0 28081.0 
class(example_data$Visibility.Score)
[1] "integer"

File for error reproduction: data.csv material-ui-range-filter.js is copied from https://glin.github.io/reactable/articles/custom-filtering-extra.html