From a6a65a6e9cf091600ed50a91753df6921b9138c4 Mon Sep 17 00:00:00 2001
From: Leander <leander.gerwing@gmail.com>
Date: Sun, 28 Jul 2024 02:42:30 +0200
Subject: [PATCH] wip: try to fix issue where nodes are assigned to the wrong
 group

---
 index.html                |  3 +--
 src/charts/bubbleChart.ts | 49 +++++++++++++++++++++++++++++----------
 src/main.ts               |  3 ++-
 src/styles/index.scss     | 26 +++++++++++++++------
 4 files changed, 59 insertions(+), 22 deletions(-)

diff --git a/index.html b/index.html
index ffd8516..5e671d1 100644
--- a/index.html
+++ b/index.html
@@ -23,10 +23,9 @@
                 <label for="group-by-setting">Gruppieren nach:</label>
 
                 <select id="group-by-setting" name="group-by-setting">
-                    <option value="default">Keine</option>
+                    <option value="team" selected>Team</option>
                     <option value="nation">Nation</option>
                     <option value="pos">Position</option>
-                    <option value="team">Team</option>
                     <option value="league">Liga</option>
                 </select>
             </div>
diff --git a/src/charts/bubbleChart.ts b/src/charts/bubbleChart.ts
index 8eab46d..30b11f0 100644
--- a/src/charts/bubbleChart.ts
+++ b/src/charts/bubbleChart.ts
@@ -1,5 +1,5 @@
 import * as d3 from "d3";
-import {HierarchyNode} from "d3";
+import {HierarchyNode, NumberValue, ScaleOrdinal, ScaleSequential} from "d3";
 import {ChartConfig, ChartConfigParam} from "@/charts/chart.ts";
 import SearchableChart from "@/charts/SearchableChart.ts";
 
@@ -7,7 +7,7 @@ import SearchableChart from "@/charts/SearchableChart.ts";
 export type BubbleChartConfig = ChartConfig & {
     groupAccessor: (d: any) => string,
     sizeAccessor: (d: any) => number,
-    colorAccessor: (d: any) => number | null,
+    colorAccessor: (d: any) => string | number | null,
     zoomExtent: [number, number],
     onZoom?: (event: any) => void
 }
