H Bar Chart Widget

I haven't seen an example of an H Bar chart for Dashboard 2 so, as I wanted one, I put this together. The flow contains (real) example data and hopefully will work for others. Be aware that the CSS for each widget pollutes the whole page.

I am sure there are improvement opportunities so let me know if you find some

The H Bar chart can also be set up as a stacked H Bar chart

[{"id":"84c4a8c53f4017b3","type":"ui-template","z":"fe7296cf2a5f7fba","group":"2a9d414817265ee2","page":"","ui":"","name":"HBar Chart Example","order":3,"width":"2","height":"1","head":"","format":"<template>\n    <div >\n        <div class = \"chart-title\">{{chartTitle}}</div>\n        <div class = \"chart-container\"><canvas ref=\"chart\"/></div>\n    </div>  \n\n</template>\n\n<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>\n\n<script>\n    export default {\n        data() {\n            // define variables available component-wide\n            // (in <template> and component functions)\n            return {\n                isChartLoaded: false,\n                seriesCount: 0,\n\n                chartType: 'HBar',                              // This is what the widget was designed for, so untested as a vertical bar chart\n                isStacked: false,                               // false for a non-stacked H Bar chart\n                \n                /******** These options can be dynamically edited ********\n                 * Use msg.ui_update.<option name>\n                */\n                chartTitle: 'Hourly Energy Useage',\n\n                // Note that these are the reverse of a normal Bar chart\n                yAxisKey: 'period',                             // String value - label, 'y' is the default\n                xAxisKey: 'value',                              // Number value - data,  'x' is the default\n                seriesKey: '',                                  // '' is the default (none). Can be 'topic' or a\n                                                                // key in the incoming data\n\n                xAxisMin: 0,\n                xAxisMax: 0.5,\n\n                yAxisTitle: '',\n                xAxisTitle: '',\n\n                /**********************************************************/\n\n                isLegendDisplay: false,\n                autoSkipOn: false,\n\n                dataSets: [],\n                \n            }\n        },\n\n        watch: {\n             msg: function () {\n                if ( Object.hasOwn(this.msg, 'ui_update') ) {\n                    if (this.msg.ui_update?.chartTitle) this.setChartTitle(this.msg.ui_update.chartTitle)\n\n                    if (this.isChartLoaded) {\n                        this.setDynamicOptions(this.msg.ui_update)\n                    }\n\n                }       \n\n                if (this.isChartLoaded) {\n                    if (this.msg.payload !== undefined) this.onInput(this.msg)\n\n                }\n                \n            }\n        },\n    \n        computed: {\n            // Colours for the first 7 entries in both stacked and non-stacked charts\n            backgroundColours() {\n                let backgroundColour = [\n                                'rgba(255, 99, 132, 1.0)',\n                                'rgba(171, 199, 232, 1.0)',\n                                'rgba(255, 127, 14, 1.0)',\n                                'rgba(44, 160, 44, 1.0)',\n                                'rgba(152, 223, 158, 1.0)',\n                                'rgba(214, 39, 40, 1.0)',\n                                'rgba(255, 155, 150, 1.0)',\n                            ]\n\n                return backgroundColour\n    \n            },\n \n            borderColours() {\n                let borderColour = [\n                                'rgb(255, 99, 132)',\n                                'rgb(171, 199, 232)',\n                                'rgb(255, 127, 14)',\n                                'rgb(44, 160, 44)',\n                                'rgb(152, 223, 158)',\n                                'rgb(214, 39, 40)',\n                                'rgb(255, 155, 150)',\n\n                            ]\n\n                return borderColour\n    \n            },\n\n        },\n\n        methods: {\n            // Expose a method to our <template> and Vue Application\n            setDynamicOptions(uiOptions) {\n                const chartOptions = this.chart.options\n                for (const [option, value] of Object.entries(uiOptions)) {\n                    let optionKey = ''\n                    let update = {}\n                    if (option === 'xAxisTitle') {\n                        optionKey = chartOptions.scales.x.title\n                        update = {display: true, text: value}\n\n                    } \n\n                    if (option === 'yAxisTitle') {\n                        optionKey = chartOptions.scales.y.title\n                        update = {display: true, text: value}\n\n                    }\n\n                    if (option === 'xAxisMin') {\n                        chartOptions.scales.x.min = value\n\n                    }\n\n                    if (option === 'xAxisMax') {\n                        chartOptions.scales.x.max = value\n\n                    }\n\n                    if (option === 'yAxisKey') {\n                        chartOptions.parsing.yAxisKey = value\n\n                    }\n\n                    if (option === 'xAxisKey') {\n                        chartOptions.parsing.xAxisKey = value\n\n                    }\n\n                    updateOption(update, optionKey)\n\n                }\n\n                if (this.chart) this.chart.update()\n\n                // Local function for updates requiring more than one entry in chart options to be updated\n                function updateOption( update, optionKey) {\n                    for (const [key, value] of Object.entries(update)) {\n                        optionKey[key] = value\n\n                    }\n\n                }\n\n            },\n\n            setChartTitle(title) {\n                this.chartTitle = title\n\n            },\n    \n            draw() {\n                const ctx = this.$refs.chart\n                \n                // Render the chart\n                const chart = new Chart(ctx, {\n                    type: 'bar',\n                    data: {\n                        labels: [],\n                        datasets: [{\n                            label: '',\n                            axis: 'y',\n                            data: [],\n                            backgroundColor: this.backgroundColours,\n\n                            borderColor: this.borderColors,\n\n                            borderWidth: 1,\n\n                        }]\n                    },\n\n                    options: {\n                        indexAxis: 'y',\n                        plugins: {\n                            legend: {\n                                display: this.isLegendDisplay,\n\n                            },\n    \n                        }, \n    \n                        elements: {\n                            bar: {\n                                borderWidth: 1,\n                            }\n                        },\n\n                        parsing: {\n                            xAxisKey: this.yAxisKey,\n                            yAxisKey: this.xAxisKey\n\n                        },\n\n                        maintainAspectRatio: false,             // Let the chart take the shape of the container\n                        animation: false,                       // Do not animate\n                        responsive: true,                       // - and please be responisve\n                        \n                        scales: {\n                            y: {\n                                title: {display: (this.yAxisTitle === '') ? false : true,\n                                        text: this.yAxisTitle,\n                                        align: 'center',\n                                       },\n\n                                ticks: {\n                                    autoSkip: this.autoSkipOn,  // Turn on/off automatic tick count calculations\n                                },\n                                stacked: this.isStacked,        // Is this a 'stacked' bar chart\n                            },\n\n                            x: {\n                                title: {display: (this.xAxisTitle ==='') ? false : true,\n                                        text: this.xAxisTitle,\n                                        align: 'center',\n                                       },\n                                beginAtZero: true,\n                                min: this.xAxisMin,\n                                max: this.xAxisMax,\n                                stacked: this.isStacked,\n                            }\n                        }\n                    }\n                })\n\n                // Make this available to all elements of the component\n                this.chart = chart\n            },\n\n            onInput(inputData) {\n                const chartData = inputData.payload\n                const chartTopic = (inputData?.topic) ?? ''\n\n                if (Array.isArray(chartData)) {\n                    if (chartData.length === 0) {\n                        this.clearChart()\n\n                        return\n        \n                    } else {\n\n                        // Configure the dataSets property as the chart datasets and clear any data values\n                        this.dataSets = this.chart.data.datasets\n                        this.dataSets.forEach(element => element.data = [])\n\n                        // For each item in array add the data to proper dataset\n                        // Series key defaults to ''\n                        // the data has format [{seriesKey: <string>, xAxisKey: <string>, yAxisKey: <number>}, and many more of such ...]\n                        chartData.forEach(point => {\n                            \n                            let dataSetName = (this.seriesKey === 'topic') ? chartTopic : ''\n                            let x = ''\n                            let y = 0\n                            \n                            if (this.seriesKey === '' || this.seriesKey === 'topic') {\n                                ({ [this.xAxisKey]: x, [this.yAxisKey]: y } = point)\n\n                            } else {\n                                ({ [this.seriesKey]: dataSetName, [this.xAxisKey]: x, [this.yAxisKey]: y } = point)\n\n                            }\n\n                            // If an HBar chart the incoming x & y axis data is swapped on the chart\n                            if (this.chartType === 'HBar') {\n                                this.addDataPoint(dataSetName, {[this.xAxisKey]: y, [this.yAxisKey]: x})\n\n                            } else {\n                                this.addDataPoint(dataSetName, {[this.xAxisKey]: x, [this.yAxisKey]: y})\n\n                            }\n\n                        })\n\n                        this.updateChart()\n                      \n                    }\n                    \n                } else {\n                    this.send({error: `H Bar Chart requires an Array of Objects`})\n                }\n\n            },\n\n            addDataPoint(dataSetName, data) {\n                let dataSet = this.dataSets.find(chartSet => chartSet.label === dataSetName)\n                if (!dataSet) {\n                    if (this.seriesCount === 0) {\n                        this.dataSets[0].label = dataSetName\n                        dataSet = this.dataSets[0]\n\n                    } else {\n                        dataSet = this.addDataset(dataSetName)\n    \n                        this.dataSets.push(dataSet)\n\n                    }\n\n                }\n\n                this.seriesCount++\n\n                dataSet.data.push(data)\n\n            },\n\n            updateChart() {\n                if (this.chart) {\n                    this.chart.data.datasets = this.dataSets\n\n                    this.chart.update()\n                }\n\n            },\n\n            clearChart() {\n                if (this.dataSets.length > 0) {\n                    this.dataSets.forEach(element => element.data = [])\n\n                    this.updateChart()\n\n                }\n\n            },\n\n            // Add a copy of the first dataset\n            addDataset(dataSetName) {\n                let dataSet = {...this.chart.data.datasets[0]}\n\n                dataSet.label = dataSetName\n\n                if (this.isStacked) {\n                    dataSet.backgroundColor = this.backgroundColours[this.dataSets.length % 6]\n                    dataSet.borderColor = this.borderColours[this.dataSets.length % 6]\n\n                }\n\n                return dataSet\n    \n            },\n\n        },\n        \n        mounted() {\n            // Code here when the component is first loaded\n            let interval = setInterval(() => {\n                if (window.Chart) {\n                    // The chart.js is loaded, so we can now use it\n                    clearInterval(interval)\n                    \n                    this.draw()\n\n                    this.isChartLoaded = true\n                \n                }\n\n            }, 100)\n\n        },\n\n        unmounted() {\n            // Code here when the component is removed from the Dashboard\n            // i.e. when the user navigates away from the page\n        }\n        \n    }\n</script>\n\n<style scoped>\n    .chart-container {\n        height: 425px;\n        position: relative;\n        font-size: 1.4rem\n\n    }\n\n    .chart-title{\n        text-align:center;\n\n    }\n\n    .centre-text {\n        text-align: center;\n        font-size: 0.8rem; \n    }\n\n</style>","storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"local","className":"","x":1400,"y":1280,"wires":[["8350eb4dabc47a74"]],"info":".base-container{\r\n    width:100%;\r\n    height: 100%;\r\n    display: flex;\r\n    justify-content: center;\r\n    align-items: center;\r\n\r\n}\r\n.chart-container{\r\n    position: relative;\r\n    margin: auto;\r\n    height: 100cqb;         /*use container query units to give full height of the container (100% of container height) */\r\n    width: 100cqi;          /*use container query units to give full width of the container (100% of container height)*/\r\n    display: flex;\r\n    justify-content: center;\r\n    align-items: center;\r\n}"},{"id":"71bfd7bb53c60e4a","type":"inject","z":"fe7296cf2a5f7fba","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"period\":\"Wed\",\"value\":0.5753218637843528},{\"period\":\"Thu\",\"value\":0.4568307622663269},{\"period\":\"Fri\",\"value\":0.48251723337081315},{\"period\":\"Sat\",\"value\":0.6762580781813892},{\"period\":\"Sun\",\"value\":0.5352040556527177},{\"period\":\"Mon\",\"value\":0.21310310680635436},{\"period\":\"Tue\",\"value\":0.49344858364974303}]","payloadType":"jsonata","x":1110,"y":1280,"wires":[["84c4a8c53f4017b3"]]},{"id":"5701a3fd6d727d3f","type":"inject","z":"fe7296cf2a5f7fba","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":1110,"y":1360,"wires":[["6ed4ba4423b95f71"]]},{"id":"6ed4ba4423b95f71","type":"change","z":"fe7296cf2a5f7fba","name":"Set Chart \\n Properties","rules":[{"t":"set","p":"ui_update.chartTitle","pt":"msg","to":" Daily Energy Useage","tot":"str"},{"t":"set","p":"ui_update.xAxisMax","pt":"msg","to":"5","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":1310,"y":1360,"wires":[["84c4a8c53f4017b3"]]},{"id":"2a9d414817265ee2","type":"ui-group","name":"HBar Chart","page":"f14e4c53d983b36e","width":"4","height":"1","order":2,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"f14e4c53d983b36e","type":"ui-page","name":"Scope Example","ui":"80f2e5f9dbf80780","path":"/page10","icon":"home","layout":"grid","theme":"a5bbf4397c8aa75a","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":true,"disabled":false},{"id":"80f2e5f9dbf80780","type":"ui-base","name":"Environment","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"showPageTitle":true,"navigationStyle":"default","titleBarStyle":"default"},{"id":"a5bbf4397c8aa75a","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094ce","bgPage":"#ffffff","groupBg":"#eeeeee","groupOutline":"#cccccc"},"sizes":{"pagePadding":"12px","groupGap":"6px","groupBorderRadius":"5px","widgetGap":"12px","density":"default"}}]