Public
Edited
Feb 15, 2023
Importers
40 stars
Also listed in…
Geo
Insert cell
Insert cell
edited_geo.features
Insert cell
viewof edited_geo = editor.mapView()
Insert cell
editor.buttonView()
Insert cell
Insert cell
editor.tableView(["color", "admin", "pop_est"])
Insert cell
editor.propertiesView()
Insert cell
editor.propertyView("admin")
Insert cell
editor.propertyView("pop_est")
Insert cell
editor.colorPropertyView()
Insert cell
editor.jsonView()
Insert cell
editor = geoEditor({
features: geo,
projection: projection
})
Insert cell
Insert cell
Insert cell
function geoEditor({ features, container, render, projection, options } = {}) {
features = features || { type: "FeatureCollection", features: [] };
projection = projection || d3.geoMercator();
options = options || { zoom: true };

const dispatch = d3.dispatch("update", "data", "zoomTo"),
editor = {
features,
container,
render,
projection,
options,
dispatch
};

editor.mapView = () => mapView(editor);
editor.buttonView = () => buttonView(editor);
editor.propertiesView = () => propertiesView(editor);
editor.propertyView = property => propertyView(editor, property);
editor.colorPropertyView = () => colorPropertyView(editor);
editor.jsonView = () => jsonView(editor);
editor.tableView = properties => tableView(editor, properties);

editor.selected = () => features.features.filter(d => d.selected);

// debounce data => update
dispatch.on(
"data",
debounce(message => {
dispatch.call("update", null, message);
}, 400)
);

return editor;
}
Insert cell
geo = {
const geo = JSON.parse(JSON.stringify(countries));

geo.features.forEach(d => (d.properties.color = d.properties.fill));
return geo;

/*({
type: "FeatureCollection",
features: []
})*/
}
Insert cell
function defaultRenderer(canvas, features, projection) {
const context = canvas.getContext("2d"),
width = canvas.width,
height = canvas.height;

const path = d3.geoPath(projection).context(context);
context.clearRect(0, 0, width, height);
context.strokeStyle = "#000";

context.beginPath();
path(d3.geoGraticule10());
context.lineWidth = 0.25;
context.stroke();

context.beginPath();
path(world);
context.fillStyle = "rgba(255,255,255,0.5)";
context.fill();
context.lineWidth = 0.5;
context.stroke();

context.beginPath();
path({ type: "Sphere" });
context.lineWidth = 2;
context.stroke();
}
Insert cell
function subject(elements, projection, maxDistance = 10) {
return function(evt) {
if (evt === undefined) evt = d3.event;

let subject = null;
let distance = maxDistance;

for (const pt of geoPoints(elements, projection)) {
const [x, y] = pt.xy;
let d = Math.hypot(evt.x - x, evt.y - y);
if (d < distance) {
distance = d;
subject = pt;
pt.x = x;
pt.y = y;
}
}

return subject;
};
}
Insert cell
function geoPoints(elements, projection) {
const data = [];
let p0;
for (const p of getPoints(
elements.filter(d => d.geometry.type === "Point" || d.selected)
)) {
p.xy = projection(p);
if (
p.xy[0] >= 0 &&
p.xy[0] <= width &&
p.xy[1] >= 0 &&
p.xy[1] <= height &&
(!p.i || !p0 || euclidian(p.xy, p0.xy) > 8 * (1 + !!p.midpoint))
) {
data.push(p);
p0 = p;
}
}
return data;

function* getPoints(elements) {
for (const elt of elements) {
const coords = elt.geometry.coordinates;
switch (elt.geometry.type) {
case "Point":
yield Object.assign(coords, { elt });
break;
case "MultiPoint":
for (const p of coords) yield Object.assign(p, { elt });
break;
case "LineString":
yield* line(coords, elt);
break;
case "Polygon":
for (const ring of elt.geometry.coordinates)
yield* line(ring, elt, true);
break;
case "MultiLineString":
for (const ring of elt.geometry.coordinates) yield* line(ring, elt);
break;
case "MultiPolygon":
for (const polygon of elt.geometry.coordinates)
for (const ring of polygon) yield* line(ring, elt, true);
break;
}
}
}

function* line(coords, elt, closed) {
let pt0,
i = 0;
for (const pt of coords) {
if (i) {
const midpoint = d3.geoInterpolate(pt0, pt)(.5);
yield Object.assign(midpoint, {
elt,
coords,
i,
midpoint: true
});
}
if (!closed || i < coords.length - 1) {
yield Object.assign(pt, { elt, coords, i, closed });
pt0 = pt;
i++;
}
}
}
}
Insert cell
function colorPropertyView(editor) {
const { features, dispatch } = editor;

function getTopColor() {
const selected = editor.selected()[0];
return selected ? d3.rgb(selected.properties.color).hex() : "";
}

const form = html`<form><input type=color>`;

const el = form.elements[0];
el.value = getTopColor();

el.addEventListener("input", listen);

dispatch.on("update.colorProperty", message => {
if (message !== "colorProperty") el.value = getTopColor();
});

form.value = features;
return form;

function listen(event) {
const selected = editor.selected()[0];
if (selected) {
const color = el.value;
selected.properties.color = color2css(color);
dispatch.call("data", null, "colorProperty");
}
event.preventDefault();
}
}
Insert cell
function propertiesView(editor) {
const { features, container, render, projection, options, dispatch } = editor;

function getTopProperties() {
const selected = editor.selected()[0];
return selected ? JSON.stringify(selected.properties, null, 2) : "";
}

const form = html`<form><label>Properties
<textarea style="width:100%" rows=10></textarea>
<div style="color:red; font:small"></div>
`;

const el = form.elements[0];
// Object.assign(el, props);
el.placeholder = "Select an element in the map";
el.value = getTopProperties();

el.addEventListener("input", listen);

dispatch.on("update.properties", message => {
if (message !== "properties") el.value = getTopProperties();
});

form.value = features;
return form;

function listen(event) {
const selected = editor.selected()[0];
if (selected) {
let message = "";
try {
selected.properties = JSON5.parse(el.value);
dispatch.call("data", null, "properties");
} catch (e) {
if (e instanceof SyntaxError) message = e;
} finally {
d3.select(form)
.select("div")
.text(message);
}
}
event.preventDefault();
}
}
Insert cell
function propertyView(editor, property) {
const { features, container, render, projection, options, dispatch } = editor;
function getTopProperty() {
const selected = editor.selected()[0];
return (selected && selected.properties[property]) || "";
}

const form = html`<form><label>${property}: <input type=text>`;

const el = form.elements[0];
el.value = getTopProperty();

el.addEventListener("input", listen);
form.addEventListener("submit", event => {
event.preventDefault();
});

dispatch.on("update." + property, message => {
if (message !== property) el.value = getTopProperty();
});

form.value = features;
return form;

function listen(event) {
const selected = editor.selected()[0];
if (selected) {
let value = el.value;
if (value !== "") {
if (String(Number(value)) == value) value = Number(value);
selected.properties[property] = value;
} else delete selected.properties[property];
dispatch.call("data", null, property);
}
event.preventDefault();
}
}
Insert cell
function jsonView({
features,
container,
render,
projection,
options,
dispatch
}) {
function getJSON() {
const a = JSON.parse(JSON.stringify(features));
a.features.forEach(a => {
delete a.selected;
});
return geoformat(options.precision || 8)(a);
}

const form = html`<form><label>JSON
<textarea style="width:100%" rows=20></textarea>
<div style="color:red; font:small"></div>
`;

const el = form.elements[0];
// Object.assign(el, props);
el.placeholder = "Select an element in the map";
el.value = getJSON();

el.addEventListener("input", listen);

dispatch.on("update.json", message => {
if (message !== "json") el.value = getJSON();
});

form.value = features;
return form;

function listen(event) {
let message = "";
try {
const f = JSON5.parse(el.value);
if (f.features) {
features.features.splice(0, Infinity, ...f.features);
dispatch.call("data", null, "json");
}
} catch (e) {
if (e instanceof SyntaxError) message = e;
} finally {
d3.select(form)
.select("div")
.text(message);
}
event.preventDefault();
}
}
Insert cell
// inspiration from https://observablehq.com/@tomgp/editable-table
function tableView(editor, properties) {
const { features, container, render, projection, options, dispatch } = editor;

const form = html`<table></table>`;

const sticky = ' style="position:sticky;top:0;background:white"';
function update() {
const theader = `<tr><th${sticky}>#</th>${properties
.map(d => `<td${sticky}>${d}</td>`)
.join("")}</tr>`,
tbody = features.features
.map(
(e, i) =>
`<tr data-rank=${i}><th>${i}</th>${properties
.map(d => `<td contenteditable>${e.properties[d] || ""}</td>`)
.join("")}</tr>`
)
.join("");

d3.select(form).html(
`<tbody style="display:block;max-height: 20em; overflow-y: scroll">${theader}${tbody}`
);

d3.select(form)
.selectAll("[contenteditable]")
.on("focus", function() {
const tr = d3.event.target.parentElement,
rank = +tr.getAttribute("data-rank"),
feature = features.features[rank];
features.features.forEach(d => delete d.selected);
feature.selected = true;
dispatch.call("zoomTo", null, feature);
});
}

update();

form.addEventListener("input", listen);

dispatch.on("update.table", message => {
if (message !== "table") update();
});

form.value = features;
return form;

function listen(event) {
const tr = event.target.parentElement,
rank = +tr.getAttribute("data-rank"),
feature = features.features[rank];

tr.querySelectorAll("td").forEach((td, j) => {
const property = properties[j],
text = td.textContent || "";
if (text)
feature.properties[property] =
String(Number(text)) === text ? Number(text) : text;
else delete feature.properties[property];
});
dispatch.call("data", null, "table");
event.preventDefault();
}
}
Insert cell
function buttonView({
features,
container,
render,
projection,
options,
dispatch
}) {
const wrapper = html`${ui(features, projection, dispatch)}`;

dispatch.on("update.button", () => {
wrapper.dispatchEvent(new CustomEvent("input"));
});

wrapper.value = features;
return wrapper;
}
Insert cell
function mapView({
features,
container,
render,
projection,
options,
dispatch
}) {
// default values
if (container === undefined) container = DOM.context2d(width, height).canvas;
if (features === undefined)
features = {
type: "FeatureCollection",
features: []
};
if (render === undefined)
render = defaultRenderer;

const w = parseInt(container.style.width) || container.width,
h = (w * container.height) / container.width,
elements = features.features,
svg = d3.create("svg");

const path = d3.geoPath(projection);

const wrapper = html`<div style="position:relative">
${svg.node()}
${container}
`;

svg
.style("position", "absolute")
.style("top", "0")
.style("z-index", 2)
//.style("pointer-events", "none")
.attr("viewBox", `0 0 ${w} ${h}`);

const gGraticule = svg
.append("path")
.attr("id", "graticule")
.style("fill", "none")
.style("stroke", "yellow")
.datum(d3.geoGraticule10());

const gPaths = svg.append("g").attr("id", "paths");
const gHandles = svg.append("g").attr("id", "handles");

function updateSvg() {
gPaths
.selectAll("path")
.data(elements)
.join("path")
.attr("d", path)
.style("stroke", d => d.properties.color)
.style("fill", d => d.properties.color)
.style("fill-opacity", d =>
["Point", "MultiPoint", "Polygon", "MultiPolygon"].includes(
d.geometry.type
)
? 0.4
: 0
)
.on("click", d => {
const sourceEvent = d3.event;
if (sourceEvent && sourceEvent.shiftKey) {
// multiple select
} else {
elements.forEach(d => delete d.selected);
}
d.selected = true;
dispatch.call("data", null, "selection");
});

const symbol = d3.symbol().size(40)();
gHandles
.selectAll("path")
.data(geoPoints(features.features, projection))
.join("path")
.attr("d", symbol)
.attr("transform", d => `translate(${d.xy})`)
.attr("opacity", 1)
.attr("fill", d => (d.elt.selected && !d.midpoint ? "black" : "none"))

.attr("stroke", "black");
}

svg
.on("click", d => {})
.call(
drag(elements, projection).on(
"start.render drag.render end.render",
() => {
// render(container, features, projection);
updateSvg();
dispatch.call("data", null, "drag");
}
)
);

let zoom,
transform = d3.zoomIdentity;
if (options.zoom) {
transform.x = projection.translate()[0];
transform.y = projection.translate()[1];
transform.k = projection.scale();

zoom = d3.zoom().on("zoom", () => {
transform = d3.event.transform;
projection.translate([transform.x, transform.y]).scale(transform.k);
render(container, features, projection);
updateSvg();
});
svg.call(zoom).call(zoom.transform, transform);
}

dispatch.on("update.map", () => {
wrapper.value = features;
render(container, features, projection);
updateSvg();
wrapper.dispatchEvent(new CustomEvent("input"));
});

dispatch.on(
"zoomTo",
debounce(d => {
if (zoom) {
const { x, y, k } = transform;
projection.fitExtent(
[
[width * 0.2, height * 0.2],
[width * (1 - 0.2), height * (1 - 0.2)]
],
d
);
transform.x = projection.translate()[0];
transform.y = projection.translate()[1];
transform.k = projection.scale();

// todo: the transition doesn't work
projection.translate([x, y]).scale(k);
svg
.transition()
.duration(750)
.call(zoom.transform, transform);
}
}, 400)
);

wrapper.value = features;
return wrapper;
}
Insert cell
function euclidian(a, b) {
return Math.hypot(a[0] - b[0], a[1] - b[1]);
}
Insert cell
drag = (elements, projection) => {
function dragstarted() {
const { subject, sourceEvent } = d3.event;
subject.elt.active = true;

elements.forEach(d => delete d.selected);
subject.elt.selected = true;

// if the point is a midpoint, insert it in the ring
if (subject.midpoint) {
subject.add = true;
} else {
if (sourceEvent && sourceEvent.shiftKey) subject.remove = true;
}
}

function dragged() {
const subject = d3.event.subject;
subject.x = d3.event.x;
subject.y = d3.event.y;

delete subject.remove;
if (subject.add) {
delete subject.add;
delete subject.midpoint;
subject.coords.splice(subject.i, 0, subject);
}

const coords = projection.invert([subject.x, subject.y]);
subject[0] = coords[0];
subject[1] = coords[1];
}

function dragended() {
const { subject } = d3.event;
delete subject.elt.active;

if (subject.remove) {
const isRing = subject.closed;
if (isRing) {
subject.coords.pop();
subject.coords.splice(subject.i % subject.coords.length, 1);
subject.coords.push(subject.coords[0]);
} else {
subject.coords.splice(subject.i, 1);
}
if (subject.coords.length < 2 + isRing) {
// empty the coords list
subject.coords.splice(0, Infinity);
// remove empty rings
subject.elt.geometry.coordinates = prune(
subject.elt.geometry.coordinates
);
// remove the element if it has no remaining ring
if (subject.elt.geometry.coordinates.flat(Infinity).length === 0) {
elements.splice(elements.indexOf(subject.elt), 1);
}
}
}
}
function prune(array) {
return array
.map(d => (Array.isArray(d) ? prune(d) : d))
.filter(d => !Array.isArray(d) || d.length);
}

return d3
.drag()
.subject(subject(elements, projection, 20))
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
Insert cell
function ui(features, projection, dispatch) {
return html`
${uiAddPoint(features, projection, dispatch)}
${uiAddLineString(features, projection, dispatch)}
${uiAddPolygon(features, projection, dispatch)}
${uiRemoveTop(features, projection, dispatch)}`;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
function randomPoint(projection) {
return (
(projection &&
projection.invert &&
projection.invert([Math.random() * width, Math.random() * height])) || [
Math.random() * 360 - 180,
90 * (Math.random() - Math.random())
]
);
}
Insert cell
height = 600
Insert cell
radius = 32
Insert cell
d3 = require("d3@5", "d3-geo-projection@2", "d3-geo-polygon@1")
Insert cell
world = {
yield null;
yield await d3.json(
"https://unpkg.com/visionscarto-world-atlas@0.0.6/world/110m_land.geojson"
);
}
Insert cell
countries = {
yield { type: "FeatureCollection", features: [] };
yield await d3.json(
"https://unpkg.com/visionscarto-world-atlas@0.0.6/world/110m_countries.geojson"
);
}
Insert cell
Insert cell
import { geoformat } from "@fil/geoformat"
Insert cell
import { color2css } from "@fil/color2css"
Insert cell
// https://github.com/json5/json5 tolerant JSON.parse
JSON5 = require("json5")
Insert cell
debounce = require("debounce").catch(() => window.debounce)
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