diff --git a/index.html b/index.html index 5959f1e00fb7cc3952120c669b3ae39510b0e57c..4e879185a9b7addaacfda6b5efd9dbbb807178a5 100644 --- a/index.html +++ b/index.html @@ -19,6 +19,13 @@ </div> <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"> <label for="group-by-setting">Einfärben nach:</label> diff --git a/src/charts/bubbleChart.ts b/src/charts/bubbleChart.ts index 6f6509e96b76e09e67bf5e72d53cc4024d332f66..d0245276846bd9206a8faab5070533447e36f849 100644 --- a/src/charts/bubbleChart.ts +++ b/src/charts/bubbleChart.ts @@ -1,5 +1,5 @@ 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 SearchableChart from "@/charts/SearchableChart.ts"; @@ -22,6 +22,7 @@ export default class BubbleChart extends SearchableChart { packRoot: HierarchyNode<any> | null = null highlightedNode: HierarchyNode<any> | null = null colorScale: ScaleSequential<any> | ScaleOrdinal<any, any> | null = null + sizeScale: ScaleLinear<any, number> | null = null constructor(data: any[], _config: BubbleChartConfigParam) { super(data, _config as ChartConfigParam) @@ -76,6 +77,7 @@ export default class BubbleChart extends SearchableChart { vis.chart.call(vis.zoom) vis.updateColorScale() + vis.updateSizeScale() } updateVis(data: any[]): void { @@ -91,6 +93,7 @@ export default class BubbleChart extends SearchableChart { private update() { let vis: BubbleChart = this; + vis.updateSizeScale() vis.updatePackRoot() vis.updateColorScale() @@ -129,7 +132,7 @@ export default class BubbleChart extends SearchableChart { .append('circle') .attr('fill', (d: any) => vis.getFillForNode(d)) .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) => { const element = d3.select('#tooltip') .style('display', 'block') @@ -161,10 +164,9 @@ export default class BubbleChart extends SearchableChart { .padding(2) ( (d3.hierarchy(groupedData) as HierarchyNode<any>) - .sum((d: any) => this.config.sizeAccessor(d)) - .sort((d: any) => this.config.sizeAccessor(d)) + .sum((d: any) => this.getSizeForNode(d)) + .sort((d: any) => this.getSizeForNode(d)) ) - } private getFillForNode(node: HierarchyNode<any>) { @@ -173,6 +175,12 @@ export default class BubbleChart extends SearchableChart { 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 { if (!input) return if (!input || !this.packRoot) return @@ -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() { return this.zoom.scale() } @@ -227,12 +245,12 @@ export default class BubbleChart extends SearchableChart { zoomToPoint(dataPoint: any) { if (dataPoint) { if (this.highlightedNode) { - d3.select(`circle[data-player="${this.highlightedNode.data.player}"]`) + d3.select(`circle[data-bubble="${this.highlightedNode.data.player}"]`) .transition() .duration(700) .attr('fill', (d: any) => this.getFillForNode(d)) } - d3.select(`circle[data-player="${dataPoint.data.player}"]`) + d3.select(`circle[data-bubble="${dataPoint.data.player}"]`) .transition() .duration(700) .attr('fill', 'var(--secondary)') diff --git a/src/main.ts b/src/main.ts index 131f983fc8a97685f2ced2d70b7b836f80ae3cf4..543ed52136ee05d1d9ca2877ac675c418bdef5c9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,7 +3,7 @@ import {dsv} from "d3-fetch"; import BubbleChart, {BubbleChartConfigParam} from "@/charts/bubbleChart.ts"; import Search from "@/search.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 bubbleChartWrapper = document.querySelector('#bubble-chart-wrapper') as HTMLDivElement; @@ -30,17 +30,36 @@ groupBySetting.oninput = (event) => { } const colorBySetting = document.querySelector('#color-by-setting') as HTMLInputElement + colorBySetting.oninput = (event) => { if (bubbleChart) { 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 => { - const parsedData = data.map((d: any) => ({ - ...d, - nation: countries.find(c => c.code === d.nation)?.name ?? d.nation, - } as Player) + const parsedData = data.map((d: any) => { + for (const column of Object.keys(numericColumns)) { + d[column] = parseFloat(d[column]) + } + return { + ...d, + nation: countries.find(c => c.code === d.nation)?.name ?? d.nation, + } as Player + } ) bubbleChart = new BubbleChart(parsedData, { diff --git a/src/player.ts b/src/player.ts index 5eb7b49863e32c77bcfa3c666949ca9f82371ed7..59142f3093a36af155257957448a3b252404eba8 100644 --- a/src/player.ts +++ b/src/player.ts @@ -1,40 +1,109 @@ -export type Player = { - league: string; - season: string; - team: string; - player: string; - nation: string; - pos: string; - age: string; - born: string; - playingTime: number; - performance: number; - expected_0: number; - progression: number; - per90Minutes: 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; - TklInt: number; - Clr: number; - Err: number; - touches: number; - penaltyKicks: number; - expected_2: number; -} \ No newline at end of file +/* + +"league": "ARG-Primera-Division", + "season": "2324", + "team": "Arg Juniors", + "player": "Alan Rodríguez", + "nation": "", + "pos": "", + "age": "", + "born": "", + "Playing Time": "", + "Performance": "", + "Expected_0": "", + "Progression": "", + "Per 90 Minutes": "", + "Standard": "0", + "Expected_1": "-1.7", + "Total": "1355", + "Short": "84.1", + "Medium": "77.6", + "Long": "43.3", + "Ast": "0", + "xAG": "0.8", + "Expected": "", + "KP": "11", + "1/3": "27", + "PPA": "8", + "CrsPA": "2", + "PrgP": "44", + "SCA": "2.16", + "GCA": "0.23", + "Tackles": "25", + "Blocks": "7", + "Int": "11", + "Tkl+Int": "47", + "Clr": "7", + "Err": "0", + "Touches": "677", + "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' +} diff --git a/src/styles/index.scss b/src/styles/index.scss index ba132167b88605273eea529853d36a98516dd9bd..5bc11119a83726659f13fc6126f7899a7a7381fc 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -72,6 +72,7 @@ h1 { border: 1px solid #ccc; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); user-select: none; + z-index: 10; } #app { @@ -94,7 +95,7 @@ h1 { .bubble-chart-settings { position: absolute; - bottom: 1rem; + top: 100%; right: 0; display: flex; padding: 1rem;