diff --git a/index.html b/index.html index d8e6cf339837b31cfef877259bef790cfdf9a9e3..ffd85166a637d2e9f2663e4d0af622259c486144 100644 --- a/index.html +++ b/index.html @@ -18,10 +18,23 @@ <div id="bubble-chart-wrapper"> </div> - <div id="zoom-wrapper"> - <button id="zoom-out">-</button> - <input type="range" min="1" max="100" value="50" class="slider" id="zoom-slider"/> - <button id="zoom-in">+</button> + <div class="bubble-chart-settings"> + <div class="group-by"> + <label for="group-by-setting">Gruppieren nach:</label> + + <select id="group-by-setting" name="group-by-setting"> + <option value="default">Keine</option> + <option value="nation">Nation</option> + <option value="pos">Position</option> + <option value="team">Team</option> + <option value="league">Liga</option> + </select> + </div> + <div id="zoom-wrapper"> + <button id="zoom-out">-</button> + <input type="range" min="1" max="100" value="50" class="slider" id="zoom-slider"/> + <button id="zoom-in">+</button> + </div> </div> </div> <div class="right"> diff --git a/src/charts/bubbleChart.ts b/src/charts/bubbleChart.ts index e3d409ab2466a44ee9ca470b5ee2ae951fd4fd85..8eab46d63293e7faf1c76488954976999f9ca16c 100644 --- a/src/charts/bubbleChart.ts +++ b/src/charts/bubbleChart.ts @@ -41,6 +41,21 @@ export default class BubbleChart extends SearchableChart { this.initVis() } + set _config(_config: BubbleChartConfigParam) { + this.config = { + ..._config, + parentElement: typeof _config.parentElement === 'string' ? document.querySelector(_config.parentElement) as HTMLElement : _config.parentElement, + containerWidth: _config.containerWidth || 500, + containerHeight: _config.containerHeight || 140, + margin: _config.margin || {top: 10, bottom: 30, right: 10, left: 30}, + tooltipPadding: _config.tooltipPadding || 15, + groupAccessor: _config.groupAccessor || (() => 'default'), + sizeAccessor: _config.sizeAccessor || (() => 5), + colorAccessor: _config.colorAccessor || (() => null), + zoomExtent: _config.zoomExtent || [0.5, 20], + } + } + initVis() { let vis = this; vis.config.parentElement.innerHTML += ` @@ -68,40 +83,47 @@ export default class BubbleChart extends SearchableChart { } updateVis(data: any[]): void { - this.data = data + this.data = data; + this.update() } - renderVis(): void { - let vis: BubbleChart = this; + updateVisConfig(_config: BubbleChartConfigParam): void { + this._config = {...this.config, ..._config}; + this.update() + } - /*vis.config.groupAccessor = (d: any) => { - return d['nation'] - }*/ + private update() { + let vis: BubbleChart = this; - const chart = vis.chart + vis.updatePackRoot() - const groupedData = d3.group( - vis.data, - (d: any) => vis.config.groupAccessor(d) - ) + const node = vis.chart + .selectAll("g") + .data(vis.packRoot?.descendants()) + .transition() + .duration(1000) + .attr("transform", (d: any) => `translate(${d.x},${d.y})`) + node.selectAll('circle') + .transition() + .duration(1000) + .attr('fill', (d: any) => vis.getFillForNode(d)) + } - const pack = d3.pack() - .size([vis.width(), vis.height()]) - .padding(2) + renderVis(): void { + let vis: BubbleChart = this; - vis.packRoot = pack((d3.hierarchy(groupedData) as HierarchyNode<any>) - .sum((d: any) => vis.config.sizeAccessor(d))) + vis.updatePackRoot() - const node = chart + const node = vis.chart .selectAll("g") - .data(vis.packRoot.descendants()) + .data(vis.packRoot?.descendants()) .join("g") .attr("transform", (d: any) => `translate(${d.x},${d.y})`) node .append('circle') - .attr('fill', (d:any) => d.children ? "transparent" : 'var(--primary)') + .attr('fill', (d: any) => vis.getFillForNode(d)) .attr('stroke', 'none') .attr('stroke-width', 2) .attr('r', (d: any) => d.r) @@ -116,12 +138,13 @@ export default class BubbleChart extends SearchableChart { .style('left', (event.layerX + vis.config.tooltipPadding) + 'px') .style('top', (event.layerY + vis.config.tooltipPadding) + 'px') }) - .on('mouseleave', () => { + .on('mouseleave', (_: Event, d: any) => { + console.log(d.data) d3.select('#tooltip').style('display', 'none'); }) } - renderTooltip(dataPoint: { player: string; league: string; pos: string; nation: string; team: string;}) { + private renderTooltip(dataPoint: { player: string; league: string; pos: string; nation: string; team: string; }) { d3.select('#tooltip') .style('display', 'block') .html(` @@ -135,6 +158,31 @@ export default class BubbleChart extends SearchableChart { `) } + /** + * Updates the d3 hierachy pack root with the current data. + * Uses the groupAccessor and sizeAccessor to group the data. + * @private + */ + private updatePackRoot() { + const groupedData = d3.group( + this.data, + (d: any) => this.config.groupAccessor(d) + ) + console.log(groupedData) + this.packRoot = d3.pack() + .size([this.width(), this.height()]) + .padding(4) + ( + (d3.hierarchy(groupedData) as HierarchyNode<any>) + .sum((d: any) => this.config.sizeAccessor(d)) + ) + } + + private getFillForNode(node: HierarchyNode<any>) { + if (node.children) return "transparent" + return this.config.colorAccessor(node) ?? 'var(--primary)' + } + search(input: string): void { if (!input) return if (!input || !this.packRoot) return diff --git a/src/main.ts b/src/main.ts index 7c06fcd56c99c04cefdfb5b1a894e3b524b26aeb..8cf09e2d218558fcdfcf33ff110ffc94b0b437a1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import "@/styles/index.scss"; // imports the default styles import {dsv} from "d3-fetch"; -import BubbleChart from "@/charts/bubbleChart.ts"; +import BubbleChart, {BubbleChartConfigParam} from "@/charts/bubbleChart.ts"; import Search from "@/search.ts"; // const radarChartWrapper = document.querySelector('#radar-chart-wrapper') as HTMLDivElement; @@ -20,6 +20,13 @@ zoomSlider.oninput = (event) => { } } +const groupBySetting = document.querySelector('#group-by-setting') as HTMLInputElement +groupBySetting.oninput = (event) => { + if (bubbleChart) { + bubbleChart.updateVisConfig({groupAccessor: (d: any) => d[event.target?.value]} as BubbleChartConfigParam) + } +} + dsv(';', 'data/output.csv').then(data => { bubbleChart = new BubbleChart(data, { parentElement: bubbleChartWrapper,