Published
Edited
Sep 3, 2021
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
const svg = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.style("background", "aliceblue");

// Big red circle coordinates
const redCircleTop = { x: 0, y: 80 };
const redCircleBottom = { x: 0, y: height - 80 };
const redCircleRight = { x: Math.min(200, width * 0.4), y: height / 2 };

// Find the center of the big red circle
const redCircleCenter = findCenter(
redCircleTop,
redCircleBottom,
redCircleRight
);

// calculate things like radius...
const [radius, larc_flag, sweep_flag] = arcWithPoints(
[redCircleTop.x, redCircleTop.y],
[redCircleBottom.x, redCircleBottom.y],
[redCircleRight.x, redCircleRight.y]
);

// create svg path object with string interpolation
// path is not closed
// e.g. M0,80 A289.2075,289.2075,0,0,1,0,587
const pathDefinition = `M${redCircleTop.x},${redCircleTop.y} A${radius},${radius},0,${larc_flag},${sweep_flag},${redCircleBottom.x},${redCircleBottom.y}`;

// Create the big red circle arc with d3 select syntax
const bigCircle = svg
.append("path")
.attr("fill", "red")
.attr("d", pathDefinition);

const path = bigCircle.node();

// Calculate position of the children circles
const circlePadding = 60; // how much space between top of red circle and children (on path)
const spaceBetweenChildren = 50; // how much padding between children

const totalLength = path.getTotalLength(); // length of visible part of big circle
const halfLength = totalLength / 2;
const maxAvailableSpace = totalLength - circlePadding * 2; // margin on top and bottom
const maxVisibleChildrenCount =
Math.floor(maxAvailableSpace / spaceBetweenChildren) + 1;

const visibleChildrenCount = Math.min(maxVisibleChildrenCount, childrenCount);
const requiredSpace = (visibleChildrenCount - 1) * spaceBetweenChildren;
const distanceWhereFirstPointShouldStart = halfLength - requiredSpace / 2; // Distance from top where first point should go

const children = [...Array(visibleChildrenCount).keys()].map((key, index) => {
const distanceFromTop =
index * spaceBetweenChildren + distanceWhereFirstPointShouldStart;
const pointOnPath = path.getPointAtLength(distanceFromTop);

// append to 'imaginary' line from the center of the big red circle to the point on the path a distance. This will move the point outside of the big red circle
const position = appendDistance(redCircleCenter, pointOnPath, 30);
return position;
});

// Create blue circles
children.forEach(child => {
svg
.append("circle")
.attr("cx", child.x)
.attr("cy", child.y)
.attr("fill", "blue")
.attr("r", 10);
});

return svg.node();
}
Insert cell
md`### Helpers`
Insert cell
// http://phrogz.net/SVG/3-point-circle.xhtml
function findCenter(p1, p2, p3) {
var d2 = p2.x * p2.x + p2.y * p2.y;
var bc = (p1.x * p1.x + p1.y * p1.y - d2) / 2;
var cd = (d2 - p3.x * p3.x - p3.y * p3.y) / 2;
var det = (p1.x - p2.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p2.y);
if (Math.abs(det) > 1e-10)
return {
x: (bc * (p2.y - p3.y) - cd * (p1.y - p2.y)) / det,
y: ((p1.x - p2.x) * cd - (p2.x - p3.x) * bc) / det
};
}
Insert cell
function getDistance(p1, p2) {
const [x, y] = [p1.x - p2.x, p1.y - p2.y]; // offset
return Math.sqrt(x * x + y * y);
}
Insert cell
// stackoverflow.com/questions/7740507/extend-a-line-segment-a-specific-distance
function appendDistance(p1, p2, length) {
const lenAB = Math.sqrt(
Math.pow(p1.x - p2.x, 2.0) + Math.pow(p1.y - p2.y, 2.0)
);

const x = p2.x + ((p2.x - p1.x) / lenAB) * length;
const y = p2.y + ((p2.y - p1.y) / lenAB) * length;

return { x, y };
}
Insert cell
// https://observablehq.com/@jrus/arc-through-a-point
function arcWithPoints([pA1, pA2], [pB1, pB2], [pP1, pP2]) {
// vectors from A -> P, B -> P, A -> B
const [a1, a2] = [pP1 - pA1, pP2 - pA2],
[b1, b2] = [pP1 - pB1, pP2 - pB2],
[v1, v2] = [pB1 - pA1, pB2 - pA2];

const adotb = a1 * b1 + a2 * b2,
awedgeb = a1 * b2 - a2 * b1,
ab_cotangent = adotb / awedgeb,
vv = v1 * v1 + v2 * v2;

const radius = (1 / 2) * Math.sqrt(vv * (1 + ab_cotangent * ab_cotangent)),
large_arc_flag = +(adotb > 0),
sweep_flag = +(awedgeb < 0);

return [radius, large_arc_flag, sweep_flag];
}
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