Published
Edited
Jan 29, 2020
7 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
mutable currentEventRef = ({ current: null })
Insert cell
{
let initial = null;
let current = null;

const leaf = scroller.elements[scroller.elements.length - 1];

const updateDrag = e => {
current = e;
currentEventRef.current = e;
};

const endDrag = e => {
initial = current = null;
currentEventRef.current = null;
document.removeEventListener('mousemove', updateDrag);
document.removeEventListener('mouseup', endDrag);
};

const startDrag = e => {
initial = current = e;
document.addEventListener('mousemove', updateDrag);
document.addEventListener('mouseup', endDrag);
};

leaf.addEventListener('mousedown', startDrag);

invalidation.then(() => {
leaf.removeEventListener('mousedown', startDrag);
document.removeEventListener('mousemove', updateDrag);
document.removeEventListener('mouseup', endDrag);
});
}
Insert cell
{
// In the real world, this logic should probably live inside a requestAnimationFrame, but just using Observable's convenience here.
while (true) {
const event = currentEventRef.current;
const root = scroller.elements[0];
const leaf = scroller.elements[scroller.elements.length - 1];
if (event) {
scrollToward(root, leaf, {
x: event.clientX,
y: event.clientY
});
}
yield;
}
}
Insert cell
function scrollToward(root, element, coord) {
const d = 10;
const { dx, dy } = getDelta(root, coord);
scrollTowardHoriz(element, coord.x, dx);
scrollTowardVert(element, coord.y, dy);
}
Insert cell
function scrollTowardHoriz(element, x, dx) {
if (element === null) {
return;
} else if (canScrollTowardHoriz(element, x)) {
const { left, right } = element.getBoundingClientRect();
if (x < left) {
element.scrollLeft -= dx / denominator;
} else if (x > right) {
element.scrollLeft += dx / denominator;
}
} else {
scrollTowardHoriz(element.parentElement, x, dx);
}
}
Insert cell
function getDelta(element, { x, y }) {
const { left, right, top, bottom } = element.getBoundingClientRect();
let dx = 0;

if (x < left) {
dx = left - x;
} else if (x > right) {
dx = x - right;
}

let dy = 0;
if (y < top) {
dy = top - y;
} else if (y > bottom) {
dy = y - bottom;
}

return { dx, dy };
}
Insert cell
function scrollTowardVert(element, y, dy) {
const d = 10;
if (element === null) {
return;
} else if (canScrollTowardVert(element, y)) {
const { top, bottom } = element.getBoundingClientRect();
if (y < top) {
element.scrollTop -= dy / denominator;
} else if (y > bottom) {
element.scrollTop += dy / denominator;
}
} else {
scrollTowardVert(element.parentElement, y, dy);
}
}
Insert cell
function canScrollTowardHoriz(element, x) {
const { left, right } = element.getBoundingClientRect();
if (x < left) {
return element.scrollLeft > 0;
} else if (x > right) {
return element.scrollLeft + element.clientWidth < element.scrollWidth;
} else {
return false;
}
}
Insert cell
function canScrollTowardVert(element, y) {
const { top, bottom } = element.getBoundingClientRect();
if (y < top) {
return element.scrollTop > 0;
} else if (y > bottom) {
return element.scrollTop + element.clientHeight < element.scrollHeight;
} else {
return false;
}
}
Insert cell
function coordOutsideElement(element, { x, y }) {
const { left, right, top, bottom } = element.getBoundingClientRect();
return x < left || x > right || y < top || y > bottom;
}
Insert cell
function minimap(root, leaf, initial, current) {
const container = svg`<svg
width=${width}
height=${height}
viewBox="-1 -1 ${width + 1} ${height + 2}"
/>
`;
const elements = [];

let el = leaf;
while (el !== root.parentElement) {
elements.push(el);
el = el.parentElement;
}

const x = d3
.scaleLinear()
.domain([0, d3.max(elements, el => el.offsetWidth)])
.range([0, width]);

const y = d3
.scaleLinear()
.domain([0, d3.max(elements, el => el.offsetHeight)])
.range([0, height]);

if (initial && current) {
// Would almost be sufficient to use the event's offsetX and offsetY as coordinates, but those are defined relative to its target, and its target may change during mouse movement. Instead creating a mapping from target's client reference frame to the target's offset reference frame.
const clientRect = initial.target.getBoundingClientRect();

const ex = d3
.scaleLinear()
.domain([clientRect.left, clientRect.right])
.range([0, initial.target.offsetWidth]);

const ey = d3
.scaleLinear()
.domain([clientRect.top, clientRect.bottom])
.range([0, initial.target.offsetHeight]);

container.appendChild(svg`
<circle
cx="${x(ex(initial.clientX))}"
cy="${y(ey(initial.clientY))}"
r="4"
fill="blue"
/>
`);

container.appendChild(svg`
<circle
cx="${x(ex(current.clientX))}"
cy="${y(ey(current.clientY))}"
r="4"
fill="red"
/>
`);
}

let st = 0;
let sl = 0;
for (const el of elements) {
const { scrollTop, scrollLeft, offsetWidth, offsetHeight } = el;
const { borderColor } = window.getComputedStyle(el);
st += scrollTop;
sl += scrollLeft;
container.appendChild(svg`
<rect
x="${x(sl)}"
y="${y(st)}"
width="${x(offsetWidth)}"
height="${y(offsetHeight)}"
fill="none"
stroke="${borderColor}"
stroke-width="2"
/>
`);
}

return container;
}
Insert cell
denominator = 4
Insert cell
Insert cell
d3 = require('d3')
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more