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

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.