How to get a visualization to auto-update when using .getBoundingClientRect()

In this visualization I am using .getBoundingClientRect() to find the positions of elements. To get that to work I render the initial graph in a renderNodes cell and then in another, renderLines I retrieve coordinates and continue adding elements.

On page load the renderLines (.getBoundingClientRect()) cell does not run, and in fact gives an error on .getBoundingClientRect(). If I manually execute it with Shift-Enter it does execute properly.

How do I get the renderLines code to execute automatically without the error?

  • Thanks, Karl

Your renderLines cell selects the output of renderNodes, but does not announce that dependency to Observable’s runtime.

The quickest fix is to just add the following line anywhere inside your renderLines cell:

renderNodes;

That way Observable will ensure that renderNodes resolves (runs) before trying to resolve renderLines.

However, I would recommend to avoid cells with side effects (i.e., that modify the contents of other cells), and instead turn renderLines into a function that receives the target element as argument.


Note that you cannot have a cell display an element from another cell’s output, as doing so would move the element in the DOM. To learn more about this, take a look at Inspector Spinner 🕵️ / Fabian Iwand / Observable.

Hi mootari,

Thanks. Can you unpack " instead turn renderLines into a function that receives the target element as argument." What is the target element? Are you suggesting calling renderLines from renderNodes? Seems like, since the elements will not have been rendered by the yield and so will return 0.

  • Karl

Will do, but first I have a couple of notes:

Selection:
You’re calling d3.selectAll() (for iNode, rNode), even though you’re only accessing the first element.

Implicit dependencies / side effects:
Cells with side effects cause problems when you start to reuse/import your code in other notebooks. E.g., if you were to import renderNodes into another notebooks, then renderLines would never run, unless you explicitely import and output it as well.

yield vs return:
A single yield at the end of a cell (or function) can be replaced with return.

Yielding in Observable:
When you use yield inside a cell, Observable turns your cell into a generator. Generator cells in Observable are evaluated on every frame (via requestAnimationFrame).

Getting an element into the DOM:
You’ve currently split your code into renderNodes and renderLines because you want to use getBoundingClientRect in the second part. But splitting isn’t necessary, because you can yield the element intermittently:

{
    const svgNode = /* ... */
    // This returns the DOM element to Observable's runtime,
    // which hands it off to Observable's Inspector,
    // who in turn attaches it to the DOM.
    yield svgNode;
    // On the next frame, Observable continues to retrieve values
    // from the generator.
    // The element is now in the DOM, and .getBoundingClientRect() will work.
    // ... do other stuff ...
}

Regarding the function:
Splitting your code into pieces still makes sense, but these pieces should have clearly defined inputs and outputs (arguments and return values). While Observable abstracts away the handling of dependencies, I still recommend to write functions whenever your code may run with different configurations.

With renderLines, your code could look something like this:

renderNodes = {
    const svgNode = /* ... */
    // do the renderNodes stuff
    yield svgNode;
   renderLines(svgNode, connectionPairs);
}

This is just an example, I didn’t dive into the code deep enough to determine wether the approach is sensible for this instance.

A pattern that I often use in my notebooks is to start a cell like this:

chart = {
  const {
    width = 400,
    height = 300,
    // other things that could be customizable
  } = {};
  // Rest of the cell.
}

Whenever I think something should be customizable I put it into this declaration block. And should I decide to turn the whole thing into a function, all I need to do is this:

function createChart(options = {}) {
  const {
    width = 400,
    height = 300,
    // other things that could be customizable
  } = options;
  // Rest of the function.
}
4 Likes

Wow. Thanks so much. I’m new and I took direction from other notebooks. Though it seems to work, there is some better ways, so it seems like there is refactoring in my future.

1 Like

You’re very welcome! Are there any specific resources that you found especially helpful for getting started on Observable and would recommend (or perhaps the opposite, documentation that you found rather unhelpful or confusing)?

After my friend Alex and StackOverflow, the Observable team (you, MikeB and Toph) and the notebooks have been most helpful. My friend Alex because he explains broad concepts, SO for tactical questions, and the Ob team/NBs for d3, Ob specific ones. The notebooks because it is easy to see what the code does both visually and in the browsers dev.

The two areas that are always difficult is how to get to a part of a complex data structure when using => and how to select the right elements. Though I have read many articles on Select and Select All, an interactive Ob tutorial that uses complex-ish data and shows "This [code] selects this/these [elements] would help a lot. Or maybe something like “Look at X notebook at this code block it is selecting …”

The old d3 site is useful but becoming outdated…for beginners having to decide if and how it’s applicable creates a lot churn. I do appreciate that there is a disclaimer at the top pointing to Ob…would be nice if, for any instances that are applicable, that there is an Ob version/example. I know that would be difficult and time consuming to do…

What would be most helpful is better annotated code. For instance, I was started looking at the Tangled tree visualization but I couldn’t parse how it manipulated data because there isn’t much comments. I forked it and added console.logs for all of the main data (levels, nodes, nodes_index, links, bundles) but couldn’t figure out how it was constructed or used. My JS just isn’t that advanced…

2 Likes