html,
body {
height: 100%;
}
body {
display: flex;
align-items: center;
justify-content: center;
background: #fafafa;
}
class MathUtils {
static distance(a, b) {
return Math.sqrt((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2);
}
static mix(values, weights) {
const n = values.length;
const tmp = new Array(n);
for (let i = 0; i < n; i++) {
const weight = weights[i];
const sum = values[i] * weight;
tmp[i] = [sum, weight];
}
let result = [0, 0];
for (let i = 0; i < n; i++) {
const p = tmp[i];
result = [p[0] + result[0], p[1] + result[1]];
}
return result[0] / result[1];
}
}
class ArrayUtils {
static minIndex(array) {
const n = array.length;
let index = 0;
for (let i = 1; i < n; i++) {
if (array[i] < array[index]) {
index = i;
}
}
return index;
}
static normalize(array, out) {
const n = array.length;
let sum = 0;
for (let i = 0; i < n; i++) {
sum += array[i];
}
const result = out || new Array(n);
for (let i = 0; i < n; i++) {
result[i] = array[i] / sum;
}
return result;
}
}
class Color {
// eslint-disable-next-line sonarjs/cognitive-complexity
static hslToRgb(h, s, l) {
let r;
let g;
let b;
if (s === 0) {
r = l;
g = l;
b = l;
} else {
const hueToRgb = (p, q, t) => {
if (t < 0) {
t += 1; // eslint-disable-line no-param-reassign
}
if (t > 1) {
t -= 1; // eslint-disable-line no-param-reassign
}
if (t < 1 / 6) {
return p + (q - p) * 6 * t;
}
if (t < 1 / 2) {
return q;
}
if (t < 2 / 3) {
return p + (q - p) * (2 / 3 - t) * 6;
}
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hueToRgb(p, q, h + 1 / 3);
g = hueToRgb(p, q, h);
b = hueToRgb(p, q, h - 1 / 3);
}
return [
Math.round(r * 255),
Math.round(g * 255),
Math.round(b * 255),
];
}
}
class RadarChart {
static #DOT_RADIUS = 3;
static #GRID_LINE_WIDTH = 1;
static #POLYGON_LINE_WIDTH = 2;
static #CENTER_HUE = -0.09;
static #EDGE_HUE = 0.4;
static #SATURATION = 0.9;
static #LIGHTNESS = 0.5;
static #LINE_COLOR = '#111111';
static #isInsidePolygon(point, polygon) {
let minX = polygon[0][0];
let maxX = polygon[0][0];
let minY = polygon[0][1];
let maxY = polygon[0][1];
const n = polygon.length;
for (let i = 0; i < n; i++) {
const p = polygon[i];
minX = Math.min(p[0], minX);
maxX = Math.max(p[0], maxX);
minY = Math.min(p[1], minY);
maxY = Math.max(p[1], maxY);
}
if ((point[0] < minX) || (point[0] > maxX)
|| (point[1] < minY) || (point[1] > maxY)) {
return false;
}
let isInside = false;
let i = 0;
let j = n - 1;
for (i, j; i < n; j = i++) {
if (((polygon[i][1] > point[1]) !== (polygon[j][1] > point[1]))
&& (point[0] < ((polygon[j][0] - polygon[i][0])
* (point[1] - polygon[i][1]))
/ (polygon[j][1] - polygon[i][1]) + polygon[i][0])) {
isInside = !isInside;
}
}
return isInside;
}
static #calculatePoints(size, values) {
const { sin, cos } = Math;
const n = values.length;
const cx = size / 2;
const cy = size / 2;
const radius = size / 2 - 2 * RadarChart.#DOT_RADIUS;
const scale = radius / 100;
const numberOfAxes = n;
const angleBetweenAxes = (2 * Math.PI) / numberOfAxes;
const gridLines = new Array(n + n * 4);
const polygon = new Array(n);
for (let i = 0; i < n; i++) {
const value = values[i];
const a = i * angleBetweenAxes;
const b = a + angleBetweenAxes;
gridLines[5 * i] = [
[cx, cy],
[cx + radius * sin(a), cy - radius * cos(a)],
];
for (let j = 1; j <= 4; j++) {
const r = radius * (j * (25 / 100));
gridLines[5 * i + j] = [
[cx + r * sin(a), cy - r * cos(a)],
[cx + r * sin(b), cy - r * cos(b)],
];
}
polygon[i] = [
cx + value * scale * sin(a),
cy - value * scale * cos(a),
];
}
return { gridLines, polygon };
}
static #drawGrid(context, gridLines) {
const n = gridLines.length;
context.lineWidth = RadarChart.#GRID_LINE_WIDTH;
context.beginPath();
for (let i = 0; i < n; i++) {
const line = gridLines[i];
context.moveTo(line[0][0], line[0][1]);
context.lineTo(line[1][0], line[1][1]);
}
context.stroke();
}
static #drawPoints(context, points) {
const n = points.length;
for (let i = 0; i < n; i++) {
const point = points[i];
context.beginPath();
context.arc(
point[0],
point[1],
RadarChart.#DOT_RADIUS,
0,
2 * Math.PI,
);
context.fill();
}
}
static #strokePolygon(context, polygon) {
const n = polygon.length;
context.lineWidth = RadarChart.#POLYGON_LINE_WIDTH;
context.beginPath();
context.moveTo(polygon[0]);
for (let i = 0; i <= n; i++) {
context.lineTo(polygon[i % n]);
}
context.stroke();
}
// eslint-disable-next-line sonarjs/cognitive-complexity
static #fillPolygon(context, rawPolygon) {
const size = context.canvas.width;
const imageData = context.createImageData(size, size);
const { data } = imageData;
const n = rawPolygon.length;
const centerHue = RadarChart.#CENTER_HUE;
const edgeHue = RadarChart.#EDGE_HUE;
const r = size / 2;
const ratio = window.devicePixelRatio;
const polygon = new Array(n);
for (let i = 0; i < n; i++) {
polygon[i] = [
rawPolygon[i][0] * ratio,
rawPolygon[i][1] * ratio,
];
}
const hues = new Array(n);
for (let i = 0; i < n; i++) {
hues[i] = centerHue + (edgeHue - centerHue)
* (MathUtils.distance(polygon[i], [r, r]) / r);
}
const distances = new Array(n);
const rawWeights = new Array(n);
const weights = new Array(n);
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
if (RadarChart.#isInsidePolygon([x, y], polygon)) {
for (let i = 0; i < n; i++) {
distances[i] = MathUtils.distance(polygon[i], [x, y]);
}
for (let i = 0; i < n; i++) {
rawWeights[i] = Math.tan(
(size - distances[i]) / size,
) ** 4;
}
ArrayUtils.normalize(rawWeights, weights);
const hue = MathUtils.mix(hues, weights);
const color = Color.hslToRgb(
hue,
RadarChart.#SATURATION,
RadarChart.#LIGHTNESS,
);
const i = 4 * (y * size + x);
[data[i], data[i + 1], data[i + 2]] = color;
data[i + 3] = 255;
}
}
}
context.putImageData(imageData, 0, 0);
}
#canvas;
#context;
#values;
constructor(root, size, values) {
this.#canvas = document.createElement('canvas');
root.appendChild(this.#canvas);
this.#context = this.#canvas.getContext('2d');
this.#context.lineCap = 'round';
this.#context.lineJoin = 'round';
this.#context.strokeStyle = RadarChart.#LINE_COLOR;
this.#context.fillStyle = RadarChart.#LINE_COLOR;
this.#values = values;
this.draw(size);
}
draw(size) {
const ratio = window.devicePixelRatio;
this.#canvas.width = Math.floor(size * ratio);
this.#canvas.height = Math.floor(size * ratio);
this.#canvas.style.width = `${size}px`;
this.#canvas.style.height = `${size}px`;
this.#context.scale(ratio, ratio);
const { gridLines, polygon } = RadarChart.#calculatePoints(
size,
this.#values,
);
RadarChart.#fillPolygon(this.#context, polygon);
RadarChart.#strokePolygon(this.#context, polygon);
RadarChart.#drawGrid(this.#context, gridLines);
RadarChart.#drawPoints(this.#context, polygon);
}
}
function main() {
const dataSamples = [
[100, 50, 100],
[50, 100, 100, 75],
[50, 50, 75, 100, 0],
[100, 75, 0, 100, 25, 100],
[25, 100, 25, 100, 25, 100, 100],
[25, 100, 100, 75, 100, 100, 100, 100],
];
const n = dataSamples.length;
const charts = new Array(n);
let size = 0.9 * (window.innerWidth / dataSamples.length);
for (let i = 0; i < n; i++) {
const root = document.createElement('div');
charts[i] = new RadarChart(root, size, dataSamples[i]);
document.body.appendChild(root);
}
window.addEventListener('resize', () => {
size = 0.9 * (window.innerWidth / dataSamples.length);
for (let i = 0; i < n; i++) {
charts[i].draw(size);
}
});
}
document.addEventListener('DOMContentLoaded', main);
View Compiled
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.