/* fundación h-orizontal Mapa de Actores y Proyectos en Territoirio de la Ciudad de Panamá v1.0 2018 Aportes y ayuda de - Andrew Reid: https://stackoverflow.com/questions/51411646/d3js-force-layout-on-a-map - rioV8: https://stackoverflow.com/questions/51529999/nodes-title-around-a-circle y https://stackoverflow.com/questions/52651626/select-feature-from-geojson-based-on-a-node-name */ //Tamaño de la viz var width = 1900; var height = 1080; // Offset de los elementos var offset_circulo = 300; var offset_descripcion = 600; var offset_cuadros = 590; var offset_logos = 590; //tamaño de los círculos var RAD2DEG = 180.0/Math.PI; var gCircleRadius = 350; var gSmallCircleRadius = 7; //Setup del mapa leaflet en un centro específico y un "estilo" (layer) var map = L.map('map', { zoomControl:false }).setView([9.001752, -79.248725], 11); mapLink = L.tileLayer('https://api.mapbox.com/styles/v1/pierreee1/cjnuqt47g2juo2snde808p6xr/tiles/256/{z}/{x}/{y}@2x?access_token=pk.eyJ1IjoicGllcnJlZWUxIiwiYSI6ImNqazh2dWlzdjFobm4za3AyN2Q5NTAwa3oifQ.ai-LoEOZs22rAXe0br8E0g').addTo(map); map.doubleClickZoom.disable(); //Ubicacion del circulo y la máscara var circuloLatLon = map.latLngToLayerPoint(map.getBounds().getCenter()); var mascaraLatLon = map.latLngToLayerPoint(map.getBounds().getCenter()); //cuando ya está el mapa se agrega un svg en donde dibujar var svgLayer = L.svg(); svgLayer.addTo(map); //seleccion el svg del mapa y crea un grupo var svg = d3.select("#map").select("svg"); var g = svg.select('g'); // crea los tooltip y le da las características var tooltip = d3.select("body") .append("div") .attr("class", "tooltip") .style("opacity", 1) ; var tooltip_1 = d3.select("body") .append("div") .attr("class", "tooltip_1") .style("opacity", 1) ; var tooltip_2 = d3.select("body") .append("div") .attr("class", "tooltip_2") .style("opacity", 1) ; var tooltip_logo0 = d3.select("body") .append("div") .style("top", ((height / 100) * 93) + "px") .style("left", ((width / 100) * 79) + "px") .html("" + "") .attr("class", "tooltip_logo") .style("opacity", 1) ; var tooltip_logo1 = d3.select("body") .append("div") .style("top", ((height / 100) * 94) + "px") .style("left", ((width / 100) * 84.5) + "px") .html("" + "") .attr("class", "tooltip_logo") .style("opacity", 1) ; var tooltip_logo2 = d3.select("body") .append("div") .style("top", ((height / 100) * 91) + "px") .style("left", ((width / 100) * 90) + "px") .html("" + "") .attr("class", "tooltip_logo") .style("opacity", 1) ; var tooltip_logo3 = d3.select("body") .append("div") .style("top", ((height / 100) * 90) + "px") .style("left", ((width / 100) * 95) + "px") .html("" + "") .attr("class", "tooltip_logo") .style("opacity", 1) ; //Define la siulación de fuerza var fuerza = d3.forceSimulation() .force("link", d3.forceLink() .id(function(d){ return d.id; }) ) ; //Leer datos de ambos json y llamar la funcion que dibuja todo d3.json('proyectos_v12.json', function(data){ //Dibuja los poligonos del geojson de mymaps d3.json("proyectos_panama.geojson", function(datos){ var datosPoli = datos; drawFeatures(datos) function projectPoint(x, y) { var point = map.latLngToLayerPoint(new L.LatLng(y, x)); this.stream.point(point.x, point.y); } function drawFeatures(data) { var transform = d3.geoTransform({point: projectPoint}); var path = d3.geoPath().projection(transform); var featureElement = g.selectAll("path") .data(data.features) .enter() .append("path") .attr("fill", "black") .attr('fill-opacity', 0.2) .attr('stroke', 'black') .attr('stroke-opacity', 1) .attr("opacity", 0) map.on("moveend", update) ; update(); function update() { featureElement.attr("d", path); } } //pasar los datos a una variable var graph = data; //Printea los datos para verificar console.log("Número de Actores y Proyectos: " + graph.nodes.length) console.log(graph.nodes); console.log("Número de links: " + graph.edges.length) console.log(graph.edges); //circulo invisible para dibujar los nodos var circle = g.append("circle") .attr('r', gCircleRadius) .style("fill", "#ffff99") .style('opacity', 0) ; //crea las lineas con un svg y los datos de "edges" var lineas = g.selectAll("line") .data(graph.edges) .enter() .append("line") .attr('class', "link") ; // decide cuales nodos van en el circulo, cuales en el mapa y cuales son las areas function isNodeOnLegend(d) { return d.tipo === "academia" || d.tipo === "financiacion" || d.tipo === "gobierno" || d.tipo === "ong" || d.tipo === "privado" || d.tipo === "gremios" ; } function isNodeOnMap(d){ return d.tipo === "proyecto" || d.tipo === "programa" || d.tipo === "plan"; } function isNodeByArea(d){ return d.area === "urbano" || d.area === "ambiental" || d.area === "inclusion"; } function constructNodes(nodeList, className) { //crea el grupo donde pintar los nodos var nodes = g.append("g") .attr("class", className) .selectAll(".nodos") .data(nodeList) .enter() .append("g") .attr("class", "gnode") ; //pinta la nodos, características y nodes.append("circle") .attr('class', function(d){ if (isNodeByArea(d)) return "nodos " + d.area; if (isNodeOnLegend(d)) return "nodos " + d.tipo; }) .style("stroke", function(d) { if (isNodeOnMap(d)) return "black"; }) .style('stroke-width', 1.5) .attr('stroke-dasharray', function(d){ if (d.tipo == "programa") return ("1,1"); if (d.tipo == "plan") return ("2,4"); }) .attr('r', 5) .attr("pointer-events","visible") .on("click", connectedNodes) .on("mouseover", function(d){ tooltip .html(function(){ return "