@@ -21,6 +21,7 @@ export default class BubbleChart extends SearchableChart {
     config: BubbleChartConfig
     packRoot: HierarchyNode<any> | null = null
     highlightedNode: HierarchyNode<any> | null = null
+    colorScale: ScaleSequential<any> | ScaleOrdinal<any, any> | null = null
 
     constructor(data: any[], _config: BubbleChartConfigParam) {
         super(data, _config as ChartConfigParam)
@@ -80,6 +81,8 @@ export default class BubbleChart extends SearchableChart {
             });
 
         vis.chart.call(vis.zoom)
+
+        vis.updateColorScale()
     }
 
     updateVis(data: any[]): void {
@@ -100,14 +103,20 @@ export default class BubbleChart extends SearchableChart {
         const node = vis.chart
             .selectAll("g")
             .data(vis.packRoot?.descendants())
-            .transition()
-            .duration(1000)
-            .attr("transform", (d: any) => `translate(${d.x},${d.y})`)
+            .join(
+                (enter: any) => enter.append("g"),
+                (update: any) => update
+                    .transition()
+                    .duration(1000)
+                    .attr("transform", (d: any) => `translate(${d.x},${d.y})`),
+                (exit: any) => exit.remove()
+            )
 
         node.selectAll('circle')
             .transition()
             .duration(1000)
             .attr('fill', (d: any) => vis.getFillForNode(d))
+            .attr('r', (d: any) => d.r)
     }
 
     renderVis(): void {
@@ -122,14 +131,13 @@ export default class BubbleChart extends SearchableChart {
             .attr("transform", (d: any) => `translate(${d.x},${d.y})`)
 
         node
+            .filter((d: any) => !d.children)
             .append('circle')
             .attr('fill', (d: any) => vis.getFillForNode(d))
             .attr('stroke', 'none')
             .attr('stroke-width', 2)
             .attr('r', (d: any) => d.r)
-            .attr('data-leaf', (d: any) => d.children ? 'false' : 'true')
             .attr('data-player', (d: any) => d.data.player)
-        node.selectAll('circle[data-leaf="true"]')
             .on('mouseover', (_: Event, d: any) => {
                 vis.renderTooltip(d.data)
             })
@@ -138,8 +146,7 @@ export default class BubbleChart extends SearchableChart {
                     .style('left', (event.layerX + vis.config.tooltipPadding) + 'px')
                     .style('top', (event.layerY + vis.config.tooltipPadding) + 'px')
             })
-            .on('mouseleave', (_: Event, d: any) => {
-                console.log(d.data)
+            .on('mouseleave', (_: Event) => {
                 d3.select('#tooltip').style('display', 'none');
             })
     }
@@ -168,10 +175,9 @@ export default class BubbleChart extends SearchableChart {
             this.data,
             (d: any) => this.config.groupAccessor(d)
         )
-        console.log(groupedData)
         this.packRoot = d3.pack()
             .size([this.width(), this.height()])
-            .padding(4)
+            .padding(2)
             (
                 (d3.hierarchy(groupedData) as HierarchyNode<any>)
                     .sum((d: any) => this.config.sizeAccessor(d))
@@ -180,7 +186,8 @@ export default class BubbleChart extends SearchableChart {
 
     private getFillForNode(node: HierarchyNode<any>) {
         if (node.children) return "transparent"
-        return this.config.colorAccessor(node) ?? 'var(--primary)'
+        if (!this.colorScale) return 'var(--primary)'
+        return this.colorScale(this.config.colorAccessor(node) as NumberValue)
     }
 
     search(input: string): void {
@@ -193,6 +200,24 @@ export default class BubbleChart extends SearchableChart {
         }
     }
 
+    updateColorScale() {
+        // The color scale is either a sequential scale or an ordinal scale
+        // depending on what type the colorAccessor returns
+        const sampleValue = this.config.colorAccessor(this.data[0])
+        if (!sampleValue) return () => 'var(--primary)'
+        if (typeof sampleValue === 'number') {
+            this.colorScale = d3.scaleSequential(d3.interpolateBlues)
+                .domain(
+                    d3.extent(
+                        this.data,
+                        this.config.colorAccessor as (d: any) => number | null
+                    ) as [number, number]
+                )
+        } else {
+            this.colorScale = d3.scaleOrdinal(d3.schemeCategory10)
+        }
+    }
+
     get zoomLevel() {
         return this.zoom.scale()
     }
diff --git a/src/main.ts b/src/main.ts
index 8cf09e2..3f78f39 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -37,7 +37,8 @@ dsv(';', 'data/output.csv').then(data => {
         onZoom: (event) => {
             if (sliderBlocked) return
             zoomSlider.value = event.transform.k.toString()
-        }
+        },
+        groupAccessor: (d: any) => d['team'],
     })
 
     bubbleChart.renderVis()
diff --git a/src/styles/index.scss b/src/styles/index.scss
index 063909e..1b73854 100644
--- a/src/styles/index.scss
+++ b/src/styles/index.scss
@@ -77,25 +77,37 @@ h1 {
   display: flex;
   gap: 1rem;
 
+
   .left, .right {
     position: relative;
     flex: 1;
     text-align: center;
+  }
+
+  .left {
+    flex: 2;
+
+    #bubble-chart-wrapper {
+      position: relative;
+    }
 
-    #zoom-wrapper {
+    .bubble-chart-settings {
       position: absolute;
       bottom: 1rem;
       right: 0;
       width: 400px;
       display: flex;
-      align-items: center;
+      flex-direction: column;
+      align-items: flex-end;
       justify-content: flex-end;
       gap: 1rem;
-    }
-  }
 
-  .left {
-    flex: 2;
+      > div {
+        align-items: center;
+        justify-content: flex-end;
+        gap: 1rem;
+      }
+    }
   }
 
   .right {
@@ -118,7 +130,7 @@ button {
   }
 }
 
-input[type="text"] {
+select, input[type="text"] {
   border-radius: .5rem;
   box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
   padding: 0.5rem;
-- 
GitLab