Skip to content
Snippets Groups Projects
Commit 74225f64 authored by Leander's avatar Leander
Browse files

feat: size by selection

parent 557ef3e3
Branches
No related tags found
No related merge requests found
...@@ -19,6 +19,13 @@ ...@@ -19,6 +19,13 @@
</div> </div>
<div class="bubble-chart-settings"> <div class="bubble-chart-settings">
<div class="size-by">
<label for="group-by-setting">Größe nach:</label>
<select id="size-by-setting" name="">
<option value="default" selected>---</option>
</select>
</div>
<div class="color-by"> <div class="color-by">
<label for="group-by-setting">Einfärben nach:</label> <label for="group-by-setting">Einfärben nach:</label>
......
import * as d3 from "d3"; import * as d3 from "d3";
import {HierarchyNode, NumberValue, ScaleOrdinal, ScaleSequential} from "d3"; import {HierarchyNode, NumberValue, ScaleLinear, ScaleOrdinal, ScaleSequential} from "d3";
import {ChartConfig, ChartConfigParam} from "@/charts/chart.ts"; import {ChartConfig, ChartConfigParam} from "@/charts/chart.ts";
import SearchableChart from "@/charts/SearchableChart.ts"; import SearchableChart from "@/charts/SearchableChart.ts";
...@@ -22,6 +22,7 @@ export default class BubbleChart extends SearchableChart { ...@@ -22,6 +22,7 @@ export default class BubbleChart extends SearchableChart {
packRoot: HierarchyNode<any> | null = null packRoot: HierarchyNode<any> | null = null
highlightedNode: HierarchyNode<any> | null = null highlightedNode: HierarchyNode<any> | null = null
colorScale: ScaleSequential<any> | ScaleOrdinal<any, any> | null = null colorScale: ScaleSequential<any> | ScaleOrdinal<any, any> | null = null
sizeScale: ScaleLinear<any, number> | null = null
constructor(data: any[], _config: BubbleChartConfigParam) { constructor(data: any[], _config: BubbleChartConfigParam) {
super(data, _config as ChartConfigParam) super(data, _config as ChartConfigParam)
...@@ -76,6 +77,7 @@ export default class BubbleChart extends SearchableChart { ...@@ -76,6 +77,7 @@ export default class BubbleChart extends SearchableChart {
vis.chart.call(vis.zoom) vis.chart.call(vis.zoom)
vis.updateColorScale() vis.updateColorScale()
vis.updateSizeScale()
} }
updateVis(data: any[]): void { updateVis(data: any[]): void {
...@@ -91,6 +93,7 @@ export default class BubbleChart extends SearchableChart { ...@@ -91,6 +93,7 @@ export default class BubbleChart extends SearchableChart {
private update() { private update() {
let vis: BubbleChart = this; let vis: BubbleChart = this;
vis.updateSizeScale()
vis.updatePackRoot() vis.updatePackRoot()
vis.updateColorScale() vis.updateColorScale()
...@@ -129,7 +132,7 @@ export default class BubbleChart extends SearchableChart { ...@@ -129,7 +132,7 @@ export default class BubbleChart extends SearchableChart {
.append('circle') .append('circle')
.attr('fill', (d: any) => vis.getFillForNode(d)) .attr('fill', (d: any) => vis.getFillForNode(d))
.attr('r', (d: any) => d.r) .attr('r', (d: any) => d.r)
.attr('data-player', (d: any) => d.data.player) .attr('data-bubble', (d: any) => d.data.player) // TODO make label accessor
.on('mouseover', (_: Event, d: any) => { .on('mouseover', (_: Event, d: any) => {
const element = d3.select('#tooltip') const element = d3.select('#tooltip')
.style('display', 'block') .style('display', 'block')
...@@ -161,10 +164,9 @@ export default class BubbleChart extends SearchableChart { ...@@ -161,10 +164,9 @@ export default class BubbleChart extends SearchableChart {
.padding(2) .padding(2)
( (
(d3.hierarchy(groupedData) as HierarchyNode<any>) (d3.hierarchy(groupedData) as HierarchyNode<any>)
.sum((d: any) => this.config.sizeAccessor(d)) .sum((d: any) => this.getSizeForNode(d))
.sort((d: any) => this.config.sizeAccessor(d)) .sort((d: any) => this.getSizeForNode(d))
) )
} }
private getFillForNode(node: HierarchyNode<any>) { private getFillForNode(node: HierarchyNode<any>) {
...@@ -173,6 +175,12 @@ export default class BubbleChart extends SearchableChart { ...@@ -173,6 +175,12 @@ export default class BubbleChart extends SearchableChart {
return this.colorScale(this.config.colorAccessor(node.data) as NumberValue) return this.colorScale(this.config.colorAccessor(node.data) as NumberValue)
} }
private getSizeForNode(dataPoint: HierarchyNode<any>) : number {
if (!dataPoint || !this.config.sizeAccessor(dataPoint)) return 0.1
if (!this.sizeScale) return 5
return this.sizeScale(this.config.sizeAccessor(dataPoint) as NumberValue)
}
search(input: string): void { search(input: string): void {
if (!input) return if (!input) return
if (!input || !this.packRoot) return if (!input || !this.packRoot) return
...@@ -204,6 +212,16 @@ export default class BubbleChart extends SearchableChart { ...@@ -204,6 +212,16 @@ export default class BubbleChart extends SearchableChart {
} }
} }
updateSizeScale() {
this.sizeScale = d3.scaleLinear(
d3.extent(
this.data,
this.config.sizeAccessor as (d: any) => number | null
) as [number, number],
[0,10]
)
}
get zoomLevel() { get zoomLevel() {
return this.zoom.scale() return this.zoom.scale()
} }
...@@ -227,12 +245,12 @@ export default class BubbleChart extends SearchableChart { ...@@ -227,12 +245,12 @@ export default class BubbleChart extends SearchableChart {
zoomToPoint(dataPoint: any) { zoomToPoint(dataPoint: any) {
if (dataPoint) { if (dataPoint) {
if (this.highlightedNode) { if (this.highlightedNode) {
d3.select(`circle[data-player="${this.highlightedNode.data.player}"]`) d3.select(`circle[data-bubble="${this.highlightedNode.data.player}"]`)
.transition() .transition()
.duration(700) .duration(700)
.attr('fill', (d: any) => this.getFillForNode(d)) .attr('fill', (d: any) => this.getFillForNode(d))
} }
d3.select(`circle[data-player="${dataPoint.data.player}"]`) d3.select(`circle[data-bubble="${dataPoint.data.player}"]`)
.transition() .transition()
.duration(700) .duration(700)
.attr('fill', 'var(--secondary)') .attr('fill', 'var(--secondary)')
......
...@@ -3,7 +3,7 @@ import {dsv} from "d3-fetch"; ...@@ -3,7 +3,7 @@ import {dsv} from "d3-fetch";
import BubbleChart, {BubbleChartConfigParam} from "@/charts/bubbleChart.ts"; import BubbleChart, {BubbleChartConfigParam} from "@/charts/bubbleChart.ts";
import Search from "@/search.ts"; import Search from "@/search.ts";
import {countries} from "@/countries.ts"; import {countries} from "@/countries.ts";
import {Player} from "@/player.ts"; import {numericColumns, Player} from "@/player.ts";
// const radarChartWrapper = document.querySelector('#radar-chart-wrapper') as HTMLDivElement; // const radarChartWrapper = document.querySelector('#radar-chart-wrapper') as HTMLDivElement;
const bubbleChartWrapper = document.querySelector('#bubble-chart-wrapper') as HTMLDivElement; const bubbleChartWrapper = document.querySelector('#bubble-chart-wrapper') as HTMLDivElement;
...@@ -30,17 +30,36 @@ groupBySetting.oninput = (event) => { ...@@ -30,17 +30,36 @@ groupBySetting.oninput = (event) => {
} }
const colorBySetting = document.querySelector('#color-by-setting') as HTMLInputElement const colorBySetting = document.querySelector('#color-by-setting') as HTMLInputElement
colorBySetting.oninput = (event) => { colorBySetting.oninput = (event) => {
if (bubbleChart) { if (bubbleChart) {
bubbleChart.updateVisConfig({colorAccessor: (d: any) => d[event.target?.value]} as BubbleChartConfigParam) bubbleChart.updateVisConfig({colorAccessor: (d: any) => d[event.target?.value]} as BubbleChartConfigParam)
} }
} }
const sizeBySetting = document.querySelector('#size-by-setting') as HTMLInputElement
for (const column of Object.keys(numericColumns)) {
const option = document.createElement('option')
option.value = column
option.text = numericColumns[column as keyof typeof numericColumns]
sizeBySetting.appendChild(option)
}
sizeBySetting.oninput = (event) => {
if (bubbleChart) {
bubbleChart.updateVisConfig({sizeAccessor: (d: any) => d[event.target?.value] ?? 0} as BubbleChartConfigParam)
}
}
dsv(';', 'data/output.csv').then(data => { dsv(';', 'data/output.csv').then(data => {
const parsedData = data.map((d: any) => ({ const parsedData = data.map((d: any) => {
for (const column of Object.keys(numericColumns)) {
d[column] = parseFloat(d[column])
}
return {
...d, ...d,
nation: countries.find(c => c.code === d.nation)?.name ?? d.nation, nation: countries.find(c => c.code === d.nation)?.name ?? d.nation,
} as Player) } as Player
}
) )
bubbleChart = new BubbleChart(parsedData, { bubbleChart = new BubbleChart(parsedData, {
......
export type Player = { /*
league: string;
season: string; "league": "ARG-Primera-Division",
team: string; "season": "2324",
player: string; "team": "Arg Juniors",
nation: string; "player": "Alan Rodríguez",
pos: string; "nation": "",
age: string; "pos": "",
born: string; "age": "",
playingTime: number; "born": "",
performance: number; "Playing Time": "",
expected_0: number; "Performance": "",
progression: number; "Expected_0": "",
per90Minutes: number; "Progression": "",
standard: number; "Per 90 Minutes": "",
expected_1: number; "Standard": "0",
total: number; "Expected_1": "-1.7",
short: number; "Total": "1355",
medium: number; "Short": "84.1",
long: number; "Medium": "77.6",
ast: number; "Long": "43.3",
xAG: number; "Ast": "0",
expected: number; "xAG": "0.8",
KP: number; "Expected": "",
'1/3': number; "KP": "11",
PPA: number; "1/3": "27",
CrsPA: number; "PPA": "8",
PrgP: number; "CrsPA": "2",
SCA: number; "PrgP": "44",
GCA: number; "SCA": "2.16",
tackles: number; "GCA": "0.23",
blocks: number; "Tackles": "25",
Int: number; "Blocks": "7",
TklInt: number; "Int": "11",
Clr: number; "Tkl+Int": "47",
Err: number; "Clr": "7",
touches: number; "Err": "0",
penaltyKicks: number; "Touches": "677",
expected_2: number; "Penalty Kicks":
*/
export type Player = { //create type based on comment above
"league": string,
"season": string,
"team": string,
"player": string,
"nation": string,
"pos": string,
"age": string,
"born": string,
"Playing Time": number,
"Performance": number,
"Expected_0": number,
"Progression": number,
"Per 90 Minutes": number,
"Standard": number,
"Expected_1": number,
"Total": number,
"Short": number,
"Medium": number,
"Long": number,
"Ast": number,
"xAG": number,
"Expected": number,
"KP": number,
"1/3": number,
"PPA": number,
"CrsPA": number,
"PrgP": number,
"SCA": number,
"GCA": number,
"Tackles": number,
"Blocks": number,
"Int": number,
"Tkl+Int": number,
"Clr": number,
"Err": number,
"Touches": number,
"Penalty Kicks": number,
}
export const numericColumns = {
"Playing Time": "Spielzeit",
'Progression': 'Progression',
'Per 90 Minutes': 'Pro 90 Minuten',
'Standard': 'Standard',
'Expected_1': 'Erwartet',
'Total': 'Gesamt',
'Short': 'Kurze Pässe',
'Medium': 'Mittellange Pässe',
'Long': 'Lange Pässe',
'Ast': 'Vorlagen',
'xAG': 'erwartete Assists',
'KP': 'KP',
'1/3': 'Pässe ins letzte Drittel',
'PPA': 'PPA',
'CrsPA': 'CrsPA',
'PrgP': 'PrgP',
'SCA': 'Schusserzeugende Aktionen',
'GCA': 'Torerzeugende Aktionen',
'Tackles': 'Tackles',
'Blocks': 'Blocks',
'Int': 'Interceptions',
'Tkl+Int': 'Tackles + Interceptions',
'Clr': 'Klärungen',
'Err': 'defensive Fehler',
'Touches': 'Ballberührungen',
'Penalty Kicks': 'Elfmeter'
} }
...@@ -72,6 +72,7 @@ h1 { ...@@ -72,6 +72,7 @@ h1 {
border: 1px solid #ccc; border: 1px solid #ccc;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
user-select: none; user-select: none;
z-index: 10;
} }
#app { #app {
...@@ -94,7 +95,7 @@ h1 { ...@@ -94,7 +95,7 @@ h1 {
.bubble-chart-settings { .bubble-chart-settings {
position: absolute; position: absolute;
bottom: 1rem; top: 100%;
right: 0; right: 0;
display: flex; display: flex;
padding: 1rem; padding: 1rem;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment