Public
Edited
May 31, 2022
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
let svg = htl.html`
<svg width="${canvasWidth}" height="${canvasHeight}">
<rect x="0" y="0" width="${canvasWidth}" height="${canvasHeight}" fill="#eee"></rect>
<g class="grid"></g>
<g class="points"></g>
<g class="vectors"></g>
<g class="estimates"></g>
</svg>
`;

let x = d3.scaleLinear(cameraExtent.x, [0, canvasWidth]);
let z = d3.scaleLinear(cameraExtent.z, [0, canvasHeight]);
let width = cameraExtent.x[1] - cameraExtent.x[0];
let gridSize = 1;
while (width / gridSize > 16) {
gridSize *= 2;
}

// vertical grid lines
d3.select(svg)
.select(".grid")
.selectAll("line.vertical")
.data(
d3.range(
roundDownTo(cameraExtent.x[0], gridSize),
roundUpTo(cameraExtent.x[1], gridSize),
gridSize
)
)
.join("line")
.classed("vertical", true)
.attr("x1", (d) => x(d))
.attr("y1", 0)
.attr("x2", (d) => x(d))
.attr("y2", canvasHeight)
.attr("stroke", "#aaa");

// horizontal grid lines
d3.select(svg)
.select(".grid")
.selectAll("line.horizontal")
.data(
d3.range(
roundDownTo(cameraExtent.z[0], gridSize),
roundUpTo(cameraExtent.z[1], gridSize),
gridSize
)
)
.join("line")
.classed("horizontal", true)
.attr("x1", 0)
.attr("y1", (d) => z(d))
.attr("x2", canvasWidth)
.attr("y2", (d) => z(d))
.attr("stroke", "#aaa");

// measurement points
d3.select(svg)
.select(".points")
.selectAll("circle")
.data(measurements)
.join("circle")
.attr("cx", (d) => x(d.x))
.attr("cy", (d) => z(d.z))
.attr("r", 5)
.attr("fill", "black");

// measurement angles
const vectorLength = 30;
d3.select(svg)
.select(".vectors")
.selectAll("line")
.data(measurements)
.join("line")
.attr("x1", (d) => x(d.x))
.attr("y1", (d) => z(d.z))
.attr("x2", (d) => x(d.x) + Math.cos(d.angRad) * 30)
.attr("y2", (d) => z(d.z) + Math.sin(d.angRad) * 30)
.attr("stroke", "black");

// measurement labels
d3.select(svg)
.select(".points")
.selectAll("text")
.data(measurements)
.join("text")
.attr("x", (d) => x(d.x))
.attr("y", (d) => z(d.z))
.attr("dy", -12)
.text((d) => `x=${d.x?.toFixed()} z=${d.z?.toFixed()}`)
.attr("text-anchor", "end")
.attr("font-size", 12)
.attr("fill", "#444");

// intersection
d3.select(svg)
.select(".estimates")
.selectAll("circle.intersection")
.data(intersections)
.join("circle")
.classed("intersection", true)
.attr("cx", (d) => x(d.x))
.attr("cy", (d) => z(d.z))
.attr("r", 1)
.attr("fill", "black");
d3.select(svg)
.select(".estimates")
.selectAll("circle.error")
.data([intersection])
.join("circle")
.classed("error", true)
.attr("cx", (d) => x(d.x))
.attr("cy", (d) => z(d.z))
.attr("r", (d) => d.radius)
.attr("stroke", "rgba(0, 0, 255, 0.4)")
.attr("lineWidth", 2)
.attr("fill", "none");
d3.select(svg)
.select(".estimates")
.selectAll("text")
.data([intersection, intersection])
.join("text")
.attr("x", (d) => x(d.x))
.attr("y", (d) => z(d.z))
.attr("dy", (d, i) => d.radius + (i + 1) * 15)
.attr("text-anchor", "end")
.text((d, i) =>
i % 2 == 0 ? `x=${d.x?.toFixed()}` : `z=${d.z?.toFixed()}`
);

return svg;
}
Insert cell
Insert cell
Insert cell
measurements = d3
.csvParse(measurementsCsv)
.map((d) => ({ x: +d.x, z: +d.z, angle: +d.angle }))
.map((d) => ({
...d,
angRad: (d.angle / 180) * Math.PI + Math.PI / 2
// angRad: d.angle
}))
Insert cell
worldCoordinateMargin = 64
Insert cell
canvasWidth = width
Insert cell
canvasHeight = Math.floor((canvasWidth / 16) * 9)
Insert cell
cameraExtent = {
let range = proportionalRanges(
d3.extent([
...measurements.flatMap((d) => [
d.x - worldCoordinateMargin,
d.x + worldCoordinateMargin
]),
...intersections.flatMap((d) => [
d.x + worldCoordinateMargin,
d.x - worldCoordinateMargin
]),
intersection.x + worldCoordinateMargin,
intersection.x - worldCoordinateMargin
]),
d3.extent([
...measurements.flatMap((d) => [
d.z - worldCoordinateMargin,
d.z + worldCoordinateMargin
]),
...intersections.flatMap((d) => [
d.z + worldCoordinateMargin,
d.z - worldCoordinateMargin
]),
intersection.z + worldCoordinateMargin,
intersection.z - worldCoordinateMargin
]),
[0, canvasWidth],
[0, canvasHeight]
);
return { x: range.x, z: range.y };
}
Insert cell
intersectionOf = (p1a, p2a) => {
// https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line
let p1b = {
x: p1a.x + Math.cos(p1a.angRad),
z: p1a.z + Math.sin(p1a.angRad)
};
let p2b = {
x: p2a.x + Math.cos(p2a.angRad),
z: p2a.z + Math.sin(p2a.angRad)
};
let [x1, y1] = [p1a.x, p1a.z];
let [x2, y2] = [p1b.x, p1b.z];
let [x3, y3] = [p2a.x, p2a.z];
let [x4, y4] = [p2b.x, p2b.z];

let d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
return {
x: ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d,
z: ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d
};
}
Insert cell
intersections = {
let pairs = [];
for (let i = 0; i < measurements.length; i++) {
for (let j = 0; j < measurements.length; j++) {
if (i === j) continue;
pairs.push([measurements[i], measurements[j]]);
}
}
return pairs.map((pair) => intersectionOf(...pair));
}
Insert cell
intersection = {
let avgX = d3.mean(intersections, (d) => d.x);
let avgZ = d3.mean(intersections, (d) => d.z);
let radius = d3.max(intersections, (d) =>
Math.sqrt(Math.pow(d.x - avgX, 2) + Math.pow(d.z - avgZ, 2))
);
return { x: avgX, z: avgZ, radius };
}
Insert cell
### Helpers
Insert cell
roundToMultipleOf = (n, m, { dir = "down" } = {}) =>
(dir === "down" ? Math.floor : Math.ceil)(n / m) * m
Insert cell
roundUpTo = (n, m) => roundToMultipleOf(n, m, {dir: "up"})
Insert cell
roundDownTo = (n, m) => roundToMultipleOf(n, m, { dir: "down" })
Insert cell
Insert cell
proportionalRanges = (
await import("https://cdn.skypack.dev/d3-proportional-ranges@0.1.11?min")
).default
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