From f9f8ef5f2afcf34295c2b6dbcf576c60d999ef8e Mon Sep 17 00:00:00 2001
From: = <=>
Date: Thu, 12 Dec 2024 11:24:25 +0100
Subject: [PATCH] =?UTF-8?q?Terminado=20la=20gestion=20de=20filtros=20y=20l?=
 =?UTF-8?q?a=20b=C3=BAsqueda=20por=20subcadenas=20del=20nombre.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/style.css          |  23 +++
 src/visualizacion.html |  30 +++-
 src/visualizacion.js   | 395 +++++++++++++++++++++++++++++++----------
 3 files changed, 348 insertions(+), 100 deletions(-)

diff --git a/src/style.css b/src/style.css
index 8b13789..2b1b6e3 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1 +1,24 @@
+body {
+    margin: 0;
+    width: 100%;
+    height: 100%;
+}
+.main_view {
+    float: left;
+    background-color: aliceblue;
+    width: 100%;
+    margin: 0;
+}
+svg {
+    float: left;
+    background-color: white;
+    border: 2px solid black;
+    border-radius: 5px;
+    margin: 1%;
+}
 
+.filters-container{
+    float: left;
+    margin-left: 3%;
+    margin-top:2%
+}
\ No newline at end of file
diff --git a/src/visualizacion.html b/src/visualizacion.html
index 2bf892f..df4a109 100644
--- a/src/visualizacion.html
+++ b/src/visualizacion.html
@@ -7,19 +7,37 @@
         <link href="style.css" rel="stylesheet">
     </head>
     <body>
-        <div>
+    <div class="main_view">
+
+        <div class="container-leyenda">
+            <!-- En esta seccion la leyenda -->
+            <!-- Interaccion con D3 para modificar HTML aqui -->
+        </div>
+        <div class="filters-container">
             <label for="color_scheme">
-                Coloreado por:
+                <h3>Coloreado por:</h3>
             </label>
             <select id="color_scheme" onchange="changeSelection(event)">
                 <option value="platform">Plataforma</option>
                 <option value="developer">Desarrollador</option>
             </select>
+            <br>
+            <label>
+                <h3>Búsqueda:</h3>
+                <input type="search" id="search">
+            </label>
+
         </div>
-        <div class="container" id="div-leyenda">
-            <!-- En esta seccion la leyenda -->
-            <!-- Interaccion con D3 para modificar HTML aqui -->
+        <div class="filters-container">
+            <h3>Filtros</h3>
+            <div id="filters">
+                <!-- Aquí se generarán los checkboxes dinámicamente -->
+            </div>
         </div>
-        <script src="visualizacion.js"></script>
+
+    </div>
+
+    <script src="visualizacion.js"></script>
+
     </body>
 </html>
\ No newline at end of file
diff --git a/src/visualizacion.js b/src/visualizacion.js
index 719f154..f6c74a3 100644
--- a/src/visualizacion.js
+++ b/src/visualizacion.js
@@ -50,60 +50,82 @@ const colores = {
 }
 
 
-
-
-
-
 let selection = "platform";
+let scatter;
 
 let elegibleData;
 
 let xScale;
+let radiusScale;
 let yScale;
 let xAxis;
 let yAxis;
-
-let radiusScale;
+let transform;
 
 let min_x = 0;
 let min_y = 0;
 let max_x = 10;
 let max_y = 10;
 
-const width = 1000;
-const height = 500;
+const width = 800;
+const height = 400;
 const margin_x = 62;
 const margin_y = 60;
 const right_margin = 300;
-const rectAparitionTime = 300;
+const rectAparitionTime = 150;
 const radiusTransition = 300;
+const maxBubbleCount = 50;
+
+
+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('body').append('svg')
-    .attr("width", width + 2 * margin_x).attr("height", height + 2 * margin_y)
-    .append('g').attr("transform", `translate(${margin_x},${margin_y})`);
+svg.append('defs').append('clipPath')
+    .attr("id", "content-clip")
+    .append('rect')
+    .attr('width', width - right_margin)
+    .attr('height', height)
+    .attr('x', margin_x)
+    .attr('y', margin_y);
 
+const contentGroup = svg.append('g')
+    .attr('clip-path', 'url(#content-clip)');
 
