I am trying to work with an already reactive library (Vega) in Observable. My current progress is at https://beta.observablehq.com/@domoritz/rotating-earth. Vega has “signals” as reactive variables. In my example, I am trying to set the signal value to the reactive rotation value. The problem is that the Observable engine re-evaluates the Vega compile step because the dependency graph links the view and the rotation variable.
My question is how I could split the dependency graph or otherwise tell Observable not to re-evaluate the compile step when the rotation changes.
I feel that I should be able to use Generators.observe
(https://beta.observablehq.com/@mbostock/more-deliberate-inputs) but I can’t quite wrap my head around how.
I’m no Vega expert, but it looks like in your current sketch, Vega waits for you to mouse back over the globe before updating with the new rotation position.
I find that if I change:
view.signal('rotation', rotation)
to
{
view.signal('rotation', rotation);
view.run();
}
… it updates as one would expect.
Well, looks like you know Vega better than I do view.signal('rotation', rotation).run()
is a shortcut for what you wrote.
I am still trying to understand the dependency graph on Observable. Why don’t all cells that depend on the view get re-evaluated when the rotation changes? I would expect view.signal('rotation', rotation).run()
to create a link between the view
and rotation
variables. Or is it that view
is not dependent on rotation because it only gets modified but not set?
There still seem to be some bugs in the dependency evaluation in Observable. For instance, if I reload the example and then rerun the first code cell, I get some errors:
Here is a little experiment: https://beta.observablehq.com/@domoritz/dependency-experiment.
Note how a.foo
is 42
and the text says so as well. When I uncomment the last cell and run it, the content of a
changes but the text does not update automatically. However, when I rerun the text cell, the text updates.
If I understand it correctly, the change tracking only listens to variable writes but not object changes. I think this makes sense and explains why my example works.
Yep. The model may be a little bit simpler than what you’re imagining.
It wraps a functional programming skin around JavaScript, so that each cell in Observable acts as a function body — whatever is returned by the cell becomes the cell’s value, and whenever that value changes (actually changes itself, not an internal property or mutation), any other cell that references it will also be re-evaluated.
So, view.signal(rotation)
doesn’t create a link between view
and rotation
— rather, it’s a cell that depends on both of them, and will be re-evaluated when either of them change.
To make this easy, avoid mutation as much as possible in Observable notebooks, or keep it confined within a cell. Especially try not to mutate cell values from other cells — that defeats much of the purpose of the reactive notebook.
1 Like
Thanks for the explanation. It’s great for Vega that you are not tracking changes to objects as it allows us to use the reactivity of Vega. I’m excited to play more with Observable!
To elaborate on Jeremy’s comment, I would say as a rule of thumb: prefer immutability, and opt-in to mutability when necessary for performance. Mutation adds complexity because now you have to worry about managing mutable state yourself, rather than relying on automatic reactivity, but gives you more control over what gets executed.
Can you explain the error I showed in the GIF above? What’s the initialization strategy of a notebook?
Sure. Your dependency graph looks like this:
The directed edges here indicate references: for example, vegaSpec references vl, so Observable won’t evaluate the vegaSpec cell until vl resolves (which involves requiring vega-lite@2 from unpkg).
The important thing missing from this graph is that the view cell doesn’t depend on patchVegaSpec. So, there’s nothing guaranteeing that the view cell will run after you’ve patched the Vega spec. Observable runs the cells in topological order, but if two cells are at the same level in the topology, the order in which these two cells run is not guaranteed.
You can fix this by making the dependency explicit: make view depend on patchVegaSpec so that the view isn’t instantiated until after the spec is patched.
These two definitions would work:
patchVegaSpec = vegaSpec['signals'] = [{
"name": "rotation",
"value": 0,
}]
view = patchVegaSpec, new vega.View(vega.parse(vegaSpec))
Alternatively, you could just patch the Vega spec inside the view cell before you parse it.
2 Likes
Thanks! It was super useful to see these dependency graphs.
1 Like
Hopefully the dependency graphs will be built-in at some point in the near future!
4 Likes