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

Range filtering #9

Open
nistara opened this issue Nov 18, 2019 · 25 comments
Open

Range filtering #9

nistara opened this issue Nov 18, 2019 · 25 comments
Labels
enhancement New feature or request

Comments

@nistara
Copy link

nistara commented Nov 18, 2019

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
Copy link
Owner

glin commented Nov 20, 2019

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
Copy link

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
Copy link

@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
Copy link
Author

nistara commented Nov 21, 2019

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
Copy link
Owner

glin commented Nov 22, 2019

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

@leungi
Copy link

leungi commented Dec 4, 2019

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

@liberrenaud
Copy link

liberrenaud commented Jan 21, 2020

@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
Copy link

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
Copy link

Hey @tyluRp

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

@tylerlittlefield
Copy link

@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
Copy link

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
Copy link

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

@jlfitz
Copy link

jlfitz commented Oct 5, 2020

@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.

  • range filter for dates and numerics
  • dropdowns for character columns with a small number of values

@mihirp161
Copy link

+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
Copy link

timelyportfolio commented May 21, 2021

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
Copy link

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
Copy link

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

@timelyportfolio
Copy link

timelyportfolio commented Jan 9, 2022

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 timelyportfolio@fae87a5#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/[email protected]/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
Copy link

januz commented Feb 18, 2022

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
Copy link

yogat3ch commented Apr 12, 2022

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

@glin
Copy link
Owner

glin commented May 2, 2022

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.

@glin glin added the enhancement New feature or request label May 2, 2022
@MxNl
Copy link

MxNl commented Jun 28, 2022

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
Copy link

@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
Copy link
Owner

glin commented Sep 25, 2022

@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
Copy link

Fluke95 commented Feb 29, 2024

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/[email protected]/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

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

No branches or pull requests