Observable and Federated Wiki

Observable is an approachable programming environment for data visualization, or really, almost anything you can imagine in Javascript. With the frame plugin, a wiki page is also a programming environment. Here we share one example integrating the two. We hope to encourage other examples.

On this page, we have code in the wiki page which runs an Observable notebook in the frame at the top of the page. We also adjust a parameter in that notebook with a value from another item on the page.

Edit the paragraph below the "space" pagefold to set the default value in our example notebook—pick a number between 20 and 300.




The frame above uses a minimal HTML page with javascript that evaluates the code described on the rest of this page below.

It would be more direct to include all of this code directly in an single-page app in the frame. We choose this less direct arrangement in order to explain how it works.

Visit the example notebook to review the code that draws circles and responds to the slider. See notebook

In that notebook we also do the opposite trick—import a wiki page into an Observable notebook.

Keep reading here to see the code we use to bring the Notebook into wiki.

# Wiki code

Our first few code blocks are concerned with interactions with the wiki client.

The following function asks the frame plugin to resize the <iframe> element to fit the framed content. We use this to prevent a scrolling pane within a scrolling page.

function resize() { window.parent.postMessage({ action: "resize", height: document.body.offsetHeight }, "*") }

We offer authors a convention to name config values with pagefolds. The following function searches the page for items immediately following pagefolds. It converts them into a data structure for use elsewhere.

function parsePagefolds(story) { return Array.from( { length: story.length - 1 }, (_, i) => story.slice(i, i + 2) ).filter( ([fold, _]) => fold.type == "pagefold" ).reduce((data, [fold, item]) => { data[fold.text] = item.text return data }, {}) }

The following function handles the handshake with the frame plugin to request the page, parse the pagefolds, and cleanup after itself. The caller is expected to do something interesting with data structure constructed from pagefolds.

function withPagefolds(fn) { window.addEventListener("message", listener) function listener({data}) { if (data.action == "frameContext") { window.removeEventListener("message", listener) const {page} = data // here's where we offer the configuration // back to our caller fn(parsePagefolds(page.story)) } } window.parent.postMessage({ action:"sendFrameContext"}, "*") }

# Observable code

Annotated screenshot from top-right of an Observable notebook. 1) click ... menu. 2) click Export submenu. 3) click Embed cells

Observable offers a way to export code for using a notebook in other websites. The next few code blocks slightly modify the code from Observable's export.

From top-right of an Observable notebook:

1) click ... menu

2) click Export submenu

3) click Embed cells

There are four details in Observable's Embed dialog.

Annotated screenshot from Observable's Embed dialog. 1) select cells. 2) Runtime with Javascript. 3) HTML. 4) Imports and Runtime.

1) we selected two cells to include in our embedded frame.

2) we selected the Runtime with Javascript option.

3) we copied the HTML into a specific block below.

4) we copy the generated script into two specific blocks below. The imports go into one block. The runtime goes into another.

We export the emit() function for our HTML script. It is here where we paste the HTML copied in #3 from Observable's Embed dialog.

export async function emit(el) { el.innerHTML = ` <div id="observablehq-circles-b3e634b1"></div> <div id="observablehq-viewof-space-b3e634b1"></div> <p>Credit: <a href="https://observablehq.com/@dobbs/fedwiki-experiment">Observable and Federated Wiki by Eric Dobbs</a></p>` }

In the following code block we pasted the imports copied in #4 from Observable's Embed dialog.

import {Runtime, Inspector} from "https://cdn.jsdelivr.net/npm/@observablehq/runtime@4/dist/runtime.js"; import define from "https://api.observablehq.com/@dobbs/fedwiki-experiment.js?v=3";

In the following function we pasted the Runtime copied in #4 from Observable's Embed dialog. We were careful to make sure the Runtime is returned from our named arrow function.

const module = () => new Runtime() .module(define, name => { if (name === "circles") return new Inspector(document.querySelector("#observablehq-circles-b3e634b1")); if (name === "viewof space") return new Inspector(document.querySelector("#observablehq-viewof-space-b3e634b1")); });

The following function waits for Observable Runtime to compute the circles in the notebook before asking the wiki frame to resize.

async function resizeAfterCircles(module) { await module.value("circles") resize() }

The function below is a key integration point that does a lot in a few lines of code.

It understands the data that comes from our page parser.

It knows one element id we copied in the HTML from Observable's Embed dialog.

It knows Observable's API: there is a <form> in that element; the form has a value we can modify; and we can signal our change of the value with a CustomEvent.

function updateNotebook() { withPagefolds(data => { const id = "#observablehq-viewof-space-b3e634b1" const form = document .querySelector(`${id} form`) console.log({ where: "updateNotebook()", data, form }) form.value = +data.space form.dispatchEvent(new CustomEvent("input")) resize() }) }

We export the bind() function for our HTML script too. This is where we bring all the pieces together.

export async function bind(el) { const mod = module() await resizeAfterCircles(mod) updateNotebook() }


Especially interested hackers may also be interested to see how the code on the page is interpreted—especially to understand our mention of emit() and bind().

View source of esm.html

Authors can create their own notebook-like pages in wiki. See ESM Snippet Template