Public
Edited
Oct 24, 2022
3 forks
7 stars
Insert cell
Insert cell
Insert cell
{
// Para más referencia respecto a los métodos utilizados:
// https://github.com/d3/d3-force#simulation_alpha
let borders = false;

const WIDTH = width / 2 - 40;
const HEIGHT = 600;
const MARGIN = { TOP: 10, BOTTOM: 10, LEFT: 10, RIGHT: 10 };
const MAX_RADIUS = 20;
const widthSVG = WIDTH - MARGIN.RIGHT - MARGIN.LEFT;
const height = HEIGHT - MARGIN.TOP - MARGIN.BOTTOM;

const FILEPATH =
"https://raw.githubusercontent.com/PUC-Infovis/syllabus-2018/master/ayudantia07/force-graph/dataset.json";

/*
Buscamos cualquier SVG y lo eliminamos. Esta línea solo se ocupa en ObservableHQ para que cuando se ejecute esta celda, no solo agregue el SVG sino que elimine el anterior.
*/
d3.select("#graph").selectAll("svg").remove();

// Se agrega un nuevo SVG dentro del div #graph
const SVG = d3
.select("#graph")
.append("svg")
.attr("width", WIDTH)
.attr("height", HEIGHT);

/*
Se agrega un "g" al SVG. Recordar que el elemento g es un contenedor usado para agrupar objetos. Las transformaciones aplicadas al elemento g son realizadas sobre todos los elementos hijos del mismo
*/
const container = SVG.append("g").attr(
"transform",
`translate(${MARGIN.LEFT}, ${MARGIN.TOP})`
);

// Definir el objeto Tooltip con opacidad 0 para que no se vea al inicio
const div = d3
.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);

/*
Creamos el ojeto simulation que se encargará de calculas las posiciones de los nodos en base a una simulación física de fuerzas. Se definen 4 atributos:

- center: Indica donde estará el punto central de la fuerza, es decir, la posición central de donde empezar a realizar la simulación.
- collission: es el radio mínimo que los nodos intentarán mantener de distancia entre otros. Esto hace que la distancia entre dos nodos sea igual a la suma de ambos radios. Si d3.forceCollide es 2, implica que tendrán una distancia de 4 pixeles.

- charge: analogo a la carga electroestática, cuanes es negativa va a repeler a los nodos. Esta carga es global, es decir, cada nodo tiene efecto sobre los otros nodos.

- link: atributo para indicarle a la simulación como conectar el identificador que tienen la lista de links (source y target) con los nodos.
*/
const simulation = d3
.forceSimulation()
.force("center", d3.forceCenter(widthSVG / 2, height / 2))
.force("collision", d3.forceCollide(20).radius(20))
.force(
"charge",
d3.forceManyBody().strength(-500).distanceMin(30).distanceMax(80)
)
.force(
"link",
d3.forceLink().id((node) => node.id)
);

const ticked = () => {
// Cada tick altera el alpha en (alphaTarget - alpha) × alphaDecay.
// Es decir:
// alpha = alpha + (alphaTarget - alpha) × alphaDecay
// alphaTarget parte como 0, alpha como 1 y alphaDecay como 0.0228
// Los ticks acaban cuando alpha es menor a una cota. Por defecto esa cota es 0.001

// Aquí actualizamos el largo de la barra naranja para ver cómo cambia el alpha en cada tick
d3.select("#alphaValue").style(
"width",
`${Math.round((simulation.alpha() * width) / 5)}px`
);

// Aquí actualizamos el valor que está al lado de la barra
d3.select("#alphaValueOutput").text(String(simulation.alpha()).slice(0, 6));

// Buscamos cada nodo y lo trasladamos a la nueva posición x,y.
// Adicionalmente restringimos cx, cy (posiciones absolutas del nodo) para que no se salga de la pantalla
if (borders) {
container
.selectAll(".node")
.attr("cx", function (d) {
return (d.x = Math.max(
MAX_RADIUS,
Math.min(widthSVG - MAX_RADIUS, d.x)
));
})
.attr("cy", function (d) {
return (d.y = Math.max(
MAX_RADIUS,
Math.min(height - MAX_RADIUS, d.y)
));
});
}
container
.selectAll(".node")
.attr("transform", (node) => `translate(${node.x}, ${node.y})`);

// Buscamos cada link y lo trasladamos a la nueva posición x,y según la posición de los nodos a los que está conectado.
container
.selectAll("line")
.attr("x1", (link) => link.source.x)
.attr("y1", (link) => link.source.y)
.attr("x2", (link) => link.target.x)
.attr("y2", (link) => link.target.y);
};

