Public
Edited
Sep 25, 2023
Insert cell
Insert cell
map = html`<svg width="600" height="400">
<path d="${path(land)}" fill="none" stroke="#ccc"></path>
<path d="${getGeoScallopedPath(area)}" fill="none" stroke="red"></path>
</svg>`

// <path d="${path(area)}" fill="none" stroke="red"></path>
Insert cell
projection = d3.geoAlbers().scale(450).translate([300, 200])
Insert cell
path = d3.geoPath(projection)
Insert cell
world = FileAttachment("land-50m.json").json()
Insert cell
land = topojson.feature(world, world.objects.land)
Insert cell
area = {
return {
type: "Feature",
properties: {},
geometry: {
coordinates: [
[
[-91.29922123804242, 37.52580499032423],
[-81.64467452059675, 39.75666412073841],
[-83.3927458599171, 31.799998562612544],
[-99.1595715827322, 31.42877945422073],
[-101.95380451795505, 34.35388778058247],
[-98.0376006871479, 36.87768939886985],
[-92.79927705349355, 36.25361640418426]
]
],
type: "Polygon"
}
};
}
Insert cell
getGeoScallopedPath(area, projection)
Insert cell
function getGeoScallopedPath(feature) {
const coodinates = feature.geometry.coordinates[0].map((coordinate) => {
const projected = projection(coordinate);

return {
x: projected[0],
y: projected[1]
};
});

return getScalloppedPath(coodinates, true, 8);
}
Insert cell
// https://stackoverflow.com/questions/40941859/drawing-scalloped-polygon-between-multiple-points

function getScalloppedPath(points, isClosed, radius = 24) {
return scapolledLine(points, isClosed, radius);

function getDirection(pts, index, closed) {
let last = pts.length - 1;
let start = index;
let end = closed && start == last ? 0 : index + 1;
let prev = closed && start == 0 ? last : start - 1;
let next = closed && end == last ? 0 : end + 1;

let isValidSegment =
0 <= start && start <= last && 0 <= end && end <= last && end !== start;

if (!isValidSegment) {
return 1;
}

let pt1 = pts[start];
let pt2 = pts[end];

let ccw = 0.0;
let theta = Math.atan2(pt2.y - pt1.y, pt2.x - pt1.x);

if (0 <= prev && prev <= last) {
ccw += getSeparationFromLine(pt1, theta, pts[prev]);
}

if (0 <= next && next <= last) {
ccw += getSeparationFromLine(pt1, theta, pts[next]);
}

return ccw > 0 ? "1" : "0";
}

function scapolledLine(pts, closed, radius) {
let scallopSize = radius;
let lastIndex = pts.length - 1;
let path = [];
let newP = null;

path.push("M", pts[0].x, pts[0].y);

pts.forEach(function (s, currentIndex) {
let stepW = scallopSize;
let lsw = 0;
let isClosingSegment = closed && currentIndex == lastIndex;
let nextIndex = isClosingSegment ? 0 : currentIndex + 1;
let e = pts[nextIndex];

if (!e) {
return;
}

let direction = getDirection(pts, currentIndex, closed);
let dist = distance(s.x, s.y, e.x, e.y);

if (dist === 0) {
return;
}

let angle = getAngle(s.x, s.y, e.x, e.y);
newP = s;

// Number of possible scallops between current pts
let n = dist / stepW,
crumb;

if (dist < stepW * 2) {
stepW = dist - stepW > stepW * 0.38 ? dist / 2 : dist;
} else {
n = n - (n % 1);
crumb = dist - n * stepW;
stepW += crumb / n;
}

// Recalculate possible scallops.
n = dist / stepW;
let aw = stepW / 2;

for (let i = 0; i < n; i++) {
newP = findNewPoint(newP.x, newP.y, angle, stepW);

if (i === n - 1) {
aw = (lsw > 0 ? lsw : stepW) / 2;
}

path.push("A", aw, aw, "0 0 " + direction, newP.x, newP.y);
}

if (isClosingSegment) {
path.push("A", stepW / 2, stepW / 2, "0 0 " + direction, e.x, e.y);
}
});

return path.join(" ");
}

function distance(x1, y1, x2, y2) {
return Math.sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

function findNewPoint(x, y, angle, distance) {
let result = {};
result.x = Math.round(Math.cos(angle) * distance + x);
result.y = Math.round(Math.sin(angle) * distance + y);

return result;
}

function getAngle(x1, y1, x2, y2) {
return Math.atan2(y2 - y1, x2 - x1);
}

function getSeparationFromLine(lineOrigin, lineAngle, pt) {
let x = pt.x - lineOrigin.x;
let y = pt.y - lineOrigin.y;

return -x * Math.sin(lineAngle) + y * Math.cos(lineAngle);
}
}
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