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 !