Public
Edited
Oct 24, 2023
7 forks
106 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3.select(newchart())
.on("mousemove", function(event) {
// the update function is defined in `chart`; call it to draw the contents
this.update(event);
})
.node()
Insert cell
Insert cell
Insert cell
Insert cell
d3.select(newchart())
.on(
"click wheel " +
"mouseenter mouseout mousedown mouseup mousemove " +
"touchstart touchend touchmove",
function(event) {
event.preventDefault();
this.update(event);
}
)
.node()
Insert cell
Insert cell
d3.select(newchart())
.on(
"click wheel " +
"mouseenter mouseout mousedown mouseup mousemove " +
"touchstart touchend touchmove " +
"pointerenter pointerout pointerup pointerdown pointermove lostpointercapture",
function(event) {
event.preventDefault();
this.update(event);
}
)
.node()
Insert cell
Insert cell
Insert cell
d3.select(newchart())
.on("touchstart", function(event) {
this.update(event);
event.preventDefault();
})
.on("mousemove touchmove", function(event) {
this.update(event);
})
.node()
Insert cell
Insert cell
d3.select(newchart({ height: 100 }))
.on("touchstart", function(event) {
this.update(event);
// event.preventDefault(); // 🧨 forgetting this line is bad™
})
.on("mousemove touchmove", function(event) {
this.update(event);
})
.node()
Insert cell
Insert cell
d3.select(newchart())
.on("touchstart", function(event) {
this.update(event);
event.preventDefault();
})
.on("pointermove", function(event) {
this.update(event);
})
.node()
Insert cell
Insert cell
d3
.select(newchart())
.on("touchstart", function(event) {
this.update(event);
event.preventDefault();
})
.on("pointerdown", function(event) {
this.setPointerCapture(event.pointerId);
this.update(event);
})
.on("pointermove", function(event) {
this.update(event);
})
.on("pointerup", function(event) {
this.update(event);
})
.on("lostpointercapture", function(event) {
this.update(event);
this.releasePointerCapture();
})
.node()
Insert cell
Insert cell
Insert cell
Insert cell
{
const height = 200;

// status of the square
let angle = 0;

// status of the pointer(s)
let pointerangle;

const canvas = d3.select(newchart({ height }))
.on("touchstart", function(event) {
event.preventDefault();
const t = d3.pointers(event, this);

// compute the initial angle between touches (if there are two or more)
pointerangle =
t.length > 1 && Math.atan2(t[1][1] - t[0][1], t[1][0] - t[0][0]);

this.update(event);
})
.on("touchmove", function(event) {
const t = d3.pointers(event, this);

if (t.length > 1) {
// substract the previous angle
angle -= pointerangle;
// compute the new angle
pointerangle = Math.atan2(t[1][1] - t[0][1], t[1][0] - t[0][0]);
// add the new angle
angle += pointerangle;
}

this.update(event);
})
.node();

canvas.callback = square;
square(canvas.context);

return canvas;

function square(context) {
context.save();
context.lineWidth = 1.5;
context.translate(width / 2, height / 2);
context.rotate(angle);
context.beginPath();
d3
.symbol(d3.symbolSquare)
.size(15000)
.context(context)();
context.fillStyle = "rgba(0,0,0,0.2)";
context.fill();
context.stroke();
context.restore();
}
}
Insert cell
Insert cell
{
const height = 350;

// status of the square
let angle = 0, // (A)
position = [width / 2, height / 2], // (B)
size = 120; // (C)

// status of the pointer(s)
let pointerangle, // (A)
pointerposition, // (B)
pointerdistance; // (C)

const canvas = d3.select(newchart({ height }))
.on("mousedown touchstart", function(event) {
event.preventDefault();
const t = d3.pointers(event, this);

pointerangle =
t.length > 1 && Math.atan2(t[1][1] - t[0][1], t[1][0] - t[0][0]); // (A)

pointerposition = [d3.mean(t, d => d[0]), d3.mean(t, d => d[1])]; // (B)

pointerdistance =
t.length > 1 && Math.hypot(t[1][1] - t[0][1], t[1][0] - t[0][0]); // (C)

this.style.cursor = "grabbing"; // (F)
this.update(event);
})
.on("mouseup touchend", function(event) {
pointerposition = null; // signals mouse up for (D) and (E)

this.style.cursor = "grab";
this.update(event);
})
.on("mousemove touchmove", function(event) {
this.update(event);
if (!pointerposition) return; // mousemove with the mouse up

const t = d3.pointers(event, this);

// (A)
position[0] -= pointerposition[0];
position[1] -= pointerposition[1];
pointerposition = [d3.mean(t, d => d[0]), d3.mean(t, d => d[1])];
position[0] += pointerposition[0];
position[1] += pointerposition[1];

if (t.length > 1) {
// (B)
angle -= pointerangle;
pointerangle = Math.atan2(t[1][1] - t[0][1], t[1][0] - t[0][0]);
angle += pointerangle;
// (C)
size /= pointerdistance;
pointerdistance = Math.hypot(t[1][1] - t[0][1], t[1][0] - t[0][0]);
size *= pointerdistance;
}

this.update(event);
})
.on("wheel", function(event) {
// (D) and (E), pointerposition also tracks mouse down/up
if (pointerposition) {
angle += event.wheelDelta / 1000;
} else {
size *= 1 + event.wheelDelta / 1000;
}
this.update(event);
event.preventDefault();
})
.style("cursor", "grab") // (F)
.node();

canvas.callback = square;
square(canvas.context);

return canvas;

function square(context) {
size = Math.max(10, Math.min(1000, size)); // clamp size

context.save();
context.lineWidth = 1.5;
context.translate(...position);
context.rotate(angle);
context.beginPath();
d3
.symbol(d3.symbolSquare)
.size(size ** 2)
.context(context)();
context.fillStyle = "rgba(0,0,0,0.2)";
context.fill();
context.stroke();
context.restore();
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
d3.select(newchart())
.style("touch-action", "none")
.on("pointermove", function(event) {
this.update(event);
})
.node()
Insert cell
Insert cell
function newchart({ height = 250 } = {}) {
const context = DOM.context2d(width, height),
canvas = context.canvas;

canvas.context = context;

const events = [];
let types = [],
locations = [];
let lastPointers = [];

const h = d3.scaleOrdinal().range(d3.range(20).map(i => 12 * i + 16));

const timer = d3.timer(tick);
timer.stop();

canvas.update = function(event, pointers) {
lastPointers = pointers || d3.pointers(event, canvas);
if (event) events.push({ date: +new Date(), event });
tick();
};

function tick() {
const now = +new Date();
// remove old events
while (events.length > 0 && events[0].date < now - 1000) events.shift();
if (!events.length) clean();
else timer.restart(tick);
paint();
}
function clean() {
lastPointers = [];
paint();
timer.stop();
}

function paint() {
context.clearRect(0, 0, width, height);

context.fillStyle = "#333";
context.fillText(`.on(…)`, 2, 8);
context.fillStyle = "#777";
for (const type of canvas.__on.map(d => d.type)) {
context.fillText(`${type}`, 2, h(type) + 8);
}

for (const [type, list] of d3.group(events, d => d.event.type)) {
list.reverse();
context.fillStyle = timeColor(list[0].date);
context.fillRect(0, h(type), 120, 10);
for (let i = 0; i < list.length; i++) {
context.fillStyle = timeColor(list[i].date);
context.fillRect(122 + 12 * i, h(type), 10, 10);
}
context.fillStyle = "black";
context.fillText(`${type}`, 2, h(type) + 8);
}

context.fillStyle = "black";
for (const pointer of lastPointers) {
context.setLineDash([2, 2]);
context.lineWidth = 0.5;
context.beginPath();
context.moveTo(pointer[0], 0);
context.lineTo(pointer[0], height);
context.moveTo(0, pointer[1]);
context.lineTo(width, pointer[1]);
context.stroke();

context.setLineDash([]);
context.lineWidth = 0.25;
context.beginPath();
context.moveTo(...pointer);
const sx = pointer[0] < width / 2 ? 1 : -1;
const sy = pointer[1] < height / 2 ? 1 : -1;
context.lineTo(pointer[0] + sx * 23, pointer[1] + sy * 23);
context.stroke();

context.textAlign = sx > 0 ? "start" : "end";
context.fillText(
`${+pointer[0].toFixed(3)}, ${+pointer[1].toFixed(3)}`,
pointer[0] + sx * 28,
pointer[1] + sy * 26
);
context.textAlign = "start";
}

if (context.canvas.callback) context.canvas.callback(context);
}

// repaint after the listeners have been attached
setTimeout(paint, 10);
return canvas;
}
Insert cell
timeColor = {
const r = d3.scaleSequential(d3.interpolateCool).domain([1000, -1000]);
return t => r(t - new Date());
}
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