diff --git a/src/charts/bubbleChart.ts b/src/charts/bubbleChart.ts index 6993414716c51386c6ea605fa50bc62a50f7cd86..85794e434548c1e6d85cf54da0c8a0d94f3426bd 100644 --- a/src/charts/bubbleChart.ts +++ b/src/charts/bubbleChart.ts @@ -160,6 +160,17 @@ export default class BubbleChart extends SearchableChart { }) } + updateSelectedNodes(nodes: any[]) { + d3.selectAll('circle[data-bubble]') + .attr('stroke', 'none') + for (const node of nodes) { + const playerNode = this.getPlayerNode(node.player) + playerNode + .attr('stroke', 'black') + .attr('stroke-width', 2 / this.zoomLevel) + } + } + private renderGroupLabels() { this.groupLabels = this.chart.selectAll('g') .filter('[group]') @@ -365,12 +376,12 @@ export default class BubbleChart extends SearchableChart { zoomToPoint(dataPoint: any) { if (dataPoint) { if (this.highlightedNode) { - d3.select(`circle[data-bubble="${this.highlightedNode.data.player}"]`) + this.getPlayerNode(this.highlightedNode.data.player) .transition() .duration(700) .attr('fill', (d: any) => this.getFillForNode(d)) } - d3.select(`circle[data-bubble="${dataPoint.data.player}"]`) + this.getPlayerNode(dataPoint.data.player) .transition() .duration(700) .attr('fill', 'var(--secondary)') @@ -389,4 +400,8 @@ export default class BubbleChart extends SearchableChart { } this.updateGroupsWithLabels() } + + private getPlayerNode(playerName: string) { + return d3.select(`circle[data-bubble="${playerName}"]`); + } } \ No newline at end of file diff --git a/src/charts/radarChart.ts b/src/charts/radarChart.ts index cc431d7de62525666967a41a27e144cabe797e15..ffe983fa3f9f732c3fc4031a3443f610388c62be 100644 --- a/src/charts/radarChart.ts +++ b/src/charts/radarChart.ts @@ -1,8 +1,9 @@ import * as d3 from "d3"; -import {curveCardinalClosed, HierarchyNode, ScaleOrdinal, ScaleRadial, ScaleSequential} from "d3"; +import {ScaleLinear} from "d3"; import Chart, {ChartConfig, ChartConfigParam} from "@/charts/chart.ts"; export type RadarChartConfig = ChartConfig & { + selectedData: any[], renderTooltip?: (dataPoint: any, tooltip: d3.Selection<d3.BaseType, unknown, HTMLElement, any>) => void, axisCircles: number, attributes: string[], @@ -14,9 +15,11 @@ export default class RadarChart extends Chart { chartId: string = 'radarChart'; chart: any config: RadarChartConfig - colorScale: ScaleSequential<any> | ScaleOrdinal<any, any> | null = null - radialScale: ScaleRadial<any, any> = null - angleSlice = 0.7853981633974483 + axes: { + scale: ScaleLinear<number, number>, + domain: [number, number], + label: string, + }[] = [] constructor(data: any[], _config: RadarChartConfigParam) { super(data, _config as ChartConfigParam) @@ -39,7 +42,8 @@ export default class RadarChart extends Chart { margin: _config.margin || {top: 10, bottom: 30, right: 10, left: 30}, tooltipPadding: _config.tooltipPadding || 15, axisCircles: _config.axisCircles || 2, - attributes: ['Per 90 Minutes _ G+A', 'Per 90 Minutes _ xG+xAG', 'PrgP', 'Total _ Cmp%', 'Tkl+Int', 'Touches _ Touches'], + selectedData: _config.selectedData || [], + attributes: _config.attributes || [], } } @@ -59,35 +63,93 @@ export default class RadarChart extends Chart { vis.chart = svg.append('g') - const domain = d3.range(0, 2 * Math.PI, vis.angleSlice) - - this.radialScale = d3.scaleLinear() - .domain(domain) - .range([0, 360]) - - vis.updateColorScale() + for (const attribute of vis.config.attributes) { + const domain: [number, number] = [0, (d3.max(vis.data, (d: any) => d[attribute] as number) as number)] + const scale = d3.scaleLinear( + domain, + [0, vis.chartCenter], + ) + vis.axes.push({ + scale, + domain, + label: attribute, + }) + } } - updateVis(data: any[]): void { - this.data = data; - this.update() + updateVis(selectedData: any[]): void { + const vis = this; + const data = vis.chart.select('.dataWrapper') + this.config.selectedData = selectedData; + + const preparedData = vis.config.selectedData.map((d: any) => { + return vis.axes.map(axis => + axis.scale(d[axis.label]) + ) + }) + + data.selectAll('.data') + .data(preparedData) + .enter() + .append('g') + .attr('class', 'data') + .append("path") + .attr("d", (d: any) => { + console.log(d) + return d3.lineRadial() + .angle((_, index) => Math.PI * 2 / vis.axes.length * index) + .radius((data) => data) + ([...d, d[0]]) + }) + .attr('fill', '#69b3a211') + .attr('stroke', 'black') } - private update() { - let vis: RadarChart = this; + private get chartCenter() { + return Math.min(this.config.containerWidth, this.config.containerWidth) / 2 } renderVis(): void { let vis: RadarChart = this; - const node = vis.chart - .selectAll("g") - .data(vis.data) - .join("g") - - } - - updateColorScale() { - + const axisGrid = vis.chart.append("g") + .attr("class", "axisWrapper") + .attr('transform', `translate(${vis.chartCenter},${vis.chartCenter})`) + + axisGrid.selectAll('.axis') + .data(vis.axes) + .enter() + .append('g') + .attr('class', 'axis') + .append("path") + .attr("d", (_: any, index: number) => d3.lineRadial() + ([[0, 0], [Math.PI * 2 * index / vis.axes.length, this.chartCenter]]) + ) + + const data = vis.chart.append("g") + .attr("class", "dataWrapper") + .attr('transform', `translate(${vis.chartCenter},${vis.chartCenter})`) + + const preparedData = vis.config.selectedData.map((d: any) => { + return vis.axes.map(axis => + axis.scale(d[axis.label]) + ) + }) + + data.selectAll('.data') + .data(preparedData) + .enter() + .append('g') + .attr('class', 'data') + .append("path") + .attr("d", (d: any) => { + console.log(d) + return d3.lineRadial() + .angle((_, index) => Math.PI * 2 / vis.axes.length * index) + .radius((data) => data) + ([...d, d[0]]) + }) + .attr('fill', '#69b3a211') + .attr('stroke', 'black') } } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 35ee2976fbff3ba9a158d1a33e4e14d1e46c7fb5..b94c63ea62505c10c75eb838453af35b8ed362ba 100644 --- a/src/main.ts +++ b/src/main.ts @@ -12,6 +12,7 @@ const bubbleChartWrapper = document.querySelector('#bubble-chart-wrapper') as HT const zoomExtent: [number, number] = [1, 5] let bubbleChart: BubbleChart | null = null let radarChart: RadarChart | null = null +let parsedData: Player[] = [] let sliderBlocked = false const zoomSlider = document.querySelector('#zoom-slider') as HTMLInputElement @@ -60,7 +61,7 @@ sizeBySetting.oninput = (event) => { const selectedNodes: Player[] = [] dsv(';', 'data/output.csv').then(data => { - const parsedData = data.map((d: any) => { + parsedData = data.map((d: any) => { for (const column of Object.keys(numericColumns)) { d[column] = parseFloat(d[column]) } @@ -81,6 +82,7 @@ dsv(';', 'data/output.csv').then(data => { containerHeight: 1000, margin: {top: 20, right: 20, bottom: 20, left: 20}, zoomExtent, + selectedNodes, onZoom: (event) => { if (sliderBlocked) return zoomSlider.value = event.transform.k.toString() @@ -127,14 +129,19 @@ function updateSelectedNodes(node: Player) { } else { selectedNodes.push(node) } + bubbleChart?.updateSelectedNodes(selectedNodes) if (!radarChart) { - radarChart = new RadarChart(selectedNodes, { + radarChart = new RadarChart(parsedData, { parentElement: radarChartWrapper, + selectedData: selectedNodes, containerWidth: 500, containerHeight: 500, margin: {top: 20, right: 20, bottom: 20, left: 20}, axisCircles: 2, + attributes: ['Per 90 Minutes _ G+A', 'Per 90 Minutes _ xG+xAG', 'PrgP', 'Total _ Cmp%', 'Tkl+Int', 'Touches _ Touches'] }) radarChart.renderVis() + } else { + radarChart.updateVis(selectedNodes) } } \ No newline at end of file diff --git a/src/player.ts b/src/player.ts index 51742764214f53fd9e599bf6ebc7f235ab26ca6d..0066aaf9ef6df9921020672ac551f1aeeba8f7c1 100644 --- a/src/player.ts +++ b/src/player.ts @@ -171,7 +171,7 @@ export const numericColumns = { 'Ast': 'Assists', 'xAG': 'Erwartete Assists (xAG)', 'Expected _ xA': 'Erwartete Assists (xA)', - 'Expected _ A-xAG': 'Erwartete Assists (xA) - Erwartete Assists (xAG)', + 'Expected _ A-xAG': 'Erwartete Assists und Tore (xAG)', 'KP': 'Key Passes', '1/3': 'Angriffsdrittel-Pässe', 'PPA': 'Pässe, die zu einem Schuss führen',