Public
Edited
Jun 13
Insert cell
Insert cell
// Celda 1: Importar librerías
d3 = require("d3@6")
Insert cell
Inputs = require("@observablehq/inputs@0.10.6")
Insert cell
// Celda 2: Cargar el NUEVO archivo CSV generado por Python
// Asegúrate de adjuntar 'datos_para_observable.csv'
file = FileAttachment("datos_para_observable_2.csv").csv({typed: true})
Insert cell
// Celda 3: Procesar los datos (convertir fechas)
data = file.map(d => ({
...d,
time: new Date(d.time)
}))
Insert cell
// Celda 4: Obtener listas únicas para los filtros
allEntities = Array.from(new Set(data.flatMap(d => [d.from_id, d.to_id]))).filter(d => d).sort();
Insert cell
allCommunities = Array.from(new Set(data.map(d => d.source_community))).filter(d => d && d !== 'N/A').sort((a,b) => a-b);
Insert cell
// Celda nueva: Obtener subtipos de monitoreo y crear escala de color
monitoringSubtypes = Array.from(new Set(data.map(d => d.monitoring_subtype))).filter(d => d);
Insert cell
monitoringColorScale = d3.scaleOrdinal(d3.schemeSet2).domain(monitoringSubtypes);
Insert cell
// Celda 5: Obtener rango de fechas
dateDomain = d3.extent(data, d => d.time);
Insert cell
minDate = dateDomain[0];
Insert cell
maxDate = dateDomain[1];
Insert cell
// Celda 6: --- FILTROS INTERACTIVOS ---
viewof communityFilter = Inputs.select(allCommunities, {
label: "Filtrar por Comunidades",
multiple: true,
value: allCommunities // Valor inicial: todas las comunidades
})
Insert cell
// Celda 7: Filtro de Entidades (ahora depende del filtro de comunidad)
entitiesToShow = allEntities.filter(entity => {
const row = data.find(d => d.from_id === entity || d.to_id === entity);
return row && communityFilter.includes(row.source_community || row.target_community);
});
Insert cell
viewof entityFilter = Inputs.select(entitiesToShow, {
label: "Filtrar por Entidades",
multiple: true,
value: entitiesToShow
})
Insert cell
// Celda 8 (nueva): Filtro de Fecha Desde
viewof startDateFilter = Inputs.date({
label: "Fecha Desde",
value: minDate,
min: minDate,
max: maxDate
})
Insert cell
// Celda 8.1 (nueva): Filtro de Fecha Hasta
viewof endDateFilter = Inputs.date({
label: "Fecha Hasta",
value: maxDate,
min: minDate,
max: maxDate
})
Insert cell
// Celda 7 (nueva): Filtro para Subtipos de Monitoreo
viewof monitoringSubtypeFilter = Inputs.select(monitoringSubtypes, {
label: "Filtrar por Subtipo de Monitoreo",
multiple: true,
value: monitoringSubtypes // Valor inicial: todos seleccionados
})
Insert cell
// Celda 9: Filtrar datos (versión corregida para usar los nuevos filtros de fecha)
filteredData = data.filter(d => {
const time = d.time.getTime();
// --- LÍNEAS CORREGIDAS ---
const start = startDateFilter.getTime();
const end = new Date(endDateFilter).setHours(23, 59, 59, 999);
const inDateRange = time >= start && time <= end;

if (d.event_type === 'Communication') {
const inCommunity = communityFilter.length === 0 || communityFilter.includes(d.source_community);
const inEntity = entityFilter.length === 0 || (entityFilter.includes(d.from_id) && entityFilter.includes(d.to_id));
return inDateRange && inCommunity && inEntity;
}
if (d.event_type === 'Monitoring') {
const inSubtype = monitoringSubtypeFilter.length === 0 || monitoringSubtypeFilter.includes(d.monitoring_subtype);
return inDateRange && inSubtype;
}
return false;
})
Insert cell
// Celda 10: Separar en comunicaciones y monitoreos
filteredCommunications = filteredData.filter(d => d.event_type === 'Communication');
Insert cell
filteredMonitoring = filteredData.filter(d => d.event_type === 'Monitoring');
Insert cell
// Celda 11: Pre-procesamiento de sesiones (no necesita cambios)
processedConversations = {
const TIME_THRESHOLD = 5 * 60 * 1000;
const sessions = [];
const singleMessages = [];
const sortedComms = filteredCommunications.sort((a, b) => a.time - b.time);
const processedIndices = new Set();
for (let i = 0; i < sortedComms.length; i++) {
if (processedIndices.has(i)) continue;
const msg1 = sortedComms[i];
let foundPair = false;
for (let j = i + 1; j < sortedComms.length; j++) {
if (processedIndices.has(j)) continue;
const msg2 = sortedComms[j];
if (msg2.time - msg1.time > TIME_THRESHOLD) break;
if (msg2.from_id === msg1.to_id && msg2.to_id === msg1.from_id) {
sessions.push({ type: 'session', participants: [msg1.from_id, msg1.to_id].sort(), startTime: msg1.time, endTime: msg2.time, messages: [msg1, msg2], community: msg1.source_community });
processedIndices.add(i);
processedIndices.add(j);
foundPair = true;
break;
}
}
if (!foundPair) { singleMessages.push(msg1); }
}
return { sessions, singleMessages };
}
Insert cell
// Celda 12: Gráfico con colores para monitoreo
chart = {
const width = 1000;
const height = 1500;
const margin = ({top: 20, right: 20, bottom: 120, left: 160});

// Escala de colores para las comunidades
const colorScale = d3.scaleOrdinal(d3.schemeCategory10).domain(allCommunities);

const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);

// --- GESTIÓN DEL TOOLTIP ---
// ... (el código del tooltip se queda igual) ...
d3.select(".custom-tooltip").remove();
const tooltip = d3.select("body").append("div").attr("class", "custom-tooltip").style("position", "absolute").style("visibility", "hidden").style("background", "rgba(0,0,0,0.85)").style("color", "white").style("border-radius", "6px").style("padding", "10px").style("font-family", "sans-serif").style("font-size", "13px").style("pointer-events", "none").style("max-width", "400px").style("white-space", "pre-wrap").style("z-index", "10");

if (entityFilter.length === 0) {
svg.append("text").attr("x", width / 2).attr("y", 100).attr("text-anchor", "middle").style("font-size", "1.2em").text("Por favor, selecciona entidades para visualizar.");
return svg.node();
}
// --- DEFINICIONES DE FLECHAS Y EJES ---
// ... (el código de flechas y ejes se queda igual) ...
const defs = svg.append("defs");
allCommunities.forEach(community => { defs.append("marker").attr("id", `arrowhead-${community}`).attr("viewBox", "0 -5 10 10").attr("refX", 10).attr("refY", 0).attr("markerWidth", 6).attr("markerHeight", 6).attr("orient", "auto").append("path").attr("d", "M0,-5L10,0L0,5").attr("fill", colorScale(community)); });
const yScale = d3.scaleUtc().domain([startDateFilter, endDateFilter]).range([margin.top, height - margin.bottom]);
const xScale = d3.scalePoint().domain(entityFilter).range([margin.left, width - margin.right]).padding(0.5);
svg.append("g").attr("transform", `translate(${margin.left},0)`).call(d3.axisLeft(yScale).ticks(d3.utcHour.every(4)).tickFormat(d3.utcFormat("%H:%M %b %d")));
svg.append("g").attr("transform", `translate(0,${height - margin.bottom})`).call(d3.axisBottom(xScale)).selectAll("text").style("text-anchor", "end").attr("dx", "-.8em").attr("dy", ".15em").attr("transform", "rotate(-45)").style("fill", d => { const row = data.find(row => row.from_id === d || row.to_id === d); return row ? colorScale(row.source_community || row.target_community) : '#000'; });

// --- LÓGICA DE FLUJO DE CONVERSACIÓN ---
// (Esta parte no cambia)
const lastState = {};
const pathData = [];
processedConversations.singleMessages.forEach(d => { if (!xScale(d.from_id) || !xScale(d.to_id)) return; const y_current = yScale(d.time); const x_source = xScale(d.from_id); const x_target = xScale(d.to_id); let x1, y1; if (lastState[d.from_id]) { x1 = lastState[d.from_id].x; y1 = lastState[d.from_id].y; } else { x1 = x_source; y1 = y_current; } pathData.push({ x1, y1, x2: x_target, y2: y_current, ...d }); lastState[d.from_id] = { x: x_source, y: y_current }; lastState[d.to_id] = { x: x_target, y: y_current }; });

// --- DIBUJAR ELEMENTOS ---

// 1. Líneas de Monitoreo (CON NUEVO COLOR)
svg.selectAll(".monitor-line").data(filteredMonitoring).join("line")
.attr("class", "monitor-line")
.attr("x1", margin.left).attr("x2", width - margin.right)
.attr("y1", d => yScale(d.time)).attr("y2", d => yScale(d.time))
.style("cursor", "pointer")
// ---- CAMBIO AQUÍ ----
.attr("stroke", d => monitoringColorScale(d.monitoring_subtype))
.attr("stroke-width", 3).attr("stroke-dasharray", "4 4").attr("opacity", 0.7)
.on("mouseover", (event, d) => tooltip.style("visibility", "visible").html(`<b>Tipo:</b> ${d.monitoring_subtype}<br><hr style="margin:4px 0;"><b>Hallazgos:</b><br>${d.event_findings}`))
.on("mousemove", (event) => tooltip.style("top", (event.pageY - 10) + "px").style("left", (event.pageX + 10) + "px"))
.on("mouseout", () => tooltip.style("visibility", "hidden"));

