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}
import {ChartConfig, ChartConfigParam} from "@/charts/chart.ts";
import SearchableChart from "@/charts/SearchableChart.ts";
import {debounce} from "@/utils.ts";
// @ts-ignore
import Legend from "@/charts/utils/legendColor.js";
// @ts-ignore
import legendCircle from "@/charts/utils/legendCircle.js";
export type BubbleChartConfig = ChartConfig & {
groupAccessor: (d: any) => string | null,
......@@ -29,6 +32,7 @@ export default class BubbleChart extends SearchableChart {
sizeScale: ScaleLinear<any, number> | null = null
groupLabels: any | null = null
groupsWithLabels: any[] = []
defaultSizeAccessor = (_: any) => 5
constructor(data: any[], _config: BubbleChartConfigParam) {
super(data, _config as ChartConfigParam)
......@@ -51,7 +55,7 @@ export default class BubbleChart extends SearchableChart {
margin: _config.margin || {top: 10, bottom: 30, right: 10, left: 30},
tooltipPadding: _config.tooltipPadding || 30,
groupAccessor: _config.groupAccessor || (() => null),
sizeAccessor: _config.sizeAccessor || (() => 5),
sizeAccessor: _config.sizeAccessor || this.defaultSizeAccessor,
colorAccessor: _config.colorAccessor || (() => null),
idAccessor: _config.idAccessor || (() => null),
zoomExtent: _config.zoomExtent || [0.5, 20],
......@@ -66,7 +70,7 @@ export default class BubbleChart extends SearchableChart {
<svg id="${this.chartId}"></svg>
`;
vis.config.parentElement.innerHTML += `
<div id="tooltip"></div>
<div id="tooltip" class="tooltip"></div>
`;
vis.config.parentElement.innerHTML += `
<div id="color-legend"></div>
......@@ -121,6 +125,7 @@ export default class BubbleChart extends SearchableChart {
.attr("transform", (d: any) => `translate(${d.x},${d.y})`),
(exit: any) => exit.remove()
)
node.selectAll('text').remove()
node.selectAll('circle')
.data((d: any) => [d])
......@@ -128,6 +133,18 @@ export default class BubbleChart extends SearchableChart {
.duration(1000)
.attr('fill', (d: any) => vis.getFillForNode(d))
.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 {
......@@ -203,16 +220,18 @@ export default class BubbleChart extends SearchableChart {
.attr('group', true)
.attr("transform", (d: any) => `translate(${d.x},${d.y})`)
.on('mouseover', (event: Event) => {
const target = event.target as HTMLElement
// hide label
d3.select(event.target.closest('g'))
d3.select(target.closest('g'))
.selectAll("*")
.transition()
.duration(100)
.attr('opacity', 0)
})
.on('mouseleave', (event: Event) => {
const target = event.target as HTMLElement
// show label
d3.select(event.target.closest('g'))
d3.select(target.closest('g'))
.selectAll("*")
.transition()
.duration(100)
......@@ -230,9 +249,9 @@ export default class BubbleChart extends SearchableChart {
.filter((d: any) => d.data[0])
.data((d: any) => [d])
.join(
enter => enter.append("rect"),
update => update,
exit => exit.remove()
(enter: any) => enter.append("rect"),
(update: any) => update,
(exit: any) => exit.remove()
)
.attr('fill', 'white')
.attr('stroke', '#333')
......@@ -244,17 +263,19 @@ export default class BubbleChart extends SearchableChart {
.selectAll('text')
.data((d: any) => [d])
.join(
enter => enter.append("text"),
update => update,
exit => exit.remove()
(enter: any) => enter.append("text"),
(update: any) => update,
(exit: any) => exit.remove()
)
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('font-size', 12 / this.zoomLevel)
.attr('fill', 'black')
.text(d => d.data[0])
.text((d: any) => d.data[0])
.call((selection: any) => selection.each(
function (d: any) {
// I can't get TS to type annotate 'this' correctly here
// @ts-ignore
d.bbox = this.getBBox()
}
))
......@@ -387,6 +408,9 @@ export default class BubbleChart extends SearchableChart {
zoomToPoint(dataPoint: any) {
if (dataPoint) {
if (typeof dataPoint === 'string') {
dataPoint = this.packRoot?.leaves().find((d: any) => this.config.idAccessor(d.data) === dataPoint)
}
if (this.highlightedNode) {
this.getNode(this.highlightedNode.data)
.transition()
......
......@@ -61,7 +61,7 @@ export default class RadarChart extends Chart {
<svg id="${this.chartId}"></svg>
`;
vis.config.parentElement.innerHTML += `
<div id="tooltip"></div>
<div id="tooltip-radar-chart" class="tooltip"></div>
`;
const svg = d3.select(`#${this.chartId}`)
......@@ -92,14 +92,18 @@ export default class RadarChart extends Chart {
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(
(d: any) => ({
data: d,
axesValues: this.axes.map(axis => (
{
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 {
.attr('class', 'axis')
axes.append("path")
.attr("pointer-events", "none")
.attr("d", (_: any, index: number) => d3.lineRadial()
([[0, 0], [Math.PI * 2 * index / vis.axes.length, this.axisLength]])
)
......@@ -162,10 +167,11 @@ export default class RadarChart extends Chart {
(enter: any) => {
const data = enter.append("g")
.attr("class", "data")
data
.append("path")
.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()
.angle((_, index) => Math.PI * 2 / this.axes.length * index)
.radius((value) => value || 0)
......@@ -195,17 +201,29 @@ export default class RadarChart extends Chart {
.attr("class", "dataPoint")
.attr("r", 5)
.attr("cx", (data: {
label: string,
value: number
}, index: number) => Math.sin(2 * Math.PI * (index / this.axes.length)) * data.value)
r: number,
}, index: number) => Math.sin(2 * Math.PI * (index / this.axes.length)) * data.r)
.attr("cy", (data: {
label: string,
value: number
}, index: number) => -Math.cos(2 * Math.PI * (index / this.axes.length)) * data.value)
r: number,
}, index: number) => -Math.cos(2 * Math.PI * (index / this.axes.length)) * data.r)
.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,
exit => exit.remove()
(update: any) => update,
(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) {
margin: {top: 20, right: 20, bottom: 20, left: 20},
axisCircles: 2,
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: [
{
key: 'Performance _ G+A',
......
......@@ -43,7 +43,7 @@ h1 {
font-size: 11px;
}
#tooltip {
.tooltip {
position: absolute;
display: none;
background: white;
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment