How to make an x axis sticky on a D3 chart?

I have a chart similar to this one by Mike Bostock: Lollipop Chart / D3 / Observable

How can I make the x axis sticky? Essentially, when the user is still scrolling down on the chart, I’d like the x axis to remain on the screen view.

You would need to put the x-axis in a different html element, and then use the technique in this notebook: Sticky Positioning / Fabian Iwand | Observable

There is some discussion in this post as well:

Hope that helps!

Unfortunately sticky positioning won’t work that well for elements at the top or bottom, because they are likely to be covered by one of Observable’s sticky toolbars.

I would suggest to put the main chart into a scrollable container. You can add this function to your notebook:

function split(chart, axisHeight = 30, chartHeight = '500px') {
  const axis = chart.querySelector(':scope > g:nth-child(2)');
  const copy = chart.cloneNode();
  copy.viewBox.baseVal.height = axisHeight;
  copy.appendChild(axis);
  return html`<div>
    <div style="overflow-y:scroll">${copy}</div>
    <div style="overflow-y:scroll;resize:vertical;height:${chartHeight}">${chart}
  `;
}

Then change the return statement your chart cell to:

  return split(svg.node());

This gives you a scrollable and resizable container:

Note that the axis container div also has overflow-y set to scroll so that the width of both containers stays identical.

… If you want to try your hand at a sticky axis, as suggested by Shan, then these are the steps:

First, import a helper function that tracks the visible area (i.e., the “viewport”):

import {observeViewport} from '@mootari/sticky-positioning'

Then add a cell with the following code:

{
  const topOffset = 60;
  const axisSelector = ':scope > g:nth-child(2)';
  
  const clamp = (min, max, value) => value <= min ? min : value >= max ? max : value;
  const unobserve = observeViewport((top, bottom) => {
    const bounds = chart.getBoundingClientRect();
    const height = chart.viewBox.baseVal.height;
    const scale = height / bounds.height;
    const offset = top - bounds.top + topOffset;
    
    const axis = chart.querySelector(axisSelector);
    axis.setAttribute('transform', `translate(0, ${clamp(30, scale * (height - topOffset), scale * offset)})`);
  });
  invalidation.then(unobserve);
}

This will modify the transform attribute of your axis group so that it remains within the viewport.

Thanks so much for the pointers and recommendations!

1 Like