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

feat: radar chart tooltip

parent 42f8e8f3
Branches
No related tags found
No related merge requests found
...@@ -3,7 +3,10 @@ import {HierarchyNode, NumberValue, ScaleLinear, ScaleOrdinal, ScaleSequential} ...@@ -3,7 +3,10 @@ import {HierarchyNode, NumberValue, ScaleLinear, ScaleOrdinal, ScaleSequential}
import {ChartConfig, ChartConfigParam} from "@/charts/chart.ts"; import {ChartConfig, ChartConfigParam} from "@/charts/chart.ts";
import SearchableChart from "@/charts/SearchableChart.ts"; import SearchableChart from "@/charts/SearchableChart.ts";
import {debounce} from "@/utils.ts"; import {debounce} from "@/utils.ts";
// @ts-ignore
import Legend from "@/charts/utils/legendColor.js"; import Legend from "@/charts/utils/legendColor.js";
// @ts-ignore
import legendCircle from "@/charts/utils/legendCircle.js";
export type BubbleChartConfig = ChartConfig & { export type BubbleChartConfig = ChartConfig & {
groupAccessor: (d: any) => string | null, groupAccessor: (d: any) => string | null,
...@@ -29,6 +32,7 @@ export default class BubbleChart extends SearchableChart { ...@@ -29,6 +32,7 @@ export default class BubbleChart extends SearchableChart {
sizeScale: ScaleLinear<any, number> | null = null sizeScale: ScaleLinear<any, number> | null = null
groupLabels: any | null = null groupLabels: any | null = null
groupsWithLabels: any[] = [] groupsWithLabels: any[] = []
defaultSizeAccessor = (_: any) => 5
constructor(data: any[], _config: BubbleChartConfigParam) { constructor(data: any[], _config: BubbleChartConfigParam) {
super(data, _config as ChartConfigParam) super(data, _config as ChartConfigParam)
...@@ -51,7 +55,7 @@ export default class BubbleChart extends SearchableChart { ...@@ -51,7 +55,7 @@ export default class BubbleChart extends SearchableChart {
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 || 30, tooltipPadding: _config.tooltipPadding || 30,
groupAccessor: _config.groupAccessor || (() => null), groupAccessor: _config.groupAccessor || (() => null),
sizeAccessor: _config.sizeAccessor || (() => 5), sizeAccessor: _config.sizeAccessor || this.defaultSizeAccessor,
colorAccessor: _config.colorAccessor || (() => null), colorAccessor: _config.colorAccessor || (() => null),
idAccessor: _config.idAccessor || (() => null), idAccessor: _config.idAccessor || (() => null),
zoomExtent: _config.zoomExtent || [0.5, 20], zoomExtent: _config.zoomExtent || [0.5, 20],
...@@ -66,7 +70,7 @@ export default class BubbleChart extends SearchableChart { ...@@ -66,7 +70,7 @@ export default class BubbleChart extends SearchableChart {
<svg id="${this.chartId}"></svg> <svg id="${this.chartId}"></svg>
`; `;
vis.config.parentElement.innerHTML += ` vis.config.parentElement.innerHTML += `
<div id="tooltip"></div> <div id="tooltip" class="tooltip"></div>
`; `;
vis.config.parentElement.innerHTML += ` vis.config.parentElement.innerHTML += `
<div id="color-legend"></div> <div id="color-legend"></div>
...@@ -121,6 +125,7 @@ export default class BubbleChart extends SearchableChart { ...@@ -121,6 +125,7 @@ export default class BubbleChart extends SearchableChart {
.attr("transform", (d: any) => `translate(${d.x},${d.y})`), .attr("transform", (d: any) => `translate(${d.x},${d.y})`),
(exit: any) => exit.remove() (exit: any) => exit.remove()
) )
node.selectAll('text').remove()
node.selectAll('circle') node.selectAll('circle')
.data((d: any) => [d]) .data((d: any) => [d])
...@@ -128,6 +133,18 @@ export default class BubbleChart extends SearchableChart { ...@@ -128,6 +133,18 @@ export default class BubbleChart extends SearchableChart {
.duration(1000) .duration(1000)
.attr('fill', (d: any) => vis.getFillForNode(d)) .attr('fill', (d: any) => vis.getFillForNode(d))
.attr('r', (d: any) => d.r) .attr('r', (d: any) => d.r)
/*setTimeout(() => {
node
.filter((d: any) => d.r > 25)
.append('text')
.text((d: any) => vis.config.sizeAccessor(d.data))
.attr('text-anchor', 'middle')
.attr('opacity', '0')
.transition()
.duration(100)
.attr('opacity', '1')
}, 1000)*/
} }
renderVis(): void { renderVis(): void {
...@@ -203,16 +220,18 @@ export default class BubbleChart extends SearchableChart { ...@@ -203,16 +220,18 @@ export default class BubbleChart extends SearchableChart {
.attr('group', true) .attr('group', true)
.attr("transform", (d: any) => `translate(${d.x},${d.y})`) .attr("transform", (d: any) => `translate(${d.x},${d.y})`)
.on('mouseover', (event: Event) => { .on('mouseover', (event: Event) => {
const target = event.target as HTMLElement
// hide label // hide label
d3.select(event.target.closest('g')) d3.select(target.closest('g'))
.selectAll("*") .selectAll("*")
.transition() .transition()
.duration(100) .duration(100)
.attr('opacity', 0) .attr('opacity', 0)
}) })
.on('mouseleave', (event: Event) => { .on('mouseleave', (event: Event) => {
const target = event.target as HTMLElement
// show label // show label
d3.select(event.target.closest('g')) d3.select(target.closest('g'))
.selectAll("*") .selectAll("*")
.transition() .transition()
.duration(100) .duration(100)
...@@ -230,9 +249,9 @@ export default class BubbleChart extends SearchableChart { ...@@ -230,9 +249,9 @@ export default class BubbleChart extends SearchableChart {
.filter((d: any) => d.data[0]) .filter((d: any) => d.data[0])
.data((d: any) => [d]) .data((d: any) => [d])
.join( .join(
enter => enter.append("rect"), (enter: any) => enter.append("rect"),
update => update, (update: any) => update,
exit => exit.remove() (exit: any) => exit.remove()
) )
.attr('fill', 'white') .attr('fill', 'white')
.attr('stroke', '#333') .attr('stroke', '#333')
...@@ -244,17 +263,19 @@ export default class BubbleChart extends SearchableChart { ...@@ -244,17 +263,19 @@ export default class BubbleChart extends SearchableChart {
.selectAll('text') .selectAll('text')
.data((d: any) => [d]) .data((d: any) => [d])
.join( .join(
enter => enter.append("text"), (enter: any) => enter.append("text"),
update => update, (update: any) => update,
exit => exit.remove() (exit: any) => exit.remove()
) )
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle') .attr('alignment-baseline', 'middle')
.attr('font-size', 12 / this.zoomLevel) .attr('font-size', 12 / this.zoomLevel)
.attr('fill', 'black') .attr('fill', 'black')
.text(d => d.data[0]) .text((d: any) => d.data[0])
.call((selection: any) => selection.each( .call((selection: any) => selection.each(
function (d: any) { function (d: any) {
// I can't get TS to type annotate 'this' correctly here
// @ts-ignore
d.bbox = this.getBBox() d.bbox = this.getBBox()
} }
)) ))
...@@ -387,6 +408,9 @@ export default class BubbleChart extends SearchableChart { ...@@ -387,6 +408,9 @@ export default class BubbleChart extends SearchableChart {
zoomToPoint(dataPoint: any) { zoomToPoint(dataPoint: any) {
if (dataPoint) { if (dataPoint) {
if (typeof dataPoint === 'string') {
dataPoint = this.packRoot?.leaves().find((d: any) => this.config.idAccessor(d.data) === dataPoint)
}
if (this.highlightedNode) { if (this.highlightedNode) {
this.getNode(this.highlightedNode.data) this.getNode(this.highlightedNode.data)
.transition() .transition()
... ...
......
...@@ -61,7 +61,7 @@ export default class RadarChart extends Chart { ...@@ -61,7 +61,7 @@ export default class RadarChart extends Chart {
<svg id="${this.chartId}"></svg> <svg id="${this.chartId}"></svg>
`; `;
vis.config.parentElement.innerHTML += ` vis.config.parentElement.innerHTML += `
<div id="tooltip"></div> <div id="tooltip-radar-chart" class="tooltip"></div>
`; `;
const svg = d3.select(`#${this.chartId}`) const svg = d3.select(`#${this.chartId}`)
...@@ -92,14 +92,18 @@ export default class RadarChart extends Chart { ...@@ -92,14 +92,18 @@ export default class RadarChart extends Chart {
this.drawData(); this.drawData();
} }
private getPreparedData(): { data: RadarChartSelection, axesValues: { label: string, value: number }[] }[] { private getPreparedData(): {
data: RadarChartSelection,
axesValues: { label: string, r: number, value: number }[]
}[] {
return this.config.selectedData.map( return this.config.selectedData.map(
(d: any) => ({ (d: any) => ({
data: d, data: d,
axesValues: this.axes.map(axis => ( axesValues: this.axes.map(axis => (
{ {
label: axis.label, label: axis.label,
value: axis.scale(d[axis.key]) r: axis.scale(d[axis.key]),
value: d[axis.key],
} }
)) ))
}) })
...@@ -131,6 +135,7 @@ export default class RadarChart extends Chart { ...@@ -131,6 +135,7 @@ export default class RadarChart extends Chart {
.attr('class', 'axis') .attr('class', 'axis')
axes.append("path") axes.append("path")
.attr("pointer-events", "none")
.attr("d", (_: any, index: number) => d3.lineRadial() .attr("d", (_: any, index: number) => d3.lineRadial()
([[0, 0], [Math.PI * 2 * index / vis.axes.length, this.axisLength]]) ([[0, 0], [Math.PI * 2 * index / vis.axes.length, this.axisLength]])
) )
...@@ -162,10 +167,11 @@ export default class RadarChart extends Chart { ...@@ -162,10 +167,11 @@ export default class RadarChart extends Chart {
(enter: any) => { (enter: any) => {
const data = enter.append("g") const data = enter.append("g")
.attr("class", "data") .attr("class", "data")
data data
.append("path") .append("path")
.attr("d", (d: any) => { .attr("d", (d: any) => {
const data = d.axesValues.map((d: any) => d.value) const data = d.axesValues.map((d: any) => d.r)
return d3.lineRadial() return d3.lineRadial()
.angle((_, index) => Math.PI * 2 / this.axes.length * index) .angle((_, index) => Math.PI * 2 / this.axes.length * index)
.radius((value) => value || 0) .radius((value) => value || 0)
...@@ -195,17 +201,29 @@ export default class RadarChart extends Chart { ...@@ -195,17 +201,29 @@ export default class RadarChart extends Chart {
.attr("class", "dataPoint") .attr("class", "dataPoint")
.attr("r", 5) .attr("r", 5)
.attr("cx", (data: { .attr("cx", (data: {
label: string, r: number,
value: number }, index: number) => Math.sin(2 * Math.PI * (index / this.axes.length)) * data.r)
}, index: number) => Math.sin(2 * Math.PI * (index / this.axes.length)) * data.value)
.attr("cy", (data: { .attr("cy", (data: {
label: string, r: number,
value: number }, index: number) => -Math.cos(2 * Math.PI * (index / this.axes.length)) * data.r)
}, index: number) => -Math.cos(2 * Math.PI * (index / this.axes.length)) * data.value)
.attr('fill', (d: any) => d.data._color) .attr('fill', (d: any) => d.data._color)
.on('mouseover', (_: Event, d: any) => {
const element = d3.select('#tooltip-radar-chart')
.style('display', 'block')
if (!this.config.renderTooltip) return
this.config.renderTooltip(d, element)
})
.on('mousemove', (event: any) => {
d3.select('#tooltip-radar-chart')
.style('left', (event.layerX + this.config.tooltipPadding) + 'px')
.style('top', (event.layerY + this.config.tooltipPadding) + 'px')
})
.on('mouseleave', (_: Event) => {
d3.select('#tooltip-radar-chart').style('display', 'none');
})
}, },
update => update, (update: any) => update,
exit => exit.remove() (exit: any) => exit.remove()
) )
... ...
......
// Circle legend, made by Harry Stevens
// https://observablehq.com/@harrystevens/circle-legend
export default function legendCircle(context){
let scale,
tickValues,
tickFormat = d => d,
tickSize = 5;
function legend(context){
let g = context.select("g");
if (!g._groups[0][0]){
g = context.append("g");
}
g.attr("transform", `translate(${[1, 1]})`);
const ticks = tickValues || scale.ticks();
const max = ticks[ticks.length - 1];
g.selectAll("circle")
.data(ticks.slice().reverse())
.enter().append("circle")
.attr("fill", "none")
.attr("stroke", "currentColor")
.attr("cx", scale(max))
.attr("cy", scale)
.attr("r", scale);
g.selectAll("line")
.data(ticks)
.enter().append("line")
.attr("stroke", "currentColor")
.attr("stroke-dasharray", "4, 2")
.attr("x1", scale(max))
.attr("x2", tickSize + scale(max) * 2)
.attr("y1", d => scale(d) * 2)
.attr("y2", d => scale(d) * 2);
g.selectAll("text")
.data(ticks)
.enter().append("text")
.attr("font-family", "'Helvetica Neue', sans-serif")
.attr("font-size", 11)
.attr("dx", 3)
.attr("dy", 4)
.attr("x", tickSize + scale(max) * 2)
.attr("y", d => scale(d) * 2)
.text(tickFormat);
}
legend.tickSize = function(_){
return arguments.length ? (tickSize = +_, legend) : tickSize;
}
legend.scale = function(_){
return arguments.length ? (scale = _, legend) : scale;
}
legend.tickFormat = function(_){
return arguments.length ? (tickFormat = _, legend) : tickFormat;
}
legend.tickValues = function(_){
return arguments.length ? (tickValues = _, legend) : tickValues;
}
return legend;
}
\ No newline at end of file
...@@ -147,6 +147,15 @@ function updateSelectedNodes(node: Player) { ...@@ -147,6 +147,15 @@ function updateSelectedNodes(node: Player) {
margin: {top: 20, right: 20, bottom: 20, left: 20}, margin: {top: 20, right: 20, bottom: 20, left: 20},
axisCircles: 2, axisCircles: 2,
idAccessor: (d: any) => d.player, idAccessor: (d: any) => d.player,
renderTooltip: (dataPoint: any, tooltip: d3.Selection<d3.BaseType, unknown, HTMLElement, any>) => {
bubbleChart?.zoomToPoint(dataPoint.data.player)
tooltip.html(`
<h4>${dataPoint.data['player']}</h4>
<table>
<tr><th>${dataPoint['label']}</th><td>${dataPoint['value']}</td></tr>
</table>
`)
},
attributes: [ attributes: [
{ {
key: 'Performance _ G+A', key: 'Performance _ G+A',
... ...
......
...@@ -43,7 +43,7 @@ h1 { ...@@ -43,7 +43,7 @@ h1 {
font-size: 11px; font-size: 11px;
} }
#tooltip { .tooltip {
position: absolute; position: absolute;
display: none; display: none;
background: white; background: white;
... ...
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment