Jump to content

Bar Gauge - Need a little script and data page help


Recommended Posts

I am implementing a new Bar Gauge from the same source that provided my Progress Bar.

Not all Bars are the same;)

I have it part-way working but my JS skills are not that great. I promise to share the solution with all when it is complete and tested.

Use Case: The Member has a BMI (Body Mass Index), in this example 25. The Bar Gauge will show that number and where it falls along a scale of varying weight attributes and colors.

I have added an HTML Block to my data page with the following code:

/**    This sets the config of the Bar Graph and passes BMI to the controlling script
*        for proccessing and formatting. ALL Formatting should be done here.
*        The controlling script (in the Header) formats and then renders the Graph to this element. */

        let el = document.querySelector('.barGaugeWrapper');
        let linearGauge = new linearBarGauge(el, {
            width: 400,
            height: 10,
            outerHeight: 100,
            range: {
                start: 0,
                end: 100
            },
            segments: [
                [10, '#FF6347'],
                [30, '#FFC107'],
                [50, '#32CD32'],
                [70, '#FFC107'],
                [90, '#FF6347']
            ],
            value: [@field:BMI#]
        });
            // .drawPointer(4.1, '#000');

 

In the Header on the data page, I added: (it may seem overwhelming, but it's not, you just need to read through it)


'use strict';
class linearBarGauge {

    /** * DOM element object */
    element;

    /*** Options object given from user*/
    options = {};

    /**
     * Constructor, gets the selected element and the options given to generate the bar onto the page
     *
     * @param element
     * @param options
     * @return Object barSlider
     */
    constructor (element, options = {}) {
        // start with a 0 to 100 percent
        this.inputLow  = 0;
        this.inputHigh = 100;
        this.element   = element;

        if (!this.constructor.isEmpty(options)) {
            this.options = options;
        } else {
            // default options
            this.options = {
                width: 300,     // canvas width
                height: 10,     // bar height
                outerHeight: 50,     // canvas height
                range: {     // range that the graph would start and end
                    start: 1,
                    end: 5
                },
                // optional additional segments
                /*segments: [
                    {
                        start: 1,
                        end: 3,
                        colour: '#32CD32'
                    },
                    {
                        start: 3,
                        end: 4,
                        colour: '#FFA500'
                    },
                    {
                        start: 4,
                        end: 5,
                        colour: '#FF6347'
                    }
                ],*/
                segments: [
                    [20, '#32CD32'],
                    [50, '#FFC107'],
                    [80, '#FF6347']
                ],
                value: [@field:BMI#]    // exact value shown in the graph
            }
        }

        this.init();

        return this;
    }

    init () {
        // create and append the canvas element
        this.canvas = document.createElement('canvas');
        this.canvas.width = this.options.width;
        this.canvas.height = this.options.outerHeight;
        this.canvas.className = 'lineGaugeCanvas';

        this.element.append(this.canvas);

        // create the canvas
        this.drawCanvas();

        if (this.options.value) {
            this.drawPointer(this.options.value);
        }
    }

    /** * Creates the range of */
    translateRange(Input , inputHigh , inputLow , outputHigh , OutputLow) {
        inputHigh = inputHigh ? inputHigh : this.inputHigh;
        inputLow = inputLow ? inputLow : this.inputLow;

        outputHigh =  outputHigh ? outputHigh : 1;
        OutputLow = OutputLow ? OutputLow : 0;

        return ((Input - inputLow) / (inputHigh - inputLow)) *
            (outputHigh - OutputLow) + OutputLow;
    }

    drawCanvas () {
        let stops = this.options.segments;

        // setup drawing context
        let ctx = this.canvas.getContext("2d");

        // define the gradient
        let gradient = ctx.createLinearGradient(
            0, 0, this.canvas.width, 0
        );

        // draw stops from an array
        // where every item is an array contains
        // the position and the color of the gradient
        for (let i = 0; i < stops.length; i++) {
            gradient.addColorStop(
                this.translateRange(stops[i][0]),
                stops[i][1]
            );
        }

        // defines the fill style on canvas
        ctx.fillStyle = gradient;

        // create the y = 0 position on the canvas (currently at 2/3rds of the canvas' overall height)
        this.zero_y_postion = ((this.options.outerHeight - this.options.height) / 1.5);

        // draw the a rect filled with created gradient
        ctx.fillRect(0, this.zero_y_postion, this.canvas.width, this.options.height);

        return this;
    }

    /**
    *  Draws the value pointer on the graph
    */
    drawPointer (value,color) {
        // convert input value into a percent value inside our predefined range
        value = Number((value / this.options.range.end) * 100);
        // setup drawing context
        let ctx = this.canvas.getContext("2d");

        const height = this.options.height ? this.options.height : 10;

        ctx.strokeStyle = color ? color : '#000';
        ctx.lineWidth = 3;

        // draw line indicate a value
        ctx.beginPath();

        const start_x = Number(this.translateRange(
            value,
            this.inputHigh,
            this.inputLow,
            this.canvas.width,
            0
        ));

        ctx.moveTo(
            start_x, // x1
            this.zero_y_postion // y1
        );

        ctx.lineTo(
            start_x, // x2
            this.zero_y_postion + height//y2
        );

        ctx.stroke();

        this.drawValue(start_x);

        return this;
    }

    /** * Draw value box and number *
     * @param {Number} start_x The starting x position
     */
    drawValue (start_x) {
        const rect_w = 45;
        const rect_h = 35;
        // get the starting x point of the rectangle
        const rect_x_start = start_x - (rect_w/2);

        let ctx = this.canvas.getContext("2d");

        // add the rectangle with the value
        ctx.lineWidth = 2;

        this.roundRect(
            rect_x_start,
            this.zero_y_postion - rect_h - ctx.lineWidth + 1,
            rect_w, // w
            rect_h,  // h
            8
        );

        // add text
        const font_size = 25; // in pixels
        ctx.fillStyle = "red";
        ctx.font = font_size + "px sans-serif";
        ctx.fillText(
            this.options.value,
            Number(rect_x_start + ((rect_w - font_size) / 4)), // center the text inside the rectangle
            this.zero_y_postion - (rect_h - font_size) + (ctx.lineWidth / 2) // get y = 0 position, move to
        );
    }

    /**
     * Draws a rounded rectangle using the current state of the canvas.
     * If you omit the last three params, it will draw a rectangle
     * outline with a 5 pixel border radius
     * Method from : https://stackoverflow.com/a/3368118
     *
     * @param {Number} x The top left x coordinate
     * @param {Number} y The top left y coordinate
     * @param {Number} width The width of the rectangle
     * @param {Number} height The height of the rectangle
     * @param {Number} [radius = 5] The corner radius; It can also be an object
     *                 to specify different radii for corners
     * @param {Number} [radius.tl = 0] Top left
     * @param {Number} [radius.tr = 0] Top right
     * @param {Number} [radius.br = 0] Bottom right
     * @param {Number} [radius.bl = 0] Bottom left
     * @param {Boolean} [fill = false] Whether to fill the rectangle.
     * @param {Boolean} [stroke = true] Whether to stroke the rectangle.
     */
    roundRect (x, y, width, height, radius, fill, stroke) {
        let ctx = this.canvas.getContext("2d");

        if (typeof stroke == 'undefined') {
            stroke = true;
        }
        if (typeof radius === 'undefined') {
            radius = 5;
        }
        if (typeof radius === 'number') {
            radius = {tl: radius, tr: radius, br: radius, bl: radius};
        } else {
            var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
            for (var side in defaultRadius) {
                radius[side] = radius[side] || defaultRadius[side];
            }
        }
        ctx.beginPath();
        ctx.moveTo(x + radius.tl, y);
        ctx.lineTo(x + width - radius.tr, y);
        ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
        ctx.lineTo(x + width, y + height - radius.br);
        ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
        ctx.lineTo(x + radius.bl, y + height);
        ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
        ctx.lineTo(x, y + radius.tl);
        ctx.quadraticCurveTo(x, y, x + radius.tl, y);
        ctx.closePath();
        if (fill) {
            ctx.fill();
        }
        if (stroke) {
            ctx.stroke();
        }
    }

    static isEmpty(obj) {
        for(let key in obj) {
            if(obj.hasOwnProperty(key))
                return false;
        }
        return true;
    }
}

 

Any help will be appreciated.

 

Lynda

Link to comment
Share on other sites

Hi Lynda,

I am not familiar with the Canvas element with JS and HTML, but it has been an interesting read. While I am not sure why the class you provided isn't working fine, it looks like the class expected in the selector inside the HTML block and the class being created are different. In addition, it may be simpler to add a wrapper for the canvas and then just select it when calling the class.

In any case, I stumbled upon this SO post which has a very neat example for a gauge style chart which may fit your use case. The value provided to it is at the bottom where the Chart is created. It uses this library https://github.com/kluverua/Chartjs-tsgauge .

https://stackoverflow.com/a/58932841

Hope this helps.

Link to comment
Share on other sites

Ok... so here goes. Remember I said my JS skills are not great (but once I get a concept, I'm good to go).

I switched the two pieces of code.

1.  I put the Instance script (where the .barGaugeWrapper is selected and then an instance of linearBarGauge is created, in the header. <== This doesn't make sense to me. I think it needs to be in the HTML Block2, but I tried it anyway

2.  I mapped value: [@field:BMI#]

3.  I put the Main script in the HTML Block2 and wrapped it in a Div class called "barGaugeWrapper" <== I tried this in the header, but it didn't work there, although I think it needs to be in the header.

Basically looks like this:

<div class="barGaugeWrapper">
<script>
class linearBarGauge {...

</div>

4.  All of my problems center around these configurations. Once I can get past what goes where and what is called what, the rest should be easy.

I truely appreciate any help and all of your time and effort. I believe that many will find this useful in the long run.

Lynda

BarGauge_Instance BarGauge_Main.js

Link to comment
Share on other sites

For some reason, it is working for me.

bargauge.PNG.a6222d98a0d56ca2bf2a48f096971a25.PNG

Here is what I did. In the Header (check the value where you put your BMI field):

<script>

'use strict';
class linearBarGauge {

    /** * DOM element object */
    element;

    /*** Options object given from user*/
    options = {};

    /**
     * Constructor, gets the selected element and the options given to generate the bar onto the page
     *
     * @param element
     * @param options
     * @return Object barSlider
     */
    constructor (element, options = {}) {
        // start with a 0 to 100 percent
        this.inputLow  = 0;
        this.inputHigh = 100;
        this.element   = element;

        if (!this.constructor.isEmpty(options)) {
            this.options = options;
        } else {
            // default options
            this.options = {
                width: 300,     // canvas width
                height: 10,     // bar height
                outerHeight: 50,     // canvas height
                range: {     // range that the graph would start and end
                    start: 1,
                    end: 5
                },
                // optional additional segments
                /*segments: [
                    {
                        start: 1,
                        end: 3,
                        colour: '#32CD32'
                    },
                    {
                        start: 3,
                        end: 4,
                        colour: '#FFA500'
                    },
                    {
                        start: 4,
                        end: 5,
                        colour: '#FF6347'
                    }
                ],*/
                segments: [
                    [20, '#32CD32'],
                    [50, '#FFC107'],
                    [80, '#FF6347']
                ],
                value: 22    // I changed this to a static number since I don't have a field readily available to work with this. Maybe try parseFloat([@field:BMI]) 
            }
        }

        this.init();

        return this;
    }

    init () {
        // create and append the canvas element
        this.canvas = document.createElement('canvas');
        this.canvas.width = this.options.width;
        this.canvas.height = this.options.outerHeight;
        this.canvas.className = 'lineGaugeCanvas';

        this.element.append(this.canvas);

        // create the canvas
        this.drawCanvas();

        if (this.options.value) {
            this.drawPointer(this.options.value);
        }
    }

    /** * Creates the range of */
    translateRange(Input , inputHigh , inputLow , outputHigh , OutputLow) {
        inputHigh = inputHigh ? inputHigh : this.inputHigh;
        inputLow = inputLow ? inputLow : this.inputLow;

        outputHigh =  outputHigh ? outputHigh : 1;
        OutputLow = OutputLow ? OutputLow : 0;

        return ((Input - inputLow) / (inputHigh - inputLow)) *
            (outputHigh - OutputLow) + OutputLow;
    }

    drawCanvas () {
        let stops = this.options.segments;

        // setup drawing context
        let ctx = this.canvas.getContext("2d");

        // define the gradient
        let gradient = ctx.createLinearGradient(
            0, 0, this.canvas.width, 0
        );

        // draw stops from an array
        // where every item is an array contains
        // the position and the color of the gradient
        for (let i = 0; i < stops.length; i++) {
            gradient.addColorStop(
                this.translateRange(stops[i][0]),
                stops[i][1]
            );
        }

        // defines the fill style on canvas
        ctx.fillStyle = gradient;

        // create the y = 0 position on the canvas (currently at 2/3rds of the canvas' overall height)
        this.zero_y_postion = ((this.options.outerHeight - this.options.height) / 1.5);

        // draw the a rect filled with created gradient
        ctx.fillRect(0, this.zero_y_postion, this.canvas.width, this.options.height);

        return this;
    }

    /**
    *  Draws the value pointer on the graph
    */
    drawPointer (value,color) {
        // convert input value into a percent value inside our predefined range
        value = Number((value / this.options.range.end) * 100);
        // setup drawing context
        let ctx = this.canvas.getContext("2d");

        const height = this.options.height ? this.options.height : 10;

        ctx.strokeStyle = color ? color : '#000';
        ctx.lineWidth = 3;

        // draw line indicate a value
        ctx.beginPath();

        const start_x = Number(this.translateRange(
            value,
            this.inputHigh,
            this.inputLow,
            this.canvas.width,
            0
        ));

        ctx.moveTo(
            start_x, // x1
            this.zero_y_postion // y1
        );

        ctx.lineTo(
            start_x, // x2
            this.zero_y_postion + height//y2
        );

        ctx.stroke();

        this.drawValue(start_x);

        return this;
    }

    /** * Draw value box and number *
     * @param {Number} start_x The starting x position
     */
    drawValue (start_x) {
        const rect_w = 45;
        const rect_h = 35;
        // get the starting x point of the rectangle
        const rect_x_start = start_x - (rect_w/2);

        let ctx = this.canvas.getContext("2d");

        // add the rectangle with the value
        ctx.lineWidth = 2;

        this.roundRect(
            rect_x_start,
            this.zero_y_postion - rect_h - ctx.lineWidth + 1,
            rect_w, // w
            rect_h,  // h
            8
        );

        // add text
        const font_size = 25; // in pixels
        ctx.fillStyle = "red";
        ctx.font = font_size + "px sans-serif";
        ctx.fillText(
            this.options.value,
            Number(rect_x_start + ((rect_w - font_size) / 4)), // center the text inside the rectangle
            this.zero_y_postion - (rect_h - font_size) + (ctx.lineWidth / 2) // get y = 0 position, move to
        );
    }

    /**
     * Draws a rounded rectangle using the current state of the canvas.
     * If you omit the last three params, it will draw a rectangle
     * outline with a 5 pixel border radius
     * Method from : https://stackoverflow.com/a/3368118
     *
     * @param {Number} x The top left x coordinate
     * @param {Number} y The top left y coordinate
     * @param {Number} width The width of the rectangle
     * @param {Number} height The height of the rectangle
     * @param {Number} [radius = 5] The corner radius; It can also be an object
     *                 to specify different radii for corners
     * @param {Number} [radius.tl = 0] Top left
     * @param {Number} [radius.tr = 0] Top right
     * @param {Number} [radius.br = 0] Bottom right
     * @param {Number} [radius.bl = 0] Bottom left
     * @param {Boolean} [fill = false] Whether to fill the rectangle.
     * @param {Boolean} [stroke = true] Whether to stroke the rectangle.
     */
    roundRect (x, y, width, height, radius, fill, stroke) {
        let ctx = this.canvas.getContext("2d");

        if (typeof stroke == 'undefined') {
            stroke = true;
        }
        if (typeof radius === 'undefined') {
            radius = 5;
        }
        if (typeof radius === 'number') {
            radius = {tl: radius, tr: radius, br: radius, bl: radius};
        } else {
            var defaultRadius = {tl: 0, tr: 0, br: 0, bl: 0};
            for (var side in defaultRadius) {
                radius[side] = radius[side] || defaultRadius[side];
            }
        }
        ctx.beginPath();
        ctx.moveTo(x + radius.tl, y);
        ctx.lineTo(x + width - radius.tr, y);
        ctx.quadraticCurveTo(x + width, y, x + width, y + radius.tr);
        ctx.lineTo(x + width, y + height - radius.br);
        ctx.quadraticCurveTo(x + width, y + height, x + width - radius.br, y + height);
        ctx.lineTo(x + radius.bl, y + height);
        ctx.quadraticCurveTo(x, y + height, x, y + height - radius.bl);
        ctx.lineTo(x, y + radius.tl);
        ctx.quadraticCurveTo(x, y, x + radius.tl, y);
        ctx.closePath();
        if (fill) {
            ctx.fill();
        }
        if (stroke) {
            ctx.stroke();
        }
    }

    static isEmpty(obj) {
        for(let key in obj) {
            if(obj.hasOwnProperty(key))
                return false;
        }
        return true;
    }
}

</script>

 

In the HTML Block:

<div class="barGaugeWrapper"></div> <!-- Make sure you have an element with a class of "barGaugeWrapper" for the bar gauge to append to -->

<script>
let el = document.querySelector('.barGaugeWrapper');
        let linearGauge = new linearBarGauge(el, {
            width: 400,
            height: 10,
            outerHeight: 100,
            range: {
                start: 0,
                end: 100
            },
            segments: [
                [10, '#FF6347'],
                [30, '#FFC107'],
                [50, '#32CD32'],
                [70, '#FFC107'],
                [90, '#FF6347']
            ],
            value: 22
        });

</script>

 

And in the footer, I included the canvas CDN:

<script src="https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

 

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Loading...
×
×
  • Create New...