Select Git revision
Testing.cmake
radarChart.ts 8.58 KiB
import * as d3 from "d3";
import {ScaleLinear} from "d3";
import Chart, {ChartConfig, ChartConfigParam} from "@/charts/chart.ts";
export type RadarChartConfig = ChartConfig & {
selectedData: RadarChartSelection[],
renderTooltip?: (dataPoint: any, tooltip: d3.Selection<d3.BaseType, unknown, HTMLElement, any>) => void,
axisCircles: number,
idAccessor: (d: any) => any,
attributes: { key: string, label: string }[] | [],
}
export type RadarChartConfigParam = ChartConfigParam & Partial<RadarChartConfig>
type RadarChartSelection = {
_color: string | null | undefined,
[key: string]: any
}
export default class RadarChart extends Chart {
chartId: string = 'radarChart';
chart: any
config: RadarChartConfig
axes: {
scale: ScaleLinear<number, number>,
domain: [number, number],
label: string,
key: string,
}[] = []
constructor(data: any[], _config: RadarChartConfigParam) {
super(data, _config as ChartConfigParam)
this.config = this.createConfig(_config)
this.initVis()
}
private setConfig(_config: RadarChartConfigParam) {
this.config = this.createConfig(_config)
}
private createConfig(_config: RadarChartConfigParam): RadarChartConfig {
return {
..._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,
axisCircles: _config.axisCircles || 2,
selectedData: _config.selectedData || [],
attributes: _config.attributes || [],
idAccessor: _config.idAccessor || (() => null),
}
}
initVis() {
let vis = this;
vis.config.parentElement.innerHTML += `
<svg id="${this.chartId}"></svg>
`;
vis.config.parentElement.innerHTML += `
<div id="tooltip-radar-chart" class="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')
for (const attribute of vis.config.attributes) {
const domain: [number, number] = [0, (d3.max(vis.data, (d: any) => d[attribute.key] as number) as number)]
const scale = d3.scaleLinear(
domain,
[0, vis.axisLength],
)
vis.axes.push({
scale,
domain,
label: attribute.label,
key: attribute.key,
})
}
}
updateVis(selectedData: RadarChartSelection[]): void {
this.config.selectedData = selectedData;
this.drawData();
}
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,
r: axis.scale(d[axis.key]),
value: d[axis.key],
}
))
})
);
}
private get chartCenter() {
return Math.min(this.config.containerWidth, this.config.containerHeight) / 2
}
private get axisLength() {
if (this.config.containerWidth < this.config.containerHeight) {
return this.width() / 2
}
return this.height() / 2
}
renderVis(): void {
let vis: RadarChart = this;
const axisGrid = vis.chart.append("g")
.attr("class", "axisWrapper")
.attr('transform', `translate(${vis.chartCenter},${vis.chartCenter})`)
const axes = axisGrid.selectAll('.axis')
.data(vis.axes)
.enter()
.append('g')
.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]])
)
axes.append('text')
.attr("x", (_: any, index: number) => Math.sin(2 * Math.PI * (index / vis.axes.length)) * (this.axisLength + 10))
.attr("y", (_: any, index: number) => -Math.cos(2 * Math.PI * (index / vis.axes.length)) * (this.axisLength + 10))
.attr('text-anchor', 'middle')
.attr('alignment-baseline', 'middle')
.attr('font-size', 12)
.attr('fill', 'black')
.text((d: any) => d.label)
this.drawData();
}
private drawData() {
let dataWrapper = this.chart.selectAll(".dataWrapper")
if (dataWrapper.empty()) {
dataWrapper = this.chart.append("g")
.attr("class", "dataWrapper")
.attr('transform', `translate(${this.chartCenter},${this.chartCenter})`)
}
const preparedData = this.getPreparedData()
dataWrapper.selectAll('.data')
.data(preparedData, (d: any) => this.config.idAccessor(d.data))
.join(
(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.r)
return d3.lineRadial()
.angle((_, index) => Math.PI * 2 / this.axes.length * index)
.radius((value) => value || 0)
.curve(d3.curveCardinalClosed.tension(0.6))
([...data, data[0]])
})
.attr('fill', (d: any) => {
const color = d3.color(d.data._color)
if (!color) {
return "rgba(50,50,50,0.1)"
}
return color.copy({opacity: 0.2}).toString()
})
.attr('stroke', (d: any) => d.data._color)
.attr('stroke-width', 3)
data.selectAll('.dataPoint')
.data((d: any) => d.axesValues.map((value: any) => ({
...value,
data: d.data,
})))
.join(
enter => enter.append("circle"),
update => update,
exit => exit.remove()
)
.attr("class", "dataPoint")
.attr("r", 5)
.attr("cx", (data: {
r: number,
}, index: number) => Math.sin(2 * Math.PI * (index / this.axes.length)) * data.r)
.attr("cy", (data: {
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: any) => update,
(exit: any) => exit.remove()
)
}
}