Select Git revision
bubbleChart.ts
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]))
)
)
}
}
}