Clicking last node of Icicle leads to incorrect widths in top levels

Hi all Observablers,

I’ve been busy updating UW Macrostrat’s Interactive Geological Timescale and I’ve made a lot of progress, see the notebook: Geological Time Scale 2021. It’s kind of a zoomable icicle. One bug that remains that I’ve have not been able to solve so far is when clicking the Holocene node or any of its children, the nodes of the top 2 levels are positioned incorrectly as the image below shows.

Some observations

  • It only occurs when clicking Holocene or it’s child nodes Greenlandian, Northgrippian, Meghalayan
  • Only the top 2 levels (0 & 1) are affected by the bug while level 2 & 3 show up fine.
  • Not just the parents in level 0 & 1 are affected, more (all?) nodes in these levels are and their labels are all incorrectly positioned in one place.
  • A similar bug occurs in the previous version which uses a d3.scaleLinear to rescale the rectangle widths.

I’ve also made a debugging fork but haven’t figured out what’s causing it. My guess it is in this part of the code where rectangles are rescaled in the clicked function in timescale.

d.target = {
  x0: leftNeighbor + ((d.x0 - p.x0) / (p.x1 - p.x0)) * (width - rightNeighbor - leftNeighbor),
 x1: leftNeighbor + ((d.x1 - p.x0) / (p.x1 - p.x0)) * (width - rightNeighbor - leftNeighbor),
 y0: d.y0,
y1: d.y1
};

I thought perhaps it might be a floating point error as the value of the Holocene node is only 0.0117 and the values of the level 0 and 1 node are in the order of 10^2 to 10^3 but I’m not so sure about this.
Any help or clues are much appreciated!

In my tests several errors popped in the JS console, so you’ll probably want to keep an eye on that.

Also, this looks fishy:

    root.each(
      (d) =>
        (d.target = {
          x0: leftNeighbor + ((d.x0 - p.x0) / (p.x1 - p.x0)) * (width - rightNeighbor - leftNeighbor),
          x1: leftNeighbor + ((d.x1 - p.x0) / (p.x1 - p.x0)) * (width - rightNeighbor - leftNeighbor),
          y0: d.y0,
          y1: d.y1
        })
    );

I recommend to deduplicate your code here (store duplicate statements in variables) and work with log and assert. A neat trick to quickly keep an eye on variables is to use console.log with property value shorthands, e.g. console.log({leftNeighbor, rightNeighbor}).

You can add conditional debugger statements to break into your code and inspect the local variable scopes, e.g. if you run into null values, or a result exceeds a known good range:

root.each((d) => {
  if(p.x1 - p.x0 < 1) debugger;
  // ...
});

Sprinkling your code with console.assert() statements is also a good way to keep your values in check without spamming the JS console.

Lastly, note that you should not reassign parameters if you can help it. This line:

    focus = focus === p ? (p = p.parent) : p;

can be reduced to

    focus = focus === p ? p.parent : p;

and further down in your root.each callback you can simply reference focus instead of p.

Thank you for the hints and the great debugging tips!. I’ve deduplicated the root.each part it to

    root.each((d) => {
      const widthMinusNeighbors = width - rightNeighbor - leftNeighbor;
      const focusWidth = focus.x1 - focus.x0; // partition width of focused node

      const target = {
        x0:
          leftNeighbor + ((d.x0 - focus.x0) / focusWidth) * widthMinusNeighbors,
        x1:
          leftNeighbor + ((d.x1 - focus.x0) / focusWidth) * widthMinusNeighbors,
        y0: d.y0,
        y1: d.y1
      };

      d.target = target;
    });

Some more console.logging leads me to believe that the error is not in the root.each part, clicking the “Meghalayan” node logs the following target values for the “Geologic Time” and “Phanerozoic” nodes

"Geologic Time" {x0: -319657159.66277957, x1: 960, y0: 0, y1: 40}
"Phanerozoic" {x0: -43232800.68439095, x1: 960, y0: 40, y1: 80}

x1 = 960 so the rectangles should be drawn to the end. All ‘incorrect’ nodes are also 960 px (ie the width) short, which is peculiar. So I guess the bug is somewhere else, I’m even more lost now.

If you slow down the transition time to e.g. 5000, you can see the “Geologic Time” label start shaking towards the end. This could indeed indicate precision problems.

I wonder if you can use custom easings that clamp towards a sane range and animate only the part within the range.

1 Like

Thanks once again! Precision problems are so far the most likely explanation. That would explain why it only happens when clicking the shortest nodes and affects the longest nodes. The custom easings are above my (current) level I think, I wouldn’t know where to start! Easings are one area of d3 I am not familiar with. I guess I’ll start by reading the d3-ease docs and the d3-ease Observable collection.