// Cargamos el JSON y luego, con los dataset cargado comenzamos a crear la simulación
d3.json(FILEPATH).then((dataset) => {
/*
Le indicamos a nuestro objeto simulación varias cosas:

- nodes: Lista de nodos a utilizar.

- on: 'tick' le indicamos que cada vez que la simulación haga un "tick", llame a la función "ticked". El tick es cada iteración que realiza la simulación para posicionar los nodos.

- links: Lista de links a utilizar .

- distance: distancia IDEAL que se busca tener entre un nodo y otro que están conectados.
*/
simulation
.nodes(dataset.nodes)
.on("tick", ticked)
.force("link")
.links(dataset.links)
.distance(80);

// Para cada link, agregamos una línea que conecta 2 nodos.
container
.selectAll("line")
.data(dataset.links)
.enter()
.append("line")
.attr("x1", (link) => link.source.x)
.attr("y1", (link) => link.source.y)
.attr("x2", (link) => link.target.x)
.attr("y2", (link) => link.target.y);

// Para cada nodo, agregamos un "g"
const nodes = container
.selectAll(".node")
.data(dataset.nodes)
.enter()
.append("g")
.attr("class", "node");

// Creamos una función llamada mouseover que recibe un nodo y se encarga de crear un div con la información de dicho nodo
const mouseover = (node) => {
// Contamos cuantos links apuntan al nodo
let targetLinks = dataset.links.filter(
(link) => link.target.id == node.id
).length;
// Contamos cuantos links salen del nodo
let sourceLinks = dataset.links.filter(
(link) => link.source.id == node.id
).length;

// Generamos un código HTML con la información
let content =
"<span style='margin-left: 2.5px;'><b>" + node.id + "</b></span><br>";
content +=
`<table style="margin-top: 2.5px;">
<tr><td>Links que apuntan a mi: </td><td style="text-align: right">` +
targetLinks +
`</td></tr>
<tr><td>Links que salen de mi: </td><td style="text-align: right">` +
sourceLinks +
`</td></tr>
</table>`;
// Le pedimos al objeto div (que tiene el tooltop) que en un tiempo de 0.2 segundos cambia su opacidad de 0 a 0.9. el método transition se encargara de ver como variar la opacidad en dicho rango para que dure 0.2 segundos.
div.transition().duration(200).style("opacity", 0.9);

// Le indicamos al tooltip su contenido HTML y la posición, que consiste en donde está el mouse (eje x) y un poco más arriba del mouse (eje y). d3.event.pageX te da la posición en el EJE X del mouse y d3.event.pageY en el EJE Y
div
.html(content)
.style("left", d3.event.pageX + "px")
.style("top", d3.event.pageY - 28 + "px");
};

// Creamos una función mouseout encargada de pedirle al tooltip que en un lapso de 0.2 segundos cambie su opacidad a 0
const mouseout = (_) => {
div.transition().duration(200).style("opacity", 0);
};

/*
Para cada nodo (que es un 'g') le agregamos un circulo de radio 20 y definimos las acciones:
- mouseover (que es lo mismo que el hover o poner el mouse encima) llame a la función mouseover definida anteriormente
- mouseout (que es lo mismo que retirar el hover o sacar el mouse de encima) llame a la función mouseout definida anteriormente
*/
nodes
.append("circle")
.attr("r", MAX_RADIUS)
.on("mouseover", mouseover)
.on("mouseout", mouseout);

// Para cada nodo (que es un 'g') le agregamos un texto que tendrá el ID del nodo y tambin le definimos las acciones de mouseover y mouseout.
nodes
.append("text")
.text((node) => node.id)
.attr("dy", 5)
.on("mouseover", mouseover)
.on("mouseout", mouseout);

/*
Definimos diferentes funciones que contectan los input del form con la simulación. Todo funcionan del siguiente modo
1. con D3 selecciono el input y le defino la función on 'input' encargada de llamarse cada vez que el input varía su valor.
2. Dentro, le solicita al input el valor actual.
3. Luego actualizo el label Output que se encarga de mostrar el valor nunmérico
4. Defino que el alpha de la simulación sea 1 (para que vuelva a intentar bajarlo a 0.001)
5. Actualizo el valor correspondiente de la simulación según el input y le digo "restart" para que se empiecen a ejecutar denuevo los ticks.
*/
d3.select("#forceInput").on("input", (d) => {
let value = d3.select("#forceInput").property("value");
d3.select("#forceOutput").text(value);
simulation.alpha(1);
simulation.force("charge", d3.forceManyBody().strength(value)).restart();
});
d3.select("#forceInputMin").on("input", (d) => {
let value = d3.select("#forceInputMin").property("value");
d3.select("#forceOutputMin").text(value);
simulation.alpha(1);
simulation
.force("charge", d3.forceManyBody().distanceMin(value))
.restart();
});
d3.select("#forceInputMax").on("input", (d) => {
let value = d3.select("#forceInputMax").property("value");
d3.select("#forceOutputMax").text(value);
simulation.alpha(1);
simulation
.force("charge", d3.forceManyBody().distanceMax(value))
.restart();
});

d3.select("#collisionInput").on("input", (d) => {
let value = d3.select("#collisionInput").property("value");
d3.select("#collisionOutput").text(value);
simulation.alpha(1);
simulation.force("collision", d3.forceCollide(value)).restart();
});
d3.select("#distanceInput").on("input", (d) => {
let value = d3.select("#distanceInput").property("value");
d3.select("#distanceOutput").text(value);
simulation.alpha(1);
simulation
.force("link")
.distance(+value)
.restart();
});
d3.select("#centerXInput").on("input", (d) => {
let value = d3.select("#centerXInput").property("value");
d3.select("#centerXOutput").text(value);
let value2 = d3.select("#centerYInput").property("value");
d3.select("#centerYOutput").text(value2);
simulation.alpha(1);
simulation.force("center", d3.forceCenter(value, value2)).restart();
});

d3.select("#centerYInput").on("input", (d) => {
let value = d3.select("#centerXInput").property("value");
d3.select("#centerXOutput").text(value);
let value2 = d3.select("#centerYInput").property("value");
d3.select("#centerYOutput").text(value2);
simulation.alpha(1);
simulation.force("center", d3.forceCenter(value, value2)).restart();
});

d3.select("#alphaBar").on("click", (d) => {
simulation.alpha(1);
simulation.restart();
});

d3.selectAll("#borders").on("change", (_, i, all) => {
borders = all[i].checked;
simulation.alpha(1);
simulation.restart();
});
});

return "Código JS para visualizar";
}
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more