Multiple filter conditions on csv with Input.select

Hello again,

Apologies if this has been asked before, I was wondering how I can filter my dataset using multiple selection options. A user would select several of these and the data plotted would only be where all the conditions match. I’ve been able to do this by “hardcoding” my expectation, but wondering if there is a way to do this more elegantly.

I see that your cells don’t have a viewof prefix. Just to tick off a few boxes so that I can give a better answer:

  • Are you asking about notebooks or Framework projects?
  • Are you familiar with Observable’s viewof macro (for notebooks) or with view() (for Framework)?
  • Have you come across Inputs.form() before?

I’m working primarily in Framework, but I’m trying to replicate to whatever extent I can in notebooks to quickly see errors and iterate.

This is what I ended up doing in Framework for a single filter

const a = FileAttachment("./data/test.csv").csv({typed: true});
const aggressor = view(Inputs.select(d3.group(await a, d => d.Aggressor)));

In my plot, I call the aggressor constant:

  x: {grid: true, label: "Measurement Number", labelAnchor: "center", anchor: "left"},
  y: {grid: true, label: "Frequency Drift (Hz)"},
  marks: [
    Plot.ruleY([0]),
    Plot.line(aggressor, {fy: "Measurement", x: "Measurement_number", y: "Measurement Value", z: "SerialNumber", stroke: "TCXO",  tip: true})
  ]
}))

}

As for “view”, I believe that is what I was trying to do in Framework, but perhaps I’ve done it incorrectly.

I haven’t explored Inputs.form() before, I can look into this.

Inputs.form() lets you compose multiple inputs into one. You can technically do the same yourself, but it removes some boilerplate for you.

That in turn makes it easier to create data-driven filters. Here’s a basic example using the built-in penguins dataset:

~~~js
const prop = (arr, name) => arr.map(d => d[name]);
const unique = (arr) => [...new Set(arr)];
~~~

~~~js
const data = penguins;
const choices = view(Inputs.form({
  species: Inputs.checkbox(unique(prop(data, "species")), {label: "Species"}),
  island: Inputs.checkbox(unique(prop(data, "island")), {label: "Island"}),
  sex: Inputs.checkbox(unique(prop(data, "sex")), {label: "Sex"}),
}));
~~~

~~~js
const filters = Object.entries(choices).map(
  ([name, values]) => values.length
    ? d => values.some(v => v === d[name])
    : () => true
  );
const filtered = data.filter(d => filters.every(f => f(d)));
display(Inputs.table(filtered));
~~~

Thanks a lot! I’ll try this out. Sorry, one quick question, I assume this is for functions that you’ve defined that can be used but when I put this in my .md file I get the following error:

const prop = (arr, name) => arr.map(d => d[name]);
const unique = (arr) => [...new Set(arr)];

TypeError: arr.map is not a function. (In ‘arr.map(d => d[name])’, ‘arr.map’ is undefined)

Am I doing something fundamentally wrong?

Remember Error grouping data by csv column - #2 by mootari ? :wink:

I’d recommend to put your const a declaration into a separate fenced code block so that the Runtime will resolve the Promise for you. I didn’t need to do that for penguins because Framework already takes care of the loading and resolving for built-in datasets.

1 Like

Ugh! Sorry! Fixed it now, it’s a lot clearer, thanks so much.

@mootari Sorry, I’ll keep digging into this, but here’s where I am now.

TypeError: values.some is not a function. (In ‘values.some(v => v === d[name])’, ‘values.some’ is undefined)

The example is tailored to checkboxes where each input returns an array of values. Selects only return a single value, so you will have to adjust the filter callbacks accordingly:

const filters = Object.entries(choices).map(
  ([name, value]) => Array.isArray(value)
    ? value.length ? d => value.some(v => v === d[name]) : () => true
    : value !== undefined ? d => value === d[name] : () => true
  );
1 Like

Is there a much of a reason to use Inputs.form in Observable Framework? I feel that’s mostly a workaround for notebooks where each cell can only expose a single value.

In Framework you can call view multiple times to declare separate reactive values within the same code block. And the advantage is that these can update independently (rather than with Inputs.form where the entire form updates, potentially causing more downstream evaluation than necessary).

So why not do this:

const species = view(Inputs.checkbox(penguins.map((d) => d.species), {label: "Species", unique: true}));
const island = view(Inputs.checkbox(penguins.map((d) => d.island), {label: "Island", unique: true}));
const sex = view(Inputs.checkbox(penguins.map((d) => d.sex), {label: "Sex", unique: true}));

This also uses the unique option of Inputs.checkbox so you don’t have to do that yourself. (And I prefer to inline the array.map instead of writing the prop helper.)

Imo yes, to generate inputs from configurations:

My example above only hints at that because I didn’t want to complicate it unnecessarily.

TIL! :pray:

Ah, sure. But that’s a pretty rare use case where you don’t know the set of inputs/dynamic variables statically, and I don’t recommend such indirection in most cases.