Dear Observable team, we are creating some interactive visualization apps over Observable. While developing reactive codes, there are a number of interesting technical issues. One of them is we found referenced cell within a function not reflecting latest reference
Dependency: “number of vertices” → “graph” → “transformGroup function”
Test case: change “number of vertices” from html input tag, and then drag the black rectangle. The drag call back function calls “transformGroup()”
Expected result: graph within “transformGroup function” reflects the latest reference
Actual result: graph within “transformGroup function” does not reflect the latest reference
(In above example, open developer console, the log shows graph references are different)
Our solution: either use mutable mutableGraph, or always pass “graph” as an input argument to “transformGroup function.”
Just want to know is it a suggested way to make “graph” mutable although the changes to graph only occurs within “graph” cell?
The issue is caused by the fact that your
visual cell does not have any explicit dependence on
transfromGroup. So when you update the “number of vertices”, your
svgGroup cell appends a new line to
visual without removing the old line (which still has the old event handlers). In your notebook, try inspecting the svg after changing the number of vertices a few times and you’ll see a bunch of extra line elements there. Furthermore, the new line is “lowered” so that it is obscured by the old line, so that dragging will target the old line and not the new one.
There are a number of ways to fix this, my first thought would be to suggest combining
visual into one cell, since reselecting elements in Observable notebooks is generally discouraged. Here’s a fork with a few tweaks along those lines:
Another change I made is to use
this to avoid recreating the SVG whenever the cell is re-run and I also select the
line if it already exists to avoid appending extra line elements.
You can visualize the dataflow of your notebook using the notebook visualizer:
I’m not sure I understand what you are trying to achieve with the example code. Is the idea that you want to have a graph cell, but have the positions of the graph nodes be draggable by the user, and then have other cells that react to the new user-specified positions? In that case, you here are two options.
Option 1. Use a view to display the graph, allow dragging, and expose the value interactively to the rest of the notebook. The view (the graph cell) will be the only cell that mutates the graph object. Here are a couple examples of this technique:
Option 2. Use a mutable. The graph display will handle the interaction and re-assign to mutable graph when a node is dragged.
The basic idea with the mutable is that you assign to mutable graph instead of emitting an input event as with a view. But I would recommend using a view unless you can elaborate on why you need a mutable.
Firstly thank you very much for digesting my notebook and pointing out the duplicate drawing of “line” elements. It was a mistake in my previous code. If i made only one line change as follows, “svgGroup” → “transformGroup” → the “graph” reference is always the latest.
It’s also a good idea to combine visual, svgs, and svgGroup in one visual cell to avoid re-selection.
However, what I don’t understand is, I made a slight change on your notebook using data().enter() followed by appending a new line element. This reproduced the old graph reference not new reference issue. Here is a fork from your notebook.
As I am not able to make the comparison link sharable using markdown, i put a screen shot below for your reference.
I wonder what makes the difference in getting graph reference.
Thank you for your time
Hi Mike, thank you for your examples. I would like to explain more details about this test case.
I was developing a constrained graph using d3.js and cola.js when i encountered the old graph reference issue and got stuck for at least 3 hours before abandoning the immutable graph reference and switching to mutable graph reference, but i still prefer to use immutable graph reference.
Here is the notebook of constrained graph. When panning the rectangle (white background) it calls “transformGroup” to move all nodes certain distance.
Based on the dependency graph, svgGroup, transformGroup are similar to the simplified example. But this time I could not see the issue as the simplified example, though their dependency looks similar.
I will dig why constrained graph doesn’t have this issue but the simplified example has. I am much appreciated if anyone is interested to give any comment.
The issue in your fork is that you used
enter(). Recall that the “enter” selection consists of elements in the joined data that did not previously exist in the selection (obligatory link to “Thinking with Joins”). In your fork, when the
visual cell is re-run, that enter selection is empty, since the number of elements in the data has not changed (and by default,
data() joins using the array index as key). The fix is to use
join() instead, since that merges the enter and update selections:
Thanks Brian, I have resolved this issue in this fork from your example, using 2 solutions.
One fix which works totally fine is to use join as you suggested. Furthermore, I would like to share this article selection.join which simplifies the need of maintaining data join’s update, enter, exit sections.
Nevertheless, the root cause of this issue is not because the selection is empty when the visual cell is re-run, as is shown in this fork. I logged the svg.selectAll(“line”).empty(). The boolean value is false each time when “visual” cell reacts on changes of number of nodes. Also there is only one “line” element throughout.
The actual root cause of this issue when using data join sections is, i didn’t include “transformGroup” in the call back functions of d3.drag() in the “update” selection. “transformGroup” only appears in “enter” selection. Therefore, when number of nodes change, “update” section updates “line” elements attributes but the “transformGroup” function object is still the one defined earlier in enter selection. When “transformGroup” is invoked, it uses an old graph reference. That’s why it’s still points to old graph. This corresponds to the point you mentioned in the first reply of this issue.
By adding “transformGroup” in “update” selection, it resolves this issue.
Despite using this same problematic code snippet, the reason why this constrained graph always gets a new graph reference in “transformGroup” is because it removes svg elements which depends on “transformGroup” whenever a new graph object is generated. This ensures the “enter” section is re-run and a new “transformGroup” with new graph reference is registered as a callback function of d3.drag().