3
\$\begingroup\$

(See also An extended JavaScript plugin for rendering radial pie charts.)

I have this JavaScript plugin:

rodde-radial-pie-chart.js:

class RadialPieChart {
    
    static #rgb_alphabet_set = new Set();

    #canvas_id;
    #entries = [];
    #maximum_radius;
    #start_angle;
    #empty_pie_chart_color = "";
    #canvas_background_color = "";

    static {
        this.#rgb_alphabet_set.add("0");
        this.#rgb_alphabet_set.add("1");
        this.#rgb_alphabet_set.add("2");
        this.#rgb_alphabet_set.add("3");
        this.#rgb_alphabet_set.add("4");
        this.#rgb_alphabet_set.add("5");
        this.#rgb_alphabet_set.add("6");
        this.#rgb_alphabet_set.add("7");
        this.#rgb_alphabet_set.add("8");
        this.#rgb_alphabet_set.add("9");
        this.#rgb_alphabet_set.add("a");
        this.#rgb_alphabet_set.add("b");
        this.#rgb_alphabet_set.add("c");
        this.#rgb_alphabet_set.add("d");
        this.#rgb_alphabet_set.add("e");
        this.#rgb_alphabet_set.add("f");
    }

    constructor(canvas_id,
                maximum_radius = 100,
                start_angle = 0.0, 
                empty_pie_chart_color = "#000",
                canvas_background_color = "#fff") {

        this.#canvas_id = canvas_id;

        this.#maximum_radius = 
            RadialPieChart.#validateMaximumRadius(maximum_radius);

        start_angle %= 360.0;

        if (start_angle < 0.0) {
            start_angle += 360.0;
        }

        this.#start_angle = start_angle;

        this.#empty_pie_chart_color = 
            RadialPieChart.#validateColor(empty_pie_chart_color);

        this.#canvas_background_color =
            RadialPieChart.#validateColor(canvas_background_color); 
    }
    
    addEntry(value, color) {
        const entry = {
            "value": RadialPieChart.#validateValue(value),
            "color": RadialPieChart.#validateColor(color)
        };

        this.#entries.push(entry);
    }

    getMaximumRadius() {
        return this.#maximum_radius;
    }

    getStartAngle() {
        return this.#start_angle;
    }

    getValueAt(index) {
        this.#checkIndex(index);
        return this.#entries[index]["value"];
    }

    getColorAt(index) {
        this.#checkIndex(index);
        return this.#entries[index]["color"];
    }

    setStartAngle(start_angle) {
        this.#start_angle = start_angle;
        this.#start_angle %= 360.0;

        if (this.#start_angle < 0.0) {
            this.#start_angle += 360.0;
        }
    }

    setMaximumRadius(new_maximum_radius) {
        this.#validateValue(new_maximum_radius);
        this.#maximum_radius = new_maximum_radius;
    }

    setValueAt(index, new_value) {
        this.#checkIndex(index);
        this.#entries[index]["value"] = this.#validateValue(new_value);
    }

    setColorAt(index, new_color) {
        this.#checkIndex(index);
        this.#entries[index]["color"] = this.#validateColor(new_color);
    }

    removeEntry(index) {
        if (this.#entries.length === 0) {
            throw "Removing from an empty list.";
        }

        this.#checkIndex(index);
        this.#entries.splice(index, 1);
    }

    render() {
        const canvas = document.getElementById(this.#canvas_id);
        const ctx = canvas.getContext("2d");

        ctx.canvas.width  = this.#maximum_radius * 2;
        ctx.canvas.height = this.#maximum_radius * 2;

        // Fill the entire chart background:
        ctx.fillStyle = this.#canvas_background_color;
        ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);

        if (this.#entries.length === 0) {
            // Once here, there is no entries in the list.
            // Just fill the circle with empty_pie_chart_color:
            this.#fillEmptyCircle(ctx);
        } else {
            this.#renderEntries(ctx);
        }
    }
    
    static #validateValue(value) {
        if (value < 0) {
            throw "Value (" + value + ") can not be negative.";
        }
        
        return value;
    }
    
    static #validateColor(color) {
        color = color.trim().toLowerCase();
        
        if (color.length != 4 && color.length != 7) {
            throw "Invalid color string. Color string of length "
                    + color.length
                    + ". Must be 4 or 7.";
        }
        
        if (color[0] !== "#") {
            throw "Invalid color string. The first character must be " + 
                  "the hash #.";
        }
        
        for (let ch in color) {
            if (!this.#rgb_alphabet_set.has(ch)) {
                throw "Character " + ch + " is not allowed in RGB values.";
            }
        }
        
        return color;
    }

    static #validateMaximumRadius(maximum_radius) {
        if (maximum_radius <= 0) {
            throw "Maximum radius (" + maximum_radius + ") is not positive.";
        }

        return maximum_radius;
    }

    #checkIndex(index) {
        if (this.#entries.length === 0) {
            throw "Indexing an empty list.";
        }

        if (index < 0) {
            throw "Index (" + index + ") cannot be negative.";
        }

        if (index >= this.#entries.length) {
            throw "Index (" + index + ") is too large. Must be at most "
                  + this.#entries.length + ".";
        }
    }

    #fillEmptyCircle(ctx) {
        const width = ctx.canvas.width;
        const height = ctx.canvas.height;
        const centerX = width / 2;
        const centerY = height / 2;
        const radius = width / 2;

        ctx.beginPath();
        ctx.arc(centerX,
                centerY,
                radius,
                0.0,
                2 * Math.PI, 
                false);

        ctx.fillStyle = this.#empty_pie_chart_color;
        ctx.fill();
    }

    #renderEntries(ctx) {
        const angle_per_entry = 360.0 / this.#entries.length;

        for (var i = 0, n = this.#entries.length; i !== n; i++) {
            var entry_start_angle = this.#start_angle + angle_per_entry * i;
            var entry_end_angle = entry_start_angle + angle_per_entry;

            entry_start_angle = Math.PI * (entry_start_angle / 180.0);
            entry_end_angle = Math.PI * (entry_end_angle / 180.0);
            
            this.#drawEntry(ctx,
                            entry_start_angle, 
                            entry_end_angle,
                            this.#entries[i]["value"],
                            this.#entries[i]["color"]);
        }
    }

    #drawEntry(ctx, start_angle, end_angle, value, color) {
        const centerX = this.#maximum_radius;
        const centerY = this.#maximum_radius;
        const maximum_value = this.#findMaximumValue();
        const radius = this.#maximum_radius * (value / maximum_value);

        ctx.beginPath();
        ctx.arc(centerX, centerY, radius, start_angle, end_angle);
        ctx.lineTo(centerX, centerY);
        ctx.fillStyle = color;
        ctx.fill();
    }

    #findMaximumValue() {
        var maximum_value = 0;

        for (var i = 0, n = this.#entries.length; i !== n; i++) {
            const tentative_maximum_value = this.#entries[i]["value"];
            maximum_value = Math.max(maximum_value, tentative_maximum_value);
        }

        return maximum_value;
    }
}

You can use it like this:

<!DOCTYPE html>
<html>
    <head>
        <title>Radial pie chart demo</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    </head>
    <body>
        <canvas id="radial-pie-chart-canvas"></canvas>
        <script src="rodde-radial-pie-chart.js"></script>
        <script>
            let rpc = new RadialPieChart("radial-pie-chart-canvas");
            rpc.setStartAngle(36.0);
            rpc.addEntry(50, "#f00");
            rpc.addEntry(30, "#a257f0")
            rpc.addEntry(25, "#06771f");
            rpc.addEntry(17, "#0ff");
            rpc.addEntry(23, "#ddd");
            rpc.render();
        </script>
    </body>
</html>

... which looks like this:

screenshot of sample radial pie chart

Critique request

As always, I would like to hear any comments, especially those regarding my JS routine.

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Bearing in mind that this code has successor code in An extended JavaScript plugin for rendering radial pie charts, which appears to have been modified significantly, I still noticed review aspects.

D.R.Y. it out

It appears a few of your other posts mention concerns about DRYness. It has a bit of redundancy - at least in the #static method:

static {
    this.#rgb_alphabet_set.add("0");
    this.#rgb_alphabet_set.add("1");
    this.#rgb_alphabet_set.add("2");
    this.#rgb_alphabet_set.add("3");
    this.#rgb_alphabet_set.add("4");
    this.#rgb_alphabet_set.add("5");
    this.#rgb_alphabet_set.add("6");
    this.#rgb_alphabet_set.add("7");
    this.#rgb_alphabet_set.add("8");
    this.#rgb_alphabet_set.add("9");
    this.#rgb_alphabet_set.add("a");
    this.#rgb_alphabet_set.add("b");
    this.#rgb_alphabet_set.add("c");
    this.#rgb_alphabet_set.add("d");
    this.#rgb_alphabet_set.add("e");
    this.#rgb_alphabet_set.add("f");
}

Those repetitive lines can be simplified using a for loop using Number.toString():

static {
    for (let i=0; i < 16; i++) {
        this.#rgb_alphabet_set.add(i.toString(16));
    }
}

Consider using let instead of var when re-assignment is necessary

It is great that const is used in many places. There are some variables declared with var, which is fine for any variable that needs to be re-assigned - e.g. i, entry_start_angle, entry_end_angle, maximum_value, etc. Consider using let to have block- scoped variables instead of function scoped variables.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.