Skip to content

andrewngu/notebook-stdlib

 
 

Repository files navigation

@observablehq/notebook-stdlib

The Observable notebook standard library.

For examples, see https://beta.observablehq.com/@mbostock/standard-library.

API Reference

  • DOM - create HTML and SVG elements.
  • Files - read local files into memory.
  • Generators - utilities for generators and iterators.
  • Promises - utilities for promises.
  • require - load third-party libraries.
  • html - render HTML.
  • md - render Markdown.
  • svg - render SVG.
  • tex - render LaTeX.
  • now - the current value of Date.now.
  • width - the current page width.
  • invalidation - dispose resources.
  • visibility - wait for visibility.

DOM

# DOM.canvas(width, height) <>

Returns a new canvas element with the specified width and height. For example, to create a 960×500 canvas:

DOM.canvas(960, 500)

This is equivalent to using the html tagged template literal:

html`<canvas width=960 height=500>`

If you are using 2D Canvas (rather than WebGL), you should use DOM.context2d instead of DOM.canvas for automatic pixel density scaling.

# DOM.context2d(width, height[, dpi]) <>

Returns a new canvas context with the specified width and height and the specified device pixel ratio dpi. If dpi is not specified, it defaults to window.devicePixelRatio. To access the context’s canvas, use context.canvas. For example, to create a 960×500 canvas:

{
  const context = DOM.context2d(960, 500);
  return context.canvas;
}

If you are using WebGL (rather than 2D Canvas), you should use DOM.canvas or the html tagged template literal instead of DOM.context2d.

# DOM.download(object[, name][, value]) <>

Returns an anchor element containing a button that when clicked will download a file representing the specified object. The object should be anything supported by URL.createObjectURL such as a file or a blob. For example, to create a button to download a Canvas element as a PNG:

DOM.download(await new Promise(resolve => canvas.toBlob(resolve)))

# DOM.element(name[, attributes]) <>

Returns a new element with the specified name. For example, to create an empty H1 element:

DOM.element("h1")

This is equivalent to using the html tagged template literal:

html`<h1>`

If attributes is specified, sets any attributes in the specified object before returning the new element. For example:

DOM.element("a", {target: "_blank"})

This is equivalent to using the html tagged template literal:

html`<a target=_blank>`

If the name has the prefix svg:, math: or xhtml:, uses document.createElementNS instead of document.createElement. For example, to create an empty SVG element (see also DOM.svg):

DOM.element("svg:svg")

This is equivalent to using the svg (or html) tagged template literal:

svg`<svg>`

In general, you probably want to use the html or svg tagged template literal instead of DOM.element.

# DOM.input([type]) <>

Returns a new input element with the specified type. If type is not specified or null, a text input is created. For example, to create a new file input:

DOM.input("file")

This is equivalent to using the html tagged template literal:

html`<input type=file>`

In general, you probably want to use the html tagged template literal instead of DOM.input.

# DOM.range([min, ][max][, step]) <>

Returns a new range input element. (See also DOM.input.) If max is specified, sets the maximum value of the range to the specified number; if max is not specified or null, sets the maximum value of the range to 1. If min is specified, sets the minimum value of the range to the specified number; if min is not specified or null, sets the minimum value of the range to 0. If step is specified, sets the step value of the range to the specified number; if step is not specified or null, sets the step value of the range to any. For example, to create a slider that ranges the integers from -180 to +180, inclusive:

DOM.range(-180, 180, 1)

This is equivalent to using the html tagged template literal:

html`<input type=range min=-180 max=180 step=1>`

In general, you probably want to use the html tagged template literal instead of DOM.input.

# DOM.select(values) <>

Returns a new select element with an option for each string in the specified values array. For example, to create a drop-down menu of three colors:

DOM.select(["red", "green", "blue"])

This is equivalent to using the html tagged template literal:

html`<select>
  <option value="red">red</option>
  <option value="green">green</option>
  <option value="blue">blue</option>
</select>`

Or, from an array of data:

html`<select>${colors.map(color => `
  <option value="${color}">${color}</option>`)}
</select>`

In general, you probably want to use the html tagged template literal instead of DOM.select, particularly if you want greater control of the display, such as to customize the displayed option labels.

# DOM.svg(width, height) <>

Returns a new SVG element with the specified width and height. For example, to create a 960×500 blank SVG:

DOM.svg(960, 500)

This is equivalent to using the svg tagged template literal:

svg`<svg width=960 height=500 viewBox="0,0,960,500">`

To create responsive SVG, set the max-width to 100% and the height to auto:

svg`<svg
  width=960
  height=500
  viewBox="0,0,960,500"
  style="max-width:100%;height:auto;"
>`

In general, you probably want to use the html or svg tagged template literal instead of DOM.svg.

# DOM.text(string) <>

Returns a new text node with the specified string value. For example, to say hello:

DOM.text("Hello, world!")

This is equivalent to using the html tagged template literal:

html`Hello, world!`

In general, you probably want to use the html tagged template literal instead of DOM.text.

# DOM.uid([name]) <>

Returns a new unique identifier. If name is specified, the identifier.id will be derived from the specified name, which may be useful for debugging. If DOM.uid is called repeatedly with the same name, every returned identifier is still unique (that is, different). Identifiers are useful in SVG: use identifier.href for IRI references, such as the xlink:href attribute; use identifier.toString for functional notation, such as the clip-path presentation attribute.

For example, to clip the Mona Lisa to a circle of radius 320px:

{
  const clip = DOM.uid("clip");
  return svg`<svg width="640" height="640">
  <defs>
    <clipPath id="${clip.id}">
      <circle cx="320" cy="320" r="320"></circle>
    </clipPath>
  </defs>
  <image
    clip-path="${clip}"
    width="640" height="640"
    preserveAspectRatio="xMidYMin slice"
    xlink:href="https://gist.githubusercontent.com/mbostock/9511ae067889eefa5537eedcbbf87dab/raw/944b6e5fe8dd535d6381b93d88bf4a854dac53d4/mona-lisa.jpg"
  ></image>
</svg>`;
}

The use of DOM.uid is strongly recommended over hand-coding as it ensures that your identifiers are still unique if your code is imported into another notebook. Because identifier.href and identifier.toString return absolute rather than local IRIs, it also works well in conjunction with a notebook’s base URL.

Files

See Reading Local Files for examples.

# Files.buffer(file) <>

Reads the specified file, returning a promise of the ArrayBuffer yielded by fileReader.readAsArrayBuffer. This is useful for reading binary files, such as shapefiles and ZIP archives.

# Files.text(file) <>

Reads the specified file, returning a promise of the string yielded by fileReader.readAsText. This is useful for reading text files, such as plain text, CSV, Markdown and HTML.

# Files.url(file) <>

Reads the specified file, returning a promise of the data URL yielded by fileReader.readAsDataURL. This is useful for reading a file into memory, represented as a data URL. For example, to display a local file as an image:

Files.url(file).then(url => {
  var image = new Image;
  image.src = url;
  return image;
})

A data URL may be significantly less efficient than URL.createObjectURL method for large files. For example:

{
  const image = new Image;
  image.src = URL.createObjectURL(file);
  invalidation.then(() => URL.revokeObjectURL(image.src));
  return image;
}

Generators

# Generators.disposable(value, dispose) <>

Returns a new generator that yields the specified value exactly once. The generator.return method of the generator will call the specified dispose function, passing in the specified value. When this generator is the return value of a cell, this allows resources associated with the specified value to be disposed automatically when a cell is re-evaluated: generator.return is called by the Observable runtime on invalidation. For example, to define a cell that creates a self-disposing Tensor:

x = Generators.disposable(tf.tensor2d([[0.0, 2.0], [4.0, 6.0]]), x => x.dispose())

See also invalidation.

# Generators.filter(iterator, test) <>

Returns a generator that yields a subset of values from the specified iterator, if and only if the specified test function returns truthy for the given value. The test function is invoked with the current value from the iterator and the current index, starting at 0 and increasing by one. For example, to yield only odd integers in [0, 100]:

x = Generators.filter(Generators.range(100), x => x & 1)

This method assumes that the specified iterator is synchronous; if the iterator yields a promise, this method does not wait for the promise to resolve before continuing. If the specified iterator is a generator, this method also does not (currently) wrap the specified generator’s return and throw methods.

# Generators.input(input) <>

Returns a new generator that yields promises to the current value of the specified input element; each promise resolves when the input element emits an event. (The promise resolves when the event is emitted, even if the value of the input is unchanged.) If the initial value of the input is not undefined, the returned generator’s first yielded value is a resolved promise with the initial value of the input.

The type of event that triggers promise resolution depends on the input.type as follows:

  • For button, submit and checkbox inputs, click events.
  • For file inputs, change events.
  • For all other types, input events.

The resolved value is likewise dependent on the input.type as follows:

  • For range and number inputs, input.valueAsNumber.
  • For date inputs, input.valueAsDate.
  • For checkbox inputs, input.checked.
  • For single-file inputs (input.multiple is falsey), input.files[0].
  • For multi-file inputs (input.multiple is truthy), input.files.
  • For all other types, input.value.

