createMap()

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);

        }