I was asked nicely.
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.