diff --git a/src/charts/bubbleChart.ts b/src/charts/bubbleChart.ts index 18c7adf9fe66c613f38f5fb2d213e69ef3bec428..194f83861f9f6043dd64876c3d6fdeb4db1703b3 100644 --- a/src/charts/bubbleChart.ts +++ b/src/charts/bubbleChart.ts @@ -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() diff --git a/src/charts/radarChart.ts b/src/charts/radarChart.ts index 499182234fd2460f034cf4ab3369e646bc7f87d9..c8c6e74d36d0d4774960d818767f6189b299b63d 100644 --- a/src/charts/radarChart.ts +++ b/src/charts/radarChart.ts @@ -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() ) diff --git a/src/charts/utils/legendCircle.js b/src/charts/utils/legendCircle.js deleted file mode 100644 index 179ea2484fa84786cfacb92c2c1fe7fea749ff9b..0000000000000000000000000000000000000000 --- a/src/charts/utils/legendCircle.js +++ /dev/null @@ -1,69 +0,0 @@ -// 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 diff --git a/src/main.ts b/src/main.ts index 480ab06733e2d29669fc1f7a58761f71879c8227..89098df075dba371f3c01ce9d8b37d07e8cbf3f0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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', diff --git a/src/styles/index.scss b/src/styles/index.scss index 0254b58452d19f6a760451c4709fd4c4743ac122..f529d95e958be468f8396c23259d30d333079bd7 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -43,7 +43,7 @@ h1 { font-size: 11px; } -#tooltip { +.tooltip { position: absolute; display: none; background: white;