Public
Edited
Feb 13, 2023
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
//https://observablehq.com/d/5dad4621115e00e0
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
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
r = d3.scaleSequential(d3.interpolateCool).domain([1000, -1000]);
Insert cell
x_zone = d3.scaleQuantize()
.domain([0, 100])
.range([1, 2, 3]);
Insert cell
x_zone(43)
Insert cell
url = "https://docs.google.com/spreadsheets/d/17aJZXUJYCIiwvAcNDApP59jWHviTJ74GUDxbn0E8SUY/edit?usp=sharing"
Insert cell
data = d3.csv(getCsvUrl(url), d3.autoType)
Insert cell
console.log(data)
Insert cell
rotation_url = "https://docs.google.com/spreadsheets/d/1-2xlDwuyv7931E2xxH6gN9LrDWrBkarynG6vCW00ojo/edit?usp=sharing"
Insert cell
rotation_data = d3.csv(getCsvUrl(rotation_url), d3.autoType)
Insert cell
console.log(data)
Insert cell
// Converts a Google Sheets website URL to its CSV URL. You can also go to “File → Publish to web”, select the “Comma-separated values (.csv)” type, select the sheet with your data, and use that CSV URL directly with `d3.csv` above.

getCsvUrl = url => {
url = new URL(url);
const id = url.pathname.split("/")[3]
const sheet = new URLSearchParams(url.hash.slice(1)).get("gid") || 0;
return `https://docs.google.com/spreadsheets/d/${id}/gviz/tq?tqx=out:csv&sheet=${sheet}`
}
Insert cell
svgPath = (points, command) => {
// build the d attributes by looping over the points
const d = points.reduce((acc, point, i, a) => i === 0
// if first point
? `M ${point[0]},${point[1]}`
// else
: `${acc} ${command(point, i, a)}`
, '')
return `<path d="${d}" fill="none" stroke="grey" />`
}
Insert cell
trace = {
let svg = d3.create('svg').style("background-color", 'white').attr("preserveAspectRatio", "none").attr("viewBox", "0 0 900 600");

const points = [[5, 10], [10, 40], [40, 30], [60, 5], [90, 45], [120, 10], [150, 45], [200, 10]]

const lineCommand = point => `L ${point[0]} ${point[1]}`

svg.innerHTML = svgPath(points, lineCommand)
return svg.node();
}
Insert cell
Insert cell
points = [[5, 10], [10, 40], [40, 30], [60, 5], [90, 45], [120, 10], [150, 45], [200, 10]]
Insert cell
lineCommand = point => `L ${point[0]} ${point[1]}`
Insert cell
svg = document.querySelector('.svg')
Insert cell
trace.innerHTML = trace.innerHTML+ svgPath(points, lineCommand)
Insert cell
line = (pointA, pointB) => {
const lengthX = pointB[0] - pointA[0]
const lengthY = pointB[1] - pointA[1]
return {
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
angle: Math.atan2(lengthY, lengthX)
}
}
Insert cell
controlPoint = (current, previous, next, reverse) => {
// When 'current' is the first or last point of the array
// 'previous' or 'next' don't exist.
// Replace with 'current'
const p = previous || current
const n = next || current
// The smoothing ratio
const smoothing = 0.2
// Properties of the opposed-line
const o = line(p, n)
// If is end-control-point, add PI to the angle to go backward
const angle = o.angle + (reverse ? Math.PI : 0)
const length = o.length * smoothing
// The control point position is relative to the current point
const x = current[0] + Math.cos(angle) * length
const y = current[1] + Math.sin(angle) * length
return [x, y]
}
Insert cell
bezierCommand = (point, i, a) => {
// start control point
const [cpsX, cpsY] = controlPoint(a[i - 1], a[i - 2], point)
// end control point
const [cpeX, cpeY] = controlPoint(point, a[i - 1], a[i + 1], true)
return `C ${cpsX},${cpsY} ${cpeX},${cpeY} ${point[0]},${point[1]}`
}
Insert cell
trace.innerHTML = trace.innerHTML+ svgPath(points, bezierCommand)
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