in src/js/modules/choropleth.js [1088:1541]
createMap() {
var self = this
this.width = document.querySelector("#mapContainer").getBoundingClientRect().width
this.height = (this.width < 500) ? this.width * 0.8 : this.width * 0.6 ;
var margin = {
top: 0,
right: 0,
bottom: 0,
left: 0
}
var active = d3.select(null);
var scaleFactor = 0.85;
if (self.place == "world") {
scaleFactor = 0.17
}
self.projection = d3.geoMercator()
.center([self.database.centreLon, self.database.centreLat])
.scale(self.width * scaleFactor)
.translate([self.width / 2, self.height / 2])
var path = d3.geoPath().projection(self.projection);
this.path = path
var graticule = d3.geoGraticule();
const maxZoom = 300
this.zoom = d3.zoom().scaleExtent([1, maxZoom]).on("zoom", zoomed);
d3.select("#mapContainer svg").remove();
var svg = d3.select("#mapContainer").append("svg")
.attr("width", self.width)
.attr("height", self.height)
.attr("id", "map")
//.attr("overflow", "hidden")
.on("mousemove", function() {
const event = d3.event || window.event;
updateTooltipPosition(event);
});
if (self.database.zoomOn) {
console.log("Zoom")
svg.call(self.zoom)
}
svg.append("svg:defs").append("svg:marker")
.attr("id", "triangle")
.attr("refX", 6)
.attr("refY", 6)
.attr("markerWidth", 30)
.attr("markerHeight", 30)
.attr("markerUnits","userSpaceOnUse")
.attr("orient", "auto")
.append("path")
.attr("d", "M 0 0 12 6 0 12 3 6")
.style("fill", "black");
var crosshatch = svg.append("defs")
.append("pattern")
.attr("id", "crosshatch")
.attr("patternUnits", "userSpaceOnUse")
.attr("width", 10)
.attr("height", 10);
// Append lines to the pattern for crosshatching
crosshatch.append("path")
.attr("d", "M-1,1 l2,-2 M0,10 l10,-10 M9,11 l2,-2")
.attr("stroke", "black")
.attr("stroke-width", 1)
.attr("shape-rendering", "auto");
var tooltip = d3.select("#mapContainer").append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.style("z-index", "20")
.style("visibility", "hidden")
.style("top", "30px")
.style("left", "55px")
.html("<div id='overlay'></div><div id='tooltip'></div>")
var features = svg.append("g")
features.append("path").datum(graticule)
.attr("class", "graticule")
.attr("d", path);
// console.log("topoKey",self.database.topoKey)
// features.append("g").selectAll("path").data(topojson.feature(self.boundaries, self.boundaries.objects[self.database.topoKey]).features).enter().append("path")
// .attr("class", `_${self.database.topoKey} mapArea`)
var geoLayers = features.append("g")
.attr("id", "geoLayers")
if (this.basemap) {
geoLayers.append("g").selectAll("path")
.data(topojson.feature(self.basemap, self.basemap.objects[this.basemapTopoKey]).features).enter().append("path")
.attr("class", "basemap")
.style("fill",'#dcdcdc')
.attr("d",path)
.attr("stroke-width", 1)
.attr("stroke", null)
}
geoLayers.append("g").selectAll("path").data(topojson.feature(self.boundaries, self.boundaries.objects[this.database.topoKey]).features).enter().append("path")
.attr("class", self.database.topoKey + " mapArea")
.attr("fill", function(d) {
return self.getFill(d)
})
.attr("d",path)
.on("mouseover", tooltipIn)
.on("mousemove", function(d) {
const event = d3.event || window.event;
updateTooltipPosition(event);
})
.on("mouseout", tooltipOut);
if (self.width > 480) {
geoLayers.append("path")
.attr("class", "mesh")
.attr("stroke-width", 0.5)
.attr("d", path(topojson.mesh(self.boundaries, self.boundaries.objects[this.database.topoKey])));
}
if (this.overlay) {
geoLayers.append("g").selectAll("path")
.data(topojson.feature(self.overlay, self.overlay.objects[this.overlayTopoKey]).features)
.enter()
.append("path")
.attr("class", "overlay")
.style("fill", "transparent")
.attr("d", path)
.attr("stroke-width", 1)
.attr("stroke", "#000")
.style("pointer-events", "all")
.on("mouseover", passThru)
.on("mousemove", passThru)
.on("mouseout", tooltipOut);
}
// geoLayers
// .on("mouseover", tooltipIn)
// .on("mouseout", tooltipOut)
function passThru(d, i, nodes) {
// Get the current event
const event = d3.event || window.event;
// Store the overlay data
var overlayData = d;
// Temporarily disable pointer events on this element
var prev = this.style.pointerEvents;
this.style.pointerEvents = 'none';
// Get the element below
var element = document.elementFromPoint(event.clientX, event.clientY);
// Restore pointer events immediately to prevent mouse event issues
this.style.pointerEvents = prev;
// If we found an element and it's a map area
if (element && element.classList.contains('mapArea')) {
// Remove highlight from all paths
d3.selectAll('.mapArea').classed('highlighted', false);
// Add highlight to current path
d3.select(element).classed('highlighted', true);
// Get the data from the underlying element
var baseData = d3.select(element).datum();
// Show combined tooltip
d3.select(".tooltip")
.style("visibility", "visible")
.select("#tooltip")
.html(() => {
// Combine data from both layers
let baseHtml = baseData.properties[self.database.currentKey] !== null ?
mustache(self.database.mapping[self.database.currentIndex].tooltip, {...utilities, ...baseData.properties}) :
"No data available";
let overlayHtml = overlayData.properties ?
mustache(self.database.mapping[self.database.currentIndex].overlayTooltip, {...utilities, ...overlayData.properties}) :
"";
return `${overlayHtml}<hr style="margin: 5px 0;">${baseHtml}`;
});
// Update tooltip position
updateTooltipPosition(event);
} else {
// If we're not over a map area, remove all highlights
d3.selectAll('.mapArea').classed('highlighted', false);
}
// Prevent event from continuing
event.stopPropagation();
}
function updateTooltipPosition(event) {
if (!event) return;
const containerRect = d3.select("#mapContainer").node().getBoundingClientRect();
const relativeX = event.clientX - containerRect.left;
const relativeY = event.clientY - containerRect.top;
const half = self.width / 2;
if (relativeX < half) {
d3.select(".tooltip").style("left", relativeX + "px");
} else {
d3.select(".tooltip").style("left", (relativeX - 200) + "px");
}
if (relativeY < (self.height / 2)) {
d3.select(".tooltip").style("top", (relativeY + 30) + "px");
} else {
d3.select(".tooltip").style("top", (relativeY - 120) + "px");
}
}
function tooltipOut(d) {
const event = d3.event || window.event;
// Add a small delay to prevent flickering when moving between features
setTimeout(() => {
// Check if we're still outside both layers
var elementAtPoint = document.elementFromPoint(event.clientX, event.clientY);
if (!elementAtPoint?.classList.contains('mapArea') &&
!elementAtPoint?.classList.contains('overlay')) {
d3.select(".tooltip").style("visibility", "hidden");
// Remove highlight from all paths when truly leaving the map area
d3.selectAll('.mapArea').classed('highlighted', false);
}
}, 100);
}
function tooltipIn(d) {
d3.select(".tooltip").style("visibility", "visible");
// Remove highlight from all paths
d3.selectAll('.mapArea').classed('highlighted', false);
// Add highlight to current path
d3.select(this).classed('highlighted', true);
if (d.properties[self.database.currentKey] === 0) {
d.properties[self.database.currentKey] = "0";
d3.select("#tooltip").html(mustache(self.database.mapping[self.database.currentIndex].tooltip, {...utilities, ...d.properties}));
} else if (d.properties[self.database.currentKey] === undefined) {
d3.select("#tooltip").html("No data available");
} else {
d3.select("#tooltip").html((d.properties[self.database.currentKey] === null) ?
"No data available" :
mustache(self.database.mapping[self.database.currentIndex].tooltip, {...utilities, ...d.properties}));
}
}
var placeLabelThreshold = 1
if (self.width <= 620) {
placeLabelThreshold = 1
}
if (self.database.showLabels) {
features.selectAll("text")
.data(self.places.features)
.enter()
.append("text")
.text((d) => d.properties.name)
.attr("x", (d) => self.projection([d.properties.longitude, d.properties.latitude])[0])
.attr("y", (d) => self.projection([d.properties.longitude, d.properties.latitude])[1])
.attr("class","labels")
.style("display", (d) => {
return (d.properties.scalerank - placeLabelThreshold < self.zoomLevel - 1) ? "block" : "none"
})
}
svg.on("click", function() {
console.log(self.projection.invert(d3.mouse(this)), self.zoomLevel)
})
this.keygen()
//145.4866862 -22.2187986 145.4866862 -22.2187986
/*
if (self.hasLabels) {
for (var i = 0; i < self.database.labels.length; i++) {
console.log("Added one label")
features.append("line")
.attr("x1", self.projection([+self.database.labels[i].lon_end, +self.database.labels[i].lat_end])[0])
.attr("y1", self.projection([+self.database.labels[i].lon_end, +self.database.labels[i].lat_end])[1])
.attr("x2", self.projection([+self.database.labels[i].lon_start, +self.database.labels[i].lat_start])[0])
.attr("y2", self.projection([+self.database.labels[i].lon_start, +self.database.labels[i].lat_start])[1])
.attr("stroke-width", 1)
.attr("stroke", "black")
.attr("marker-end", "url(#triangle)")
features.append("text")
.text((d) => self.database.labels[i].text)
.attr("x", self.projection([+self.database.labels[i].lon_end, +self.database.labels[i].lat_end])[0] + 10)
.attr("y", self.projection([+self.database.labels[i].lon_end, +self.database.labels[i].lat_end])[1] + 10)
.attr("class","labels")
}
}*/
var utilities = {
commas: function(num) {
var result = parseFloat(this[num]).toFixed();
result = result.replace(/(\d)(?=(\d{3})+$)/g, '$1,');
return result
},
big: function(big) {
var num = parseFloat(this[big]);
if ( num > 0 ) {
if ( num > 1000000000 ) { return ( num / 1000000000 ).toFixed(1) + 'bn' }
if ( num > 1000000 ) { return ( num / 1000000 ).toFixed(1) + 'm' }
if (num % 1 != 0) { return num.toFixed(2) }
else { return num.toLocaleString() }
}
if ( num < 0 ) {
var posNum = num * -1;
if ( posNum > 1000000000 ) return [ "-" + String(( posNum / 1000000000 ).toFixed(1)) + 'bn'];
if ( posNum > 1000000 ) return ["-" + String(( posNum / 1000000 ).toFixed(1)) + 'm'];
else { return num.toLocaleString() }
}
return num;
},
decimals: function(items) {
var nums = items.split(",")
return parseFloat(this[nums[0]]).toFixed(nums[1]);
}
}
d3.select("#zoomIn").on("click", function(d) {
self.zoom.scaleBy(svg.transition().duration(750), 1.5);
});
d3.select("#zoomOut").on("click", function(d) {
self.zoom.scaleBy(svg.transition().duration(750), 1 / 1.5);
});
/*
d3.select("#zoomToggle").on("click", function(d) {
toggleZoom();
});
*/
self.zoom.scaleBy(svg, self.database.zoomScale);
/*
function toggleZoom() {
if (self.database.zoomOn == false) {
d3.select("#zoomToggle").classed("zoomLocked", false)
d3.select("#zoomToggle").classed("zoomUnlocked", true)
svg.call(self.zoom);
self.database.zoomOn = true
} else if (self.database.zoomOn == true) {
svg.on('.zoom', null);
d3.select("#zoomToggle").classed("zoomLocked", true)
d3.select("#zoomToggle").classed("zoomUnlocked", false)
self.database.zoomOn = false
} else if (self.database.zoomOn == null) {
svg.on('.zoom', null);
d3.select("#zoomToggle").classed("zoomLocked", true)
d3.select("#zoomToggle").classed("zoomUnlocked", false)
svg.call(self.zoom);
self.database.zoomOn = false
}
}
*/
function zoomed(event) {
console.log("Zoom it")
scaleFactor = d3.event.transform.k;
d3.selectAll(".mesh").style("stroke-width", 0.5 / d3.event.transform.k + "px");
d3.selectAll(".burbs").style("stroke-width", 0.5 / d3.event.transform.k + "px").attr("stroke-dasharray", `${2 / d3.event.transform.k }, ${2 / d3.event.transform.k }`)
d3.selectAll("#suburb").attr("transform", d3.event.transform);
features.style("stroke-width", 0.5 / d3.event.transform.k + "px");
features.selectAll(".overlay").attr("stroke-width", 1 / d3.event.transform.k + "px");
features.attr("transform", d3.event.transform);
features.selectAll(".placeContainers").style("display", function(d) {
return (d['properties']['scalerank'] - 3 < d3.event.transform.k) ? "block" : "none" ;
})
d3.select('#crosshatch')
.attr('patternTransform', 'scale(' + 1 / d3.event.transform.k + ')');
features.selectAll(".placeText")
.style("font-size", 0.8 / d3.event.transform.k + "rem")
.attr("dx", 5 / d3.event.transform.k)
.attr("dy", 5 / d3.event.transform.k);
clearTimeout(document.body.data)
var now = d3.event.transform.k;
//console.log(self.projection.invert([d3.event.transform.y, d3.event.transform.x]))
document.body.data = setTimeout( function() {
if (now!=self.zoomLevel) {
self.zoomLevel = now
self.placeNames()
}
}, 200);
}