Published
Edited
Oct 3, 2019
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const root = d3.create('div')
root.append('div')
.style('height', '25px')
.style('font-weight', 'bold')
.text('Using line dictionary:');
const segmentsDiv = root.append('div')
.style('height', '25px');
const initializationDiv = root.append('div')
.style('height', '25px');
const foundSegmentsDiv = root.append('div')
.style('height', '25px')
.text('Found segments: ---');
const lookupPerformanceDiv = root.append('div')
.style('height', '25px')
.text('Look-up performance: ---');
const getClosestDiv = root.append('div')
.style('height', '25px')
.text('Get closest performance: ---');
// Lines with ID to segments
const fullLines = [];
// List of line segments
const lineSegments = [];
let prevSelection = null;
const init0 = performance.now(); // Init timer start
const dict = new LineDictionary(-2);
const firstLine = lines[0];
for (let n = 0; n < lines.length; n++) {
const d = lines[n];
const segmentIDs = [];
for (let i = 1; i < d.length; i++) {
const p1 = d[i - 1];
const p2 = d[i];
// Append new line segments
const segmentID = lineSegments.length;
segmentIDs.push(segmentID);
lineSegments.push({
segmentID,
lineID: n,
geometry: {x1: p1.x, y1: p1.y, x2: p2.x, y2: p2.y},
line: null, // Set later
});
dict.add(p1.x, p1.y, p2.x, p2.y, segmentID);
}
fullLines.push(segmentIDs);
};
const init1 = performance.now(); // Init timer end
segmentsDiv.text(`Total segments: ${lineSegments.length}`);
initializationDiv.text(`Initialization: ${(init1 - init0).toFixed(2)} ms`);
const svg = root.append('svg')
.style('width', `${width}px`)
.style('height', `${height}px`)
.style('border', '2px dotted DimGrey')
.on('mouseout', () => {
if (prevSelection) {
prevSelection.forEach(d => {
lineSegments[d].line.attr('stroke', 'SteelBlue');
});
}
prevSelection = null;
})
.on('mousemove', () => {
// Undo previous selection
if (prevSelection) {
prevSelection.forEach(d => {
lineSegments[d].line.attr('stroke', 'SteelBlue');
});
}
const mousePos = d3.mouse(d3.event.target);
const t0 = performance.now();
const linesOnTile = dict.getLinesOn3Grid(mousePos[0], mousePos[1]);
const t1 = performance.now();
foundSegmentsDiv.text(`Found segments: ${linesOnTile.length}`);
lookupPerformanceDiv.text(`Look-up performance: ${(t1 - t0).toFixed(2)} ms`);
if(!linesOnTile) return;
// Position as point
const point = new Vector2(mousePos[0], mousePos[1]);
const t2 = performance.now();
let minDist = Infinity;
let minLineID = null;
linesOnTile.forEach(d => {
const seg = lineSegments[d];
const dist = DistanceToLine(
point,
new Vector2(seg.geometry.x1, seg.geometry.y1),
new Vector2(seg.geometry.x2, seg.geometry.y2),
);
if (dist < minDist) {
minDist = dist;
minLineID = seg.lineID;
}
});
const t3 = performance.now();
getClosestDiv.text(`Get closest performance: ${(t3 - t2).toFixed(2)} ms`);
// Color all segments
fullLines[minLineID].forEach(d => {
lineSegments[d].line.attr('stroke', 'Tomato');
});
prevSelection = fullLines[minLineID];
});
// Line function
function appendLine(x1, y1, x2, y2, color) {
svg.append('line')
.attr('x1', x1).attr('y1', y1)
.attr('x2', x2).attr('y2', y2)
.attr('stroke', color)
.attr('pointer-events', 'none');
}
// Append grid
for (let x = 100; x < width; x+=100) appendLine(x, 0, x, height, 'LightGrey');
for (let y = 100; y < height; y+=100) appendLine(0, y, width, y, 'LightGrey');
lineSegments.forEach(d => {
d.line = svg.append('line')
.attr('x1', d.geometry.x1).attr('y1', d.geometry.y1)
.attr('x2', d.geometry.x2).attr('y2', d.geometry.y2)
.attr('stroke', 'SteelBlue')
.attr('pointer-events', 'none');
});
return root.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class LineDictionary {
constructor(decimals = 0) {
this.resolution = 10 ** decimals;
this.lineSegments = []; // List of line segments
}
// Add a line section to the dictionary by start and end points
add(x1, y1, x2, y2, val) {
var tiles = {};
// Is line going down
let downwards;
// Coordinates for possible cross sections
let xMin, xMax, yMin, yMax;
// Line variables
let m, y0;
// Ensure data line is calculated left to right
if(x1 < x2) {
xMin = Math.floor(x1 * this.resolution);
xMax = Math.floor(x2 * this.resolution);
const deltaX = x2 - x1;
const deltaY = y2 - y1;
m = deltaY / deltaX;
y0 = this.resolution * (y1 - x1 * m);
downwards = y2 < y1;
// Add first
tiles[[Math.floor(x1 * this.resolution), Math.floor(y1 * this.resolution)]] = val;
} else {
xMin = Math.floor(x2 * this.resolution);
xMax = Math.floor(x1 * this.resolution);
const deltaX = x1 - x2;
const deltaY = y1 - y2;
m = deltaY / deltaX;
y0 = this.resolution * (y2 - x2 * m);
downwards = y1 < y2;
// Add first
tiles[[Math.floor(x2 * this.resolution), Math.floor(y2 * this.resolution)]] = val;
}
// Calculate y range
if (y1 < y2) {
yMin = Math.floor(y1 * this.resolution);
yMax = Math.floor(y2 * this.resolution);
} else {
yMin = Math.floor(y2 * this.resolution);
yMax = Math.floor(y1 * this.resolution);
}
// Iterate over x-crossing
for (let x = xMin + 1; x <= xMax; x++) {
const y = y0 + x * m;
tiles[[x, Math.floor(y)]] = val;
}
// Iterate over y-crossing
for (let y = yMin + 1; y <= yMax; y++) {
const x = (y - y0) / m;
tiles[[Math.floor(x), downwards ? y - 1 : y]] = val;
}
for (const key of Object.keys(tiles)) {
// Attempt to get tile
const tile = this[key];
// Append to tile
if (tile) {
tile.push(val);
} else {
this[key] = [val];
}
}
}
// Get all lines registered on tile
getLinesOnTile(x, y) {
const key = [Math.floor(x * this.resolution), Math.floor(y * this.resolution)];
return this[key];
}
// Get unique lines within a 3 x 3 grid
getLinesOn3Grid(x, y) {
const unique = {};
const lines = [];
const keyX = Math.floor(x * this.resolution);
const keyY = Math.floor(y * this.resolution);
for (x = -1; x <= 1; x++) {
for (y = -1; y <= 1; y++) {
const key = [keyX + x, keyY + y];
const tile = this[key];
if (!tile) continue;
tile.forEach(val => { // Iterate over values
if(!unique.hasOwnProperty(val)) {
lines.push(val);
unique[val] = true;
}
});
}
}
return lines;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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