Lynda Posted December 16, 2023 Report Share Posted December 16, 2023 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 Quote Link to comment Share on other sites More sharing options...
DrSimi Posted December 19, 2023 Report Share Posted December 19, 2023 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. Lynda 1 Quote Link to comment Share on other sites More sharing options...
kpcollier Posted December 19, 2023 Report Share Posted December 19, 2023 Could you share what problems/errors you are encountering with the code you provided? Lynda 1 Quote Link to comment Share on other sites More sharing options...
Lynda Posted December 20, 2023 Author Report Share Posted December 20, 2023 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 Quote Link to comment Share on other sites More sharing options...
Lynda Posted December 20, 2023 Author Report Share Posted December 20, 2023 Oh... problems... I really don't think they are relevant, but "Uncaught Reference Error: linearBarGuage Not Defined" and Unexpected token "{" Quote Link to comment Share on other sites More sharing options...
kpcollier Posted December 20, 2023 Report Share Posted December 20, 2023 For some reason, it is working for me. 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> Lynda and DrSimi 2 Quote Link to comment Share on other sites More sharing options...
kpcollier Posted December 20, 2023 Report Share Posted December 20, 2023 Also, try closing the barGaugeWrapper div without putting the script in between in the HTML block. Lynda 1 Quote Link to comment Share on other sites More sharing options...
Lynda Posted December 20, 2023 Author Report Share Posted December 20, 2023 You are my hero! I got myself so twisted around, I feel foolish. It worked like a charm. I applied my modifications to it and got exactly what I needed. Thank you! Lynda kpcollier 1 Quote Link to comment Share on other sites More sharing options...
Recommended Posts
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.