+const translate_group = svg.append('g').attr("transform", `translate(${margin_x},${margin_y})`);
 
 function createAxis() {
     xAxis = d3.axisBottom(xScale).ticks(10);
     yAxis = d3.axisLeft(yScale).ticks(10);
 
-    svg.append("g")
+    translate_group.append("g")
+        .attr("class", "title")
+        .attr("transform", "translate(10, -20)")
+        .append('text')
+        .style("font-family", "Georgia, serif")
+        .style("font-size", "26px")
+        .style("font-weight", "bold")
+        .text("Críticas en los Videojuegos: Usuarios y Expertos")
+
+
+    translate_group.append("g")
         .attr("class", "xAxis")
         .attr("transform", `translate(0,${height})`)
         .call(xAxis);
-    svg.append('text')
+
+    translate_group.append('text')
         .attr('class', 'xAxisLabel')
-        .attr('x', width / 5)
+        .attr('x', width / 10)
         .attr('y', height + 40)
         .style('font-weight', 'bold')
         .style('font-size', '20px')
         .style('font-family', 'Georgia, serif')
         .text("Calificación dada por usuarios")
-    svg.append("g")
+
+    translate_group.append("g")
         .attr("class", "yAxis")
         .call(yAxis);
-    svg.append('text')
+
+    translate_group.append('text')
         .attr('class', 'yAxisLabel')
         .attr('x', -height*4/5)
         .attr('y', -30)
@@ -112,137 +134,251 @@ function createAxis() {
         .style('font-family', 'Georgia, serif')
         .attr('transform', 'rotate(-90)')
         .text("Calificación dada por expertos")
+
+    contentGroup.selectAll("line.horizontal-grid")
+        .data(yScale.ticks(10))
+        .enter()
+        .append("line")
+        .attr("class", "horizontal-grid")
+        .attr("x1", 0)
+        .attr("x2", width)
+        .attr("y1", d => yScale(d) + margin_y)
+        .attr("y2", d => yScale(d) + margin_y)
+        .attr("stroke", "gray")
+        .attr("stroke-width", 0.5);
+
+    contentGroup.selectAll("line.vertical-grid")
+        .data(xScale.ticks(10))
+        .enter()
+        .append("line")
+        .attr("class", "vertical-grid")
+        .attr("x1", d => xScale(d) + margin_x)
+        .attr("x2", d => xScale(d) + margin_x)
+        .attr("y1", 0)
+        .attr("y2", height + margin_y)
+        .attr("stroke", "gray")
+        .attr("stroke-width", 0.5);
+
+    translate_group.append('defs').append('clipPath')
+        .attr("id", "clip")
+        .append('rect')
+        .attr('width', width)
+        .attr('height', height)
+        .attr('x', 0)
+        .attr('y', 0);
+
+    scatter = contentGroup.append('g');
 }
 
 d3.csv('files/games-data.csv').then((data) => {
 
     data.forEach(elem => {
         elem["user score"] = +elem["user score"];
-        if(!elem["user score"]) elem["critics"] = -14;
-        elem["score"] = +elem["score"] / 10;
+        if(!elem["user score"]) elem.critics = -14;
+        // Datos que no se puedan representar se eliminan del gráfico así
+        elem.score = +elem.score / 10;
         // Los datos estan entre 0 y 100, asi que normalizo a entre 0 y 10
-        elem["critics"] = +elem["critics"];
-        elem["users"] = +elem["users"];
+        elem.critics = +elem.critics;
+        elem.users = +elem.users;
         elem.genre = [...new Set(elem.genre.split(','))];
-        elem.id = elem.name.replaceAll(/[ :'"()\[\]]*/g, '');
+        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]);
 
     elegibleData = data.sort((a, b) => {
         return (b["critics"] + b["users"]) - (a["critics"] + a["users"])
-    }).slice(0, 50);
+    }).slice(0, maxBubbleCount);
 
+    generateFilters(data);
+    enableSearch(data);
     createAxis();
     createBubbles(elegibleData);
-
 });
 
-function handleMouseOver(event, elem) {
+function handleMouseOver(event, d) {
+    transform = d3.zoomTransform(svg.node()); // Obtiene la transformación actual (zoom y traslación)
+
     d3.select(this)
         .transition()
         .duration(radiusTransition)
         .ease(d3.easeQuadInOut)
-        .attr("r", radiusScale(elem.critics) * 1.4)
-    const rectGroup = svg.append('g')
-        .attr('class', 'rects')
-
-    const textGroup = svg.append('g')
-        .attr('class', 'texts')
-
-    const text = textGroup.append('text')
-        .attr('id', `text${elem.id}-${elem.platform}`)
-        .attr('class', 'videogame_title')
-        .attr('x', () => xScale(elem["user score"]) + 35)
-        .attr('y', () => yScale(elem["score"]) - 5)
-        .attr('font-family', 'Georgia, serif')
-        .attr('font-weight', 'bold')
-        .attr('font-size', '15px')
-        .attr('fill', 'black')
-        .attr('opacity', 0)
+        .attr("r", radiusScale(d.critics) * 1.4)
+
+    // Calcula las posiciones del círculo transformadas
+    const cx = transform.applyX(xScale(d["user score"]));
+    const cy = transform.applyY(yScale(d["score"]));
+
+    // Define la distancia visual fija para el tooltip
+    const visualOffsetX = 70; // Distancia horizontal fija en píxeles
+    const visualOffsetY = 55; // Distancia vertical fija en píxeles
+
+    // Crea el grupo del tooltip
+    const rects = svg.append("g").attr("class", "rects");
+    const texts = svg.append("g").attr("class", "texts");
+
+    // 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")
+        .style("font-family", "Georgia, serif")
+        .style("fill", "black")
+
+    text.append("tspan")
+        .text(`${d.name}`)
+        .style("font-size", "18px")
+        .style("font-weight", "bold")
+        .attr("x", cx + visualOffsetX + 10)
+        .attr("dy", "1.2em")
+        .style("opacity", 0)
         .transition()
         .duration(rectAparitionTime)
-        .ease(d3.easeQuadInOut)
-        .attr('opacity', 1)
-
-    const lines = [
-        elem.name,
-        `Generos: ${elem.genre}`,
-        `Fecha de lanzamiento: ${elem["r-date"]}`
-    ];
-    svg.select(`#text${elem.id}-${elem.platform}`).selectAll('tspan')
-        .data(lines)
-        .enter()
-        .append('tspan')
-        .attr('x', () => xScale(elem["user score"]) + 35)
-        .attr('dy', (d, i) => (i === 0 ? 0 : 20))
-        .text(d => d);
-
+        .ease(d3.easeQuadOut)
+        .style("opacity", 1)
+
+    text.append("tspan")
+        .text(`Géneros: ${d.genre.join(", ")}`)
+        .attr("x", cx + visualOffsetX + 10)
+        .attr("dy", "1.2em")
+        .style("opacity", 0)
+        .transition()
+        .duration(rectAparitionTime)
+        .ease(d3.easeQuadOut)
+        .style("opacity", 1)
+
+    text.append("tspan")
+        .text(`Fecha: ${d["r-date"]}`)
+        .attr("x", cx + visualOffsetX + 10)
+        .attr("dy", "1.2em")
+        .style("opacity", 0)
+        .transition()
+        .duration(rectAparitionTime)
+        .ease(d3.easeQuadOut)
+        .style("opacity", 1)
+
+    text.append("tspan")
+        .text(`Desarrolladores: ${d.developer}`)
+        .attr("x", cx + visualOffsetX + 10)
+        .attr("dy", "1.2em")
+        .style("opacity", 0)
+        .transition()
+        .duration(rectAparitionTime)
+        .ease(d3.easeQuadOut)
+        .style("opacity", 1)
+
+    text.append("tspan")
+        .text(`Consola: ${d.platform}`)
+        .attr("x", cx + visualOffsetX + 10)
+        .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();
 
-    rectGroup.append("rect")
-        .attr('id', `rect${elem.id}-${elem.platform}`)
-        .attr("x", bbox.x - 5)
+    rects.insert("rect")
+        .attr("x", bbox.x - 10) // Deja un margen alrededor del texto
         .attr("y", bbox.y - 5)
-        .attr("width", bbox.width + 10)
+        .attr("width", bbox.width + 20)
         .attr("height", bbox.height + 10)
-        .attr('stroke', 'black')
-        .attr('stroke-width', 1)
-        .attr('rx', 4)
-        .attr('ry', 4)
-        .attr('opacity', 0)
-        .attr("fill", () => colores[selection][elem[selection]])
+        .attr("fill", () => {
+            if (colores[selection][d[selection]]){
+                return colores[selection][d[selection]];
+            } else return "#aaaaaa";
+        })
+        .attr("rx", 5)
+        .attr("ry", 5)
+        .style("opacity", 0)
+        .transition()
+        .duration(rectAparitionTime)
+        .ease(d3.easeQuadOut)
+        .style("opacity", 0.9)
+
+}
+
+function handleMouseOut(event, d) {
+    svg.selectAll(".rects")
         .transition()
         .duration(rectAparitionTime)
+        .ease(d3.easeQuadOut)
+        .style("opacity", 0)
+        .remove();
+    svg.selectAll(".texts").remove();
+
+    d3.select(this)
+        .transition()
+        .duration(radiusTransition/2)
         .ease(d3.easeQuadInOut)
-        .attr('opacity', 0.85);
+        .attr("r", radiusScale(d.critics))
 
+}
 
-    svg.select(`#rect${elem.id}-${elem.platform}`).lower();
+function zoomed(event) {
+    transform = event.transform;
+    if (transform.k < 1) {
+        transform.k = 1.1
+        return;
+    }
 
+    const newXScale = transform.rescaleX(xScale);
+    const 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])]);
 
 
-}
 
