class RadarChart {
constructor(container) {
this._container = d3.select(container);
this._data = null;
this._chartData = [];
this._width = 640;
this._height = 640;
this._margins = {
top: 60,
right: 60,
bottom: 60,
left: 60
};
this._radius = 0;
this._boundedRadius = 0;
this._boundedWidth = 0;
this._boundedHeight = 0;
this._dotRadius = 4;
this._baseAngleOffset = -0.5 * Math.PI;
this._angleOffset = 0;
this._axesLabels = new Set();
this._labelWidth = 120;
this._field = {
axis: "axis",
value: "value"
};
this._maxValue = 5;
this._baselineValue = 2.5;
this._bg = undefined;
this._curve = d3.curveCardinalClosed;
this._palette = palette;
this._strokeDasharray = "2 2";
this._fontFamily = "system-ui";
this._axisAccessor = (d) => {
return d[this._field.axis];
};
this._valueAccessor = (d) => {
return d[this._field.value];
};
this._getCoordinatesForAngle = this._getCoordinatesForAngle.bind(this);
}
data(_) {
return arguments.length ? ((this._data = _), this) : this._data;
}
field(_) {
return arguments.length ? ((this._field = _), this) : this._field;
}
margins(_) {
if (typeof _ === "number") {
return (
(this._margins = {
top: _,
right: _,
bottom: _,
left: _
}),
this
);
}
return arguments.length ? ((this._margins = _), this) : this._margins;
}
palette(_) {
return arguments.length ? ((this._palette = _), this) : this._palette;
}
curve(_) {
return arguments.length ? ((this._curve = _), this) : this._curve;
}
fontFamily(_) {
return arguments.length ? ((this._fontFamily = _), this) : this._fontFamily;
}
baselineValue(_) {
return arguments.length
? ((this._baselineValue = _), this)
: this._baselineValue;
}
angleOffset(_) {
return arguments.length
? ((this._angleOffset = _), this)
: this._angleOffset;
}
size(w, h) {
return arguments.length
? ((this._width = w), (this._height = h), this)
: this._field;
}
_radiusFromSize(w, h) {
return Math.floor(Math.min(w, h) / 2);
}
_updateBounds() {
this._boundedWidth =
this._width - (this._margins.left + this._margins.right);
this._boundedHeight =
this._height - (this._margins.top + this._margins.top);
this._radius = this._radiusFromSize(this._width, this._height);
this._boundedRadius =
this._radius -
Math.max(
this._margins.top + this._margins.bottom,
this._margins.left + this._margins.right
) /
2;
this._bounds = this._container
.append("g")
.style(
"transform",
`translate(${this._margins.left + this._boundedWidth / 2}px, ${
this._margins.top + this._boundedHeight / 2
}px)`
);
// this._bounds
// .append("rect")
// .attr("width", this._boundedWidth)
// .attr("height", this._boundedHeight)
// .attr("x", -this._boundedRadius)
// .attr("y", -this._boundedRadius);
}
_getCoordinatesForAngle(angle, r = this._boundedRadius, offset = 1) {
return [Math.cos(angle) * r * offset, Math.sin(angle) * r * offset];
}
_process() {
this._chartData = [...this._data];
}
_init() {
this._updateBounds();
// Scales
this._axesLabels = this._chartData.map(this._axisAccessor);
const effectiveAngleOffset = this._baseAngleOffset + this._angleOffset;
this._angleScale = d3
.scaleBand()
.domain(this._axesLabels)
.range([effectiveAngleOffset, Math.PI * 2 + effectiveAngleOffset]);
const maxValue = Math.max(
this._maxValue,
...this._chartData.map(this._valueAccessor)
);
this._radiusScale = d3
.scaleLinear()
.domain([0, maxValue])
.range([0, this._boundedRadius]);
// Curves
this._radarLine = d3
.lineRadial()
.curve(this._curve)
.radius((d) => this._radiusScale(this._valueAccessor(d)))
.angle((d) => Math.PI / 2 + this._angleScale(this._axisAccessor(d)));
}
_renderBg() {
this._container
.insert("rect", ":first-child")
.attr("fill", this._palette.bg)
.attr("width", this._width)
.attr("height", this._height);
}
_renderAxis() {
const peripherals = this._bounds.append("g").attr("class", "peripherals");
// Add bg circles
// peripherals
// .append("circle")
// .attr("r", this._boundedRadius)
// .attr("fill", "white");
// peripherals
// .append("circle")
// .attr("r", this._boundedRadius)
// .attr("fill", opacify(this._palette.gridAccent, 0.075));
// peripherals
// .append("circle")
// .attr("r", this._radiusScale(this._baselineValue))
// .attr("fill", "white");
// Add tick circles
const ticks = this._radiusScale.ticks(5);
ticks.forEach((r) => {
if (!r) return;
const tick = peripherals.append("g");
tick
.append("circle")
.attr("class", "tick-circle")
.attr("r", this._radiusScale(r))
.attr("data-radius", this._radiusScale(r))
.attr("fill", "none")
.attr("stroke", this._palette.grid)
.attr("stroke-dasharray", this._strokeDasharray);
const [_, max] = this._radiusScale.domain();
if (r >= max) return; // Don't draw last tick label
tick
.append("text")
.attr("x", 3)
.attr("y", -this._radiusScale(r) - 4)
.attr("class", "tick-label")
.style("fill", opacify(this._palette.text, 0.5))
.style("font-family", this._fontFamily)
.style("font-size", "0.75rem")
.text(r);
});
const getCoords = (d) => this._getCoordinatesForAngle(this._angleScale(d));
console.log(this._axesLabels);
// Add grid lines
peripherals
.selectAll("line")
.data(this._axesLabels)
.join("line")
.attr("stroke-dasharray", this._strokeDasharray)
.style("stroke", this._palette.grid)
.each(function (d) {
// console.log(d);
const [x2, y2] = getCoords(d);
d3.select(this).attr("x2", x2).attr("y2", y2);
});
// Add Axis labels
const labelRadiusScale = 1.1;
const getCoordsForText = (d) =>
this._getCoordinatesForAngle(
this._angleScale(d),
this._boundedRadius,
labelRadiusScale
);
// setTimeout(() => {
peripherals
.append("g")
.attr("class", "axis-labels")
.selectAll("text")
.data(this._axesLabels)
.join("text")
.each(function (d) {
const [x, y] = getCoordsForText(d);
d3.select(this)
.attr("x", x)
.attr("y", y)
.style(
"text-anchor",
Math.abs(x) < 5 ? "middle" : x > 0 ? "start" : "end"
);
})
.text((d) => d)
.style("fill", this._palette.text)
.style("font-family", this._fontFamily)
.style("font-size", "0.75rem")
.style("dominant-baseline", "middle")
.attr("dy", "0em")
// .call(wrap, this._labelWidth);
// });
}
_renderPlots() {
const plots = this._bounds.append("g");
const plot = plots.append("g");
// Add curve
plot
.append("g")
.attr("fill", opacify(this._palette.line, 0.15))
.attr("stroke", this._palette.line)
.append("path")
.attr("d", () => this._radarLine(this._chartData));
console.log(this._chartData);
// Add dots
// const getCoordsForPlot = (d) =>
// this._getCoordinatesForAngle(
// this._angleScale(this._axisAccessor(d)),
// this._radiusScale(this._valueAccessor(d))
// );
// plot
// .append("g")
// .attr("fill", this._palette.dot)
// .selectAll("circle")
// .data(this._chartData)
// .join("circle")
// .attr("r", this._dotRadius)
// .each(function (d) {
// const [cx, cy] = getCoordsForPlot(d);
// d3.select(this).attr("cx", cx).attr("cy", cy);
// })
// .attr(
// "title",
// (d) => `${this._axisAccessor(d)}: ${this._valueAccessor(d)}`
// );
}
render() {
this._process();
this._init();
// this._renderBg();
this._renderAxis();
this._renderPlots();
return this;
}
}