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

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