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.
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!