-function handleMouseOut(event, elem) {
-    d3.select(this)
-        .transition()
-        .duration(radiusTransition)
-        .ease(d3.easeQuadInOut)
-        .attr('r', d => radiusScale(d.critics));
-    d3.select(`#text${elem.id}-${elem.platform}`)
-        .transition()
-        .duration(rectAparitionTime/2)
-        .ease(d3.easeQuadInOut)
-        .style('opacity', 0)
-        .remove();
+    translate_group.select(".xAxis").call(d3.axisBottom(newXScale));
+    translate_group.select(".yAxis").call(d3.axisLeft(newYScale));
 
-    d3.select(`#rect${elem.id}-${elem.platform}`)
-        .transition()
-        .duration(rectAparitionTime/2)
-        .ease(d3.easeQuadInOut)
-        .style('opacity', 0)
-        .remove();
+    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)
+
+    scatter.select('.bubbles').selectAll('circle')
+        .attr('cx', d => newXScale(d["user score"]) + margin_x)
+        .attr('cy', d => newYScale(d["score"]) + margin_y)
 
 
 }
 
 function createBubbles(data) {
-    const bubbleGroup = svg.append('g')
+    const bubbleGroup = scatter.append('g')
         .attr('class', 'bubbles');
 
-
+    const zoom = d3.zoom()
+        .scaleExtent([1, 10])
+        .translateExtent([[0, 0], [width + 2*margin_x, height + 2*margin_y]])
+        .on("zoom", zoomed)
+        .filter((event) => event.type === "mousedown" || event.type === "mousemove" || event.type === "wheel");
 
     bubbleGroup.selectAll("circle")
         .data(data)
         .enter()
         .append("circle")
-        .attr("cx", d => xScale(d["user score"]))
-        .attr("cy", d => yScale(d["score"]))
+        .attr("cx", d => xScale(d["user score"]) + margin_x)
+        .attr("cy", d => yScale(d["score"]) + margin_y)
         .attr("r",  d => radiusScale(d["critics"]))
         .attr("fill", d => {
             if (colores[selection][d[selection]]){
@@ -255,11 +391,12 @@ function createBubbles(data) {
         .on("mouseover", handleMouseOver)
         .on("mouseout", handleMouseOut)
 
+    svg.call(zoom);
 
 }
 
 function removeBubbles() {
-    svg.selectAll('circle').remove()
+    d3.select(".bubbles").remove();
 }
 
 function changeSelection (event) {
@@ -267,3 +404,73 @@ function changeSelection (event) {
     selection = event.target.value;
     createBubbles(elegibleData);
 }
+
+function generateFilters(data) {
+    const filtersDiv = d3.select("#filters");
+    filtersDiv.html(""); // Limpia los filtros existentes
+
+    // Obtiene valores únicos para el filtro seleccionado
+    // Genera un checkbox para cada valor único
+    const generos = [
+        "Fantasy",
+        "Sports",
+        "Open-World",
+        "Action",
+        "Shooter",
+        "Arcade",
+        "2D",
+        "3D",
+        "Rhythm",
+        "Team",
+        "Roguelike"
+    ];
+
+    generos.forEach(value => {
+        const filter = filtersDiv.append("div").attr("class", "filter");
+
+        filter.append("input")
+            .attr("type", "checkbox")
+            .attr("id", `filter-${value}`)
+            .attr("value", value)
+            .on("change", () => applyFilters(data)); // Aplica los filtros al cambiar
+
+        filter.append("label")
+            .attr("for", `filter-${value}`)
+            .text(value);
+    });
+}
+
+function applyFilters(data) {
+    let selectedFilters = d3.selectAll("#filters input[type='checkbox']")
+        .nodes()
+        .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) {
+            if (!d.genre.includes(filter)) return false;
+        }
+        return true;
+    }).splice(0, maxBubbleCount);
+    // Actualiza la visualización con los datos filtrados
+    removeBubbles();
+    createBubbles(elegibleData);
+}
+
+function enableSearch(data){
+    d3.select("#search")
+        .on("input", search)
+
+    function search(event) {
+        const keyName = event.target.value.toLowerCase();
+        removeBubbles();
+        elegibleData = data.filter(d => d.name.toLowerCase().includes(keyName)).splice(0, maxBubbleCount);
+
+        createBubbles(elegibleData);
+    }
+
+}
+
-- 
GitLab