# Dashboard 2 widget positioning

**URL:** <https://discourse.nodered.org/t/dashboard-2-widget-positioning/88491>\
**Category:** Dashboard\
**Tags:** dashboard-2\
**Created:** [4 June 2024 05:31 UTC](https://discourse.nodered.org/t/dashboard-2-widget-positioning/88491 "2024-06-04T05:31:39Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![Kundepuu](https://avatars.discourse-cdn.com/v4/letter/k/bcef8e/32.png) [@Kundepuu](https://discourse.nodered.org/u/Kundepuu)\
**Post date:** [4 June 2024 05:31 UTC](https://discourse.nodered.org/t/dashboard-2-widget-positioning/88491/1 "2024-06-04T05:31:39Z")

</div>

How I can reach same looking layout as in your post?  
 ![noderedforum2](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/e/d/edd59e385df885e9330abc54692c5494ad81f96c.png)

My layout comes like this

 ![nodeforum](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/0/a/0a3de395de59e257fc071b9945fcc2f2e53ef4cf.jpeg)

I want that those 3 vertical bars are all in right side same way as your buttons.

my flow

```auto
[
    {
        "id": "f999d4f28598e2d0",
        "type": "ui-template",
        "z": "51ef46c8e45f5993",
        "group": "634ef21adaf4d809",
        "page": "",
        "ui": "",
        "name": "B1",
        "order": 3,
        "width": "3",
        "height": "2",
        "head": "",
        "format": "<template>\n <div ref=\"hng\" :class=\"icon ? 'ag-wrapper-2' : 'ag-wrapper-1'\" :style=\"`--line-color:${colors[0]};`\">\n <div v-if=\"icon\" class=\"ag-icon\">\n <v-icon aria-hidden=\"false\">{{icon}}</v-icon>\n </div>\n <div class=\"ag-content\">\n <div class=\"ag-text\">\n <span class=\"ag-label\">{{label}}</span>\n <span class=\"ag-value\">{{formattedValue}}<span class=\"ag-unit\">{{unit}}</span></span>\n </div>\n <div class=\"ag-track\" ref=\"agLine\">\n <div class=\"ag-track-background\"></div>\n <div class=\"ag-track-foreground\" :style=\"{'width': linesize +'%'}\"></div>\n </div>\n <div class=\"ag-limits\">\n <span class=\"ag-min\">{{min}}</span>\n <span class=\"ag-max\">{{max}}</span>\n </div>\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data(){\n return {\n //Define me here\n \n label:\"Listeria\", // The label\n //icon:\"mdi-account\", // (type: artless) (optional) the icon\n zeroCross:false,// (type: artless) line changes color depending on value being positive or negative (at least 2 colors must be defined)\n min:0, // Smallest expected value\n max:8, // Highest expected value\n unit:\"log10\",// The unit of the measurement \n animate:true, // Animating led's is not most performant thing in the world. \n \n // Define colors \n colors:[\n \"red\",\n \"red\", \n \"red\",\n \"red\", \n \"orange\",\n \"orange\",\n \"orange\",\n \"orange\",\n \"#0fb60f\"\n], \n \n //no need to change those\n value:0, \n inited:false\n }\n },\n\n\n \n methods: { \n getElement: function(name,base){ \n if(base){\n return this.$refs[name]\n }\n return this.$refs[name][0]\n },\n validate(data){\n let ret\n if(typeof data !== \"number\"){\n ret = parseFloat(data)\n if(isNaN(ret)){\n console.log(\"BAD DATA! gauge type:\",this.type, \"id:\",this.id,\"data:\",data)\n return null\n } \n }\n else{\n ret = data\n } \n return ret\n },\n changeLine:function(){\n const line = this.getElement(\"agLine\",true);\n if(!line){\n console.log(\"no line found\")\n return \n }\n \n let c = Math.floor(this.colors.length * this.percentage / 100)\n if(c >= this.colors.length){\n c = this.colors.length - 1\n }\n if(c < 0){\n c = 0\n }\n if(this.zeroCross){\n c = this.value > 0 ? (this.colors.length - 1) : 0\n }\n line.style.setProperty('--line-color',this.colors[c])\n\n }\n },\n \n watch: {\n msg: function(){ \n if(this.msg.payload !== undefined){ \n const v = this.validate(this.msg.payload) \n if(v === null){\n return\n } \n this.value = v\n this.changeLine() \n }\n }\n },\n computed: {\n formattedValue: function () {\n return this.value.toFixed(2)\n },\n percentage: function(){\n return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);\n },\n linesize:function(){\n if(this.zeroCross){\n return Math.floor(((Math.abs(this.value) - this.min) / (this.max - this.min)) * 100); \n }\n else{\n return Math.max(0,this.percentage)\n }\n }\n },\n mounted(){\n const line = this.getElement(\"agLine\",true);\n line.style.setProperty('--line-color',this.colors[0])\n if(this.animate == true){\n if(!line){\n console.log(\"artless init() no line found\")\n return\n }\n line.style.transition = \"width 0.5s\";\n }\n \n this.inited = true;\n }\n}\n</script>\n\n<style>\n.ag-wrapper-2 {\ndisplay: grid;\ngrid-template-columns: 3em 1fr;\ngap:1em;\n}\n.ag-wrapper-1 {\ndisplay: grid;\ngrid-template-columns: 1fr;\n}\n.ag-icon{\nfont-size: 2em;\ndisplay: flex;\nflex-direction: column;\njustify-content: center;\n}\n.ag-content{\ndisplay: grid;\ngrid-template-rows: 1fr 7px 0.75em;\ngap: 2px;\n}\n.ag-text{\nfont-size: 1.25em;\nline-height: 1em;\nalign-self: end;\ndisplay: flex;\nflex-wrap: wrap;\njustify-content: space-between;\nuser-select: none;\n}\n.ag-value{\nfont-weight:bold;\n}\n.ag-unit{\nfont-size:.75em;\nfont-weight:normal;\npadding-inline-start: 0.15em;\n}\n.ag-limits{\ndisplay: flex;\njustify-content: space-between;\nfont-size: .75em;\nline-height: .75em;\nalign-content: center;\nflex-wrap: wrap;\nuser-select: none;\n}\n\n.ag-track{\nposition:relative;\ndisplay:flex;\nalign-items: center;\nwidth: 100%;\nborder-radius: 6px;\n}\n\n.ag-track-background{\nposition:absolute;\nbackground: var(--line-color,rgb(var(--v-theme-primary)));\nopacity: 0.45;\nwidth: 100%;\nheight: 50%;\nborder-radius:inherit;\n}\n.ag-track-foreground{\nposition:absolute;\nbackground-color: var(--line-color,rgb(var(--v-theme-primary)));\nwidth: 50%;\nheight: 100%;\nmax-width: 100%;\nborder-radius:inherit;\ntransition:inherit;\n}\n</style>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 650,
        "y": 260,
        "wires": [
            []
        ]
    },
    {
        "id": "634ef21adaf4d809",
        "type": "ui-group",
        "name": "T1",
        "page": "4f1edd9ca629751a",
        "width": "6",
        "height": "1",
        "order": -1,
        "showTitle": false,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "4f1edd9ca629751a",
        "type": "ui-page",
        "name": "testi",
        "ui": "e7bb0a62b3d9dd57",
        "path": "/page1",
        "icon": "home",
        "layout": "grid",
        "theme": "4d024ff372f88d1f",
        "order": -1,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "e7bb0a62b3d9dd57",
        "type": "ui-base",
        "name": "UI Name",
        "path": "/dashboard",
        "includeClientData": true,
        "acceptsClientConfig": [
            "ui-notification",
            "ui-control"
        ],
        "showPathInSidebar": false
    },
    {
        "id": "4d024ff372f88d1f",
        "type": "ui-theme",
        "name": "Theme Name",
        "colors": {
            "surface": "#979191",
            "primary": "#0094ce",
            "bgPage": "#eeeeee",
            "groupBg": "#ffffff",
            "groupOutline": "#cccccc"
        },
        "sizes": {
            "pagePadding": "12px",
            "groupGap": "12px",
            "groupBorderRadius": "4px",
            "widgetGap": "12px"
        }
    }
]

```

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [4 June 2024 07:28 UTC](https://discourse.nodered.org/t/dashboard-2-widget-positioning/88491/2 "2024-06-04T07:28:21Z")

</div>

You have group width 6 units.  
Round thing should be first in order  
Round thing size should be configured 3x3  
Artless (all) size should be configured 3x1

```auto
+-------+-------+-------+-----------+-----------+-----------+
| Round | Round | Round | Artless 1 | Artless 1 | Artless 1 |
+-------+-------+-------+-----------+-----------+-----------+
| Round | Round | Round | Artless 2 | Artless 2 | Artless 2 |
+-------+-------+-------+-----------+-----------+-----------+
| Round | Round | Round | Artless 3 | Artless 3 | Artless 3 |
+-------+-------+-------+-----------+-----------+-----------+

```

---

<div class="post-metadata">

**Author:** ![Kundepuu](https://avatars.discourse-cdn.com/v4/letter/k/bcef8e/32.png) [@Kundepuu](https://discourse.nodered.org/u/Kundepuu)\
**Post date:** [4 June 2024 07:46 UTC](https://discourse.nodered.org/t/dashboard-2-widget-positioning/88491/3 "2024-06-04T07:46:31Z")

</div>

I have tried also artless 3x1 but result iwas same

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [4 June 2024 07:51 UTC](https://discourse.nodered.org/t/dashboard-2-widget-positioning/88491/4 "2024-06-04T07:51:36Z")

</div>

There's no more tools but size of group, sizes for widgets and the order of the widgets. If to start to mess with CSS, the dashboard layout fights back hard.

---

<div class="post-metadata">

**Author:** ![Paul-Reed](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/paul-reed/32/66906_2.png) [@Paul-Reed](https://discourse.nodered.org/u/Paul-Reed)\
**Post date:** [4 June 2024 08:19 UTC](https://discourse.nodered.org/t/dashboard-2-widget-positioning/88491/5 "2024-06-04T08:19:07Z")

</div>

> [@Kundepuu](#):
>
> but result iwas same

Just to repeat what @hotNipi has said,  
you need 2 groups sized 3x3 each.  
As your page width is 6 wide, the groups will position themselves side by side.  
In the left group, add your round gauge (sized 3x3).  
In the right group, add 3 artless gauges (sized 3x1 each) which should stack up vertically.

---

<div class="post-metadata">

**Author:** ![Kundepuu](https://avatars.discourse-cdn.com/v4/letter/k/bcef8e/32.png) [@Kundepuu](https://discourse.nodered.org/u/Kundepuu)\
**Post date:** [4 June 2024 13:52 UTC](https://discourse.nodered.org/t/dashboard-2-widget-positioning/88491/6 "2024-06-04T13:52:14Z")

</div>

heights of the groups are different and I need no gap between groups

 ![node-red](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/3X/b/1/b1853a54675227fd3b48a34c4c17fdea7b5b3166.jpeg)

flow

```auto
[
    {
        "id": "51ef46c8e45f5993",
        "type": "tab",
        "label": "Koemittarit",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "9ccc34f8f12f8d82",
        "type": "ui-template",
        "z": "51ef46c8e45f5993",
        "group": "48106afca278ac11",
        "page": "",
        "ui": "",
        "name": "T1",
        "order": 4,
        "width": "3",
        "height": "3",
        "head": "",
        "format": "<template>\n <div class=\"hn-sng\">\n <div class=\"label\">{{label}}</div>\n <svg ref=\"hn-gauge\" width=\"100%\" height=\"100%\" viewBox=\"0 0 100 100\">\n <g>\n <path v-for=\"(item, index) in sectors\" :key=\"index\" :ref=\"'sector-' + index\" class=\"sector\" stroke-width=\"5\" d=\"M 10 90 A 47.5 47.5 25 1 1 90 90\"></path> \n </g>\n <g>\n <path class=\"tick-minor\" stroke-width=\"5\" d=\"M 10 90 A 47.5 47.5 25 1 1 90 90\"></path>\n <path ref=\"arc\" class=\"tick-major\" stroke-width=\"5\" d=\"M 10 90 A 47.5 47.5 25 1 1 90 90\"></path>\n </g> \n <g>\n <text v-for=\"(item, index) in numbers\" :key=\"index\" class=\"num\" text-anchor=\"middle\" y=\"-37\" :style=\"`rotate: ${item.r}deg;`\">{{item.n}}</text>\n </g>\n <g>\n <text class=\"measurement\" y=\"48\" x=\"50%\" text-anchor=\"middle\">{{measurement}}</text>\n <text class=\"unit\" y=\"75\" x=\"50%\" text-anchor=\"middle\">{{unit}}</text>\n <text class=\"value\" y=\"90\" x=\"50%\" text-anchor=\"middle\">{{formattedValue}}</text>\n <text v-if=\"showUpdate\" ref=\"update\" class=\"update\" y=\"100\" x=\"0\" text-anchor=\"left\">{{formattedUpdate}}</text>\n </g>\n <g ref=\"o-needle\" class=\"o-needle\">\n <path d=\"M 0,0 -1.5,0 -0.15,-40 0.15,-40 1.5,0 z\"></path>\n <circle cx=\"0\" cy=\"0\" r=\"3\"></circle>\n </g>\n </svg>\n </div>\n</template>\n<script>\nexport default{\n data(){\n return {\n //define me here\n min:0,\n max:100,\n unit:\"°C\",\n label:\"T1\",\n measurement:\"Lämpötila\",\n //updateLabel:\"Last update:\",\n sectors:[{start:0,end:40,color:\"lightblue\"},{start:40,end:54,color:\"orange\"},{start:54,end:80,color:\"#63cc25\"},{start:80,end:100,color:\"red\"}],\n //showUpdate:true,\n //no need to change\n value:0,\n updateTime:0 \n }\n },\n methods:{\n getElement: function(name,base){\n if(base){\n return this.$refs[name]\n }\n return this.$refs[name][0]\n },\n validate: function(data){\n let ret \n if(typeof data !== \"number\"){\n ret = parseFloat(data)\n if(isNaN(ret)){\n console.log(\"BAD DATA! gauge id:\",this.id,\"data:\",data)\n return null\n }\n } \n else{\n ret = data\n } \n return ret\n },\n range:function (n, p, r) { \n if (n < p.minIn) {\n n=p.minIn;\n }\n if (n > p.maxIn) {\n n = p.maxIn;\n }\n if(r){\n return Math.round(((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut);\n }\n return ((n - p.minIn) / (p.maxIn - p.minIn) * (p.maxOut - p.minOut)) + p.minOut;\n },\n generateNumbers:function(min,max){ \n const nums = [{r:238,n:0},{r:262,n:0},{r:286,n:0},{r:310,n:0},{r:335,n:0},{r:360,n:0},{r:385,n:0},{r:409,n:0},{r:434,n:0},{r:458,n:0},{r:123,n:0}];\n let t = (max - min) / 10;\n let i = min;\n for (let e = 0; e < 11; ++e){\n nums[e].n = i\n i = parseFloat((i + t).toFixed(2)); \n }\n return nums \n },\n sectorData:function(full){ \n let params, pos, span, sec, str\n let ret = [] \n this.sectors.forEach((sector,idx) => {\n sec = {name:'sector-'+idx,color:sector.color}\n params = {minIn:this.min, maxIn:this.max, minOut:0, maxOut:full} \n pos = this.range(sector.start,params,false)\n params = {minIn:0, maxIn:this.max-this.min, minOut:0, maxOut:full}\n span = this.range(sector.end - sector.start,params,false) \n sec.css = \"0 \"+pos+\" \"+span+\" var(--dash)\"\n ret.push(sec)\n })\n return ret\n },\n rotation:function(v){\n const params = {minIn:this.min, maxIn:this.max, minOut:-123, maxOut:123};\n return this.range(v,params,false) +'deg' \n }\n\n },\n watch: {\n msg: function(){\n if(this.msg.payload != undefined){ \n const v = this.validate(this.msg.payload) \n if(v == null){\n return\n }\n this.value = v\n this.updateTime = Date.now() \n this.getElement('o-needle',true).style.rotate = this.rotation(this.value) \n }\n }\n },\n computed: {\n formattedValue: function () {\n return this.value.toFixed(1)\n },\n formattedUpdate:function (){\n return this.updateLabel+\" \"+this.time\n },\n time:function(){\n return new Intl.DateTimeFormat('default', {hour: 'numeric', minute: 'numeric',second: 'numeric'}).format(this.updateTime);\n },\n numbers:function(){\n return this.generateNumbers(this.min,this.max)\n } \n },\n mounted(){\n \n const dal = this.getElement('arc',true).getTotalLength()\n const sec = this.sectorData(dal) \n const gauge = this.getElement('hn-gauge',true)\n gauge.style.setProperty('--dash',dal)\n \n sec.forEach(s =>{\n const sector = this.getElement(s.name,false)\n sector.style.setProperty(\"stroke-dasharray\",s.css)\n sector.style.setProperty(\"stroke\",s.color)\n }) \n\n }\n\n }\n</script>\n<style>\n.hn-sng{\nposition:relative;\n}\n.hn-sng .label{\nposition:absolute;\nfont-size:1rem;\ncolor:currentColor;\ntext-align:center;\nwidth:100%;\noverflow: hidden;\nwhite-space: nowrap;\ntext-overflow: ellipsis;\n}\n.hn-sng .value {\nfill:currentColor;\n//fill:red;\nfont-weigth:600;\n}\n.hn-sng .unit {\nfill:currentColor;\nfont-size:0.4rem;\n}\n.hn-sng .measurement {\nfill:currentColor;\nfont-size:0.5rem;\n}\n.hn-sng .update {\nfill:currentColor;\nfill-opacity:0.6;\nfont-size:0.35rem;\n}\n.hn-sng .num{\ntransform-origin: center 64%;\ntransform: translate(50%, 64%);\nfill:currentColor;\nfill-opacity:0.6;\nfont-size:.35rem;\n}\n.hn-sng .tick-minor{\nstroke-dasharray: 0.5 0 0 3.53;\nstroke-dashoffset:-0.5;\nfill:none;\nstroke:currentColor;\nstroke-opacity:0.6;\n}\n.hn-sng .tick-major{\nstroke-dasharray: 2 0 0 18.1;\nfill:none;\nstroke:currentColor;\n}\n.hn-sng .sector{\nfill:none;\nstroke:transparent;\n}\n.hn-sng .o-needle{\ntransform-origin: center 64%;\ntransform: translate(50%, 64%);\ntransition:rotate .5s;\n}\n.hn-sng .o-needle path, .hn-sng .o-needle circle{\nfill:currentColor;\n//fill:red;\n}\n</style>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 650,
        "y": 80,
        "wires": [
            []
        ]
    },
    {
        "id": "1f97fc8ae5645bd0",
        "type": "ui-template",
        "z": "51ef46c8e45f5993",
        "group": "1e0364d1c4ba9e0d",
        "page": "",
        "ui": "",
        "name": "Salmonella",
        "order": 2,
        "width": "3",
        "height": "1",
        "head": "",
        "format": "<template>\n <div ref=\"hng\" :class=\"icon ? 'ag-wrapper-2' : 'ag-wrapper-1'\" :style=\"`--line-color:${colors[0]};`\">\n <div v-if=\"icon\" class=\"ag-icon\">\n <v-icon aria-hidden=\"false\">{{icon}}</v-icon>\n </div>\n <div class=\"ag-content\">\n <div class=\"ag-text\">\n <span class=\"ag-label\">{{label}}</span>\n <span class=\"ag-value\">{{formattedValue}}<span class=\"ag-unit\">{{unit}}</span></span>\n </div>\n <div class=\"ag-track\" ref=\"agLine\">\n <div class=\"ag-track-background\"></div>\n <div class=\"ag-track-foreground\" :style=\"{'width': linesize +'%'}\"></div>\n </div>\n <div class=\"ag-limits\">\n <span class=\"ag-min\">{{min}}</span>\n <span class=\"ag-max\">{{max}}</span>\n </div>\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data(){\n return {\n //Define me here\n \n label:\"Salmonella\", // The label\n //icon:\"mdi-account\", // (type: artless) (optional) the icon\n zeroCross:false,// (type: artless) line changes color depending on value being positive or negative (at least 2 colors must be defined)\n min:0, // Smallest expected value\n max:8, // Highest expected value\n unit:\"log10\",// The unit of the measurement \n animate:true, // Animating led's is not most performant thing in the world. \n \n // Define colors \n colors:[\n \"red\",\n \"red\", \n \"red\",\n \"red\", \n \"orange\",\n \"orange\",\n \"orange\",\n \"orange\",\n \"#0fb60f\"\n], \n \n //no need to change those\n value:0, \n inited:false\n }\n },\n\n\n \n methods: { \n getElement: function(name,base){ \n if(base){\n return this.$refs[name]\n }\n return this.$refs[name][0]\n },\n validate(data){\n let ret\n if(typeof data !== \"number\"){\n ret = parseFloat(data)\n if(isNaN(ret)){\n console.log(\"BAD DATA! gauge type:\",this.type, \"id:\",this.id,\"data:\",data)\n return null\n } \n }\n else{\n ret = data\n } \n return ret\n },\n changeLine:function(){\n const line = this.getElement(\"agLine\",true);\n if(!line){\n console.log(\"no line found\")\n return \n }\n \n let c = Math.floor(this.colors.length * this.percentage / 100)\n if(c >= this.colors.length){\n c = this.colors.length - 1\n }\n if(c < 0){\n c = 0\n }\n if(this.zeroCross){\n c = this.value > 0 ? (this.colors.length - 1) : 0\n }\n line.style.setProperty('--line-color',this.colors[c])\n\n }\n },\n \n watch: {\n msg: function(){ \n if(this.msg.payload !== undefined){ \n const v = this.validate(this.msg.payload) \n if(v === null){\n return\n } \n this.value = v\n this.changeLine() \n }\n }\n },\n computed: {\n formattedValue: function () {\n return this.value.toFixed(2)\n },\n percentage: function(){\n return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);\n },\n linesize:function(){\n if(this.zeroCross){\n return Math.floor(((Math.abs(this.value) - this.min) / (this.max - this.min)) * 100); \n }\n else{\n return Math.max(0,this.percentage)\n }\n }\n },\n mounted(){\n const line = this.getElement(\"agLine\",true);\n line.style.setProperty('--line-color',this.colors[0])\n if(this.animate == true){\n if(!line){\n console.log(\"artless init() no line found\")\n return\n }\n line.style.transition = \"width 0.5s\";\n }\n \n this.inited = true;\n }\n}\n</script>\n\n<style>\n.ag-wrapper-2 {\ndisplay: grid;\ngrid-template-columns: 3em 1fr;\ngap:1em;\n}\n.ag-wrapper-1 {\ndisplay: grid;\ngrid-template-columns: 1fr;\n}\n.ag-icon{\nfont-size: 2em;\ndisplay: flex;\nflex-direction: column;\njustify-content: center;\n}\n.ag-content{\ndisplay: grid;\ngrid-template-rows: 1fr 7px 0.75em;\ngap: 2px;\n}\n.ag-text{\nfont-size: 1.25em;\nline-height: 1em;\nalign-self: end;\ndisplay: flex;\nflex-wrap: wrap;\njustify-content: space-between;\nuser-select: none;\n}\n.ag-value{\nfont-weight:bold;\n}\n.ag-unit{\nfont-size:.75em;\nfont-weight:normal;\npadding-inline-start: 0.15em;\n}\n.ag-limits{\ndisplay: flex;\njustify-content: space-between;\nfont-size: .75em;\nline-height: .75em;\nalign-content: center;\nflex-wrap: wrap;\nuser-select: none;\n}\n\n.ag-track{\nposition:relative;\ndisplay:flex;\nalign-items: center;\nwidth: 100%;\nborder-radius: 6px;\n}\n\n.ag-track-background{\nposition:absolute;\nbackground: var(--line-color,rgb(var(--v-theme-primary)));\nopacity: 0.45;\nwidth: 100%;\nheight: 50%;\nborder-radius:inherit;\n}\n.ag-track-foreground{\nposition:absolute;\nbackground-color: var(--line-color,rgb(var(--v-theme-primary)));\nwidth: 50%;\nheight: 100%;\nmax-width: 100%;\nborder-radius:inherit;\ntransition:inherit;\n}\n</style>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 670,
        "y": 180,
        "wires": [
            []
        ]
    },
    {
        "id": "897c7005ca8b3a31",
        "type": "ui-template",
        "z": "51ef46c8e45f5993",
        "group": "1e0364d1c4ba9e0d",
        "page": "",
        "ui": "",
        "name": "E.Coli",
        "order": 3,
        "width": "3",
        "height": "1",
        "head": "",
        "format": "<template>\n <div ref=\"hng\" :class=\"icon ? 'ag-wrapper-2' : 'ag-wrapper-1'\" :style=\"`--line-color:${colors[0]};`\">\n <div v-if=\"icon\" class=\"ag-icon\">\n <v-icon aria-hidden=\"false\">{{icon}}</v-icon>\n </div>\n <div class=\"ag-content\">\n <div class=\"ag-text\">\n <span class=\"ag-label\">{{label}}</span>\n <span class=\"ag-value\">{{formattedValue}}<span class=\"ag-unit\">{{unit}}</span></span>\n </div>\n <div class=\"ag-track\" ref=\"agLine\">\n <div class=\"ag-track-background\"></div>\n <div class=\"ag-track-foreground\" :style=\"{'width': linesize +'%'}\"></div>\n </div>\n <div class=\"ag-limits\">\n <span class=\"ag-min\">{{min}}</span>\n <span class=\"ag-max\">{{max}}</span>\n </div>\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data(){\n return {\n //Define me here\n \n label:\"E.Coli\", // The label\n //icon:\"mdi-account\", // (type: artless) (optional) the icon\n zeroCross:false,// (type: artless) line changes color depending on value being positive or negative (at least 2 colors must be defined)\n min:0, // Smallest expected value\n max:8, // Highest expected value\n unit:\"log10\",// The unit of the measurement \n animate:true, // Animating led's is not most performant thing in the world. \n \n // Define colors \n colors:[\n \"red\",\n \"red\", \n \"red\",\n \"red\", \n \"orange\",\n \"orange\",\n \"orange\",\n \"orange\",\n \"#0fb60f\"\n], \n \n //no need to change those\n value:0, \n inited:false\n }\n },\n\n\n \n methods: { \n getElement: function(name,base){ \n if(base){\n return this.$refs[name]\n }\n return this.$refs[name][0]\n },\n validate(data){\n let ret\n if(typeof data !== \"number\"){\n ret = parseFloat(data)\n if(isNaN(ret)){\n console.log(\"BAD DATA! gauge type:\",this.type, \"id:\",this.id,\"data:\",data)\n return null\n } \n }\n else{\n ret = data\n } \n return ret\n },\n changeLine:function(){\n const line = this.getElement(\"agLine\",true);\n if(!line){\n console.log(\"no line found\")\n return \n }\n \n let c = Math.floor(this.colors.length * this.percentage / 100)\n if(c >= this.colors.length){\n c = this.colors.length - 1\n }\n if(c < 0){\n c = 0\n }\n if(this.zeroCross){\n c = this.value > 0 ? (this.colors.length - 1) : 0\n }\n line.style.setProperty('--line-color',this.colors[c])\n\n }\n },\n \n watch: {\n msg: function(){ \n if(this.msg.payload !== undefined){ \n const v = this.validate(this.msg.payload) \n if(v === null){\n return\n } \n this.value = v\n this.changeLine() \n }\n }\n },\n computed: {\n formattedValue: function () {\n return this.value.toFixed(2)\n },\n percentage: function(){\n return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);\n },\n linesize:function(){\n if(this.zeroCross){\n return Math.floor(((Math.abs(this.value) - this.min) / (this.max - this.min)) * 100); \n }\n else{\n return Math.max(0,this.percentage)\n }\n }\n },\n mounted(){\n const line = this.getElement(\"agLine\",true);\n line.style.setProperty('--line-color',this.colors[0])\n if(this.animate == true){\n if(!line){\n console.log(\"artless init() no line found\")\n return\n }\n line.style.transition = \"width 0.5s\";\n }\n \n this.inited = true;\n }\n}\n</script>\n\n<style>\n.ag-wrapper-2 {\ndisplay: grid;\ngrid-template-columns: 3em 1fr;\ngap:1em;\n}\n.ag-wrapper-1 {\ndisplay: grid;\ngrid-template-columns: 1fr;\n}\n.ag-icon{\nfont-size: 2em;\ndisplay: flex;\nflex-direction: column;\njustify-content: center;\n}\n.ag-content{\ndisplay: grid;\ngrid-template-rows: 1fr 7px 0.75em;\ngap: 2px;\n}\n.ag-text{\nfont-size: 1.25em;\nline-height: 1em;\nalign-self: end;\ndisplay: flex;\nflex-wrap: wrap;\njustify-content: space-between;\nuser-select: none;\n}\n.ag-value{\nfont-weight:bold;\n}\n.ag-unit{\nfont-size:.75em;\nfont-weight:normal;\npadding-inline-start: 0.15em;\n}\n.ag-limits{\ndisplay: flex;\njustify-content: space-between;\nfont-size: .75em;\nline-height: .75em;\nalign-content: center;\nflex-wrap: wrap;\nuser-select: none;\n}\n\n.ag-track{\nposition:relative;\ndisplay:flex;\nalign-items: center;\nwidth: 100%;\nborder-radius: 6px;\n}\n\n.ag-track-background{\nposition:absolute;\nbackground: var(--line-color,rgb(var(--v-theme-primary)));\nopacity: 0.45;\nwidth: 100%;\nheight: 50%;\nborder-radius:inherit;\n}\n.ag-track-foreground{\nposition:absolute;\nbackground-color: var(--line-color,rgb(var(--v-theme-primary)));\nwidth: 50%;\nheight: 100%;\nmax-width: 100%;\nborder-radius:inherit;\ntransition:inherit;\n}\n</style>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 650,
        "y": 220,
        "wires": [
            []
        ]
    },
    {
        "id": "f999d4f28598e2d0",
        "type": "ui-template",
        "z": "51ef46c8e45f5993",
        "group": "1e0364d1c4ba9e0d",
        "page": "",
        "ui": "",
        "name": "Listeria",
        "order": 1,
        "width": "3",
        "height": "1",
        "head": "",
        "format": "<template>\n <div ref=\"hng\" :class=\"icon ? 'ag-wrapper-2' : 'ag-wrapper-1'\" :style=\"`--line-color:${colors[0]};`\">\n <div v-if=\"icon\" class=\"ag-icon\">\n <v-icon aria-hidden=\"false\">{{icon}}</v-icon>\n </div>\n <div class=\"ag-content\">\n <div class=\"ag-text\">\n <span class=\"ag-label\">{{label}}</span>\n <span class=\"ag-value\">{{formattedValue}}<span class=\"ag-unit\">{{unit}}</span></span>\n </div>\n <div class=\"ag-track\" ref=\"agLine\">\n <div class=\"ag-track-background\"></div>\n <div class=\"ag-track-foreground\" :style=\"{'width': linesize +'%'}\"></div>\n </div>\n <div class=\"ag-limits\">\n <span class=\"ag-min\">{{min}}</span>\n <span class=\"ag-max\">{{max}}</span>\n </div>\n </div>\n </div>\n</template>\n\n<script>\n export default {\n data(){\n return {\n //Define me here\n \n label:\"Listeria\", // The label\n //icon:\"mdi-account\", // (type: artless) (optional) the icon\n zeroCross:false,// (type: artless) line changes color depending on value being positive or negative (at least 2 colors must be defined)\n min:0, // Smallest expected value\n max:8, // Highest expected value\n unit:\"log10\",// The unit of the measurement \n animate:true, // Animating led's is not most performant thing in the world. \n \n // Define colors \n colors:[\n \"red\",\n \"red\", \n \"red\",\n \"red\", \n \"orange\",\n \"orange\",\n \"orange\",\n \"orange\",\n \"#0fb60f\"\n], \n \n //no need to change those\n value:0, \n inited:false\n }\n },\n\n\n \n methods: { \n getElement: function(name,base){ \n if(base){\n return this.$refs[name]\n }\n return this.$refs[name][0]\n },\n validate(data){\n let ret\n if(typeof data !== \"number\"){\n ret = parseFloat(data)\n if(isNaN(ret)){\n console.log(\"BAD DATA! gauge type:\",this.type, \"id:\",this.id,\"data:\",data)\n return null\n } \n }\n else{\n ret = data\n } \n return ret\n },\n changeLine:function(){\n const line = this.getElement(\"agLine\",true);\n if(!line){\n console.log(\"no line found\")\n return \n }\n \n let c = Math.floor(this.colors.length * this.percentage / 100)\n if(c >= this.colors.length){\n c = this.colors.length - 1\n }\n if(c < 0){\n c = 0\n }\n if(this.zeroCross){\n c = this.value > 0 ? (this.colors.length - 1) : 0\n }\n line.style.setProperty('--line-color',this.colors[c])\n\n }\n },\n \n watch: {\n msg: function(){ \n if(this.msg.payload !== undefined){ \n const v = this.validate(this.msg.payload) \n if(v === null){\n return\n } \n this.value = v\n this.changeLine() \n }\n }\n },\n computed: {\n formattedValue: function () {\n return this.value.toFixed(2)\n },\n percentage: function(){\n return Math.floor(((this.value - this.min) / (this.max - this.min)) * 100);\n },\n linesize:function(){\n if(this.zeroCross){\n return Math.floor(((Math.abs(this.value) - this.min) / (this.max - this.min)) * 100); \n }\n else{\n return Math.max(0,this.percentage)\n }\n }\n },\n mounted(){\n const line = this.getElement(\"agLine\",true);\n line.style.setProperty('--line-color',this.colors[0])\n if(this.animate == true){\n if(!line){\n console.log(\"artless init() no line found\")\n return\n }\n line.style.transition = \"width 0.5s\";\n }\n \n this.inited = true;\n }\n}\n</script>\n\n<style>\n.ag-wrapper-2 {\ndisplay: grid;\ngrid-template-columns: 3em 1fr;\ngap:1em;\n}\n.ag-wrapper-1 {\ndisplay: grid;\ngrid-template-columns: 1fr;\n}\n.ag-icon{\nfont-size: 2em;\ndisplay: flex;\nflex-direction: column;\njustify-content: center;\n}\n.ag-content{\ndisplay: grid;\ngrid-template-rows: 1fr 7px 0.75em;\ngap: 2px;\n}\n.ag-text{\nfont-size: 1.25em;\nline-height: 1em;\nalign-self: end;\ndisplay: flex;\nflex-wrap: wrap;\njustify-content: space-between;\nuser-select: none;\n}\n.ag-value{\nfont-weight:bold;\n}\n.ag-unit{\nfont-size:.75em;\nfont-weight:normal;\npadding-inline-start: 0.15em;\n}\n.ag-limits{\ndisplay: flex;\njustify-content: space-between;\nfont-size: .75em;\nline-height: .75em;\nalign-content: center;\nflex-wrap: wrap;\nuser-select: none;\n}\n\n.ag-track{\nposition:relative;\ndisplay:flex;\nalign-items: center;\nwidth: 100%;\nborder-radius: 6px;\n}\n\n.ag-track-background{\nposition:absolute;\nbackground: var(--line-color,rgb(var(--v-theme-primary)));\nopacity: 0.45;\nwidth: 100%;\nheight: 50%;\nborder-radius:inherit;\n}\n.ag-track-foreground{\nposition:absolute;\nbackground-color: var(--line-color,rgb(var(--v-theme-primary)));\nwidth: 50%;\nheight: 100%;\nmax-width: 100%;\nborder-radius:inherit;\ntransition:inherit;\n}\n</style>",
        "storeOutMessages": true,
        "passthru": true,
        "resendOnRefresh": true,
        "templateScope": "local",
        "className": "",
        "x": 660,
        "y": 260,
        "wires": [
            []
        ]
    },
    {
        "id": "48106afca278ac11",
        "type": "ui-group",
        "name": "T1",
        "page": "4f1edd9ca629751a",
        "width": "3",
        "height": "3",
        "order": 1,
        "showTitle": false,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "1e0364d1c4ba9e0d",
        "type": "ui-group",
        "name": "Bakteerit",
        "page": "4f1edd9ca629751a",
        "width": "3",
        "height": "3",
        "order": 2,
        "showTitle": false,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "4f1edd9ca629751a",
        "type": "ui-page",
        "name": "testi",
        "ui": "e7bb0a62b3d9dd57",
        "path": "/page1",
        "icon": "home",
        "layout": "grid",
        "theme": "4d024ff372f88d1f",
        "order": -1,
        "className": "",
        "visible": "true",
        "disabled": "false"
    },
    {
        "id": "e7bb0a62b3d9dd57",
        "type": "ui-base",
        "name": "UI Name",
        "path": "/dashboard",
        "includeClientData": true,
        "acceptsClientConfig": [
            "ui-notification",
            "ui-control"
        ],
        "showPathInSidebar": false
    },
    {
        "id": "4d024ff372f88d1f",
        "type": "ui-theme",
        "name": "Theme Name",
        "colors": {
            "surface": "#979191",
            "primary": "#0094ce",
            "bgPage": "#eeeeee",
            "groupBg": "#ffffff",
            "groupOutline": "#cccccc"
        },
        "sizes": {
            "pagePadding": "12px",
            "groupGap": "12px",
            "groupBorderRadius": "4px",
            "widgetGap": "12px"
        }
    }
]

```

---

<div class="post-metadata">

**Author:** ![hotNipi](https://sea2.discourse-cdn.com/flex026/user_avatar/discourse.nodered.org/hotnipi/32/383_2.png) [@hotNipi](https://discourse.nodered.org/u/hotNipi)\
**Post date:** [4 June 2024 16:43 UTC](https://discourse.nodered.org/t/dashboard-2-widget-positioning/88491/7 "2024-06-04T16:43:13Z")

</div>

It was arrangements for things in single group but yeah. None of currently available layout options have fixed or at least common row heights. That way it is not possible to have similarities over cards. Thus the dashboard as whole is not pice of art but as the result of coincidences.

I shared an option to force the heights of rows to be locked here

> [@Dashboard 2.0 Classic design multi needle gauge. Call for testing](https://discourse.nodered.org/t/dashboard-2-0-classic-design-multi-needle-gauge-call-for-testing/88383/13):
>
> It is not related to the Gauge at all. This can be achieved but may bring in some other issues ... .nrdb-layout-group--grid{ grid-template-rows: repeat(1, 0fr) !important; } Makes all rows to have same height.

It is not tested against different widgets or layout options, just a snippet to play with. Who knows, it may be useful. I don't have time enough to go through all it takes to say something strong.

---

<div class="post-metadata">

**Author:** ![system](https://us1.discourse-cdn.com/flex026/uploads/nodered/original/1X/d073cd938eafa2e558d7c2cd59003b3ef4963033.png) [@system](https://discourse.nodered.org/u/system)\
**Post date:** [4 July 2024 16:43 UTC](https://discourse.nodered.org/t/dashboard-2-widget-positioning/88491/8 "2024-07-04T16:43:27Z")

</div>

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.
