Public
Edited
Oct 24, 2023
6 forks
107 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

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