The specified input need not be an HTMLInputElement, but it must support the target.addEventListener and target.removeEventListener methods of the EventTarget interface.

Generators.input is used by Observable’s viewof operator to define the current value of a view, and is based on Generators.observe. One often does not use Generators.input directly, but it can be used to define a generator cell exposing the current value of an input, and you can also read the yielded values by hand. For example, to accumulate the first four values:

{
  const values = [];
  for (const value of Generators.input(element)) {
    if (values.push(await value) >= 4) {
      return values;
    }
  }
}

Generators.input is lossy and may skip values: if more than one event is emitted before the next promise is pulled from the generator (more than once per animation frame), then the next promise returned by the generator will be resolved with the latest input value, potentially skipping intermediate values. See Generators.queue for a non-debouncing generator.

# Generators.map(iterator, transform) <>

Returns a generator that yields transformed values from the specified iterator, applying the specified transform function to each value. The transform function is invoked with the current value from the iterator and the current index, starting at 0 and increasing by one. For example, to yield perfect squares:

x = Generators.map(Generators.range(100), x => x * x)

This method assumes that the specified iterator is synchronous; if the iterator yields a promise, this method does not wait for the promise to resolve before continuing. If the specified iterator is a generator, this method also does not (currently) wrap the specified generator’s return and throw methods.

# Generators.observe(initialize) <>

Returns a generator that yields promises to an observable value, adapting a push-based data source (such as an Observable, an EventEmitter or an EventTarget) to a pull-based one.

The specified initialize function is invoked before Generators.observe returns, being passed a change function; calling change triggers the resolution of the current promise with the passed value. The initialize function may also return a dispose function; this function will be called when the generator is disposed. (See invalidation.)

For example, to observe the current value of a text input element, you might say:

Generators.observe(change => {

  // An event listener to yield the element’s new value.
  const inputted = () => change(element.value);

  // Attach the event listener.
  element.addEventListener("input", inputted);

  // Yield the element’s initial value.
  change(element.value);

  // Detach the event listener when the generator is disposed.
  return () => element.removeEventListener("input", inputted);
})

(See also Generators.input.)

Generators.observe is typically used to define a generator cell, but you can also read the yielded values by hand. For example, to accumulate the first four values:

{
  const generator = Generators.observe();
  const values = [];
  for (const value of generator) {
    if (values.push(await value) >= 4) {
      return values;
    }
  }
}

Generators.observe is lossy and may skip values: if change is called more than once before the next promise is pulled from the generator (more than once per animation frame), then the next promise returned by the generator will be resolved with the latest value passed to change, potentially skipping intermediate values. See Generators.queue for a non-debouncing generator.

# Generators.queue(initialize) <>

Returns a generator that yields promises to an observable value, adapting a push-based data source (such as an Observable, an EventEmitter or an EventTarget) to a pull-based one. The specified initialize function is invoked before Generators.queue returns, being passed a change function; calling change triggers the resolution of the current promise with the passed value. The initialize function may also return a dispose function; this function will be called when the generator is disposed. (See invalidation.)

For example, to observe the value of a text input element, you might say:

Generators.queue(change => {

  // An event listener to yield the element’s new value.
  const inputted = () => change(element.value);

  // Attach the event listener.
  element.addEventListener("input", inputted);

  // Yield the element’s initial value.
  change(element.value);

  // Detach the event listener when the generator is disposed.
  return () => element.removeEventListener("input", inputted);
})

(See also Generators.input.)

Generators.queue is typically used to define a generator cell, but you can also read the yielded values by hand. For example, to accumulate the first four values:

{
  const generator = Generators.queue();
  const values = [];
  for (const value of generator) {
    if (values.push(await value) >= 4) {
      return values;
    }
  }
}

Generators.queue is non-lossy and, as a result, may yield “stale” values: if change is called more than once before the next promise is pulled from the generator (more than once per animation frame), the passed values are queued in order and the generator will return resolved promises until the queue is empty again. See Generators.observe for a debouncing generator.

# Generators.range([start, ]stop[, step]) <>

Returns a generator yielding an arithmetic progression, similar to the Python built-in range. This method is often used to iterate over a sequence of uniformly-spaced numeric values, such as the indexes of an array or the ticks of a linear scale. (See also d3.range.)

For example, to iterator over the integers from 0 to 100:

i = {
  for (const i of Generators.range(0, 100, 1)) {
    yield i;
  }
}

Or more simply:

i = Generators.range(100)

