/*
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);
});
});