// Detectar si es un dispositivo táctil function isTouchDevice() { return 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; } // Variables globales para manejar toques let lastTouchTime = 0; let touchTimer = null; let lastTouchTarget = null; const TOUCH_DELAY = 300; // ms para detectar doble toque // Código D3.js con tooltips - Versión con carga de datos externos function createChart(data) { // Specify the chart's dimensions. const width = 928; const height = 924; // Get the tooltip element const tooltip = d3.select("#tooltip"); // Detectar dispositivo const isTouch = isTouchDevice(); // Función para posicionar el tooltip inteligentemente function positionTooltip(event, tooltipElement) { const tooltipHeight = tooltipElement.offsetHeight; const tooltipWidth = tooltipElement.offsetWidth; const windowHeight = window.innerHeight; const windowWidth = window.innerWidth; let left, top; // Manejar eventos táctiles vs mouse if (event.type.includes('touch')) { const touch = event.touches && event.touches.length > 0 ? event.touches[0] : event.changedTouches[0]; left = touch.pageX + 15; top = touch.pageY - 15; } else { left = event.pageX + 15; top = event.pageY - 15; } // Ajustar posición horizontal si se sale por la derecha if (left + tooltipWidth > windowWidth - 20) { left = (event.type.includes('touch') ? ((event.touches && event.touches[0]) || event.changedTouches[0]).pageX : event.pageX) - tooltipWidth - 15; } // Ajustar posición vertical si se sale por abajo if (top + tooltipHeight > windowHeight - 20) { top = (event.type.includes('touch') ? ((event.touches && event.touches[0]) || event.changedTouches[0]).pageY : event.pageY) - tooltipHeight - 15; } // Ajustar si se sale por arriba if (top < 20) { top = 20; } // Ajustar si se sale por la izquierda if (left < 20) { left = 20; } return { left, top }; } // Paletas de colores mejoradas const parentColors = [ "#4e79a7", "#f28e2c", "#e15759", "#76b7b2", "#59a14f", "#edc949", "#af7aa1", "#ff9da7", "#9c755f", "#bab0ab", "#8cd17d", "#499894", "#f1ce63", "#86bcb6", "#d37295" ]; const leafColors = [ "#d4a6c8", "#a0cbe8", "#f1ce63", "#b6992d", "#d37295", "#fabfd2", "#b07aa1", "#9d7660", "#79706e", "#d7b5a6", "#ff9d9a", "#a0d6db", "#c5e0b4", "#f8cbad", "#c6c6c6" ]; // Esta función genera un color único basado en la jerarquía function getColor(d, index) { if (d === root) return "#fff"; if (d.children) { // Para nodos padre: color basado en posición en jerarquía const level = d.depth; const positionInLevel = index || 0; // Generar un hash único combinando depth y posición const hash = level * 10 + positionInLevel; return parentColors[hash % parentColors.length]; } else { // Para nodos hoja: color basado en nombre y profundidad let hash = 0; for (let i = 0; i < d.data.name.length; i++) { hash = d.data.name.charCodeAt(i) + ((hash << 5) - hash); } const index = Math.abs(hash + d.depth) % leafColors.length; return leafColors[index]; } } // This custom tiling function adapts the built-in binary tiling function // for the appropriate aspect ratio when the treemap is zoomed-in. function tile(node, x0, y0, x1, y1) { d3.treemapBinary(node, 0, 0, width, height); for (const child of node.children) { child.x0 = x0 + child.x0 / width * (x1 - x0); child.x1 = x0 + child.x1 / width * (x1 - x0); child.y0 = y0 + child.y0 / height * (y1 - y0); child.y1 = y0 + child.y1 / height * (y1 - y0); } } // Compute the layout. const hierarchy = d3.hierarchy(data) .sum(d => d.value) .sort((a, b) => b.value - a.value); const root = d3.treemap().tile(tile)(hierarchy); // Create the scales. const x = d3.scaleLinear().rangeRound([0, width]); const y = d3.scaleLinear().rangeRound([0, height]); // Formatting utilities. const format = d3.format(",d"); const formatPercent = d3.format(".01%"); const name = d => d.ancestors().reverse().map(d => d.data.name).join(" → "); // Calcular porcentajes const totalValue = root.value; // Create the SVG container. const svg = d3.select("#domainDrillDown") .append("svg") .attr("viewBox", [0.5, -50.5, width, height + 50]) .attr("width", width) .attr("height", height + 50) .attr("style", "max-width: 100%; height: auto; cursor: pointer;") .style("font", "10px sans-serif"); // Display the root. let group = svg.append("g") .call(render, root); function render(group, root) { const node = group .selectAll("g") .data(root.children.concat(root)) .join("g"); // Función para mostrar tooltip function showTooltip(event, d, element) { // Ocultar tooltip anterior primero tooltip.style("opacity", 0); // Remover highlight anterior d3.selectAll("rect.highlight").classed("highlight", false); // Highlight the rectangle d3.select(element).select("rect") .classed("highlight", true); // Calculate percentage const percentage = (d.value / totalValue) * 100; const parentPercentage = d.parent ? (d.value / d.parent.value) * 100 : 100; // Build tooltip content let tooltipHTML = `
${d.data.name}
`; tooltipHTML += `
Asistencia: ${format(d.value)}
`; if (d.data.description) { tooltipHTML += `
${d.data.description}
`; } tooltipHTML += `
${percentage.toFixed(3)}% del total
`; if (d.parent && d.parent !== root) { tooltipHTML += `
${parentPercentage.toFixed(1)}% de ${d.parent.data.name}
`; } tooltipHTML += `
Ruta: ${name(d)}
`; // Show tooltip tooltip .html(tooltipHTML) .style("opacity", 1); // Posicionar tooltip inteligentemente const tooltipElement = tooltip.node(); const pos = positionTooltip(event, tooltipElement); tooltip .style("left", pos.left + "px") .style("top", pos.top + "px"); return true; } // Función para ocultar tooltip function hideTooltip() { tooltip.style("opacity", 0); d3.selectAll("rect.highlight").classed("highlight", false); } // Track index for color assignment let nodeIndex = 0; // Crear rectángulos con eventos según el dispositivo node.selectAll("rect") .data(d => [d]) .join("rect") .attr("id", d => `leaf-${d.data.name.replace(/\s+/g, '-')}-${Math.random().toString(36).substr(2, 9)}`) .attr("fill", d => { // Asignar color único basado en posición const color = getColor(d, nodeIndex); nodeIndex++; return color; }) .attr("stroke", "#fff") .attr("stroke-width", 1); // Asignar eventos según el dispositivo if (isTouch) { // Para dispositivos táctiles node.filter(d => d === root ? d.parent : d.children) .attr("cursor", "pointer") .on("touchstart", function(event, d) { event.preventDefault(); event.stopPropagation(); const currentTime = new Date().getTime(); const timeDiff = currentTime - lastTouchTime; const sameTarget = (lastTouchTarget === this); if (timeDiff < TOUCH_DELAY && timeDiff > 0 && sameTarget) { // Es un doble toque en el mismo elemento - hacer zoom clearTimeout(touchTimer); lastTouchTime = 0; lastTouchTarget = null; if (d === root) { zoomout(root); } else { zoomin(d); } // Ocultar tooltip si está visible hideTooltip(); } else { // Es un toque simple - guardar referencia lastTouchTime = currentTime; lastTouchTarget = this; // Mostrar tooltip inmediatamente showTooltip(event, d, this); // Configurar temporizador para ocultar tooltip después de 3 segundos clearTimeout(touchTimer); touchTimer = setTimeout(() => { hideTooltip(); }, 3000); } }); // Ocultar tooltip al tocar fuera o al hacer scroll document.addEventListener('touchstart', function(event) { if (!event.target.closest('rect')) { hideTooltip(); } }); } else { // Para escritorio: comportamiento original node.filter(d => d === root ? d.parent : d.children) .attr("cursor", "pointer") .on("click", (event, d) => { d === root ? zoomout(root) : zoomin(d); // Ocultar tooltip al hacer zoom hideTooltip(); }); // Eventos de mouse para tooltips node.selectAll("rect") .on("mouseover", function(event, d) { showTooltip(event, d, this.parentNode); }) .on("mousemove", function(event) { // Solo mover tooltip si está visible if (tooltip.style("opacity") > 0) { const tooltipElement = tooltip.node(); const pos = positionTooltip(event, tooltipElement); tooltip .style("left", pos.left + "px") .style("top", pos.top + "px"); } }) .on("mouseout", function(event, d) { hideTooltip(); }); } node.append("text") .attr("font-weight", d => d === root ? "bold" : null) .attr("fill", d => d === root ? "#000" : "#fff") .attr("pointer-events", "none") .selectAll("tspan") .data(d => { const displayName = d === root ? name(d) : d.data.name; const words = displayName.split(/(?=[A-Z][^A-Z])/g); return words.concat(format(d.value)); }) .join("tspan") .attr("x", 3) .attr("y", (d, i, nodes) => `${(i === nodes.length - 1) * 0.3 + 1.1 + i * 0.9}em`) .attr("fill-opacity", (d, i, nodes) => i === nodes.length - 1 ? 1 : 1) .attr("font-weight", (d, i, nodes) => i === nodes.length - 1 ? "normal" : null) .text(d => d); group.call(position, root); } function position(group, root) { group.selectAll("g") .attr("transform", d => d === root ? `translate(0,-50)` : `translate(${x(d.x0)},${y(d.y0)})`) .select("rect") .attr("width", d => d === root ? width : x(d.x1) - x(d.x0)) .attr("height", d => d === root ? 50 : y(d.y1) - y(d.y0)); // Ajustar visibilidad del texto group.selectAll("text") .each(function(d) { const rect = d3.select(this.parentNode).select("rect"); const rectWidth = parseFloat(rect.attr("width")); const rectHeight = parseFloat(rect.attr("height")); if (rectWidth < 5 || rectHeight < 2) { d3.select(this).style("display", "none"); } else { d3.select(this).style("display", null); } }); } // When zooming in, draw the new nodes on top, and fade them in. function zoomin(d) { const group0 = group.attr("pointer-events", "none"); const group1 = group = svg.append("g").call(render, d); x.domain([d.x0, d.x1]); y.domain([d.y0, d.y1]); svg.transition() .duration(750) .call(t => group0.transition(t).remove() .call(position, d.parent)) .call(t => group1.transition(t) .attrTween("opacity", () => d3.interpolate(0, 1)) .call(position, d)); } // When zooming out, draw the old nodes on top, and fade them out. function zoomout(d) { const group0 = group.attr("pointer-events", "none"); const group1 = group = svg.insert("g", "*").call(render, d.parent); x.domain([d.parent.x0, d.parent.x1]); y.domain([d.parent.y0, d.parent.y1]); svg.transition() .duration(750) .call(t => group0.transition(t).remove() .attrTween("opacity", () => d3.interpolate(1, 0)) .call(position, d)) .call(t => group1.transition(t) .call(position, d.parent)); } return svg.node(); } // Función para cargar datos desde la URL function loadDataAndCreateChart() { const dataUrl = 'https://resquivel0810.github.io/resquivel/data/cine-mex-00-24.json'; // Mostrar mensaje de carga const loadingElement = d3.select("#domainDrillDown"); loadingElement.html('
Cargando datos del cine mexicano 2000-2024...
'); // Cargar datos desde la URL d3.json(dataUrl) .then(data => { // Limpiar el contenedor loadingElement.html(''); // Crear el gráfico con los datos cargados createChart(data); }) .catch(error => { console.error('Error al cargar los datos:', error); loadingElement.html(`

Error al cargar los datos

No se pudieron cargar los datos desde: ${dataUrl}

Error: ${error.message}

`); }); } // Crear el gráfico cuando el DOM esté cargado document.addEventListener('DOMContentLoaded', function() { loadDataAndCreateChart(); });