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

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more