Skip to content
Snippets Groups Projects
Select Git revision
  • e7622fc54c4c2c1b8f7d676f1063839c099b3523
  • main default protected
2 results

bubbleChart.ts

Blame
  • bubbleChart.ts 6.52 KiB
    import * as d3 from "d3";
    import {HierarchyNode} from "d3";
    import {ChartConfig, ChartConfigParam} from "@/charts/chart.ts";
    import SearchableChart from "@/charts/SearchableChart.ts";
    
    
    export type BubbleChartConfig = ChartConfig & {
        groupAccessor: (d: any) => string,
        sizeAccessor: (d: any) => number,
        colorAccessor: (d: any) => number | null,
        zoomExtent: [number, number],
        onZoom?: (event: any) => void
    }
    
    export type BubbleChartConfigParam = ChartConfigParam & Partial<BubbleChartConfig>
    
    export default class BubbleChart extends SearchableChart {
        chartId: string = 'bubbleChart';
        chart: any
        zoom: any;
        config: BubbleChartConfig
        packRoot: HierarchyNode<any> | null = null
        highlightedNode: HierarchyNode<any> | null = null
    
        constructor(data: any[], _config: BubbleChartConfigParam) {
            super(data, _config as ChartConfigParam)
    
            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],
            }
    
            this.initVis()
        }
    
        initVis() {
            let vis = this;
            vis.config.parentElement.innerHTML += `
                <svg id="${this.chartId}"></svg>
            `;
            vis.config.parentElement.innerHTML += `
                <div id="tooltip"></div>
            `;
    
            const svg = d3.select(`#${this.chartId}`)
                .attr('width', vis.config.containerWidth)
                .attr('height', vis.config.containerHeight)
                .attr('transform', `translate(${vis.config.margin.left},${vis.config.margin.top})`)
    
            vis.chart = svg.append('g')
    
            vis.zoom = d3.zoom()
                .scaleExtent(vis.config.zoomExtent)
                .on('zoom', (event) => {
                    vis.chart.attr('transform', event.transform);
                    vis.config.onZoom?.(event)
                });
    
            vis.chart.call(vis.zoom)
        }
    
        updateVis(data: any[]): void {
            this.data = data
        }
    
        renderVis(): void {
            let vis: BubbleChart = this;
    
            /*vis.config.groupAccessor = (d: any) => {
                return d['nation']
            }*/
    
            const chart = vis.chart
    
            const groupedData = d3.group(
                vis.data,
                (d: any) => vis.config.groupAccessor(d)
            )
    
    
            const pack = d3.pack()
                .size([vis.width(), vis.height()])
                .padding(2)
    
            vis.packRoot = pack((d3.hierarchy(groupedData) as HierarchyNode<any>)
                .sum((d: any) => vis.config.sizeAccessor(d)))
    
            const node = chart
                .selectAll("g")
                .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('stroke', 'none')
                .attr('stroke-width', 2)
                .attr('r', (d: any) => d.r)
                .attr('data-leaf', (d: any) => d.children ? 'false' : 'true')
                .attr('data-player', (d: any) => d.data.player)
            node.selectAll('circle[data-leaf="true"]')
                .on('mouseover', (_: Event, d: any) => {
                    vis.renderTooltip(d.data)
                })
                .on('mousemove', (event: any) => {
                    d3.select('#tooltip')
                        .style('left', (event.layerX + vis.config.tooltipPadding) + 'px')
                        .style('top', (event.layerY + vis.config.tooltipPadding) + 'px')
                })
                .on('mouseleave', () => {
                    d3.select('#tooltip').style('display', 'none');
                })
        }
    
        renderTooltip(dataPoint: { player: string; league: string; pos: string; nation: string; team: string;}) {
            d3.select('#tooltip')
                .style('display', 'block')
                .html(`
                    <table>
                        <tr><th>Name</th><td>${dataPoint['player']}</td></tr>
                        <tr><th>Liga</th><td>${dataPoint['league']}</td></tr>
                        <tr><th>Position</th><td>${dataPoint['pos']}</td></tr>
                        <tr><th>Nationalität</th><td>${dataPoint['nation']}</td></tr>
                        <tr><th>Team</th><td>${dataPoint['team']}</td></tr>
                    </table>
            `)
        }
    
        search(input: string): void {
            if (!input) return
            if (!input || !this.packRoot) return
            const result = this.packRoot.leaves()
                .find((d: any) => d.data.player.toLowerCase().includes(input.toLowerCase()))
            if (result) {
                this.zoomToPoint(result)
            }
        }
    
        get zoomLevel() {
            return this.zoom.scale()
        }
    
        zoomIn() {
            this.chart.transition()
                .duration(300)
                .call(this.zoom.scaleBy, 1.3)
        }
    
        zoomOut() {
            this.chart.transition()
                .duration(300)
                .call(this.zoom.scaleBy, 0.75)
        }
    
        zoomTo(zoomLevel: number) {
            this.chart.call(this.zoom.scaleTo, zoomLevel)
        }
    
        zoomToPoint(dataPoint: any) {
            if (dataPoint) {
                if (this.highlightedNode) {
                    d3.select(`circle[data-player="${this.highlightedNode.data.player}"]`)
                        .transition()
                        .duration(700)
                        .attr('fill', 'var(--primary)')
                }
                d3.select(`circle[data-player="${dataPoint.data.player}"]`)
                    .transition()
                    .duration(700)
                    .attr('fill', 'var(--secondary)')
                this.highlightedNode = dataPoint
                this.chart.transition()
                    .duration(300)
                    .call(
                        this.zoom.transform,
                        d3.zoomIdentity
                            .scale(Math.max(this.config.zoomExtent[1], this.config.zoomExtent[0] / dataPoint.r))
                            .translate(
                                -dataPoint.x + (this.width() / (2 * this.config.zoomExtent[1])),
                                -dataPoint.y + (this.height() / (2 * this.config.zoomExtent[1]))
                            )
                    )
            }
        }
    }