//Tamaño de la viz
var width = 1900;
var height = 1080;
//Width y height de las convenciones
var width_convenciones = 200;
var height_convenciones = 205;
//width y height del tooltip
var width_tooltip = 350;
var height_tooltip = 250;
//tamaño de los círculos
var RAD2DEG = 180.0/Math.PI;
var gCircleRadius = 370;
var gSmallCircleRadius = 7;
//Setup del mapa leaflet en un centro específico y un "estilo" (layer) b/n
var map = L.map('map', { zoomControl:false }).setView([8.995738, -79.460254], 11);
//mapLink = L.tileLayer('https://tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png').addTo(map);
mapLink = L.tileLayer('http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png').addTo(map);
//mapLink = L.tileLayer('http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png').addTo(map);
map.doubleClickZoom.disable();
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');
//svg en donde dibujar las convenciones
var svg2 = d3.selectAll("#map")
.append("svg")
.attr('class', "convenciones")
.attr('width', width_convenciones)
.attr('height', width_convenciones)
.style("top", svg.node().parentNode.offsetTop + 70 + "px")
.style("left", svg.node().parentNode.offsetLeft + 320 + "px")
;
// crea el tooltip y le da las características
var tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0)
;
//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){
//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);
//dibuja las convenciones
convenciones();
//circulo invisible para dibujar los nodos
var circle = g.append("circle")
.attr('cx', circuloLatLon.x)
.attr('cy', circuloLatLon.y)
.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(){
if (d.tipo == "proyecto" || d.tipo == "programa" || d.tipo == "plan"){
return "
"
+ d.id
+ "
"
+ "
"
+ "Año: " + d.year
+ "
"
+ "
"
+ "Estado: " + d.estado
+ "
"
+ "
"
+ d.descripcion
} else{
return "
"
+ d.id
+ "
"
+ "
"
+ d.descripcion;
}
})
.style("top", svg.node().parentNode.offsetTop + (height - (height_tooltip * 1.4)) + "px")
.style("left", svg.node().parentNode.offsetLeft + (width - (width_tooltip * 1.2)) + "px")
.style("opacity", 1)
;
})
.on("mouseout", function() { tooltip.style("opacity", 0); })
;
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, 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("x", mascaraLatLon.x)
.attr("y", mascaraLatLon.y)
.attr("width", width)
.attr("height", height)
.style("fill", "white")
.style("opacity", 1)
;
//agrega el circulo
var circMask = mask.append("circle")
.attr("cx", circuloLatLon.x)
.attr("cy", circuloLatLon.y)
.attr("r", gCircleRadius + 7)
;
// cuadro debajo de los actores con la máscara
var cuadro = g.append("rect")
.attr("x", mascaraLatLon.x)
.attr("y", mascaraLatLon.y)
.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() {
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.5 : 0;
})
;
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)
;
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)
.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;
});
//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) * 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)
.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()
;
}
// Función para saber cómo hacer lo de las convenciones
function convenciones(){
//cuadro de las convenciones
var cuadroConv = svg2.append("rect")
.attr('x', 10)
.attr('y', 10)
.attr('width', width_convenciones)
.attr('height', width_convenciones)
.style('fill', "#fafafa")
.style('opacity', 1)
;
// crea el nido donde se miran las categorias de los nodos
var convencion_area = d3.nest()
.key(function(d) { return d.area; })
.rollup(function(d) { return d.length; })
.entries(graph.nodes)
;
var convencion_tipo = d3.nest()
.key(function(d) { return d.tipo; })
.rollup(function(d) { return d.length; })
.entries(graph.nodes)
;
// Circulos del Area
var areaX = 20;
var areaY = 25;
var radio = 5;
var offset_texto = 0.7;
var espaciado = 15;
//svg en donde dibujar los circulos de las convenciones
var legend = svg2.append("g")
.append("svg")
.attr("class", "legend")
.selectAll("g")
.data(convencion_area)
.enter()
.append("g")
.attr("transform", function(d, i) {
return "translate(0," + i * espaciado + ")";
})
;
// agrega los circulos, caracteristicas y funciones
legend.append("circle")
.attr('cx', areaX + radio)
.attr('cy', areaY)
.attr('r', radio)
.attr('class', function (d) {
return "nodos" + (d.key ? " " + d.key: "");
})
.on("click", function(d){
// determine if current line is visible
var active = nodesAll.active ? false : true
var newOpacity = active ? 0.1 : 1;
if(d.key == "urbano"){
d3.selectAll(".nodos.urbano")
.style("opacity", newOpacity);
} else if (d.key == "ambiental"){
d3.selectAll(".nodos.ambiental")
.style("opacity", newOpacity);
} else if (d.key == "inclusion"){
d3.selectAll(".nodos.inclusion")
.style("opacity", newOpacity);
}
nodesAll.active = active;
})
;
// texto de las convenciones por area
legend.append("text")
.attr("x", areaX + 15)
.attr('y', areaY - offset_texto)
.attr("dy", ".35em")
.text(function(d) { return d.key; })
.attr('class', 'text')
;
// Circulos del Tipo
var tipoX = 20;
var tipoY = 70;
var legend_2 = svg2.append("g")
.append("svg")
.attr("class", "legend")
.selectAll("g")
.data(convencion_tipo)
.enter()
.append("g")
.attr("transform", function(d, i) {
return "translate(0," + i * espaciado + ")";
})
;
legend_2.append("circle")
.attr('cx', tipoX + radio)
.attr('cy', tipoY)
.attr('r', radio)
.attr('class', function (d) {
return "nodos" + (d.key ? " " + d.key: "");
})
.filter(function(d){
return d.key == "proyecto" || d.key == "programa" || d.key == "plan" ;
})
.style('fill', 'white')
//.style('fill-opacity', 0)
.style('stroke', "black")
.style("stroke-width", 2)
.attr('stroke-dasharray', function(d){
if(d.key == "programa"){
return ("1,1");
} else if(d.key == "plan"){
return ("2,4")
}
})
;
legend_2.append("text")
.attr("x", tipoX + 15)
.attr('y', tipoY - offset_texto)
.attr("dy", ".35em")
.text(function(d) {
return d.key;
})
.attr('class', 'text')
;
}
//Dibuja los circulos actualizados en el mapa
drawAndUpdateCircles();
map.on("moveend", drawAndUpdateCircles);
});