function MapaColombiaFuerzas({
data = null,
id = (d) => d.id,
drawCities = true,
drawDepts = true,
idFeatures = (d) =>
drawCities ? d.properties.MPIO_CDPMP : d.properties.DPTO_CDPMP,
value = (d) => d.properties.DPTO_CCDGO,
color,
fill = null,
projection,
drawMap = true,
fillMap = null,
strokeMap = "#aaa",
strokeMapDepts = "#333",
strokeNodes = "#eee",
strokeWidthNodes = 1,
alphaNodes = 1,
alphaMap = 0.3,
width = 600,
height = 600,
container = htl.html`<div style="position: relative"></div>`,
context = this?.context || DOM.context2d(width, height),
contextMap = this?.contextMap || DOM.context2d(width, height),
margin = { left: 10, top: 10, right: 10, bottom: 10 },
rRange = [1, 20],
rByPopulation = true,
r = d3
.scaleSqrt()
.domain([
0,
d3.max(
(drawCities ? land : landDepts).features,
(d) => d.properties.STP27_PERS
)
])
.range(rRange),
forceToCentroid = 0.1,
collide = true,
collideMargin = 1,
warmup = 200,
labelColor = "#000",
labelFont = "9px sans-serif",
labelAlign = "center",
labelIfBiggerThan = 10,
parentInvalidation = invalidation,
backgroundColor = null,
drag = false,
fmtVal = d3.format(".2s")
} = {}) {
color = color || d3.scaleSequential(d3.interpolateReds);
r.range(rRange);
if (!rByPopulation) {
r.domain(d3.extent(data, value));
}
projection =
projection ||
d3
.geoTransverseMercator()
.rotate([74 + 30 / 60, -38 - 50 / 60])
.fitExtent(
[
[margin.left, margin.top],
[width - margin.right, height - margin.bottom]
],
landDepts
);
// Radius accessor
const rFn = (n) => {
if (!n) return;
if (rByPopulation) {
return r(n.properties.STP27_PERS);
} else {
if (n.data) return r(value(n.data));
}
};
let selected = null;
let dataMap = null;
if (data) {
dataMap = new Map(data.map((d) => [id(d), d]));
}
contextMap = this?.contextMap || DOM.context2d(width, height);
context = this?.context || DOM.context2d(width, height);
if (backgroundColor) {
contextMap.canvas.style["background-color"] = backgroundColor;
}
const nodes = drawCities
? land.features.map((d) => ({ ...d }))
: landDepts.features.map((d) => ({ ...d }));
const pathCanvas = d3.geoPath().projection(projection);
const path2D = new Path2D();
const path2DDepts = new Path2D();
if (drawMap) {
contextMap.save();
pathCanvas.context(path2D)(drawCities ? landMesh : landDeptsMesh);
contextMap.beginPath();
contextMap.strokeStyle = strokeMap;
contextMap.globalAlpha = alphaMap;
contextMap.stroke(path2D);
if (fillMap) {
contextMap.fillStyle = fillMap;
contextMap.fill(path2D);
}
if (drawDepts) {
pathCanvas.context(path2DDepts)(landDeptsMesh);
contextMap.beginPath();
contextMap.strokeStyle = strokeMapDepts;
contextMap.globalAlpha = alphaMap;
contextMap.stroke(path2DDepts);
}
contextMap.restore();
}
for (let n of nodes) {
n.centroid = pathCanvas.centroid(n);
[n.x, n.y] = n.centroid;
if (data) {
n.data = dataMap.get(idFeatures(n));
}
n.r = rFn(n);
}
// console.log("r.domain()", r.domain(), r.range());
const simulation = d3
.forceSimulation(nodes)
// .velocityDecay(0.3)
.force("x", d3.forceX((d) => d.centroid[0]).strength(forceToCentroid))
.force("y", d3.forceY((d) => d.centroid[1]).strength(forceToCentroid))
// .force("charge", d3.forceManyBody())
.force(
"collide",
collide ? d3.forceCollide((n) => n.r + collideMargin).iterations(1) : null
)
.stop();
for (let i = 0; i < warmup; i++) simulation.tick();
simulation.on("tick", ticked).alpha(0.3).restart();
if (data) {
if (color.domain && color.domain().join(",") === "0,1") {
color.domain(d3.extent(data, value));
}
}
function ticked() {
// console.log("tick", simulation.alpha());
context.clearRect(0, 0, width, height);
context.save();
for (const n of nodes) {
if (strokeNodes) context.strokeStyle = strokeNodes;
const nForColor = data ? dataMap.get(idFeatures(n)) : n;
if (!nForColor) {
console.log(`Couldn't find ${idFeatures(n)} in the data`);
continue;
}
context.fillStyle = fill ? fill(n) : color(value(nForColor));
context.beginPath();
context.lineWidth = strokeWidthNodes;
context.globalAlpha = !selected
? alphaNodes
: n === selected
? alphaNodes
: n.properties.DPTO_CCDGO === selected.properties.DPTO_CCDGO
? alphaNodes * 0.8
: 0.3;
context.arc(n.x, n.y, n.r, 0, 2 * Math.PI);
if (strokeNodes) context.stroke();
context.fill();
}
context.restore();
context.save();
context.fillStyle = labelColor;
context.textAlign = labelAlign;
context.font = labelFont;
for (const n of nodes) {
if (n.r > labelIfBiggerThan || n === selected) {
context.fillText(
n.properties[drawCities ? "MPIO_CNMBR" : "DPTO_CNMBR"],
n.x,
n.y
);
context.fillText(fmtVal(value(data ? n.data : n)), n.x, n.y + 10);
}
}
context.restore();
}
function _drag(simulation) {
function dragsubject(event) {
return simulation.find(event.x, event.y);
}
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}
function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
return d3
.drag()
.subject(dragsubject)
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
if (drag) {
d3.select(context.canvas).call(_drag(simulation));
}
d3.select(context.canvas)
.on("mouseout", () => {
selected = null;
ticked();
// d3.select("#vTooltip").style("display", "none");
})
.on("click", onHighlight)
.on("mousemove", onHighlight);
function onHighlight(evt, d) {
const newSelected = simulation.find(evt.offsetX, evt.offsetY);
if (newSelected !== selected) {
selected = newSelected;
console.log("selected", selected);
// console.log("redraw", selected.properties.MPIO_CNMBR);
ticked();
}
// console.log("xy",d3.event, d3.event.offsetX, d3.event.offsetY);
// d3.select("#vTooltip")
// .style("display", "block")
// .select("p")
// .html(
// "Diferencia: " +
// fmtPct(Math.abs(selected.pct)) +
// " a favor de " +
// (selected.pct > 0 ? " Duque " : " Petro ") +
// "<br>" +
// "Total Votantes:" +
// fmt(selected.votantes)
// );
// d3.select("#vTooltip")
// .select("h3")
// .text(selected.municipio + ", " + selected.departamento);
// let bar = stackedBar().keys([
// "iván duque result",
// "votos_en_blanco result",
// "nulos_no_marcados",
// "gustavo petro result"
// ]);
// d3.select("#barChart").datum(selected).call(bar);
}
ticked();
parentInvalidation.then(() => {
console.log("invalidation stop");
simulation.stop();
});
// save the context for reusal
container.context = context;
container.contextMap = contextMap;
container.color = color;
container.dataMap = dataMap;
context.canvas.style.position = "absolute";
context.canvas.style.top = "0px";
context.canvas.style.left = "0px";
container.innnerHTML = "";
container.appendChild(contextMap.canvas);
container.appendChild(context.canvas);
return container;
}