observablehq / inputs

Better input elements
https://observablehq.com/framework/lib/inputs
ISC License
127 stars 34 forks source link

Input filters to filter multiple values from an underlying dataset #232

Closed novotny1akub closed 2 years ago

novotny1akub commented 2 years ago

Is there any built-in way to filter an underlying dataset by mulltiple values coming from an input filter? Something like the IN clause in SQL. An example of what I mean by that below. The filtering is done using a custom function in_array, but I was just wondering if I am not reinventing the wheel.

{ojs}
d3 = require("d3@6")

// dummy data
cars = d3.csvParse(`name,cyl,mpg
car1,2,21.4
car2,4,18
car2,6,54
car3,2,18.6
`)

// filter for the car name
viewof name_filter = Inputs.select(
  cars.map(d => d.name),
  {value: [...new Set(cars.map(d => d.name))],
  label: "Car Name", multiple: true, sort: true, unique: true}
  )
// mask for the filter (array of booleans)
name_mask = in_array(cars.map(d => d.name), name_filter)

// filter for the number of cylinders
viewof cyl_filter = Inputs.select(
  cars.map(d => d.cyl),
  {value: [...new Set(cars.map(d => d.cyl))],
  label: "Number of Cylinders", multiple: true, sort: true, unique: true}
  )
cyl_mask = in_array(cars.map(d => d.cyl), cyl_filter)

// applying the filters above
cars_filtered = cars.
  filter((name, i) => name_mask[i]).
  filter((cyl, i) => cyl_mask[i])

Inputs.table(cars_filtered)

// function that allow the filtering of multiple arrays
function in_array(arr1, arr2) {
  var arr_out = new Array(arr1.length).fill(false)
  for(let i = 0; i < arr1.length; ++i) {
    for(let j = 0; j < arr2.length; ++j) {
        if(arr1[i] === arr2[j]) {
            arr_out[i] = true;
        }
    }
  }
  return arr_out
}
enjalot commented 2 years ago

I think you could simplify your code if you did your filter in one function:

cars_filtered = cars.filter(d => name_filter.indexOf(d.name) >= 0 && cyl_filter.indexOf(d.cylinder) >= 0)
novotny1akub commented 2 years ago

I tried to change the code based on your suggestion, but it does not seem to be working. Is this what you had in mind? It does not return any table not only when selecting multiple values, but even when selecting single value in both filters.

d3 = require("d3@6")

// dummy data
cars = d3.csvParse(`name,cyl,mpg
car1,2,21.4
car2,4,18
car2,6,54
car3,2,18.6
`)

// filter for the car name
viewof name_filter = Inputs.select(
  cars.map(d => d.name),
  {value: [...new Set(cars.map(d => d.name))],
  label: "Car Name", multiple: true, sort: true, unique: true}
  )

// filter for the number of cylinders
viewof cyl_filter = Inputs.select(
  cars.map(d => d.cyl),
  {value: [...new Set(cars.map(d => d.cyl))],
  label: "Number of Cylinders", multiple: true, sort: true, unique: true}
  )

// applying the filters above
cars_filtered = cars.filter(d => name_filter.indexOf(d.name) >= 0 && cyl_filter.indexOf(d.cylinder) >= 0)

Inputs.table(cars_filtered)
enjalot commented 2 years ago

I think it's because in your code it should be d.cyl instead of d.cylinder I've added this example to a notebook: https://observablehq.com/d/97e99dde46096bf5#cell-17

novotny1akub commented 2 years ago

I like your simplification a lot @enjalot thanks!