Public
Edited
Sep 12
Fork of Simple D3
Importers
Insert cell
Insert cell
visual = DOM.svg(width+2*margin, height+2*margin)
Insert cell
viewof n = Inputs.range([10,80], {step: 1, label: "num points(about)"})
Insert cell
viewof n.addEventListener("input", (e) => {
console.log('event!',e)
draw();
})
Insert cell
draw();
Insert cell
function draw() {
const gMargin = drawMargin();
drawV(gMargin, delaunay, voronoi, [r + margin, r + margin], [-r, -r, r, r]);
drawV(
gMargin,
delaunay2,
voronoi2,
[2 * r + 2 * margin, margin], // 第二张图的左上角
[0, 0, 2 * r, 2 * r],
false
);
drawV(
gMargin,
delaunay3,
voronoi3,
[5 * r + 3 * margin, r + margin], // 第3张图的圆心
[-r, -r, r, r]
);
}
Insert cell
function drawMargin() {
const svg = d3.select(visual);
svg.selectAll('g').remove();
const gMargin = svg
.append("g")
.attr("transform", `translate(${margin}, ${margin})`);
gMargin
.append("rect")
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("fill", "none")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
return gMargin;
}
Insert cell
function drawV(
gMargin,
delaunay,
voronoi,
center = [0, 0],
ext = [],
isDrawCircle = true
) {
const gCircle = gMargin
.append("g")
.attr("transform", `translate(${center[0]}, ${center[1]})`);

if (isDrawCircle)
gCircle
.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", r)
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("fill", "none");

// gCircle
// .selectAll("circle")
// .data(points)
// .enter()
// .append("circle")
// .attr("stroke", "gold")
// .attr('stroke-width',0.5)
// .attr("fill", "red")
// .attr("r", 2)
// .attr("cx", (d) => d[0])
// .attr("cy", (d) => d[1])

gCircle
.append("path")
.attr("d", voronoi.renderBounds(null))
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("fill", "none");
gCircle
.append("path")
.attr("d", delaunay.renderHull(null))
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("fill", "none");
gCircle
.append("path")
.attr("d", delaunay.renderPoints())
.attr("stroke", "none")
.attr("fill", "black");
const voronoiCellPathArr = d3
.pairs(delaunay.points)
.map((d, i) => voronoi.renderCell(i));
const voronoiPolygonArr = d3
.pairs(delaunay.points)
.map((d, i) => voronoi.cellPolygon(i));
gCircle
.append("g")
.classed("g-cell", true)
.selectAll("path")
.data(voronoiCellPathArr)
.enter()
.append("path")
.attr("d", (d, i) => {
return d;
})
.attr("stroke", (d, i) => {
if (isOnBorder(i)) {
return "#bbb";
} else {
return "#000";
}
})
.attr("stroke-width", (d, i) => {
if (isOnBorder(i)) {
return 0.5;
} else {
return 1.5;
}
})
.attr("fill", "none");

function isOnBorder(i) {
const polygon = voronoiPolygonArr[i];

for (const [x, y] of polygon) {
if (x === undefined || y === undefined) return true;
if (x <= ext[0] || x >= ext[2] || y <= ext[1] || y >= ext[3]) {
return true; // 顶点在边界上,说明与边界接触
}
}
return false;
}
}
Insert cell
delaunay = d3.Delaunay.from(points);
Insert cell
voronoi = delaunay.voronoi([-r, -r, r, r])
Insert cell
Insert cell
delaunay2 = d3.Delaunay.from(poissonPoints);
Insert cell
voronoi2 = delaunay2.voronoi([0, 0, 2 * r, 2 * r])
Insert cell
delaunay3 = d3.Delaunay.from(poissonPointsCircle)
Insert cell
voronoi3 = delaunay3.voronoi([-r, -r, r, r])
Insert cell
Insert cell
r = 150
Insert cell
numFig = 3
Insert cell
margin = 30
Insert cell
width = numFig * 2 * r + 2 * margin + (numFig - 1) * margin
Insert cell
height = 2*r+2*margin
Insert cell
points = makeCirclePoints(n,r,[0,0])
Insert cell
sepRadius2 = Math.sqrt((2 * r * 2 * r) / n / (Math.PI / 2))
Insert cell
poissonPoints = runSample(sepRadius2, r,false)
Insert cell
sepRadius3 = Math.sqrt((Math.PI * r * r) / n / (Math.PI / 2))
Insert cell
poissonPointsCircle = runSample(sepRadius3,r, true)
Insert cell
function makeCirclePoints(n = 100, r = 50, center = [0, 0]) {
const [centerX, centerY] = center;
return d3.range(n).map(() => {
let angle = Math.random() * 2 * Math.PI; // 随机角度
let radius = Math.pow( Math.random(),1/2) * r; // 随机半径
return [
centerX + radius * Math.cos(angle),
centerY + radius * Math.sin(angle)
];
});
}
Insert cell
function runSample(sepRadius, boundRadius, isCircle = false) {
let sampleSites = isCircle
? poissonDiscCircleSampler(boundRadius, sepRadius)
: poissonDiscSampler(2 * boundRadius, 2 * boundRadius, sepRadius);
function sites() {
let points = [];
for (var i = 0; i < 10000; ++i) {
let s = sampleSites();
if (s) points.push(s);
}
return points;
}

return sites();
}
Insert cell
// Based on https://www.jasondavies.com/poisson-disc/
function poissonDiscSampler(width, height, radius) {
var k = 40, // maximum number of samples before rejection
radius2 = radius * radius,
R = 3 * radius2,
cellSize = radius * Math.SQRT1_2,
gridSize = Math.ceil(width / cellSize),
grid = new Array(gridSize * gridSize),
queue = [],
queueSize = 0,
sampleSize = 0;

return function() {
if (!sampleSize) return sample(Math.random() * width, Math.random() * height);

// Pick a random existing sample and remove it from the queue.
while (queueSize) {
var i = Math.random() * queueSize | 0,
s = queue[i];

// Make a new candidate between [radius, 2 * radius] from the existing sample.
for (var j = 0; j < k; ++j) {
var a = 2 * Math.PI * Math.random(),
r = Math.sqrt(Math.random() * R + radius2),
x = s[0] + r * Math.cos(a),
y = s[1] + r * Math.sin(a);

// Reject candidates that are outside the allowed extent,
// or closer than 2 * radius to any existing sample.
if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) return sample(x, y);
}

queue[i] = queue[--queueSize];
queue.length = queueSize;
}
};

