Skip to content

Commit

Permalink
inputs edits
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Feb 14, 2024
1 parent 4a75582 commit c7af7af
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 108 deletions.
12 changes: 11 additions & 1 deletion docs/javascript/display.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,14 @@ Inputs.button("Click me", {value: 0, reduce: (i) => displayThere(++i)})

## view(*element*)

The [`view` function](./inputs#viewelement) is a special type of display function that inserts the given DOM *element* (typically an input), then returns its corresponding value [generator](./generators) via [`Generators.input`](../lib/generators#input(element)). When the user interacts with the input, this triggers the [reactive evaluation](reactivity) of all the JavaScript code that reference this value.
The [`view` function](./inputs#view(element)) is a wrapper for `display` that returns a [value generator](./generators) for the given input element (rather than the input element itself). For example, below we display an input element and expose its value to the page as the variable `text`.

```js echo
const text = view(html`<input type="text" placeholder="Type something here">`);
```

```js echo
text // Try typing into the box above
```

When you type into the textbox, the generator will yield a new value, triggering the [reactive evaluation](./reactivity) of any code blocks that reference `text`. See [Inputs](./inputs) for more.
137 changes: 31 additions & 106 deletions docs/javascript/inputs.md
Original file line number Diff line number Diff line change
@@ -1,156 +1,81 @@
# JavaScript: Inputs

Inputs are user-interface elements that accept data from a user. In a data app, inputs might prompt a viewer to:
Inputs are graphical user interface elements such as dropdowns, radios, sliders, and text boxes that accept data from a user and enable interaction via [reactivity](./reactivity). They can also be custom elements that you design, such as charts that support interactive selection via pointing or brushing.

- Select a URL from a dropdown list to view site traffic for a specific page
- Interactively subset a table of users by typing in a domain name
- Choose a date range to explore software downloads over a period of interest
Inputs might prompt a viewer to:

Inputs can be displayed with the [`view`](#view(element)) function, which is a special type of [display](display) function that additionally returns the input’s value generator, which can then be assigned to a variable for use elsewhere.
- Filter a table of users by typing in a name
- Select a URL from a dropdown to view traffic to a specific page
- Choose a date range to explore data within a period of interest

For example, the radio input below prompts a user to select one from a series of values:
Inputs are typically displayed using the built-in [`view`](#view(element)) function, which [displays](./display) the given element and returns a [value generator](./generators). The generator can then be declared as a [top-level variable](./reactivity) to expose the input’s value to the page. For example, the radio input below prompts the user to select their favorite team:

```js echo
const team = view(Inputs.radio(["Metropolis Meteors", "Rockford Peaches", "Bears"], {label: "Favorite team:", value: "Metropolis Meteors"}));
```

The `team` variable in this example now reactively updates when the user interacts with the radio input, triggering a new evaluation of the dependent code. Select different teams in the radio input above to update the text.
The `team` variable here will reactively update when the user interacts with the radio input, triggering re-evaluation of referencing code blocks. Select different teams in the radio input above to update the text.

```md
My favorite baseball team is the ${team}!
```

```md run=false
My favorite baseball team is the ${team}!

## view(*element*)

The `view` function used above does two things: (1) it displays the given DOM *element*, then (2) returns its corresponding value [generator](./generators), using [`Generators.input`](../lib/generators#input(element)) under the hood. Use `view` to display an input while also exposing the input’s value as a [reactive variable](./reactivity). You can reference the input’s value anywhere, and the code will run whenever the input changes.

The `view` function is not limited to Observable Inputs. For example, here is a simple range slider created with [html](../lib/htl):

```js echo
const topn = view(html`<input type=range step=1 min=1 max=15 value=10>`);
```

Now we can reference `topn` elsewhere, for example to control how many groups are displayed in a sorted bar chart:

```js echo
Plot.plot({
marginLeft: 50,
marks: [
Plot.barX(olympians, Plot.groupY({x: "count"}, {y: "nationality", sort: {y: "x", reverse: true, limit: topn}})),
Plot.ruleX([0])
]
})
```

## Observable Inputs

The [Observable Inputs](../lib/inputs) library implements commonly used inputs — buttons, sliders, dropdowns, tables, and the like — as functions. Each input function returns an HTML element that emits *input* events for compatibility with [`view`](#view(element)) and [Generators.input](../lib/generators#input(element)).

### Usage

Inputs are generally declared as follows:

```js run=false
const value = view(Inputs.inputName(...));
```

where *value* is the named input value, and *inputName* is the input name (like `radio`, `button`, or `checkbox`). See the full list of [available inputs](../lib/inputs) with live examples, or visit the [Observable Inputs API reference](https://github.com/observablehq/inputs/blob/main/README.md) for more detail and specific input options.

Options differ between inputs. For example, the checkbox input accepts options to disable all or certain values, sort displayed values, and only display repeated values *once* (among others):

```js echo
const checkout = view(Inputs.checkbox(["B","A","Z","Z","F","D","G","G","G","Q"], {disabled: ["F", "Q"], sort: true, unique: true, value: "B", label: "Choose categories:"}));
```

```js echo
checkout
```

### Analysis with Observable Inputs

To demonstrate Observable Inputs for data analysis, we’ll use the `olympians` sample dataset containing records on athletes that participated in the 2016 Rio olympics (from [Matt Riggott](https://flother.is/2017/olympic-games-data/)).
You can implement custom inputs using arbitrary HTML. For example, here is a [range input](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range) that lets you choose an integer between 1 and 15 (inclusive):

```js echo
Inputs.table(olympians)
const n = view(html`<input type=range step=1 min=1 max=15>`);
```

Here, we create a subset of columns to simplify outputs:

```js echo
const columns = olympians.columns.slice(1, -1); // hide the id and info column to simplify
n // Try dragging the slider above
```

Now let’s wire up our data to a search input. Type whatever you want into the box and search will find matching rows in the data which we can then use in a table below.
<div class="tip">To be compatible with <code>view</code>, custom inputs must emit <code>input</code> events and expose their current value as <i>element</i>.value. See <a href="../lib/generators#input(element)"><code>Generators.input</code></a> for more.</div>

A few examples to try: **[mal]** will match *sex* = male, but also names that start with “mal”, such as Anna Malova; **[1986]** will match anyone born in 1986 (and a few other results); **[USA gym]** will match USA’s gymnastics team. Each space-separated term in your query is prefix-matched against all columns in the data.
More often, you’ll use a helper library such as [Observable Inputs](../lib/inputs) or [Observable Plot](../lib/plot) to declare inputs. For example, here is [`Inputs.range`](../lib/inputs#range):

```js echo
const search = view(Inputs.search(olympians, {
datalist: ["mal", "1986", "USA gym"],
placeholder: "Search athletes"
}))
const m = view(Inputs.range([1, 15], {label: "Favorite number", step: 1}));
```

```js echo
Inputs.table(search, {columns})
m // Try dragging the slider above
```

If you like, you can sort the table columns by clicking on the column name. Click once to sort ascending, and click again to sort descending. Note that the sort order is temporary: it’ll go away if you reload the page. Specify the column name as the *sort* option above if you want this order to persist.

For a more structured approach, we can use a select input to choose a sport, then *array*.filter to determine which rows are shown in the table. The *sort* and *unique* options tell the input to show only distinct values and to sort them alphabetically.
To use a chart as an input, you can use Plot’s [pointer interaction](https://observablehq.com/plot/interactions/pointer), say by setting the **tip** option on a mark. In the scatterplot below, the penguin closest to the pointer is exposed as the reactive variable `penguin`.

```js echo
const sport = view(Inputs.select(olympians.map(d => d.sport), {sort: true, unique: true, label: "sport"}));
const penguin = view(Plot.dot(penguins, {x: "culmen_length_mm", y: "flipper_length_mm", tip: true}).plot());
```

```js echo
const selectedAthletes = display(olympians.filter(d => d.sport === sport));
penguin
```

```js echo
Inputs.table(selectedAthletes, {columns})
```
In the future, Plot will support more interaction methods, including brushing. Please upvote [#5](https://github.com/observablehq/plot/issues/5) if you are interested in this feature.

To visualize a column of data as a histogram, use the value of the select input with [Observable Plot](https://observablehq.com/plot/).
## view(*element*)

```js echo
Plot.plot({
x: {
domain: [1.3, 2.2]
},
marks: [
Plot.rectY(selectedAthletes, Plot.binX({y: "count"}, {x: "height", fill: "steelblue"})),
Plot.ruleY([0])
]
})
```
The `view` function used above does two things:

You can also pass grouped data to the select input as a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) from key to array of values, say using [d3.group](https://d3js.org/d3-array/group). The value of the select input in this mode is the data in the selected group. Note that *unique* is no longer required, and that *sort* works here, too, sorting the keys of the map returned by d3.group.
1. it [displays](./display) the given DOM *element*, and then
2. returns its corresponding [value generator](./generators).

```js echo
const groups = display(d3.group(olympians, d => d.sport));
```
The `view` function uses [`Generators.input`](../lib/generators#input(element)) under the hood. You can also call `Generators.input` directly, say if you want to declare the input as a top-level variable while placing it elsewhere on the page, say using an [inline expression](../javascript#inline-expressions).

```js echo
const sportAthletes = view(Inputs.select(groups, {sort: true, label: "sport"}));
const nameInput = html`<input type="text" placeholder="anonymous">`;
const name = Generators.input(nameInput);
```

```js echo
Inputs.table(sportAthletes, {columns})
```
Enter your name: ${nameInput}.

The select input works well for categorical data, such as sports or nationalities, but how about quantitative dimensions such as height or weight? Here’s a range input that lets you pick a target weight; we then filter the table rows for any athlete within 10% of the target weight. Notice that some columns, such as sport, are strongly correlated with weight.
Hi ${name || "anonymous"}!

```js echo
const weight = view(Inputs.range(d3.extent(olympians, d => d.weight), {step: 1, label: "weight (kg)"}));
```
```md run=false
Enter your name: ${nameInput}.

```js echo
Inputs.table(olympians.filter(d => d.weight < weight * 1.1 && weight * 0.9 < d.weight), {sort: "weight", columns})
Hi ${name || "anonymous"}!
```

### License

Observable Inputs are released under the [ISC license](https://github.com/observablehq/inputs/blob/main/LICENSE) and depend only on [Hypertext Literal](../lib/htl), our tagged template literal for safely generating dynamic HTML. If you are interested in contributing or wish to report an issue, please see [our repository](https://github.com/observablehq/inputs). For recent changes, please see our [release notes](https://github.com/observablehq/inputs/releases).
2 changes: 1 addition & 1 deletion docs/lib/generators.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {Generators} from "npm:@observablehq/stdlib";

## input(*element*)

Returns an async generator that yields whenever the given *element* emits an *input* event, with the given *element*’s current value. (It’s a bit fancier than that because we special-case a few element types.) The built-in [`view` function](<../javascript/display#view(element)>) uses this.
Returns an async generator that yields whenever the given *element* emits an *input* event, with the given *element*’s current value. (It’s a bit fancier than that because we special-case a few element types.) The built-in [`view` function](<../javascript/inputs#view(element)>) uses this.

```js echo
const nameInput = display(document.createElement("input"));
Expand Down
107 changes: 107 additions & 0 deletions docs/lib/inputs.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Observable Inputs

Observable Inputs implements commonly used inputs — buttons, sliders, dropdowns, tables, and the like — as functions. Each input function returns an HTML element that emits *input* events for compatibility with [`view`](#view(element)) and [Generators.input](../lib/generators#input(element)).

[Observable Inputs](https://github.com/observablehq/inputs) provides “lightweight interface components — buttons, sliders, dropdowns, tables, and the like — to help you explore data and build interactive displays.” Observable Inputs is available by default as `Inputs` in Markdown, but you can import it explicitly like so:

```js echo
Expand All @@ -12,6 +14,111 @@ Or, just import the specific inputs you want:
import {Button, Color} from "npm:@observablehq/inputs";
```

### Usage

Inputs are generally declared as follows:

```js run=false
const value = view(Inputs.inputName(...));
```

where *value* is the named input value, and *inputName* is the input name (like `radio`, `button`, or `checkbox`). See the full list of [available inputs](../lib/inputs) with live examples, or visit the [Observable Inputs API reference](https://github.com/observablehq/inputs/blob/main/README.md) for more detail and specific input options.

Options differ between inputs. For example, the checkbox input accepts options to disable all or certain values, sort displayed values, and only display repeated values *once* (among others):

```js echo
const checkout = view(Inputs.checkbox(["B","A","Z","Z","F","D","G","G","G","Q"], {disabled: ["F", "Q"], sort: true, unique: true, value: "B", label: "Choose categories:"}));
```

```js echo
checkout
```

### Analysis with Observable Inputs

To demonstrate Observable Inputs for data analysis, we’ll use the `olympians` sample dataset containing records on athletes that participated in the 2016 Rio olympics (from [Matt Riggott](https://flother.is/2017/olympic-games-data/)).

```js echo
Inputs.table(olympians)
```

Here, we create a subset of columns to simplify outputs:

```js echo
const columns = olympians.columns.slice(1, -1); // hide the id and info column to simplify
```

Now let’s wire up our data to a search input. Type whatever you want into the box and search will find matching rows in the data which we can then use in a table below.

A few examples to try: **[mal]** will match *sex* = male, but also names that start with “mal”, such as Anna Malova; **[1986]** will match anyone born in 1986 (and a few other results); **[USA gym]** will match USA’s gymnastics team. Each space-separated term in your query is prefix-matched against all columns in the data.

```js echo
const search = view(Inputs.search(olympians, {
datalist: ["mal", "1986", "USA gym"],
placeholder: "Search athletes"
}))
```

```js echo
Inputs.table(search, {columns})
```

If you like, you can sort the table columns by clicking on the column name. Click once to sort ascending, and click again to sort descending. Note that the sort order is temporary: it’ll go away if you reload the page. Specify the column name as the *sort* option above if you want this order to persist.

For a more structured approach, we can use a select input to choose a sport, then *array*.filter to determine which rows are shown in the table. The *sort* and *unique* options tell the input to show only distinct values and to sort them alphabetically.

```js echo
const sport = view(Inputs.select(olympians.map(d => d.sport), {sort: true, unique: true, label: "sport"}));
```

```js echo
const selectedAthletes = display(olympians.filter(d => d.sport === sport));
```

```js echo
Inputs.table(selectedAthletes, {columns})
```

To visualize a column of data as a histogram, use the value of the select input with [Observable Plot](https://observablehq.com/plot/).

```js echo
Plot.plot({
x: {
domain: [1.3, 2.2]
},
marks: [
Plot.rectY(selectedAthletes, Plot.binX({y: "count"}, {x: "height", fill: "steelblue"})),
Plot.ruleY([0])
]
})
```

You can also pass grouped data to the select input as a [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) from key to array of values, say using [d3.group](https://d3js.org/d3-array/group). The value of the select input in this mode is the data in the selected group. Note that *unique* is no longer required, and that *sort* works here, too, sorting the keys of the map returned by d3.group.

```js echo
const groups = display(d3.group(olympians, d => d.sport));
```

```js echo
const sportAthletes = view(Inputs.select(groups, {sort: true, label: "sport"}));
```

```js echo
Inputs.table(sportAthletes, {columns})
```

The select input works well for categorical data, such as sports or nationalities, but how about quantitative dimensions such as height or weight? Here’s a range input that lets you pick a target weight; we then filter the table rows for any athlete within 10% of the target weight. Notice that some columns, such as sport, are strongly correlated with weight.

```js echo
const weight = view(Inputs.range(d3.extent(olympians, d => d.weight), {step: 1, label: "weight (kg)"}));
```

```js echo
Inputs.table(olympians.filter(d => d.weight < weight * 1.1 && weight * 0.9 < d.weight), {sort: "weight", columns})
```

## Basic inputs

These basic inputs will get you started.

* [Button](#button) - do something when a button is clicked
Expand Down

0 comments on commit c7af7af

Please sign in to comment.