{"tag":"progress bar","articles":{"projects\/code-doodle-gradient-progress-bar":{"key":"projects\/code-doodle-gradient-progress-bar","type":"article","published":true,"meta":{"createdAt":"2022-01-02T15:32:29+01:00","publishedAt":"2022-01-02T15:32:29+01:00","group":null,"links":[{"icon":"globe-europe","colour":"primary","url":"https:\/\/progressbar.avris.it","displayUrl":null},{"icon":"brands gitlab","colour":"secondary","url":"https:\/\/gitlab.com\/Avris\/ProgressBar","displayUrl":null}],"category":"projects","subcategory":null,"slug":"code-doodle-gradient-progress-bar"},"content":{"en":{"slug":"code-doodle-gradient-progress-bar","title":"Code Doodle: a gradient progress bar","intro":"\u003Cfigure\u003E\n                \u003Cnoscript\u003E\n                    \u003Cimg src=\u0022https:\/\/avris.it\/image\/avris-progress-bar_small.png\u0022 alt=\u0022Screenshot of the visual effect: a number of progress bar with increasing values, and their colour gradually changes from red to yellow to green\u0022 class=\u0022border-bottom\u0022 width=\u0022480\u0022 height=\u0022240\u0022\u003E                \n                \u003C\/noscript\u003E\n                \u003Cspan class=\u0022hide-noscript\u0022\u003E\u003Cimg src=\u0022data:image\/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAASCAYAAAAzI3woAAAACXBIWXMAAA7EAAAOxAGVKw4bAAADJklEQVRIibWWT0\/jVhTFf++P7SR2SJygiSGtVFBQqDKbqhJSRSWWVN1WfEU+Aiu6oGqrqk0W1QCaVIzIVChlqoY2JE3s2Im7YPCQIZPJMNOzee8+nWefe8+7zxbh6ZM4On\/GojC\/+BLpFhfmvyt091kTfv4OQ4jFNtQeI90icRwjXu6J4xhgKhZCPIij09UqflpjabWQIJl3k4ffjre4Gz+Uo4cvzvn76Q\/4WpGz3i5Kbz5G5goLiX8IdLr8MfA5AMKQKDnfOmFn72X9QQWNgg793jkAQykoOBo57zxN\/MTzg4MDtNaMRiOKxSKNRoONjQ22trbY39\/HdV0ymQwrKysANBoNrq6u2N3dJQgCarUarutOCzJzeexyOVmItCQ9zzrTBG58V+qGd3p6yng8xvM8Dg8PqVQqaK1RSnF8fEyr1aJQKNButymVShwdHbG2tkatVps67HEcI3ovfoz\/vXoy9c6sbWAacqYeVfgKjEdvFgxEUUS\/3yefzwMQhiGGYczdcwstLYV0psm+FFhZg5nOKZlkc3JyQq\/XI5vN0ul0CIKAbreLaZqkUikuLi5YX1\/n7OyMvb09crnc29s+Nk3ijD2dIdCLJZm0vp+BkAhuSuw4Do7jMBgM8H2farVKt9slDEOKxSKe51EqlVhdXQVe3UF356+viT+vv487vV9nli\/nmpjWtHVL4msM4QHQ7\/cJggApJVEUEUURmUwG3\/exLIswDAEYDAY4jsNwOMS27aRSMyskjTyG\/dFMQcORAGu6SjFWchDr9TqXl5dorbFtm1arhed5jMdjBoMBUkrK5TKpVIput0u9XmdnZ4ft7e1EwN0RQPz2z7fx79f1mYIAzLTCcc0k3pDf4IgbC9rtNsvLy0nrN5tNNjc3kVIymUySzJVSRFHEeDzGsqykO2dB\/DFsxn8Fz99IADBSCvWy68riM1IszeW\/D3Q7OOfp9U9zSaIvsJctpBIUVIW0yP1\/gh5Z60yy97vpdchQYGiFRRbg3oX2oea6HTzn5PqXhdRbSwZerkImfmXZrK\/3+8y1q0p8Yn+6kCDGYEQWwlg843f9H\/oPQE+Gnfb7BO8AAAAASUVORK5CYII=\u0022 data-src=\u0022https:\/\/avris.it\/image\/avris-progress-bar_small.png\u0022 alt=\u0022Screenshot of the visual effect: a number of progress bar with increasing values, and their colour gradually changes from red to yellow to green\u0022 class=\u0022border-bottom\u0022 width=\u0022480\u0022 height=\u0022240\u0022\u003E\u003C\/span\u003E\n                \n            \u003C\/figure\u003E\u003C\/p\u003E\n\u003Cp\u003EA simple doodling project \u2013 probably not that useful, but I enjoyed creating it \ud83e\udd37\u003C\/p\u003E\u003Csvg xmlns=\u0022http:\/\/www.w3.org\/2000\/svg\u0022 style=\u0022display: none;\u0022\u003E\u003C\/svg\u003E","content":"\u003Cfigure\u003E\n                \u003Cnoscript\u003E\n                    \u003Cimg src=\u0022https:\/\/avris.it\/image\/avris-progress-bar_big.png\u0022 alt=\u0022Screenshot of the visual effect: a number of progress bar with increasing values, and their colour gradually changes from red to yellow to green\u0022 class=\u0022border\u0022 width=\u0022960\u0022 height=\u0022480\u0022\u003E                \n                \u003C\/noscript\u003E\n                \u003Cspan class=\u0022hide-noscript\u0022\u003E\u003Cimg src=\u0022data:image\/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAASCAYAAAAzI3woAAAACXBIWXMAAA7EAAAOxAGVKw4bAAADJklEQVRIibWWT0\/jVhTFf++P7SR2SJygiSGtVFBQqDKbqhJSRSWWVN1WfEU+Aiu6oGqrqk0W1QCaVIzIVChlqoY2JE3s2Im7YPCQIZPJMNOzee8+nWefe8+7zxbh6ZM4On\/GojC\/+BLpFhfmvyt091kTfv4OQ4jFNtQeI90icRwjXu6J4xhgKhZCPIij09UqflpjabWQIJl3k4ffjre4Gz+Uo4cvzvn76Q\/4WpGz3i5Kbz5G5goLiX8IdLr8MfA5AMKQKDnfOmFn72X9QQWNgg793jkAQykoOBo57zxN\/MTzg4MDtNaMRiOKxSKNRoONjQ22trbY39\/HdV0ymQwrKysANBoNrq6u2N3dJQgCarUarutOCzJzeexyOVmItCQ9zzrTBG58V+qGd3p6yng8xvM8Dg8PqVQqaK1RSnF8fEyr1aJQKNButymVShwdHbG2tkatVps67HEcI3ovfoz\/vXoy9c6sbWAacqYeVfgKjEdvFgxEUUS\/3yefzwMQhiGGYczdcwstLYV0psm+FFhZg5nOKZlkc3JyQq\/XI5vN0ul0CIKAbreLaZqkUikuLi5YX1\/n7OyMvb09crnc29s+Nk3ijD2dIdCLJZm0vp+BkAhuSuw4Do7jMBgM8H2farVKt9slDEOKxSKe51EqlVhdXQVe3UF356+viT+vv487vV9nli\/nmpjWtHVL4msM4QHQ7\/cJggApJVEUEUURmUwG3\/exLIswDAEYDAY4jsNwOMS27aRSMyskjTyG\/dFMQcORAGu6SjFWchDr9TqXl5dorbFtm1arhed5jMdjBoMBUkrK5TKpVIput0u9XmdnZ4ft7e1EwN0RQPz2z7fx79f1mYIAzLTCcc0k3pDf4IgbC9rtNsvLy0nrN5tNNjc3kVIymUySzJVSRFHEeDzGsqykO2dB\/DFsxn8Fz99IADBSCvWy68riM1IszeW\/D3Q7OOfp9U9zSaIvsJctpBIUVIW0yP1\/gh5Z60yy97vpdchQYGiFRRbg3oX2oea6HTzn5PqXhdRbSwZerkImfmXZrK\/3+8y1q0p8Yn+6kCDGYEQWwlg843f9H\/oPQE+Gnfb7BO8AAAAASUVORK5CYII=\u0022 data-src=\u0022https:\/\/avris.it\/image\/avris-progress-bar_big.png\u0022 alt=\u0022Screenshot of the visual effect: a number of progress bar with increasing values, and their colour gradually changes from red to yellow to green\u0022 class=\u0022border\u0022 width=\u0022960\u0022 height=\u0022480\u0022\u003E\u003C\/span\u003E\n                \n            \u003C\/figure\u003E\u003C\/p\u003E\n\u003Cp\u003EA simple doodling project \u2013 probably not that useful, but I enjoyed creating it \ud83e\udd37\u003C\/p\u003E\n\u003Cp\u003EI liked the way Wendover Productions displayed the HDI score in their video\n\u003Ca href=\u0022https:\/\/www.youtube.com\/watch?v=W3qZIPiWKc4\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022\u003E\u003Csvg class=\u0022icon\u0022\u003E\u003Cuse xlink:href=\u0022#light-link\u0022\u003E\u003C\/use\u003E\u003C\/svg\u003E \u201eThe News You Missed in 2021\u201d\u003C\/a\u003E,\nso of course I decided to create a widget like this for VueJS \ud83d\ude05\u003C\/p\u003E\n\u003Cp\u003EHere\u0027s a demo:\u003C\/p\u003E\n\u003Cp\u003E\u003Ca href=\u0022https:\/\/progressbar.avris.it\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022 class=\u0022btn btn-primary btn-lg d-block\u0022\u003E\nDemo\n\u003C\/a\u003E\u003C\/p\u003E\n\u003Cp\u003EAnd here\u0027s how I made it:\u003C\/p\u003E\n\u003Cp\u003ETo minimise the initial bootsrapping, I picked a framework I already know and absolutely love:\n\u003Ca href=\u0022https:\/\/v3.nuxtjs.org\/\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022\u003E\u003Csvg class=\u0022icon\u0022\u003E\u003Cuse xlink:href=\u0022#light-link\u0022\u003E\u003C\/use\u003E\u003C\/svg\u003E Nuxt\u003C\/a\u003E.\nIts upcoming version, v3, cuts all the bootstrapping crap to the minimum, while remaining highly flexible.\nAll I had to do to start working were those commands:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022hljs bash border\u0022\u003Enpx nuxi init ProgressBar\n\u003Cspan class=\u0022hljs-built_in\u0022\u003Ecd\u003C\/span\u003E ProgressBar\nyarn\nyarn dev -o\n\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EWhat they do is fetch a project template, install dependencies, start a development server\nand open \u003Ccode\u003Ehttp:\/\/localhost:3000\/\u003C\/code\u003E in the browser.\u003C\/p\u003E\n\u003Cdiv class=\u0022alert alert-warning\u0022\u003E\n    Watch out: it\u0027s not the best idea to use v3 yet, this version is not stable!\n    I did, because I like experimenting and I want to see where the project is going,\n    but I had to pay the price (in this case: inability to generate a static website on production,\n    and a weird issue with including stylesheets that I made \u003Ca href=\u0022https:\/\/gitlab.com\/Avris\/ProgressBar\/-\/blob\/main\/app.vue#L117\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022\u003E\u003Csvg class=\u0022icon\u0022\u003E\u003Cuse xlink:href=\u0022#light-link\u0022\u003E\u003C\/use\u003E\u003C\/svg\u003E an ugly workaround for\u003C\/a\u003E).\n\u003C\/div\u003E\n\u003Cp\u003EBootrapping done; now I can focus on work. It\u0027s a simple project, so only two files will be relevant:\u003C\/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003Eapp.vue\u003C\/code\u003E is the main entrypoint, the homepage.\nIt contains what you see in the demo: just some introduction and lots of \u003Ccode\u003E\u0026lt;AvrisProgressBar\/\u0026gt;\u003C\/code\u003E components\nembedded to showcase their usage.\nI won\u0027t focus here on the \u003Ccode\u003Eapp.vue\u003C\/code\u003E code, because it\u0027s really straightforward \u2013\nyou can check it out \u003Ca href=\u0022https:\/\/gitlab.com\/Avris\/ProgressBar\/-\/blob\/main\/app.vue\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022\u003E\u003Csvg class=\u0022icon\u0022\u003E\u003Cuse xlink:href=\u0022#light-link\u0022\u003E\u003C\/use\u003E\u003C\/svg\u003E here\u003C\/a\u003E.\nSidenote: if I ever need to add multiple pages, I\u0027ll just create a \u003Ccode\u003Eroutes\u003C\/code\u003E directory\nand move \u003Ccode\u003Eapp.vue\u003C\/code\u003E to the appropriate structure inside of that folder, it\u0027s as simple as that! \u2013\nand for the simplest case it\u0027s all already preconfigured and working!\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003Ecomponents\/AvrisProgressBar.vue\u003C\/code\u003E is the actual component I\u0027ll be working on.\nNuxt automatically configures everything so that throughout the application\nyou can simply use the \u003Ccode\u003E\u0026lt;AvrisProgressBar\/\u0026gt;\u003C\/code\u003E tag.\u003C\/li\u003E\n\u003C\/ul\u003E\n\u003Cp\u003EThe template is really simple:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022hljs xml border\u0022\u003E\u003Cspan class=\u0022hljs-tag\u0022\u003E\u0026lt;\u003Cspan class=\u0022hljs-name\u0022\u003Etemplate\u003C\/span\u003E\u0026gt;\u003C\/span\u003E\n    \u003Cspan class=\u0022hljs-tag\u0022\u003E\u0026lt;\u003Cspan class=\u0022hljs-name\u0022\u003Ediv\u003C\/span\u003E \u003Cspan class=\u0022hljs-attr\u0022\u003Eclass\u003C\/span\u003E=\u003Cspan class=\u0022hljs-string\u0022\u003E\u0022outer\u0022\u003C\/span\u003E \u003Cspan class=\u0022hljs-attr\u0022\u003E:style\u003C\/span\u003E=\u003Cspan class=\u0022hljs-string\u0022\u003E\u0022`background-color: ${colourOuter}`\u0022\u003C\/span\u003E\u0026gt;\u003C\/span\u003E\n        \u003Cspan class=\u0022hljs-tag\u0022\u003E\u0026lt;\u003Cspan class=\u0022hljs-name\u0022\u003Ediv\u003C\/span\u003E \u003Cspan class=\u0022hljs-attr\u0022\u003Eclass\u003C\/span\u003E=\u003Cspan class=\u0022hljs-string\u0022\u003E\u0022inner\u0022\u003C\/span\u003E \u003Cspan class=\u0022hljs-attr\u0022\u003E:style\u003C\/span\u003E=\u003Cspan class=\u0022hljs-string\u0022\u003E\u0022`width: ${percent}%; background-color: ${colourInner}`\u0022\u003C\/span\u003E\n             \u003Cspan class=\u0022hljs-attr\u0022\u003Erole\u003C\/span\u003E=\u003Cspan class=\u0022hljs-string\u0022\u003E\u0022progressbar\u0022\u003C\/span\u003E \u003Cspan class=\u0022hljs-attr\u0022\u003E:aria-valuenow\u003C\/span\u003E=\u003Cspan class=\u0022hljs-string\u0022\u003E\u0022percent\u0022\u003C\/span\u003E \u003Cspan class=\u0022hljs-attr\u0022\u003Earia-valuemin\u003C\/span\u003E=\u003Cspan class=\u0022hljs-string\u0022\u003E\u00220\u0022\u003C\/span\u003E \u003Cspan class=\u0022hljs-attr\u0022\u003Earia-valuemax\u003C\/span\u003E=\u003Cspan class=\u0022hljs-string\u0022\u003E\u0022100\u0022\u003C\/span\u003E\u0026gt;\u003C\/span\u003E\u003Cspan class=\u0022hljs-tag\u0022\u003E\u0026lt;\/\u003Cspan class=\u0022hljs-name\u0022\u003Ediv\u003C\/span\u003E\u0026gt;\u003C\/span\u003E\n    \u003Cspan class=\u0022hljs-tag\u0022\u003E\u0026lt;\/\u003Cspan class=\u0022hljs-name\u0022\u003Ediv\u003C\/span\u003E\u0026gt;\u003C\/span\u003E\n\u003Cspan class=\u0022hljs-tag\u0022\u003E\u0026lt;\/\u003Cspan class=\u0022hljs-name\u0022\u003Etemplate\u003C\/span\u003E\u0026gt;\u003C\/span\u003E\n\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EAll we need is two divs:\u003C\/p\u003E\n\u003Cul\u003E\n\u003Cli\u003Ethe outer one, taking a full width of the container\nand coloured with the \u201emain\u201d colour depending on the value of the progress bar,\u003C\/li\u003E\n\u003Cli\u003Ethe inner one, taking a fraction of the outer div\u0027s width proportional to the progress bar\u0027s value\nand coloured with a shade slightly darker than the \u201emain\u201d colour.\u003C\/li\u003E\n\u003C\/ul\u003E\n\u003Cp\u003EAnd here\u0027s how I styled them:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022hljs css border\u0022\u003E\u0026lt;\u003Cspan class=\u0022hljs-selector-tag\u0022\u003Estyle\u003C\/span\u003E \u003Cspan class=\u0022hljs-selector-tag\u0022\u003Escoped\u003C\/span\u003E\u0026gt;\n\u003Cspan class=\u0022hljs-selector-class\u0022\u003E.outer\u003C\/span\u003E {\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003E--apb-height\u003C\/span\u003E: \u003Cspan class=\u0022hljs-number\u0022\u003E16px\u003C\/span\u003E;\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003E--apb-border-width\u003C\/span\u003E: \u003Cspan class=\u0022hljs-number\u0022\u003E0px\u003C\/span\u003E;\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003E--apb-border-color\u003C\/span\u003E: \u003Cspan class=\u0022hljs-number\u0022\u003E#aaa\u003C\/span\u003E;\n}\n\n\u003Cspan class=\u0022hljs-selector-class\u0022\u003E.outer\u003C\/span\u003E {\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003Ewidth\u003C\/span\u003E: \u003Cspan class=\u0022hljs-number\u0022\u003E100%\u003C\/span\u003E;\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003Eborder-radius\u003C\/span\u003E: \u003Cspan class=\u0022hljs-built_in\u0022\u003Ecalc\u003C\/span\u003E(var(--apb-height) \/ \u003Cspan class=\u0022hljs-number\u0022\u003E2\u003C\/span\u003E);\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003Eheight\u003C\/span\u003E: \u003Cspan class=\u0022hljs-built_in\u0022\u003Evar\u003C\/span\u003E(--apb-height);\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003Emargin\u003C\/span\u003E: \u003Cspan class=\u0022hljs-number\u0022\u003E4px\u003C\/span\u003E;\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003Epadding\u003C\/span\u003E: \u003Cspan class=\u0022hljs-built_in\u0022\u003Ecalc\u003C\/span\u003E(var(--apb-height) \/ \u003Cspan class=\u0022hljs-number\u0022\u003E4\u003C\/span\u003E - \u003Cspan class=\u0022hljs-built_in\u0022\u003Evar\u003C\/span\u003E(--apb-border-width));\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003Edisplay\u003C\/span\u003E: inline-block;\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003Eborder\u003C\/span\u003E: \u003Cspan class=\u0022hljs-built_in\u0022\u003Evar\u003C\/span\u003E(--apb-border-width) solid \u003Cspan class=\u0022hljs-built_in\u0022\u003Evar\u003C\/span\u003E(--apb-border-color);\n}\n\u003Cspan class=\u0022hljs-selector-class\u0022\u003E.outer\u003C\/span\u003E \u003Cspan class=\u0022hljs-selector-class\u0022\u003E.inner\u003C\/span\u003E {\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003Eheight\u003C\/span\u003E: \u003Cspan class=\u0022hljs-built_in\u0022\u003Ecalc\u003C\/span\u003E(var(--apb-height) \/ \u003Cspan class=\u0022hljs-number\u0022\u003E2\u003C\/span\u003E);\n    \u003Cspan class=\u0022hljs-attribute\u0022\u003Eborder-radius\u003C\/span\u003E: \u003Cspan class=\u0022hljs-built_in\u0022\u003Ecalc\u003C\/span\u003E(var(--apb-height) \/ \u003Cspan class=\u0022hljs-number\u0022\u003E2\u003C\/span\u003E);\n}\n\u0026lt;\/\u003Cspan class=\u0022hljs-selector-tag\u0022\u003Estyle\u003C\/span\u003E\u0026gt;\n\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EI used CSS variables so that each usage of this widget can override the default height and border.\u003C\/p\u003E\n\u003Cp\u003ENow all we need to do is calculate \u003Ccode\u003Epercent\u003C\/code\u003E, \u003Ccode\u003EcolourOuter\u003C\/code\u003E and \u003Ccode\u003EcolourInner\u003C\/code\u003E. Easy!\u003C\/p\u003E\n\u003Cp\u003ELet\u0027s start with declaring \u003Ccode\u003Eprops\u003C\/code\u003E of our component:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022hljs xml border\u0022\u003E\u003Cspan class=\u0022hljs-tag\u0022\u003E\u0026lt;\u003Cspan class=\u0022hljs-name\u0022\u003Escript\u003C\/span\u003E\u0026gt;\u003C\/span\u003E\u003Cspan class=\u0022javascript\u0022\u003E\n\u003Cspan class=\u0022hljs-keyword\u0022\u003Eexport\u003C\/span\u003E \u003Cspan class=\u0022hljs-keyword\u0022\u003Edefault\u003C\/span\u003E {\n    \u003Cspan class=\u0022hljs-attr\u0022\u003Eprops\u003C\/span\u003E: {\n        \u003Cspan class=\u0022hljs-attr\u0022\u003Evalue\u003C\/span\u003E: { \u003Cspan class=\u0022hljs-attr\u0022\u003Erequired\u003C\/span\u003E: \u003Cspan class=\u0022hljs-literal\u0022\u003Etrue\u003C\/span\u003E },\n        \u003Cspan class=\u0022hljs-attr\u0022\u003Emin\u003C\/span\u003E: { \u003Cspan class=\u0022hljs-string\u0022\u003E\u0027default\u0027\u003C\/span\u003E: \u003Cspan class=\u0022hljs-number\u0022\u003E0\u003C\/span\u003E },\n        \u003Cspan class=\u0022hljs-attr\u0022\u003Emax\u003C\/span\u003E: { \u003Cspan class=\u0022hljs-string\u0022\u003E\u0027default\u0027\u003C\/span\u003E: \u003Cspan class=\u0022hljs-number\u0022\u003E1000\u003C\/span\u003E },\n        \u003Cspan class=\u0022hljs-attr\u0022\u003Ecolours\u003C\/span\u003E: { \u003Cspan class=\u0022hljs-string\u0022\u003E\u0027default\u0027\u003C\/span\u003E: \u003Cspan class=\u0022hljs-function\u0022\u003E\u003Cspan class=\u0022hljs-params\u0022\u003E()\u003C\/span\u003E =\u0026gt;\u003C\/span\u003E {\n            \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E [\n                \u003Cspan class=\u0022hljs-string\u0022\u003E\u0027#ff0000\u0027\u003C\/span\u003E,\n                \u003Cspan class=\u0022hljs-string\u0022\u003E\u0027#ffff00\u0027\u003C\/span\u003E,\n                \u003Cspan class=\u0022hljs-string\u0022\u003E\u0027#00ff00\u0027\u003C\/span\u003E,\n            ];\n        }}\n    },\n};\n\u003C\/span\u003E\u003Cspan class=\u0022hljs-tag\u0022\u003E\u0026lt;\/\u003Cspan class=\u0022hljs-name\u0022\u003Escript\u003C\/span\u003E\u0026gt;\u003C\/span\u003E\n\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EWe expect the user of our widget to provide a value (eg. \u003Ccode\u003E\u0026lt;AvrisProgressBar :value=\u0022123\u0022\/\u0026gt;\u003C\/code\u003E)\nand we allow them to overwrite the default values of \u003Ccode\u003Emin\u003C\/code\u003E (0), \u003Ccode\u003Emax\u003C\/code\u003E (1000) and \u003Ccode\u003Ecolours\u003C\/code\u003E (red, yellow, green)\nwith their own (eg. \u003Ccode\u003E\u0026lt;AvrisProgressBar :value=\u002269\u0022 :min=\u002224\u0022 :max=\u0022169\u0022\/\u0026gt;\u003C\/code\u003E).\u003C\/p\u003E\n\u003Cp\u003ENow let\u0027s calculate the \u003Ccode\u003Epercent\u003C\/code\u003E.\nIt\u0027s pretty simple: we divide the current progress by the full range, and multiply by 100%.\nIf \u003Ccode\u003Emin == 0\u003C\/code\u003E, the formula is trivial: \u003Ccode\u003E100% * value \/ max\u003C\/code\u003E.\nBut in any other case we should add the \u003Ccode\u003Emin\u003C\/code\u003E to the formula:\n\u003Ccode\u003E100% * (value - min) \/ (max - min)\u003C\/code\u003E.\nPlus let\u0027s cut it at no less than 0 and no more than 100, just in case the user provides some weird input.\nHere\u0027s a Vue code that implements that:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022hljs javascript border\u0022\u003Ecomputed: {\n    percent() {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Elet\u003C\/span\u003E percent = \u003Cspan class=\u0022hljs-number\u0022\u003E100\u003C\/span\u003E * (\u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.value - \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.min) \/ (\u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.range);\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Eif\u003C\/span\u003E (percent \u0026lt; \u003Cspan class=\u0022hljs-number\u0022\u003E0\u003C\/span\u003E) { \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-number\u0022\u003E0\u003C\/span\u003E; }\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Eif\u003C\/span\u003E (percent \u0026gt; \u003Cspan class=\u0022hljs-number\u0022\u003E100\u003C\/span\u003E) { \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-number\u0022\u003E100\u003C\/span\u003E; }\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E percent;\n    },\n    range() {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.max - \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.min;\n    },\n},\n\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EAnd for the remaining two properties that we need, let\u0027s first mock their values to simply be green and black respectively,\njust so that we can validate that \u003Ccode\u003Epercent\u003C\/code\u003E works well without worrying about the colours.\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022hljs javascript border\u0022\u003Ecomputed: {\n    colourOuter() {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-string\u0022\u003E\u0027#00ff00\u0027\u003C\/span\u003E;\n    },\n    colourInner() {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-string\u0022\u003E\u0027#000000\u0027\u003C\/span\u003E;\n    },\n},\n\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EOf course, in a full-blown, production-ready library, we should cover a lot more ground than in my simple doodle:\nsupporting multiple colour models, implementing colour mixing algorythms that adjust for human perception, etc.\nBut here I wanted to keep it simple:\u003C\/p\u003E\n\u003Cul\u003E\n\u003Cli\u003Ewe only support RGB,\u003C\/li\u003E\n\u003Cli\u003Ewe only support the HTML colour syntax with six digits (eg. \u003Ccode\u003E#C71585\u003C\/code\u003E),\u003C\/li\u003E\n\u003Cli\u003Ewe darken the colour by simply decreesing all of its RGB components by the same percent,\u003C\/li\u003E\n\u003Cli\u003Ewe mix the colours to create the gradient by simply calculating a weighted average for each RGB component.\u003C\/li\u003E\n\u003C\/ul\u003E\n\u003Cp\u003EActually, that\u0027s my main purpose for creating this widget \u2013\nI\u0027m curious whether such a simple setup would give a visually pleasant effect.\nSpoiler alert: it\u0027s not perfect, but it works well enough indeed!\u003C\/p\u003E\n\u003Cp\u003ESo now, let\u0027s compute two more values that will help us in further calculations.\n\u003Ccode\u003EcoloursHighpoints\u003C\/code\u003E will be a map between the primary colours and the values where they should appear.\nIn the default case, we have \u003Ccode\u003En = 3\u003C\/code\u003E colours (red, yellow, green) and a range from 0 to 1000,\nso to spread them out evenly we\u0027d need red to be at 0, yellow at 500 and green at 1000.\nThe code below splits the available range into \u003Ccode\u003En - 1\u003C\/code\u003E sections (so in this case sections have a width of 500)\nand then produces points at: \u003Ccode\u003Emin\u003C\/code\u003E, \u003Ccode\u003Emin + range\u003C\/code\u003E, \u003Ccode\u003Emin + 2*range\u003C\/code\u003E, \u2026\nSo in our case the methods retuns a map like this: \u003Ccode\u003E{0: \u0022#ff0000\u0022, 500: \u0022#ffff00\u0022, 1000: \u0022#00ff00\u0022}\u003C\/code\u003E:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022hljs javascript border\u0022\u003EcoloursHighpoints() {\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Econst\u003C\/span\u003E highpoints = {};\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Econst\u003C\/span\u003E sectionRange = \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.range \/ (\u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.colours.length - \u003Cspan class=\u0022hljs-number\u0022\u003E1\u003C\/span\u003E);\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Efor\u003C\/span\u003E (\u003Cspan class=\u0022hljs-keyword\u0022\u003Elet\u003C\/span\u003E i \u003Cspan class=\u0022hljs-keyword\u0022\u003Ein\u003C\/span\u003E \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.colours) {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Econst\u003C\/span\u003E point = \u003Cspan class=\u0022hljs-built_in\u0022\u003EparseInt\u003C\/span\u003E(\u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.min + i * sectionRange, \u003Cspan class=\u0022hljs-number\u0022\u003E10\u003C\/span\u003E);\n        highpoints[point] = \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.colours[i];\n    }\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E highpoints;\n},\n\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EThe next step would be to figure out which colours to mix for a given value.\nLet\u0027s say our \u003Ccode\u003Evalue = 400\u003C\/code\u003E. We know it\u0027s between 0 \/ red and 500 \/ yellow,\nso we expect it be an orange-ish yellow.\nBut how do we put that into code?\u003C\/p\u003E\n\u003Cp\u003EWe need a method that will return an object telling us what\u0027s the closest highpoint\nbefore our value, and what\u0027s the closest one after our value.\nLet\u0027s iterate over the highpoints and keep assigning their values to the variable \u003Ccode\u003Eleft\u003C\/code\u003E as long as it\u0027s smaller than \u003Ccode\u003Ethis.value\u003C\/code\u003E.\nAs soon as a highpoint appears that\u0027s higher than \u003Ccode\u003Ethis.value\u003C\/code\u003E, we assign it to the \u003Ccode\u003Eright\u003C\/code\u003E variable.\nAnd then let\u0027s handle the edge case of \u003Ccode\u003Evalue \u0026gt;= max\u003C\/code\u003E.\nHere\u0027s the code for that, and it produces the following output for our parameters: \u003Ccode\u003E{left: 0, right: 500}\u003C\/code\u003E.\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022hljs javascript border\u0022\u003EcoloursBetween() {\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Elet\u003C\/span\u003E left = \u003Cspan class=\u0022hljs-literal\u0022\u003Enull\u003C\/span\u003E;\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Elet\u003C\/span\u003E right = \u003Cspan class=\u0022hljs-literal\u0022\u003Enull\u003C\/span\u003E;\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Efor\u003C\/span\u003E (\u003Cspan class=\u0022hljs-keyword\u0022\u003Elet\u003C\/span\u003E val \u003Cspan class=\u0022hljs-keyword\u0022\u003Eof\u003C\/span\u003E \u003Cspan class=\u0022hljs-built_in\u0022\u003EObject\u003C\/span\u003E.keys(\u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.coloursHighpoints)) {\n        val = \u003Cspan class=\u0022hljs-built_in\u0022\u003EparseInt\u003C\/span\u003E(val);\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Eif\u003C\/span\u003E (val \u0026lt;= \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.value) {\n            left = val;\n            \u003Cspan class=\u0022hljs-keyword\u0022\u003Econtinue\u003C\/span\u003E;\n        }\n        right = val;\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ebreak\u003C\/span\u003E;\n    }\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Eif\u003C\/span\u003E (!right) {\n        right = left;\n    }\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E {left, right};\n},\n\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EBefore we actually calculate our colours, let\u0027s prepare some helpers that we\u0027ll need:\u003C\/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EhexToDec\u003C\/code\u003E and \u003Ccode\u003EdecToHex\u003C\/code\u003E \u2013 simply converting the number base and adding some hex-colour specific padding,\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003EsplitColour\u003C\/code\u003E: turns \u003Ccode\u003E#C71585\u003C\/code\u003E into \u003Ccode\u003E[199, 21, 133]\u003C\/code\u003E,\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003EmergeColour\u003C\/code\u003E: turns \u003Ccode\u003E[199, 21, 133]\u003C\/code\u003E into \u003Ccode\u003E#C71585\u003C\/code\u003E,\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003EadjustValue\u003C\/code\u003E: takes an RGB component and increases it by a given \u003Ccode\u003Epercent\u003C\/code\u003E,\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003EshadeColour\u003C\/code\u003E: splits a colour into components, adjusts their value and merges them back into a colour,\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003EmixColours\u003C\/code\u003E: takes two colours and a ratio in which they should be mixed, splits them into components,\nfor each of them calculates a weighted average, and then merges them back into a colour.\u003C\/li\u003E\n\u003C\/ul\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022hljs javascript border\u0022\u003Emethods: {\n    hexToDec(hex) {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-built_in\u0022\u003EparseInt\u003C\/span\u003E(hex, \u003Cspan class=\u0022hljs-number\u0022\u003E16\u003C\/span\u003E);\n    },\n    decToHex(dec) {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-built_in\u0022\u003EparseInt\u003C\/span\u003E(dec, \u003Cspan class=\u0022hljs-number\u0022\u003E10\u003C\/span\u003E).toString(\u003Cspan class=\u0022hljs-number\u0022\u003E16\u003C\/span\u003E).padStart(\u003Cspan class=\u0022hljs-number\u0022\u003E2\u003C\/span\u003E, \u003Cspan class=\u0022hljs-string\u0022\u003E\u00270\u0027\u003C\/span\u003E);\n    },\n    splitColour(colour) {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E [\n            \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.hexToDec(colour.substring(\u003Cspan class=\u0022hljs-number\u0022\u003E1\u003C\/span\u003E, \u003Cspan class=\u0022hljs-number\u0022\u003E3\u003C\/span\u003E)),\n            \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.hexToDec(colour.substring(\u003Cspan class=\u0022hljs-number\u0022\u003E3\u003C\/span\u003E, \u003Cspan class=\u0022hljs-number\u0022\u003E5\u003C\/span\u003E)),\n            \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.hexToDec(colour.substring(\u003Cspan class=\u0022hljs-number\u0022\u003E5\u003C\/span\u003E, \u003Cspan class=\u0022hljs-number\u0022\u003E7\u003C\/span\u003E)),\n        ];\n    },\n    mergeColour(r, g, b) {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-string\u0022\u003E`#\u003Cspan class=\u0022hljs-subst\u0022\u003E${\u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.decToHex(r)}\u003C\/span\u003E\u003Cspan class=\u0022hljs-subst\u0022\u003E${\u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.decToHex(g)}\u003C\/span\u003E\u003Cspan class=\u0022hljs-subst\u0022\u003E${\u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.decToHex(b)}\u003C\/span\u003E`\u003C\/span\u003E;\n    },\n    adjustValue(val, percent) {\n        val = \u003Cspan class=\u0022hljs-built_in\u0022\u003EparseInt\u003C\/span\u003E(val * (\u003Cspan class=\u0022hljs-number\u0022\u003E100\u003C\/span\u003E + percent) \/ \u003Cspan class=\u0022hljs-number\u0022\u003E100\u003C\/span\u003E);\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Eif\u003C\/span\u003E (val \u0026gt; \u003Cspan class=\u0022hljs-number\u0022\u003E255\u003C\/span\u003E) { val = \u003Cspan class=\u0022hljs-number\u0022\u003E255\u003C\/span\u003E; }\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Eif\u003C\/span\u003E (val \u0026lt; \u003Cspan class=\u0022hljs-number\u0022\u003E0\u003C\/span\u003E) { val = \u003Cspan class=\u0022hljs-number\u0022\u003E0\u003C\/span\u003E; }\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E val;\n    },\n    shadeColour(colour, percent) {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Econst\u003C\/span\u003E [r, g, b] = \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.splitColour(colour);\n\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.mergeColour(\n            \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.adjustValue(r, percent),\n            \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.adjustValue(g, percent),\n            \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.adjustValue(b, percent),\n        );\n    },\n    mixColours(colour1, colour2, ratio) {\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Econst\u003C\/span\u003E [r1, g1, b1] = \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.splitColour(colour1);\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Econst\u003C\/span\u003E [r2, g2, b2] = \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.splitColour(colour2);\n\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.mergeColour(\n            r1 * ratio + r2 * (\u003Cspan class=\u0022hljs-number\u0022\u003E1\u003C\/span\u003E - ratio),\n            g1 * ratio + g2 * (\u003Cspan class=\u0022hljs-number\u0022\u003E1\u003C\/span\u003E - ratio),\n            b1 * ratio + b2 * (\u003Cspan class=\u0022hljs-number\u0022\u003E1\u003C\/span\u003E - ratio),\n        );\n    },\n},\n\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EAnd now it\u0027s finally time to put it all together.\u003C\/p\u003E\n\u003Cp\u003ETo calculate \u003Ccode\u003EcolourOuter\u003C\/code\u003E we take the \u003Ccode\u003EcoloursBetween\u003C\/code\u003E and we calculate\nhow far away from \u003Ccode\u003Eleft\u003C\/code\u003E is our value.\nIn our case that\u0027s \u003Ccode\u003E(value - left) \/ (right - left) = (400 - 0) \/ (500 - 0) = 400 \/ 500 = 0.8\u003C\/code\u003E.\nWe will use that value as our weight when calculating the weighted average\nbetween the colour in the \u003Ccode\u003Eleft\u003C\/code\u003E highpoint and the colour in the \u003Ccode\u003Eright\u003C\/code\u003E highpoint.\u003C\/p\u003E\n\u003Cp\u003EAnd \u003Ccode\u003EcolourInner\u003C\/code\u003E will just be just \u003Ccode\u003EcolourOuter\u003C\/code\u003E darkened by 30%.\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022hljs javascript border\u0022\u003EcolourOuter() {\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Econst\u003C\/span\u003E {left, right} = \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.coloursBetween;\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Econst\u003C\/span\u003E ratio = (\u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.value - left) \/ ((right - left) || \u003Cspan class=\u0022hljs-number\u0022\u003E1\u003C\/span\u003E);\n\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.mixColours(\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.coloursHighpoints[left],\n        \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.coloursHighpoints[right],\n        \u003Cspan class=\u0022hljs-number\u0022\u003E1\u003C\/span\u003E - ratio,\n    );\n},\ncolourInner() {\n    \u003Cspan class=\u0022hljs-keyword\u0022\u003Ereturn\u003C\/span\u003E \u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.shadeColour(\u003Cspan class=\u0022hljs-keyword\u0022\u003Ethis\u003C\/span\u003E.colourOuter, \u003Cspan class=\u0022hljs-number\u0022\u003E-30\u003C\/span\u003E);\n},\n\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EAnd that\u0027s it, we\u0027re done!\u003C\/p\u003E\u003Csvg xmlns=\u0022http:\/\/www.w3.org\/2000\/svg\u0022 style=\u0022display: none;\u0022\u003E\u003C\/svg\u003E","tags":["vue","nuxt","javascript","widget","progress bar","colours","gradient"],"hasMore":true,"image":"https:\/\/avris.it\/image\/avris-progress-bar_small.png","introLite":"\u003Cfigure\u003E\u003Ca href=\u0022https:\/\/avris.it\/image\/avris-progress-bar_big.png\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022\u003E\u003Cimg src=\u0022https:\/\/avris.it\/image\/avris-progress-bar_mini.png\u0022 alt=\u0022Screenshot of the visual effect: a number of progress bar with increasing values, and their colour gradually changes from red to yellow to green\u0022 width=\u0022240\u0022 height=\u0022120\u0022 loading=\u0022lazy\u0022\u003E\u003C\/a\u003E\u003C\/figure\u003E\u003C\/p\u003E\n\u003Cp\u003EA simple doodling project \u2013 probably not that useful, but I enjoyed creating it \ud83e\udd37\u003C\/p\u003E","contentLite":"\u003Cfigure\u003E\u003Ca href=\u0022https:\/\/avris.it\/image\/avris-progress-bar_big.png\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022\u003E\u003Cimg src=\u0022https:\/\/avris.it\/image\/avris-progress-bar_mini.png\u0022 alt=\u0022Screenshot of the visual effect: a number of progress bar with increasing values, and their colour gradually changes from red to yellow to green\u0022 width=\u0022240\u0022 height=\u0022120\u0022 loading=\u0022lazy\u0022\u003E\u003C\/a\u003E\u003C\/figure\u003E\u003C\/p\u003E\n\u003Cp\u003EA simple doodling project \u2013 probably not that useful, but I enjoyed creating it \ud83e\udd37\u003C\/p\u003E\n\u003Cp\u003EI liked the way Wendover Productions displayed the HDI score in their video\n\u003Ca href=\u0022https:\/\/www.youtube.com\/watch?v=W3qZIPiWKc4\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022\u003E \u201eThe News You Missed in 2021\u201d\u003C\/a\u003E,\nso of course I decided to create a widget like this for VueJS \ud83d\ude05\u003C\/p\u003E\n\u003Cp\u003EHere\u0027s a demo:\u003C\/p\u003E\n\u003Cp\u003E\u003Ca href=\u0022https:\/\/progressbar.avris.it\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022 class=\u0022btn btn-primary btn-lg d-block\u0022\u003E\nDemo\n\u003C\/a\u003E\u003C\/p\u003E\n\u003Cp\u003EAnd here\u0027s how I made it:\u003C\/p\u003E\n\u003Cp\u003ETo minimise the initial bootsrapping, I picked a framework I already know and absolutely love:\n\u003Ca href=\u0022https:\/\/v3.nuxtjs.org\/\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022\u003E Nuxt\u003C\/a\u003E.\nIts upcoming version, v3, cuts all the bootstrapping crap to the minimum, while remaining highly flexible.\nAll I had to do to start working were those commands:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022language-bash\u0022\u003Enpx nuxi init ProgressBar\ncd ProgressBar\nyarn\nyarn dev -o\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EWhat they do is fetch a project template, install dependencies, start a development server\nand open \u003Ccode\u003Ehttp:\/\/localhost:3000\/\u003C\/code\u003E in the browser.\u003C\/p\u003E\n\u003Cdiv class=\u0022alert alert-warning\u0022\u003E\n    Watch out: it\u0027s not the best idea to use v3 yet, this version is not stable!\n    I did, because I like experimenting and I want to see where the project is going,\n    but I had to pay the price (in this case: inability to generate a static website on production,\n    and a weird issue with including stylesheets that I made \u003Ca href=\u0022https:\/\/gitlab.com\/Avris\/ProgressBar\/-\/blob\/main\/app.vue#L117\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022\u003E an ugly workaround for\u003C\/a\u003E).\n\u003C\/div\u003E\n\u003Cp\u003EBootrapping done; now I can focus on work. It\u0027s a simple project, so only two files will be relevant:\u003C\/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003Eapp.vue\u003C\/code\u003E is the main entrypoint, the homepage.\nIt contains what you see in the demo: just some introduction and lots of \u003Ccode\u003E\u0026lt;AvrisProgressBar\/\u0026gt;\u003C\/code\u003E components\nembedded to showcase their usage.\nI won\u0027t focus here on the \u003Ccode\u003Eapp.vue\u003C\/code\u003E code, because it\u0027s really straightforward \u2013\nyou can check it out \u003Ca href=\u0022https:\/\/gitlab.com\/Avris\/ProgressBar\/-\/blob\/main\/app.vue\u0022 target=\u0022_blank\u0022 rel=\u0022noopener\u0022\u003E here\u003C\/a\u003E.\nSidenote: if I ever need to add multiple pages, I\u0027ll just create a \u003Ccode\u003Eroutes\u003C\/code\u003E directory\nand move \u003Ccode\u003Eapp.vue\u003C\/code\u003E to the appropriate structure inside of that folder, it\u0027s as simple as that! \u2013\nand for the simplest case it\u0027s all already preconfigured and working!\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003Ecomponents\/AvrisProgressBar.vue\u003C\/code\u003E is the actual component I\u0027ll be working on.\nNuxt automatically configures everything so that throughout the application\nyou can simply use the \u003Ccode\u003E\u0026lt;AvrisProgressBar\/\u0026gt;\u003C\/code\u003E tag.\u003C\/li\u003E\n\u003C\/ul\u003E\n\u003Cp\u003EThe template is really simple:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022language-html\u0022\u003E\u0026lt;template\u0026gt;\n    \u0026lt;div class=\u0022outer\u0022 :style=\u0022`background-color: ${colourOuter}`\u0022\u0026gt;\n        \u0026lt;div class=\u0022inner\u0022 :style=\u0022`width: ${percent}%; background-color: ${colourInner}`\u0022\n             role=\u0022progressbar\u0022 :aria-valuenow=\u0022percent\u0022 aria-valuemin=\u00220\u0022 aria-valuemax=\u0022100\u0022\u0026gt;\u0026lt;\/div\u0026gt;\n    \u0026lt;\/div\u0026gt;\n\u0026lt;\/template\u0026gt;\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EAll we need is two divs:\u003C\/p\u003E\n\u003Cul\u003E\n\u003Cli\u003Ethe outer one, taking a full width of the container\nand coloured with the \u201emain\u201d colour depending on the value of the progress bar,\u003C\/li\u003E\n\u003Cli\u003Ethe inner one, taking a fraction of the outer div\u0027s width proportional to the progress bar\u0027s value\nand coloured with a shade slightly darker than the \u201emain\u201d colour.\u003C\/li\u003E\n\u003C\/ul\u003E\n\u003Cp\u003EAnd here\u0027s how I styled them:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022language-css\u0022\u003E\u0026lt;style scoped\u0026gt;\n.outer {\n    --apb-height: 16px;\n    --apb-border-width: 0px;\n    --apb-border-color: #aaa;\n}\n\n.outer {\n    width: 100%;\n    border-radius: calc(var(--apb-height) \/ 2);\n    height: var(--apb-height);\n    margin: 4px;\n    padding: calc(var(--apb-height) \/ 4 - var(--apb-border-width));\n    display: inline-block;\n    border: var(--apb-border-width) solid var(--apb-border-color);\n}\n.outer .inner {\n    height: calc(var(--apb-height) \/ 2);\n    border-radius: calc(var(--apb-height) \/ 2);\n}\n\u0026lt;\/style\u0026gt;\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EI used CSS variables so that each usage of this widget can override the default height and border.\u003C\/p\u003E\n\u003Cp\u003ENow all we need to do is calculate \u003Ccode\u003Epercent\u003C\/code\u003E, \u003Ccode\u003EcolourOuter\u003C\/code\u003E and \u003Ccode\u003EcolourInner\u003C\/code\u003E. Easy!\u003C\/p\u003E\n\u003Cp\u003ELet\u0027s start with declaring \u003Ccode\u003Eprops\u003C\/code\u003E of our component:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode\u003E\u0026lt;script\u0026gt;\nexport default {\n    props: {\n        value: { required: true },\n        min: { \u0027default\u0027: 0 },\n        max: { \u0027default\u0027: 1000 },\n        colours: { \u0027default\u0027: () =\u0026gt; {\n            return [\n                \u0027#ff0000\u0027,\n                \u0027#ffff00\u0027,\n                \u0027#00ff00\u0027,\n            ];\n        }}\n    },\n};\n\u0026lt;\/script\u0026gt;\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EWe expect the user of our widget to provide a value (eg. \u003Ccode\u003E\u0026lt;AvrisProgressBar :value=\u0022123\u0022\/\u0026gt;\u003C\/code\u003E)\nand we allow them to overwrite the default values of \u003Ccode\u003Emin\u003C\/code\u003E (0), \u003Ccode\u003Emax\u003C\/code\u003E (1000) and \u003Ccode\u003Ecolours\u003C\/code\u003E (red, yellow, green)\nwith their own (eg. \u003Ccode\u003E\u0026lt;AvrisProgressBar :value=\u002269\u0022 :min=\u002224\u0022 :max=\u0022169\u0022\/\u0026gt;\u003C\/code\u003E).\u003C\/p\u003E\n\u003Cp\u003ENow let\u0027s calculate the \u003Ccode\u003Epercent\u003C\/code\u003E.\nIt\u0027s pretty simple: we divide the current progress by the full range, and multiply by 100%.\nIf \u003Ccode\u003Emin == 0\u003C\/code\u003E, the formula is trivial: \u003Ccode\u003E100% * value \/ max\u003C\/code\u003E.\nBut in any other case we should add the \u003Ccode\u003Emin\u003C\/code\u003E to the formula:\n\u003Ccode\u003E100% * (value - min) \/ (max - min)\u003C\/code\u003E.\nPlus let\u0027s cut it at no less than 0 and no more than 100, just in case the user provides some weird input.\nHere\u0027s a Vue code that implements that:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022language-js\u0022\u003Ecomputed: {\n    percent() {\n        let percent = 100 * (this.value - this.min) \/ (this.range);\n        if (percent \u0026lt; 0) { return 0; }\n        if (percent \u0026gt; 100) { return 100; }\n        return percent;\n    },\n    range() {\n        return this.max - this.min;\n    },\n},\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EAnd for the remaining two properties that we need, let\u0027s first mock their values to simply be green and black respectively,\njust so that we can validate that \u003Ccode\u003Epercent\u003C\/code\u003E works well without worrying about the colours.\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022language-js\u0022\u003Ecomputed: {\n    colourOuter() {\n        return \u0027#00ff00\u0027;\n    },\n    colourInner() {\n        return \u0027#000000\u0027;\n    },\n},\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EOf course, in a full-blown, production-ready library, we should cover a lot more ground than in my simple doodle:\nsupporting multiple colour models, implementing colour mixing algorythms that adjust for human perception, etc.\nBut here I wanted to keep it simple:\u003C\/p\u003E\n\u003Cul\u003E\n\u003Cli\u003Ewe only support RGB,\u003C\/li\u003E\n\u003Cli\u003Ewe only support the HTML colour syntax with six digits (eg. \u003Ccode\u003E#C71585\u003C\/code\u003E),\u003C\/li\u003E\n\u003Cli\u003Ewe darken the colour by simply decreesing all of its RGB components by the same percent,\u003C\/li\u003E\n\u003Cli\u003Ewe mix the colours to create the gradient by simply calculating a weighted average for each RGB component.\u003C\/li\u003E\n\u003C\/ul\u003E\n\u003Cp\u003EActually, that\u0027s my main purpose for creating this widget \u2013\nI\u0027m curious whether such a simple setup would give a visually pleasant effect.\nSpoiler alert: it\u0027s not perfect, but it works well enough indeed!\u003C\/p\u003E\n\u003Cp\u003ESo now, let\u0027s compute two more values that will help us in further calculations.\n\u003Ccode\u003EcoloursHighpoints\u003C\/code\u003E will be a map between the primary colours and the values where they should appear.\nIn the default case, we have \u003Ccode\u003En = 3\u003C\/code\u003E colours (red, yellow, green) and a range from 0 to 1000,\nso to spread them out evenly we\u0027d need red to be at 0, yellow at 500 and green at 1000.\nThe code below splits the available range into \u003Ccode\u003En - 1\u003C\/code\u003E sections (so in this case sections have a width of 500)\nand then produces points at: \u003Ccode\u003Emin\u003C\/code\u003E, \u003Ccode\u003Emin + range\u003C\/code\u003E, \u003Ccode\u003Emin + 2*range\u003C\/code\u003E, \u2026\nSo in our case the methods retuns a map like this: \u003Ccode\u003E{0: \u0022#ff0000\u0022, 500: \u0022#ffff00\u0022, 1000: \u0022#00ff00\u0022}\u003C\/code\u003E:\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022language-js\u0022\u003EcoloursHighpoints() {\n    const highpoints = {};\n    const sectionRange = this.range \/ (this.colours.length - 1);\n    for (let i in this.colours) {\n        const point = parseInt(this.min + i * sectionRange, 10);\n        highpoints[point] = this.colours[i];\n    }\n    return highpoints;\n},\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EThe next step would be to figure out which colours to mix for a given value.\nLet\u0027s say our \u003Ccode\u003Evalue = 400\u003C\/code\u003E. We know it\u0027s between 0 \/ red and 500 \/ yellow,\nso we expect it be an orange-ish yellow.\nBut how do we put that into code?\u003C\/p\u003E\n\u003Cp\u003EWe need a method that will return an object telling us what\u0027s the closest highpoint\nbefore our value, and what\u0027s the closest one after our value.\nLet\u0027s iterate over the highpoints and keep assigning their values to the variable \u003Ccode\u003Eleft\u003C\/code\u003E as long as it\u0027s smaller than \u003Ccode\u003Ethis.value\u003C\/code\u003E.\nAs soon as a highpoint appears that\u0027s higher than \u003Ccode\u003Ethis.value\u003C\/code\u003E, we assign it to the \u003Ccode\u003Eright\u003C\/code\u003E variable.\nAnd then let\u0027s handle the edge case of \u003Ccode\u003Evalue \u0026gt;= max\u003C\/code\u003E.\nHere\u0027s the code for that, and it produces the following output for our parameters: \u003Ccode\u003E{left: 0, right: 500}\u003C\/code\u003E.\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022language-js\u0022\u003EcoloursBetween() {\n    let left = null;\n    let right = null;\n    for (let val of Object.keys(this.coloursHighpoints)) {\n        val = parseInt(val);\n        if (val \u0026lt;= this.value) {\n            left = val;\n            continue;\n        }\n        right = val;\n        break;\n    }\n    if (!right) {\n        right = left;\n    }\n    return {left, right};\n},\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EBefore we actually calculate our colours, let\u0027s prepare some helpers that we\u0027ll need:\u003C\/p\u003E\n\u003Cul\u003E\n\u003Cli\u003E\u003Ccode\u003EhexToDec\u003C\/code\u003E and \u003Ccode\u003EdecToHex\u003C\/code\u003E \u2013 simply converting the number base and adding some hex-colour specific padding,\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003EsplitColour\u003C\/code\u003E: turns \u003Ccode\u003E#C71585\u003C\/code\u003E into \u003Ccode\u003E[199, 21, 133]\u003C\/code\u003E,\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003EmergeColour\u003C\/code\u003E: turns \u003Ccode\u003E[199, 21, 133]\u003C\/code\u003E into \u003Ccode\u003E#C71585\u003C\/code\u003E,\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003EadjustValue\u003C\/code\u003E: takes an RGB component and increases it by a given \u003Ccode\u003Epercent\u003C\/code\u003E,\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003EshadeColour\u003C\/code\u003E: splits a colour into components, adjusts their value and merges them back into a colour,\u003C\/li\u003E\n\u003Cli\u003E\u003Ccode\u003EmixColours\u003C\/code\u003E: takes two colours and a ratio in which they should be mixed, splits them into components,\nfor each of them calculates a weighted average, and then merges them back into a colour.\u003C\/li\u003E\n\u003C\/ul\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022language-js\u0022\u003Emethods: {\n    hexToDec(hex) {\n        return parseInt(hex, 16);\n    },\n    decToHex(dec) {\n        return parseInt(dec, 10).toString(16).padStart(2, \u00270\u0027);\n    },\n    splitColour(colour) {\n        return [\n            this.hexToDec(colour.substring(1, 3)),\n            this.hexToDec(colour.substring(3, 5)),\n            this.hexToDec(colour.substring(5, 7)),\n        ];\n    },\n    mergeColour(r, g, b) {\n        return `#${this.decToHex(r)}${this.decToHex(g)}${this.decToHex(b)}`;\n    },\n    adjustValue(val, percent) {\n        val = parseInt(val * (100 + percent) \/ 100);\n        if (val \u0026gt; 255) { val = 255; }\n        if (val \u0026lt; 0) { val = 0; }\n        return val;\n    },\n    shadeColour(colour, percent) {\n        const [r, g, b] = this.splitColour(colour);\n\n        return this.mergeColour(\n            this.adjustValue(r, percent),\n            this.adjustValue(g, percent),\n            this.adjustValue(b, percent),\n        );\n    },\n    mixColours(colour1, colour2, ratio) {\n        const [r1, g1, b1] = this.splitColour(colour1);\n        const [r2, g2, b2] = this.splitColour(colour2);\n\n        return this.mergeColour(\n            r1 * ratio + r2 * (1 - ratio),\n            g1 * ratio + g2 * (1 - ratio),\n            b1 * ratio + b2 * (1 - ratio),\n        );\n    },\n},\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EAnd now it\u0027s finally time to put it all together.\u003C\/p\u003E\n\u003Cp\u003ETo calculate \u003Ccode\u003EcolourOuter\u003C\/code\u003E we take the \u003Ccode\u003EcoloursBetween\u003C\/code\u003E and we calculate\nhow far away from \u003Ccode\u003Eleft\u003C\/code\u003E is our value.\nIn our case that\u0027s \u003Ccode\u003E(value - left) \/ (right - left) = (400 - 0) \/ (500 - 0) = 400 \/ 500 = 0.8\u003C\/code\u003E.\nWe will use that value as our weight when calculating the weighted average\nbetween the colour in the \u003Ccode\u003Eleft\u003C\/code\u003E highpoint and the colour in the \u003Ccode\u003Eright\u003C\/code\u003E highpoint.\u003C\/p\u003E\n\u003Cp\u003EAnd \u003Ccode\u003EcolourInner\u003C\/code\u003E will just be just \u003Ccode\u003EcolourOuter\u003C\/code\u003E darkened by 30%.\u003C\/p\u003E\n\u003Cpre\u003E\u003Ccode class=\u0022language-js\u0022\u003EcolourOuter() {\n    const {left, right} = this.coloursBetween;\n    const ratio = (this.value - left) \/ ((right - left) || 1);\n\n    return this.mixColours(\n        this.coloursHighpoints[left],\n        this.coloursHighpoints[right],\n        1 - ratio,\n    );\n},\ncolourInner() {\n    return this.shadeColour(this.colourOuter, -30);\n},\u003C\/code\u003E\u003C\/pre\u003E\n\u003Cp\u003EAnd that\u0027s it, we\u0027re done!\u003C\/p\u003E","words":1514,"readTime":7,"lang":"en"}}}}}