function far(x, y) {
var i = x / cellSize | 0,
j = y / cellSize | 0,
i0 = Math.max(i - 2, 0),
j0 = Math.max(j - 2, 0),
i1 = Math.min(i + 3, gridSize),
j1 = Math.min(j + 3, gridSize);

for (j = j0; j < j1; ++j) {
var o = j * gridSize;
for (i = i0; i < i1; ++i) {
if (s = grid[o + i]) {
var s,
dx = s[0] - x,
dy = s[1] - y;
if (dx * dx + dy * dy < radius2) return false;
}
}
}

return true;
}

function sample(x, y) {
var s = [x, y];
queue.push(s);
grid[gridSize * (y / cellSize | 0) + (x / cellSize | 0)] = s;
++sampleSize;
++queueSize;
return s;
}
}
Insert cell
// Based on https://www.jasondavies.com/poisson-disc/
function poissonDiscCircleSampler(boundR, radius) {
var k = 40, // maximum number of samples before rejection
radius2 = radius * radius,
R3 = 3 * radius2,
cellSize = radius * Math.SQRT1_2,
gridSize = Math.ceil((2 * boundR) / cellSize), // 仍然笛卡尔坐标系,只是限定在圆内
gridSize = Math.ceil((2 * boundR) / cellSize),
grid = new Array(gridSize * gridSize),
queue = [],
queueSize = 0,
sampleSize = 0;

return function () {
if (!sampleSize) {
const angle = 2 * Math.PI * Math.random();
const dist = boundR * Math.sqrt(Math.random());
return sample(dist * Math.cos(angle), dist * Math.sin(angle)); // 相对于圆心的
}

// Pick a random existing sample and remove it from the queue.
while (queueSize) {
var i = (Math.random() * queueSize) | 0,
s = queue[i];//相对于圆心的

// Make a new candidate between [radius, 2 * radius] from the existing sample.
for (var j = 0; j < k; ++j) {
var a = 2 * Math.PI * Math.random(),
r = Math.sqrt(Math.random() * R3 + radius2),
x = s[0] + r * Math.cos(a),
y = s[1] + r * Math.sin(a);

// Reject candidates that are outside the allowed extent,
// or closer than 2 * radius to any existing sample.
if (x * x + y * y <= boundR * boundR && far(x, y)) return sample(x, y);
}

queue[i] = queue[--queueSize];
queue.length = queueSize;
}
};

function far(x, y) {
//相对圆心的,要先转化成相对矩形原点的,才能算网格
const gx = (x + boundR) / cellSize | 0;
const gy = (y + boundR) / cellSize | 0;
const i0 = Math.max(gx - 2, 0),
j0 = Math.max(gy - 2, 0),
i1 = Math.min(gx + 3, gridSize),
j1 = Math.min(gy + 3, gridSize);

for (let j = j0; j < j1; ++j) {
for (let i = i0; i < i1; ++i) {
const s = grid[j * gridSize + i];
if (s) {
const dx = s[0] - x,
dy = s[1] - y;
if (dx * dx + dy * dy < radius2) return false;
}
}
}
return true;
}

function sample(x, y) {
//相对于圆心的
var s = [x, y];
queue.push(s);
grid[gridSize * (( (y+boundR)/ cellSize) | 0) + (((x+boundR) / cellSize) | 0)] = s; //相对圆心的,要先转化成相对矩形原点的,才能算网格
++sampleSize;
++queueSize;
return s;
}
}
Insert cell
function parseRectPath(pathStr) {
const points = pathStr.match(/[ML](-?\d+),(-?\d+)/g).map((cmd) => {
const [x, y] = cmd.slice(1).split(",").map(Number);
return { x, y };
});
return points; // 返回矩形的四个顶点坐标
}
Insert cell
voronoi3.renderBounds()
Insert cell
voronoiPolygonArr = d3.pairs(delaunay.points).map((d,i)=>voronoi.cellPolygon(i))
Insert cell
voronoiPolygonArr.filter(poly=>poly.some(point=>point.every(coord=>coord===undefined)))
Insert cell
voronoiCellPathArr = d3.pairs(delaunay.points).map((d,i)=>voronoi.renderCell(i))
Insert cell
parseRectPath(voronoi3.renderBounds())
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