From b091916dcc36a9ff2a861009cfc9ec9853f3faf9 Mon Sep 17 00:00:00 2001 From: = <=> Date: Sat, 14 Dec 2024 13:52:52 +0100 Subject: [PATCH] =?UTF-8?q?C=C3=B3digo=20de=20la=20p=C3=A1gina=20web=20ter?= =?UTF-8?q?minado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/style.css | 5 +- src/visualizacion.html | 6 +- src/visualizacion.js | 186 +++++++++++++++++++++++++++++++++-------- 3 files changed, 156 insertions(+), 41 deletions(-) diff --git a/src/style.css b/src/style.css index 2b1b6e3..e4cd5e9 100644 --- a/src/style.css +++ b/src/style.css @@ -1,11 +1,12 @@ body { margin: 0; + background-color: aliceblue; width: 100%; height: 100%; } .main_view { float: left; - background-color: aliceblue; + width: 100%; margin: 0; } @@ -14,7 +15,7 @@ svg { background-color: white; border: 2px solid black; border-radius: 5px; - margin: 1%; + margin: 2% 2% 0 2%; } .filters-container{ diff --git a/src/visualizacion.html b/src/visualizacion.html index df4a109..42cd73a 100644 --- a/src/visualizacion.html +++ b/src/visualizacion.html @@ -15,7 +15,7 @@ </div> <div class="filters-container"> <label for="color_scheme"> - <h3>Coloreado por:</h3> + <h2>Coloreado por:</h2> </label> <select id="color_scheme" onchange="changeSelection(event)"> <option value="platform">Plataforma</option> @@ -23,13 +23,13 @@ </select> <br> <label> - <h3>Búsqueda:</h3> + <h2>Búsqueda:</h2> <input type="search" id="search"> </label> </div> <div class="filters-container"> - <h3>Filtros</h3> + <h2>Filtros</h2> <div id="filters"> <!-- Aquà se generarán los checkboxes dinámicamente --> </div> diff --git a/src/visualizacion.js b/src/visualizacion.js index f6c74a3..c27e1de 100644 --- a/src/visualizacion.js +++ b/src/visualizacion.js @@ -67,18 +67,20 @@ let min_y = 0; let max_x = 10; let max_y = 10; -const width = 800; +const width = 950; const height = 400; const margin_x = 62; const margin_y = 60; -const right_margin = 300; +const right_margin = 400; const rectAparitionTime = 150; const radiusTransition = 300; -const maxBubbleCount = 50; +const maxBubbleCount = 100; -const svg = d3.select('.main_view').insert('svg', '.filters-container') - .attr("width", width + 2 * margin_x).attr("height", height + 2 * margin_y); +const svg = d3.select('.main_view') + .insert('svg', '.filters-container') + .attr("width", width + 2 * margin_x) + .attr("height", height + 2 * margin_y) svg.append('defs').append('clipPath') .attr("id", "content-clip") @@ -168,6 +170,15 @@ function createAxis() { .attr('y', 0); scatter = contentGroup.append('g'); + + svg.append("text") + .attr("x", width - right_margin + 1.3*margin_x) + .attr("y", 3.7*margin_y) + .attr("class", "legend-title") + .style("font-size", "26px") + .style("font-weight", "bold") + .style("font-family", "Georgia, serif") + .text("Leyenda"); } d3.csv('files/games-data.csv').then((data) => { @@ -181,13 +192,12 @@ d3.csv('files/games-data.csv').then((data) => { elem.critics = +elem.critics; elem.users = +elem.users; elem.genre = [...new Set(elem.genre.split(','))]; - elem.id = elem.name.replaceAll(/[ :'"()\[\]]*/g, '') + "-" + elem.platform; }); xScale = d3.scaleLinear().domain([min_x, max_x]).range([0, width-right_margin]); yScale = d3.scaleLinear().domain([min_y, max_y]).range([height, 0]); - radiusScale = d3.scaleSqrt().domain([0, d3.max(data, elem => elem["critics"]) || 1]).range([5, 15]); + radiusScale = d3.scaleSqrt().domain([0, d3.max(data, elem => elem["critics"]) || 10]).range([7, 15]); elegibleData = data.sort((a, b) => { return (b["critics"] + b["users"]) - (a["critics"] + a["users"]) @@ -206,7 +216,7 @@ function handleMouseOver(event, d) { .transition() .duration(radiusTransition) .ease(d3.easeQuadInOut) - .attr("r", radiusScale(d.critics) * 1.4) + .attr("r", radiusScale(d.critics) * 1.5) // Calcula las posiciones del cÃrculo transformadas const cx = transform.applyX(xScale(d["user score"])); @@ -220,19 +230,21 @@ function handleMouseOver(event, d) { const rects = svg.append("g").attr("class", "rects"); const texts = svg.append("g").attr("class", "texts"); + const textLocation = width - right_margin + 1.4*margin_x; + // Añade el texto al tooltip const text = texts.append("text") - .attr("x", cx + visualOffsetX + 10) // Posiciona el texto con offset - .attr("y", cy + visualOffsetY + 20) - .style("font-size", "15px") + .attr("x", width + margin_x) // Posiciona el texto con offset + .attr("y", margin_y) + .style("font-size", "14px") .style("font-family", "Georgia, serif") .style("fill", "black") text.append("tspan") .text(`${d.name}`) - .style("font-size", "18px") + .style("font-size", "20px") .style("font-weight", "bold") - .attr("x", cx + visualOffsetX + 10) + .attr("x", textLocation) .attr("dy", "1.2em") .style("opacity", 0) .transition() @@ -242,7 +254,7 @@ function handleMouseOver(event, d) { text.append("tspan") .text(`Géneros: ${d.genre.join(", ")}`) - .attr("x", cx + visualOffsetX + 10) + .attr("x", textLocation) .attr("dy", "1.2em") .style("opacity", 0) .transition() @@ -252,7 +264,7 @@ function handleMouseOver(event, d) { text.append("tspan") .text(`Fecha: ${d["r-date"]}`) - .attr("x", cx + visualOffsetX + 10) + .attr("x", textLocation) .attr("dy", "1.2em") .style("opacity", 0) .transition() @@ -262,7 +274,7 @@ function handleMouseOver(event, d) { text.append("tspan") .text(`Desarrolladores: ${d.developer}`) - .attr("x", cx + visualOffsetX + 10) + .attr("x", textLocation) .attr("dy", "1.2em") .style("opacity", 0) .transition() @@ -272,23 +284,26 @@ function handleMouseOver(event, d) { text.append("tspan") .text(`Consola: ${d.platform}`) - .attr("x", cx + visualOffsetX + 10) + .attr("x", textLocation) .attr("dy", "1.2em") .style("opacity", 0) .transition() .duration(rectAparitionTime) .ease(d3.easeQuadOut) .style("opacity", 1) + if(d.players !== "No info"){ + text.append("tspan") + .text(`Jugadores: ${d.players}`) + .attr("x", textLocation) + .attr("dy", "1.2em") + .style("opacity", 0) + .transition() + .duration(rectAparitionTime) + .ease(d3.easeQuadOut) + .style("opacity", 1) + } + - text.append("tspan") - .text(`Jugadores: ${d.players}`) - .attr("x", cx + visualOffsetX + 10) - .attr("dy", "1.2em") - .style("opacity", 0) - .transition() - .duration(rectAparitionTime) - .ease(d3.easeQuadOut) - .style("opacity", 1) // Usa el bounding box del texto para calcular el tamaño del rectángulo const bbox = text.node().getBBox(); @@ -303,6 +318,8 @@ function handleMouseOver(event, d) { return colores[selection][d[selection]]; } else return "#aaaaaa"; }) + .style("stroke", "black") + .style("stroke-width", "3px") .attr("rx", 5) .attr("ry", 5) .style("opacity", 0) @@ -343,8 +360,6 @@ function zoomed(event) { newXScale.domain([Math.max(min_x, newXScale.domain()[0]), Math.min(max_x, newXScale.domain()[1])]); newYScale.domain([Math.max(min_y, newYScale.domain()[0]), Math.min(max_y, newYScale.domain()[1])]); - - translate_group.select(".xAxis").call(d3.axisBottom(newXScale)); translate_group.select(".yAxis").call(d3.axisLeft(newYScale)); @@ -364,8 +379,54 @@ function zoomed(event) { } function createBubbles(data) { + removeLegend(); + const showNoData = () => { + svg.append('text') + .attr('x', width - right_margin + 1.2*margin_x) + .attr('y', 2*margin_y) + .attr('class', 'no-shown-data-text') + .text("No hay datos para los filtros seleccionados") + .style("font-size", "18px") + .style("font-family", "Georgia, serif") + .style("font-weight", "bold") + .attr('fill', 'red') + } + + svg.select('.no-shown-data-text').remove(); + + if(data.length === 0){ + showNoData(); + return; + } + + const bubbleGroup = scatter.append('g') .attr('class', 'bubbles'); + let newXScale, newYScale; + + if(transform){ + newXScale = transform.rescaleX(xScale); + newYScale = transform.rescaleY(yScale); + + newXScale.domain([Math.max(min_x, newXScale.domain()[0]), Math.min(max_x, newXScale.domain()[1])]); + newYScale.domain([Math.max(min_y, newYScale.domain()[0]), Math.min(max_y, newYScale.domain()[1])]); + + translate_group.select(".xAxis").call(d3.axisBottom(newXScale)); + translate_group.select(".yAxis").call(d3.axisLeft(newYScale)); + + contentGroup.selectAll('.vertical-grid') + .attr('x1', d => newXScale(d) + margin_x) + .attr('x2', d => newXScale(d) + margin_x) + + contentGroup.selectAll('.horizontal-grid') + .attr('y1', d => newYScale(d) + margin_y) + .attr('y2', d => newYScale(d) + margin_y) + } else { + newXScale = xScale; + newYScale = yScale; + } + + let selectedColors = []; const zoom = d3.zoom() .scaleExtent([1, 10]) @@ -377,13 +438,17 @@ function createBubbles(data) { .data(data) .enter() .append("circle") - .attr("cx", d => xScale(d["user score"]) + margin_x) - .attr("cy", d => yScale(d["score"]) + margin_y) + .attr("cx", d => newXScale(d["user score"]) + margin_x) + .attr("cy", d => newYScale(d["score"]) + margin_y) .attr("r", d => radiusScale(d["critics"])) .attr("fill", d => { if (colores[selection][d[selection]]){ + selectedColors.push([d[selection], colores[selection][d[selection]]]) return colores[selection][d[selection]]; - } else return "#aaaaaa"; + } else{ + selectedColors.push(["Otros", "#aaaaaa"]); + return "#aaaaaa"; + } }) .attr("stroke", "#000000") .attr("stroke-width", 1) @@ -393,6 +458,19 @@ function createBubbles(data) { svg.call(zoom); + const unique = (vector) => { + const conjunto = new Set( + vector.map((elemento) => JSON.stringify(elemento)) + ); + return Array.from(conjunto).map((elemento) => JSON.parse(elemento)); + }; + + selectedColors = unique(selectedColors); + selectedColors.sort((a, b) => a[0].localeCompare(b[0], undefined, {sensitivity:"base"})); + + console.log(selectedColors) + setLegend(selectedColors); + } function removeBubbles() { @@ -417,12 +495,14 @@ function generateFilters(data) { "Open-World", "Action", "Shooter", - "Arcade", + "First-Person", "2D", "3D", "Rhythm", - "Team", - "Roguelike" + "Gambling", + "Role-Playing", + "Survival", + "Horror" ]; generos.forEach(value => { @@ -446,8 +526,6 @@ function applyFilters(data) { .filter(input => input.checked) .map(input => input.value); - - // Filtra los datos en base a los filtros seleccionados elegibleData = data.filter(d => { if (selectedFilters.length === 0) return true; for (filter of selectedFilters) { @@ -471,6 +549,42 @@ function enableSearch(data){ createBubbles(elegibleData); } +} + +function setLegend(entries){ + // Crear grupos para cada elemento de la leyenda + const legend = svg.selectAll(".legend-item") + .data(entries) + .enter() + .append("g") + .attr("class", "legend-item") + .attr("transform", (d, i) =>{ + if(i > 7){ + return `translate(${width-right_margin + 4*margin_x}, ${(i-8) * 23 + height - 2.6 * margin_y})` + } + return `translate(${width-right_margin + 1.2*margin_x}, ${i * 23 + height - 2.6 * margin_y})` + }); + + // Dibujar los rectángulos de color + legend.append("rect") + .attr("width", 20) + .attr("height", 20) + .attr("fill", d => d[1]) + .attr("rx", 2) + .attr("ry", 2) + .attr("stroke", "black") + .attr("stroke-width", 1.5) + + // Agregar los textos descriptivos + legend.append("text") + .attr("x", 30) + .attr("y", 15) + .style("font-size", "16px") + .style("font-family", "Georgia, serif") + .text(d => d[0]); } +function removeLegend(){ + svg.selectAll('.legend-item').remove(); +} \ No newline at end of file -- GitLab