If step is omitted, it defaults to 1. If start is omitted, it defaults to 0. The stop value is exclusive; it is not included in the result. If step is positive, the last element is the largest start + i * step less than stop; if step is negative, the last element is the smallest start + i * step greater than stop. If the returned array would contain an infinite number of values, an empty range is returned.

The arguments are not required to be integers; however, the results are more predictable if they are. The values in the returned array are defined as start + i * step, where i is an integer from zero to one minus the total number of elements in the returned array. For example:

Generators.range(0, 1, 0.2) // 0, 0.2, 0.4, 0.6000000000000001, 0.8

This unexpected behavior is due to IEEE 754 double-precision floating point, which defines 0.2 * 3 = 0.6000000000000001. Use d3-format to format numbers for human consumption with appropriate rounding.

Likewise, if the returned array should have a specific length, consider using array.map on an integer range. For example:

[...Generators.range(0, 1, 1 / 49)] // BAD: returns 50 elements!
[...Generators.range(49)].map(d => d / 49) // GOOD: returns 49 elements.

# Generators.valueAt(iterator, index) <>

Returns the value from the specified iterator at the specified index. For example, to return the first element from the iterator:

first = Generators.valueAt(iterator, 0)

This method assumes that the specified iterator is synchronous; if the iterator yields a promise, this method does not wait for the promise to resolve before continuing.

# Generators.worker(source) <>

Returns a new disposable generator that yields a dedicated Worker running the specified JavaScript source. For example, to create a worker that echos messages sent to it:

worker = Generators.worker(`
onmessage = function({data}) {
  postMessage({echo: data});
};
`)

The worker will be automatically terminated when generator.return is called.

Promises

# Promises.delay(duration[, value]) <>

Returns a promise that resolves with the specified value after the specified duration in milliseconds. For example, to define a cell that increments approximately every second:

i = {
  let i = 0;
  yield i;
  while (true) {
    yield Promises.delay(1000, ++i);
  }
}

If you desire precise synchronization, such as a timer that ticks exactly every second, use Promises.tick instead of Promises.delay.

# Promises.tick(duration[, value]) <>

Returns a promise that resolves with the specified value at the next integer multiple of milliseconds since the UNIX epoch. This is much like Promises.delay, except it allows promises to be synchronized. For example, to define a cell that increments every second, on the second:

i = {
  let i = 0;
  yield i;
  while (true) {
    yield Promises.tick(1000, ++i);
  }
}

Or, as an async generator:

i = {
  let i = 0;
  while (true) {
    yield i++;
    await Promises.tick(1000);
  }
}

# Promises.when(date[, value]) <>

Returns a promise that resolves with the specified value at the specified date. This method relies on setTimeout, and thus the specified date must be no longer than 2,147,483,647 milliseconds (24.9 days) from now.

Specials

# invalidation

A promise that resolves when the current cell is re-evaluated: when the cell’s code changes, when it is run using Shift-Enter, or when a referenced input changes. This promise is typically used to dispose of resources that were allocated by the cell. For example, to abort a fetch if the cell is invalidated:

{
  const controller = new AbortController;
  invalidation.then(() => controller.abort());
  const response = await fetch(url, {signal: controller.signal});
  return response.json();
}

The invalidation promise is provided by the runtime rather than the standard library because it resolves to a new promise each time a cell is evaluated. See also Generators.disposable.

# now <>

The current value of Date.now. For example, to display the current time in Markdown:

md`The current time is: ${new Date(now).toISOString()}`

# width <>

The current width of cells. For example, to make a rounded rectangle in SVG that resizes to fit the page:

html`<svg width=${width} height=200>
  <rect width=${width} height=200 rx=10 ry=10></rect>
</svg>`

# visibility([value]) <>

Returns a promise that resolves with the specified value when this cell is visible in the viewport.

HTML

# html`string` <>

Returns the HTML element represented by the specified HTML string literal. This function is intended to be used as a tagged template literal. Leading and trailing whitespace is automatically trimmed. For example, to create an H1 element whose content is “Hello, world!”:

html`<h1>Hello, world!`

If the resulting HTML fragment is not a single HTML element or node, is it wrapped in a DIV element. For example, this expression:

html`Hello, <b>world</b>!`

Is equivalent to this expression:

html`<div>Hello, <b>world</b>!</div>`

If an embedded expression is a DOM element, it is embedded in generated HTML. For example, to embed TeX within HTML:

html`I like ${tex`\KaTeX`} for math.`

If an embedded expression is an array, the elements of the array are embedded in the generated HTML. For example, to create a table from an array of values:

html`<table>
  <tbody>${["zero", "one", "two"].map((name, i) => html`<tr>
    <td>${name}</td><td>${i}</td>
  </tr>`)}</tbody>
</table>`

# svg`string` <>

Returns the SVG element represented by the specified SVG string literal. This function is intended to be used as a tagged template literal. Leading and trailing whitespace is automatically trimmed. For example, to create an SVG element whose content is a circle:

svg`<svg width=16 height=16>
  <circle cx=8 cy=8 r=4></circle>
</svg>`

If the resulting SVG fragment is not a single SVG element, is it wrapped in a G element. For example, this expression:

svg`
<circle cx=8 cy=4 r=4></circle>
<circle cx=8 cy=8 r=4></circle>
`

Is equivalent to this expression:

svg`<g>
  <circle cx=8 cy=4 r=4></circle>
  <circle cx=8 cy=8 r=4></circle>
</g>`

If an embedded expression is a DOM element, it is embedded in generated SVG. If an embedded expression is an array, the elements of the array are embedded in the generated SVG.

Markdown

# md`string` <>

Returns the HTML element represented by the specified Markdown string literal. Implemented by Marked. Leading and trailing whitespace is automatically trimmed. For example, to create an H1 element whose content is “Hello, world!”:

md`# Hello, world!`

If an embedded expression is a DOM element, it is embedded in generated HTML. For example, to embed LaTeX within Markdown:

md`My *favorite* number is ${tex`\tau`}.`

If an embedded expression is an array, the elements of the array are embedded in the generated HTML. The elements may either be strings, which are interpreted as Markdown, or DOM elements. For example, given an array of data:

elements = [
  {symbol: "Co", name: "Cobalt", number: 27},
  {symbol: "Cu", name: "Copper", number: 29},
  {symbol: "Sn", name: "Tin", number: 50},
  {symbol: "Pb", name: "Lead", number: 82}
]

To create a table:

md`
| Name      | Symbol      | Atomic number |
|-----------|-------------|---------------|${elements.map(e => `
| ${e.name} | ${e.symbol} | ${e.number}   |`)}
`

TeX

# tex`string` <>

Returns the HTML element represented by the specified LaTeX string literal. Implemented by KaTeX.

tex`E = mc^2`

# tex.block`string` <>

Equivalent to tex, but uses KaTeX’s display mode to produce a bigger block element rather than a smaller inline element.

tex.block`E = mc^2`

require

# require(names…) <>

Returns a promise of the asynchronous module definition (AMD) with the specified names, loaded from unpkg. Each module name can be a package (or scoped package) name optionally followed by the at sign (@) and a semver range. For example, to load d3-array:

d3 = require("d3-array")

Or, to load d3-array and d3-color and merge them into a single object:

d3 = require("d3-array", "d3-color")

Or, to load d3-array 1.1.x:

d3 = require("[email protected]")

See d3-require for more information.

# require.resolve(name) <>

Returns a promise to the resolved URL to require the module with the specified name. For example:

require.resolve("d3-array") // "https://unpkg.com/[email protected]/build/d3-array.js"

# require.alias(aliases) <>

Returns a require function with the specified aliases. For each key in the specified aliases object, any require of that key is substituted with the corresponding value. For example:

React = require("react@16/umd/react.production.min.js")
ReactDOM = require("react-dom@16/umd/react-dom.production.min.js")
Semiotic = require.alias({"react": React, "react-dom": ReactDOM})("semiotic@1")

Equivalently:

r = require.alias({
  "react": "react@16/umd/react.production.min.js",
  "react-dom": "react-dom@16/umd/react-dom.production.min.js",
  "semiotic": "semiotic@1"
})

Then to require the libraries:

React = r("react")
ReactDOM = r("react-dom")
Semiotic = r("semiotic")

Installing

The Observable notebook standard library is built-in to Observable, so you don’t normally need to install or instantiate it directly. If you use NPM, npm install @observablehq/notebook-stdlib.

# Library([resolve]) <>

Returns a new standard library object. If a resolve function is specified, it is a function that returns a promise to the URL of the module with the specified name; this is used internally by require (and by extension, md and tex). See d3-require for details.

For example, to create the default standard library, and then use it to create a canvas:

const library = new Library();
const canvas = library.DOM.canvas(960, 500);

The properties on the returned library instance correspond to the symbols (documented above) that are available in Observable notebook cells. However, note that the library fields (such as library.now) are definitions, not values: the values may be wrapped in a function which, when invoked, returns the corresponding value.

About

The Observable notebook standard library.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 100.0%