// ... (El resto del código para sesiones y mensajes únicos se queda igual) ...
svg.append("g").selectAll(".session-rect").data(processedConversations.sessions).join("rect").attr("class", "session-rect").attr("x", d => Math.min(xScale(d.participants[0]), xScale(d.participants[1]))).attr("y", d => yScale(d.startTime)).attr("width", d => Math.abs(xScale(d.participants[0]) - xScale(d.participants[1]))).attr("height", d => Math.max(1, yScale(d.endTime) - yScale(d.startTime))).attr("fill", d => colorScale(d.community)).attr("opacity", 0.4).style("cursor", "pointer").on("mouseover", (event, d) => { const messagesText = d.messages.map(m => `<b>${m.from_id} (${d3.utcFormat("%H:%M:%S")(m.time)}):</b><br>${m.text}`).join('<br><hr style="margin: 4px 0;">'); tooltip.style("visibility", "visible").html(`<b>Conversación:</b><br><hr style="margin: 4px 0;">${messagesText}`); }).on("mousemove", (event) => tooltip.style("top", (event.pageY - 10) + "px").style("left", (event.pageX + 10) + "px")).on("mouseout", () => tooltip.style("visibility", "hidden"));
svg.selectAll(".comm-line").data(pathData).join("line").attr("class", "comm-line").attr("x1", d => d.x1).attr("y1", d => d.y1).attr("x2", d => d.x2).attr("y2", d => d.y2).style("cursor", "pointer").attr("stroke", d => colorScale(d.source_community)).attr("stroke-width", 3).attr("marker-end", d => `url(#arrowhead-${d.source_community})`).on("mouseover", (event, d) => tooltip.style("visibility", "visible").html(`<b>De:</b> ${d.from_id}<br><b>A:</b> ${d.to_id}<hr><b>Mensaje:</b><br>${d.text}`)).on("mousemove", (event) => tooltip.style("top", (event.pageY - 10) + "px").style("left", (event.pageX + 10) + "px")).on("mouseout", () => tooltip.style("visibility", "hidden"));


invalidation.then(() => tooltip.remove());
return svg.node();
}
Insert cell
// Celda 13: Leyenda para Tipos de Monitoreo
monitoringLegend = {
const legendSvg = d3.create("svg")
.attr("width", 250)
.attr("height", monitoringSubtypes.length * 20 + 30);

const legend = legendSvg.append("g")
.attr("transform", `translate(10, 20)`);

legend.append("text")
.attr("x", 0)
.attr("y", -10)
.style("font-weight", "bold")
.style("font-size", "14px")
.text("Tipos de Monitoreo");

const legendItems = legend.selectAll(".legend-item")
.data(monitoringSubtypes)
.join("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => `translate(0, ${i * 20})`);

legendItems.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 15)
.attr("height", 15)
.style("fill", d => monitoringColorScale(d));

legendItems.append("text")
.attr("x", 20)
.attr("y", 12)
.text(d => d) // Muestra el nombre del subtipo
.style("font-size", "12px")
.attr("alignment-baseline", "middle");

return legendSvg.node();
}
Insert cell
const yScale = d3.scaleUtc().domain([startDateFilter, endDateFilter]).range([margin.top, height - margin.bottom]);
Insert cell
// Celda 13: Leyenda de Comunidades (en su propia celda)
legend = {
const legendSvg = d3.create("svg")
.attr("width", 200) // Ancho fijo para la leyenda
.attr("height", allCommunities.length * 20 + 30); // Altura dinámica según el número de comunidades

// Escala de colores (la misma que usamos en el gráfico)
const colorScale = d3.scaleOrdinal(d3.schemeCategory10).domain(allCommunities);

const legend = legendSvg.append("g")
.attr("transform", `translate(10, 20)`); // Margen interno

legend.append("text")
.attr("x", 0)
.attr("y", -10)
.style("font-weight", "bold")
.style("font-size", "14px")
.text("Comunidades");

const legendItems = legend.selectAll(".legend-item")
.data(allCommunities)
.join("g")
.attr("class", "legend-item")
.attr("transform", (d, i) => `translate(0, ${i * 20})`);

legendItems.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", 15)
.attr("height", 15)
.style("fill", d => colorScale(d));

legendItems.append("text")
.attr("x", 20)
.attr("y", 12)
.text(d => `Comunidad ${d}`)
.style("font-size", "12px")
.attr("alignment-baseline", "middle");

return legendSvg.node();
}
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