Line chart as schedule interface. Drag points to modify hourly values

I was asked nicely. :dollar:
Is it possible to use a line chart to make a schedule interface. So that you can move the line from the points and then save the changed schedule.

It is possible.
Here it is:

[{"id":"fbb85d60bcc43b2f","type":"ui-template","z":"9ab81d258b54a577","group":"1c29530020f6b63c","page":"","ui":"","name":"Schedule Chart","order":1,"width":"6","height":"4","head":"","format":"<template>\n    <div class=\"base-container\">\n        <div class=\"chart-container\"><canvas :ref=\"`chart-${id}`\" /></div>        \n    </div>\n</template>\n\n<script>\n    export default {        \n        data() {\n            return {\n                title:\"Chart\",\n                pendingdata:null\n            }\n        },\n        computed: {\n            \n        },\n        mounted() {\n            this.$socket.on('msg-input:' + this.id, this.onInput)            \n            let interval = setInterval(() => {\n                if (window.Chart) {                  \n                    clearInterval(interval)\n                    this.draw()\n                }\n            }, 100)\n        },\n        methods: {\n            draw () {\n                const ctx = this.$refs['chart-'+this.id] \n                const chart = new Chart(ctx, {\n                    type: 'line',\n                    data: {\n                        datasets: [{\n                            data: [20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20],\n                        }],\n                        labels: [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23]\n                    },\n                    options: {                        \n                        locale:\"et-EE\",\n                        maintainAspectRatio: false,\n                        animation: false,\n                        responsive: true,\n                        scales: {\n                            x: {\n                                type: 'linear',                               \n                                ticks: {\n                                    stepSize: 1, \n                                    autoSkip: false, \n                                    maxTicksLimit:24, \n                                    maxRotation:0 \n                                },\n                                grid:{\n                                    color:\"#66666620\"\n                                }\n                            },\n                            y:{\n                                position: 'right',\n                                max: 24,\n                                min: 16,\n                                ticks: {\n                                    stepSize: 1\n                                },\n                                grid:{\n                                    color:\"#66666620\"\n                                }\n                            }\n                        },\n                        elements:{\n                            line:{\n                                borderWidth:1,\n                                tension:0.25 \n                            },\n                            point:{\n                                pointRadius:3\n                            }\n                        },                        \n                        plugins: {\n                            legend: {\n                                display: false                               \n                            },\n                            title: {\n                                display: true,\n                                text:\"Schedule\"                              \n                            }\n                        }\n                    },\n                })\n\n                this.chart = chart\n                let activePoint = null\n                const mapPos = function (value, start1, stop1, start2, stop2) {\n                    let v = Math.round(start2 + (stop2 - start2) * ((value - start1) / (stop1 - start1)))\n                    return Math.min(stop2, Math.max(start2, v));\n                }\n                const downHandler = (event) => {\n                    const points = this.chart.getElementsAtEventForMode(event, 'nearest', { intersect: false }, false)                 \n                    if (points.length > 0) {                        \n                        activePoint = points[0]\n                        ctx.onpointermove = moveHandler\n                    }\n                }\n\n                const upHandler = (e) => {                   \n                    activePoint = null\n                    ctx.onpointermove = null\n                    this.sendChanges()\n                }\n\n                const moveHandler = (event) => {\n                    if (activePoint != null) {                     \n                        const datasetIndex = activePoint.datasetIndex\n                        const position = getRelativePosition(event, this.chart)                       \n                        const chartArea = this.chart.chartArea\n                        const yAxis = this.chart.scales.y\n                        const yValue = mapPos(position.y, chartArea.bottom, chartArea.top, yAxis.min, yAxis.max)\n                        this.chart.data.datasets[datasetIndex].data[activePoint.index] = yValue\n                        this.chart.update()\n                    }\n                }\n\n                ctx.onpointerdown = downHandler\n                ctx.onpointerup = upHandler\n                ctx.onpointermove = null\n\n                if(this.pendingdata != null){\n                    this.setDataset(this.pendingdata.name, this.pendingdata.color, this.pendingdata.data)\n                    this.updateChart()\n                }\n                else{\n                    this.updateChart()\n                }\n            },\n            clearChart() {               \n                if (this.chart) {\n                    this.chart.data.datasets.forEach(ds => ds.data = [])\n                    this.updateChart()\n                }                \n            },          \n            setDataset(name,color,data){\n                if(!this.chart){\n                    return\n                }\n                let dataset = this.chart.data.datasets.find(e => e.label === name)\n                if(!dataset){\n                    this.chart.data.datasets.push({label:name,borderColor:color,data:data})\n                }\n                else{\n                    dataset.data = data\n                }\n                this.chart.options.plugins.title.text = name\n            },\n            updateChart() {\n                if (this.chart) {\n                    this.chart.update()\n                }\n            },\n            sendChanges(){\n                let n = this.chart.options.plugins.title.text\n                let dataset = this.chart.data.datasets.find(e => e.label === n)\n                this.send({payload:{name:n,color: dataset.borderColor,data:dataset.data}})\n            },\n            onInput (msg) {                \n                if(!this.chart){\n                    this.pendingdata = msg.payload\n                }\n                else{\n                    this.clearChart()\n                    this.pendingdata = null\n                    this.setDataset(msg.payload.name, msg.payload.color, msg.payload.data)\n                    this.updateChart()\n                }               \n            }\n        }\n    }\n</script>\n\n<style>\n    .base-container {\n        width: 100%;\n        height: 100%;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n        padding-inline: .5rem;\n        border-radius: 8px;\n        border: 1px solid rgb(var(--v-theme-group-outline));\n        container: chat / size;    \n    }\n    \n    .chart-container {\n        position: relative;\n        margin: auto;\n        height: 100cqb;\n        width: 100cqi;\n        display: flex;\n        justify-content: center;\n        align-items: center;\n    }\n</style>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":560,"y":440,"wires":[["ed0e0c1d6cf3aae6"]]},{"id":"6b472dd07145836b","type":"ui-template","z":"9ab81d258b54a577","group":"","page":"","ui":"cf21e7dda9e29be4","name":"load chart","order":0,"width":0,"height":0,"head":"","format":"<script src=\"https://cdn.jsdelivr.net/npm/chart.js\"></script>","storeOutMessages":true,"passthru":true,"resendOnRefresh":true,"templateScope":"widget:ui","className":"","x":540,"y":400,"wires":[[]]},{"id":"09b444d28cd78e4f","type":"function","z":"9ab81d258b54a577","name":"feed shedule","func":"const schedule = global.get(\"schedule\") ?? {\n    name:\"Temperature schedule\",\n    color:\"red\",\n    data: Array(24).fill(21)\n};\n\n\nmsg.payload = schedule\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":440,"wires":[["fbb85d60bcc43b2f"]]},{"id":"879e30ecc3db9166","type":"inject","z":"9ab81d258b54a577","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":230,"y":440,"wires":[["09b444d28cd78e4f"]]},{"id":"ed0e0c1d6cf3aae6","type":"function","z":"9ab81d258b54a577","name":"store schedule","func":"global.set(\"schedule\", msg.payload);\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":740,"y":440,"wires":[[]]},{"id":"1c29530020f6b63c","type":"ui-group","name":"Schedule","page":"a3c04197f8e84242","width":"6","height":"1","order":1,"showTitle":true,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"cf21e7dda9e29be4","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"showPageTitle":true,"navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":1,"showDisconnectNotification":true},{"id":"a3c04197f8e84242","type":"ui-page","name":"Main","ui":"cf21e7dda9e29be4","path":"/main","icon":"home","layout":"grid","theme":"1b65e7d639a485c8","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":"1b65e7d639a485c8","type":"ui-theme","name":"Nipi","colors":{"surface":"#3d3d3d","primary":"#0094ce","bgPage":"#292929","groupBg":"#1f1f1f","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}}]

Edit: Changed the drag behavior to snap to closest integer and to avoid to drag over limits.

7 Likes