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

wip: radar chart

parent 9a4b6a85
No related branches found
No related tags found
No related merge requests found
...@@ -160,6 +160,17 @@ export default class BubbleChart extends SearchableChart { ...@@ -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() { private renderGroupLabels() {
this.groupLabels = this.chart.selectAll('g') this.groupLabels = this.chart.selectAll('g')
.filter('[group]') .filter('[group]')
...@@ -365,12 +376,12 @@ export default class BubbleChart extends SearchableChart { ...@@ -365,12 +376,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-bubble="${this.highlightedNode.data.player}"]`) this.getPlayerNode(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-bubble="${dataPoint.data.player}"]`) this.getPlayerNode(dataPoint.data.player)
.transition() .transition()
.duration(700) .duration(700)
.attr('fill', 'var(--secondary)') .attr('fill', 'var(--secondary)')
...@@ -389,4 +400,8 @@ export default class BubbleChart extends SearchableChart { ...@@ -389,4 +400,8 @@ export default class BubbleChart extends SearchableChart {
} }
this.updateGroupsWithLabels() this.updateGroupsWithLabels()
} }
private getPlayerNode(playerName: string) {
return d3.select(`circle[data-bubble="${playerName}"]`);
}
} }
\ No newline at end of file
import * as d3 from "d3"; 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"; import Chart, {ChartConfig, ChartConfigParam} from "@/charts/chart.ts";
export type RadarChartConfig = ChartConfig & { export type RadarChartConfig = ChartConfig & {
selectedData: any[],
renderTooltip?: (dataPoint: any, tooltip: d3.Selection<d3.BaseType, unknown, HTMLElement, any>) => void, renderTooltip?: (dataPoint: any, tooltip: d3.Selection<d3.BaseType, unknown, HTMLElement, any>) => void,
axisCircles: number, axisCircles: number,
attributes: string[], attributes: string[],
...@@ -14,9 +15,11 @@ export default class RadarChart extends Chart { ...@@ -14,9 +15,11 @@ export default class RadarChart extends Chart {
chartId: string = 'radarChart'; chartId: string = 'radarChart';
chart: any chart: any
config: RadarChartConfig config: RadarChartConfig
colorScale: ScaleSequential<any> | ScaleOrdinal<any, any> | null = null axes: {
radialScale: ScaleRadial<any, any> = null scale: ScaleLinear<number, number>,
angleSlice = 0.7853981633974483 domain: [number, number],
label: string,
}[] = []
constructor(data: any[], _config: RadarChartConfigParam) { constructor(data: any[], _config: RadarChartConfigParam) {
super(data, _config as ChartConfigParam) super(data, _config as ChartConfigParam)
...@@ -39,7 +42,8 @@ export default class RadarChart extends Chart { ...@@ -39,7 +42,8 @@ export default class RadarChart extends Chart {
margin: _config.margin || {top: 10, bottom: 30, right: 10, left: 30}, margin: _config.margin || {top: 10, bottom: 30, right: 10, left: 30},
tooltipPadding: _config.tooltipPadding || 15, tooltipPadding: _config.tooltipPadding || 15,
axisCircles: _config.axisCircles || 2, 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 { ...@@ -59,35 +63,93 @@ export default class RadarChart extends Chart {
vis.chart = svg.append('g') vis.chart = svg.append('g')
const domain = d3.range(0, 2 * Math.PI, vis.angleSlice) for (const attribute of vis.config.attributes) {
const domain: [number, number] = [0, (d3.max(vis.data, (d: any) => d[attribute] as number) as number)]
this.radialScale = d3.scaleLinear() const scale = d3.scaleLinear(
.domain(domain) domain,
.range([0, 360]) [0, vis.chartCenter],
)
vis.updateColorScale() vis.axes.push({
scale,
domain,
label: attribute,
})
}
} }
updateVis(data: any[]): void { updateVis(selectedData: any[]): void {
this.data = data; const vis = this;
this.update() 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() { private get chartCenter() {
let vis: RadarChart = this; return Math.min(this.config.containerWidth, this.config.containerWidth) / 2
} }
renderVis(): void { renderVis(): void {
let vis: RadarChart = this; let vis: RadarChart = this;
const node = vis.chart const axisGrid = vis.chart.append("g")
.selectAll("g") .attr("class", "axisWrapper")
.data(vis.data) .attr('transform', `translate(${vis.chartCenter},${vis.chartCenter})`)
.join("g")
axisGrid.selectAll('.axis')
} .data(vis.axes)
.enter()
updateColorScale() { .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
...@@ -12,6 +12,7 @@ const bubbleChartWrapper = document.querySelector('#bubble-chart-wrapper') as HT ...@@ -12,6 +12,7 @@ const bubbleChartWrapper = document.querySelector('#bubble-chart-wrapper') as HT
const zoomExtent: [number, number] = [1, 5] const zoomExtent: [number, number] = [1, 5]
let bubbleChart: BubbleChart | null = null let bubbleChart: BubbleChart | null = null
let radarChart: RadarChart | null = null let radarChart: RadarChart | null = null
let parsedData: Player[] = []
let sliderBlocked = false let sliderBlocked = false
const zoomSlider = document.querySelector('#zoom-slider') as HTMLInputElement const zoomSlider = document.querySelector('#zoom-slider') as HTMLInputElement
...@@ -60,7 +61,7 @@ sizeBySetting.oninput = (event) => { ...@@ -60,7 +61,7 @@ sizeBySetting.oninput = (event) => {
const selectedNodes: Player[] = [] const selectedNodes: Player[] = []
dsv(';', 'data/output.csv').then(data => { dsv(';', 'data/output.csv').then(data => {
const parsedData = data.map((d: any) => { parsedData = data.map((d: any) => {
for (const column of Object.keys(numericColumns)) { for (const column of Object.keys(numericColumns)) {
d[column] = parseFloat(d[column]) d[column] = parseFloat(d[column])
} }
...@@ -81,6 +82,7 @@ dsv(';', 'data/output.csv').then(data => { ...@@ -81,6 +82,7 @@ dsv(';', 'data/output.csv').then(data => {
containerHeight: 1000, containerHeight: 1000,
margin: {top: 20, right: 20, bottom: 20, left: 20}, margin: {top: 20, right: 20, bottom: 20, left: 20},
zoomExtent, zoomExtent,
selectedNodes,
onZoom: (event) => { onZoom: (event) => {
if (sliderBlocked) return if (sliderBlocked) return
zoomSlider.value = event.transform.k.toString() zoomSlider.value = event.transform.k.toString()
...@@ -127,14 +129,19 @@ function updateSelectedNodes(node: Player) { ...@@ -127,14 +129,19 @@ function updateSelectedNodes(node: Player) {
} else { } else {
selectedNodes.push(node) selectedNodes.push(node)
} }
bubbleChart?.updateSelectedNodes(selectedNodes)
if (!radarChart) { if (!radarChart) {
radarChart = new RadarChart(selectedNodes, { radarChart = new RadarChart(parsedData, {
parentElement: radarChartWrapper, parentElement: radarChartWrapper,
selectedData: selectedNodes,
containerWidth: 500, containerWidth: 500,
containerHeight: 500, containerHeight: 500,
margin: {top: 20, right: 20, bottom: 20, left: 20}, margin: {top: 20, right: 20, bottom: 20, left: 20},
axisCircles: 2, axisCircles: 2,
attributes: ['Per 90 Minutes _ G+A', 'Per 90 Minutes _ xG+xAG', 'PrgP', 'Total _ Cmp%', 'Tkl+Int', 'Touches _ Touches']
}) })
radarChart.renderVis() radarChart.renderVis()
} else {
radarChart.updateVis(selectedNodes)
} }
} }
\ No newline at end of file
...@@ -171,7 +171,7 @@ export const numericColumns = { ...@@ -171,7 +171,7 @@ export const numericColumns = {
'Ast': 'Assists', 'Ast': 'Assists',
'xAG': 'Erwartete Assists (xAG)', 'xAG': 'Erwartete Assists (xAG)',
'Expected _ xA': 'Erwartete Assists (xA)', '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', 'KP': 'Key Passes',
'1/3': 'Angriffsdrittel-Pässe', '1/3': 'Angriffsdrittel-Pässe',
'PPA': 'Pässe, die zu einem Schuss führen', 'PPA': 'Pässe, die zu einem Schuss führen',
... ...
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment