Calling a function defined in one cell from another cell

Hello,

Imaging you have a cell with a canvas running a force based simulation. That cell may have some functions that are part of the simulation, like adding a node, resetting, taking out nodes, etc.

I’d like to be able to have some buttons whose onClick handlers trigger these functions. The examples I’ve seen involve relying on basically global booleans, like ‘isPaused’ to get this kind of behavior, where a button in one cell will be able to toggle this global boolean defined in another cell, and the cell with the canvas will have some logic around that global boolean as well. Apologies if it’s not really a ‘global’, I’m not sure how you’d call it in the context of observable.

Anyway, I tried the following: https://beta.observablehq.com/@leoshmu/sample-notebook-testing-calling-a-function-defined-in-one-c

My thought was to have objects that define the canvas associated parameters and methods, and then a button in another cell can trigger that method. I’m having trouble having it actually run a simulation so I must be doing something wrong.

Am I doing an antipattern as far as observable is concerned? Is there a way to propagate custom events from cell to cell, and if so would you want to have a button trigger an event in one cell and another cell have a listener for that event?
Or should the control inputs for a simulation cell be somehow embedded in that cell?

Overall the issue of SCOPE as it relates to a single cell vs the group of cells in a notebook is a bit confusing, at least to me!

Thanks in advance for your help, sorry for the wordy question

Hi!

Sure, so -

Running the simulation

This one’s unrelated to the pattern discussed: the problem is this section:

  	var simulation = d3.forceSimulation(testCanvasParameters.nodes)
    	.force("charge", d3.forceManyBody().strength(-10))
	    .on("tick", this.ticked())

.on('tick', this.ticked()) doesn’t call this.ticked when a ticked event occurs - it calls this.ticked() immediately, because it’s a function call, not the name of a function. Changing this code to

  	var simulation = d3.forceSimulation(testCanvasParameters.nodes)
    	.force("charge", d3.forceManyBody().strength(-10))
	    .on("tick", () => this.ticked())

Fixes that issue and correctly animates your example.

Relative to the other code structure stuff, there are a few things in the notebook that are unnecessary:

  • Using viewof with btnTest isn’t doing anything, because the value of btnTest is never used from another cell.
  • Likewise, testCanvasParameters.context.canvas.dispatchEvent(new CustomEvent("input")); isn’t necessary - the canvas animates because there are drawing methods being applied to it.

Scope in Observable

Here are the levels of scope that are exposed in Observable:

  • Local scope: things declared with var, let, const, and declarations (function or class) in the same cell as your code
  • Cells, accessible by their names (probably what you’re familiar with as globals, because that’s what they look like as JavaScript, but this is one of the parts that Observable diverges from vanilla JavaScript - cell names don’t leak into the global scope, they’re more like automatic const variables with reactivity built in.)
  • stdlib functions, like DOM.context2d
  • globals from the window, like Array or RegExp
  • the imported name of anything you import with an import statement

Whether this example is an antipattern

I kind of regret making the anti-patterns notebook, because, well, we don’t want to tell anyone that what you’re doing is wrong if it works - and your notebook works, after one tiny tweak! The question is more like “is this built in such a way that it’s going to be super easy to expand on and understand”, which, well - depends on what’s comfortable to you. I’ll throw out this refactor: https://beta.observablehq.com/@tmcw/sample-notebook-testing-calling-a-function-defined-in-one-c

Which I’d posit dodges some of the pitfalls of the original, like:

  • Using this in JavaScript is notoriously tricky - like if I had written .on('tick', this.ticked) above, then it would be an error, because the this value when this.ticked runs would be incorrect. Which, you can certainly master, but it’s also nice to avoid this most of the time.
  • It’s kind of annoying to have to deal with one cell that creates a UI and then another that reacts to it, in the Observable scene, and it’s also ideal that cells update each other because their values change, which is what this refactor does.

What the refactor entails is:

The button now looks like this:

viewof nodes = {
  let add = html`<div><button>add Node</button></div>`;
  add.value = [{index:0}, {index:1}];
  add.onclick = () => {
    add.value.push({index:add.value.length});
    add.dispatchEvent(new CustomEvent('input'));
  }
  return add;
}

So the button cell is a viewof, and the value it returns is nodes: when you click the button, a node gets added to the end, and the visualization updates automatically because a value that it relies upon updates.

So the ‘run visualization’ cell looks like:

d3.forceSimulation(nodes)
  .force("charge", d3.forceManyBody().strength(-10))
  .on("tick", ticked)

Just that is enough to set it up, and ticked refers to the ticked cell, which is a function.

It’s a bit late so hopefully this all makes sense, please let me know if there’s anything I can clarify :slight_smile:!

  • Tom
3 Likes

Wow, thanks so much for this reply. Very instructive.
I guess an extension then would be if you wanted multiple buttons within a cell, like one button to add nodes and another to subtract nodes, you’d set up both buttons to return nodes as a value and then the animation would update.

Thanks again, I’m having a lot of fun exploring observable and hopefully will be able to publish some useful stuff soon.