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