" + d.id + "

" + d.descripcion; }) .style("top", svg.node().parentNode.offsetTop + 300 + "px") .style("left", svg.node().parentNode.offsetLeft + (width - offset_descripcion) + "px") .style("opacity", 1) .on("mouseover", ) ; tooltip_1 .html(function(){ if(d.tipo == "academia" || d.tipo == "gobierno" || d.tipo == "financiacion" || d.tipo == "ong" || d.tipo == "privado" || d.tipo == "gremios"){ return d.tipo; } else{ return d.area; } }) .style("top", svg.node().parentNode.offsetTop + 650 + "px") .style("left", svg.node().parentNode.offsetLeft + (width - offset_cuadros) + "px") .style("opacity", 1) .style("background", function(){ if (d.tipo == "academia") return "#c6e18d"; else if(d.tipo == "financiacion") return "#ffda47"; else if(d.tipo == "gobierno") return "#ffa182"; else if(d.tipo == "ong") return "#ff7ab8"; else if(d.tipo == "privado") return "#ff0046"; else if(d.tipo == "gremios") return "#ba0000"; else if(d.area == "urbano") return "#99d9f4"; else if(d.area == "ambiental") return "#009fe3"; else if(d.area == "inclusion") return "#0000ff"; }) ; tooltip_2 .html(function(){ if(d.tipo == "proyecto" || d.tipo == "programa" || d.tipo == "plan"){ return d.tipo; } }) .style("top", svg.node().parentNode.offsetTop + 650 + "px") .style("left", svg.node().parentNode.offsetLeft + (width - offset_cuadros + 140) + "px") .style("opacity", 1) .style("background", "white") .style('border', function(){ if (d.tipo == "proyecto") return "2px solid #000000"; else if(d.tipo == "programa") return "2px dotted #000000"; else if(d.tipo == "plan") return "2px dashed #000000"; }) ; }) ; return nodes; } // calcula los espacios de los circulos en el circulo var circleCoord = function(node, index, num_nodes){ var circumference = circle.node().getTotalLength(); var pointAtLength = function(l){ return circle.node().getPointAtLength(l) }; var sectionLength = (circumference) / num_nodes; var position = sectionLength * index + sectionLength/2; return pointAtLength(circumference-position); } //guarda los nodos en una variable dependiendo de un filtro var nodesOnMap = constructNodes(graph.nodes.filter(isNodeOnMap), "onmap"); var nodesOnLegend = constructNodes(graph.nodes.filter(isNodeOnLegend), "onlegend"); var nodesOnLegendNum = graph.nodes.filter(isNodeOnLegend); var nodesAll = g.selectAll(".gnode"); //los que estan en el circulo pintelos en el circulo nodesOnLegend.each(function(d, i) { var coord = circleCoord(d, i, nodesOnLegendNum.length); d.fx = coord.x; d.fy = coord.y; }); // agrega los textos de los nodos de los actores var text = svg .append("g") .selectAll("text") .data(graph.nodes.filter(isNodeOnLegend)) .enter() .append("text") .attr('class', "text") .style('font-size', "10px") .attr("x", 8) .attr("y", ".31em") .text(function(d) { return d.id; }) .attr('opacity', 1) ; //Calcula el radio y la posicion del texto var radiusScale = (gCircleRadius + 1.5 * gSmallCircleRadius) / gCircleRadius; var textPosition = function(d){ var circX = (circuloLatLon.x - offset_circulo), circY = circuloLatLon.y; var dX = (d.fx - circX) * radiusScale, dY = (d.fy - circY) * radiusScale; return { x: circX + dX, y: circY + dY }; }; //crea la máscara var mask = g .append("defs") .append("mask") .attr("id", "myMask") ; //agrega el rectángulo grande var rectMask = mask.append("rect") .attr("width", width) .attr("height", height) .style("fill", "white") .style("opacity", 1) ; //agrega el circulo var circMask = mask.append("circle") .attr("r", gCircleRadius + 7) ; // cuadro debajo de los actores con la máscara var cuadro = g.append("rect") .attr('width', width) .attr('height', height) .attr("mask", "url(#myMask)") .style('fill', "#ffffff") .style('opacity', 1) ; //le dice a la simulacion cuales son los nodos y los links fuerza.nodes(graph.nodes); fuerza.force("link").links(graph.edges); //simulación y actualizacion de la posicion de los nodos en cada "tick" fuerza.on("tick", function (){ lineas .attr('x1', function(d){ return d.source.x; }) .attr('y1', function(d){ return d.source.y; }) .attr('x2', function(d){ return d.target.x; }) .attr('y2', function(d){ return d.target.y; }) ; //funcion para actualizar la posicion de los nodos (rioV8) function nodeUpdate(nodes) { nodes.attr("transform", function (d) { return `translate(${d.x},${d.y})`; }); } nodeUpdate(nodesOnMap); nodeUpdate(nodesOnLegend); }); //saber si las conexiones se ven o no var toggle = 0; //Create an array logging what is connected to what var linkedByIndex = {}; for (i = 0; i < graph.nodes.length; i++) { linkedByIndex[i + "," + i] = 1; }; graph.edges.forEach(function (d) { linkedByIndex[d.source.index + "," + d.target.index] = 1; }); //This function looks up whether a pair are neighbours function neighboring(a, b) { return linkedByIndex[a.index + "," + b.index]; } // Función para saber que hacer cuando se haga click function connectedNodes() { var poligono = g.selectAll("path"); var result = datosPoli.features; var nombres = []; if (toggle == 0) { //Reduce the opacity of all but the neighbouring nodes d = d3.select(this.parentNode).datum(); r = d3.select(this).datum(); nodesAll .transition() .style("opacity", function (o) { return neighboring(d, o) | neighboring(o, d) ? 1 : 0; }) ; lineas .transition() .style("opacity", function (o) { return d.index==o.source.index | d.index==o.target.index ? 0.3 : 0; }) ; poligono .filter(function (o) { return d.id === o.properties.Name; }) .transition() .attr('opacity', 1) ; text .transition() .style('opacity', function(o){ return neighboring(d, o) | neighboring(o, d) ? 1 : 0; }) toggle = 1; } else { // devuelve los nodos a la normalidadlos links invisibles nodesAll .transition() .style("opacity", 1) ; lineas .transition() .style("opacity", 0) ; poligono .transition() .attr("opacity", 0) ; text .transition() .style('opacity', 1) ; toggle = 0; } } //Function which sets the transformation attribute to move the circles to the correct location on the map function drawAndUpdateCircles() { var circuloLatLon = map.latLngToLayerPoint(map.getBounds().getCenter()); var mascaraLatLon = map.latLngToLayerPoint(map.getBounds().getNorthWest()); circle .attr('cx', circuloLatLon.x - offset_circulo) .attr('cy', circuloLatLon.y) ; //si tiene ubicacion, anclelos al mapa nodesOnMap.each(function(d) { if (d.lon && d.lat) { p = new L.LatLng(d.lat, d.lon); var layerPoint = map.latLngToLayerPoint(p); d.fx = layerPoint.x; d.fy = layerPoint.y; } }); // si esta en el circulo, mantengalos ahí nodesOnLegend.each(function(d, i) { var coord = circleCoord(d, i, nodesOnLegendNum.length); d.fx = coord.x; d.fy = coord.y; }); //Calcula el radio y la posicion del texto var radiusScale = (gCircleRadius + 1.5 * gSmallCircleRadius) / gCircleRadius; var textPosition = function(d){ var circX = (circuloLatLon.x - offset_circulo), circY = circuloLatLon.y; var dX = (d.fx - circX) * radiusScale, dY = (d.fy - circY) * radiusScale; return { x: circX + dX, y: circY + dY }; }; //actualice la posicion de los textos del circulo text.each(function(d, i, nodesOnLegend){ var textPos = textPosition(d); var angle = Math.atan2(textPos.y - circuloLatLon.y, textPos.x - (circuloLatLon.x - offset_circulo)) * RAD2DEG; d3.select(nodesOnLegend[i]) .attr('x', textPos.x) .attr('y', textPos.y) .attr('dy', (angle>90 || angle<-90) ? "0.3em" : "0.4em") .style("text-anchor", (angle > 90 || angle < - 90) ? "end" : "start") .attr("transform", `rotate(${ (angle > 90 || angle < -90) ? angle + 180 : angle}, ${textPos.x}, ${textPos.y})`) }); //actualice la posicion de la mascara rectMask .attr("x", mascaraLatLon.x) .attr("y", mascaraLatLon.y) ; circMask .attr("cx", circuloLatLon.x - offset_circulo) .attr("cy", circuloLatLon.y) ; cuadro .attr("x", mascaraLatLon.x) .attr("y", mascaraLatLon.y) ; // reinicie la simulación para que los puntos puedan quedar en donde son si se hace zoom o drag fuerza .alpha(1) .restart() ; } //Dibuja los circulos actualizados en el mapa drawAndUpdateCircles(); map.on("moveend", drawAndUpdateCircles); }); });