From a4275a238f2a913923ee5b09edecba9c89ee4df1 Mon Sep 17 00:00:00 2001 From: dodoradio Date: Wed, 19 Jul 2023 14:12:49 +0100 Subject: Rewrite the labelling system The label code has been split into its own set of components: -TimeLabels: These try to display time labels as best as possible. They can label intervals from a minute to a day, but the code for labelling weeks is still missing. There's also no code protecting against strings overlapping with each other. - VerticalLabels: The code for these is a bit more 'analogue' than that of the time labels, but it's roughly analogous to previous code. These have also been implemented for the heartrate graph. --- src/graphs/HrGraph.qml | 66 ++----------------- src/graphs/TimeLabels.qml | 149 ++++++++++++++++++++++++++++++++++++++++++ src/graphs/VerticalLabels.qml | 79 ++++++++++++++++++++++ 3 files changed, 235 insertions(+), 59 deletions(-) create mode 100644 src/graphs/TimeLabels.qml create mode 100644 src/graphs/VerticalLabels.qml (limited to 'src/graphs') diff --git a/src/graphs/HrGraph.qml b/src/graphs/HrGraph.qml index ac5113e..cba15a5 100644 --- a/src/graphs/HrGraph.qml +++ b/src/graphs/HrGraph.qml @@ -26,42 +26,16 @@ Item { id: graph width: parent.width*0.9 anchors.horizontalCenter: parent.horizontalCenter - property var labelsArr: [] - property var valueDivisionsInterval: 0 - property var valueDivisionsCount: 0 - property var startValueDivision: 0 - property real valuesDelta: hrGraph.relativeMode ? hrGraph.maxValue - hrGraph.minValue : hrGraph.maxValue - property var timeDivisionsCount: 0 - property var timeDivisionsInterval: 0 - property var startTimeDivision: 0 - property var timeDelta: 0 - property var minTimeSeconds: 0 - - function dataLoadingDone() { - labelsRepeater.model = labelsArr.length - } - Component.onCompleted: { // I am so so sorry about this code. - console.log(typeof hrDataLoader.getTodayData()) + Component.onCompleted: { hrGraph.loadGraphData(hrDataLoader.getTodayData()) - console.log("minValue", hrGraph.minValue, "maxValue",hrGraph.maxValue) - //values divisions - valueDivisionsInterval = valuesDelta >= 50 ? (valuesDelta >= 100 ? 20: 10) : 5 - valueDivisionsCount = hrGraph.relativeMode ? (Math.ceil(hrGraph.maxValue/valueDivisionsInterval)) - (Math.ceil(hrGraph.minValue/valueDivisionsInterval)) : Math.ceil(hrGraph.maxValue/valueDivisionsInterval) + 1 - startValueDivision = hrGraph.relativeMode ? Math.ceil(hrGraph.minValue/valueDivisionsInterval)*valueDivisionsInterval : 0 - //times divisions - minTimeSeconds = dateToDaySecs(hrGraph.minTime) - timeDelta = dateToDaySecs(hrGraph.maxTime) - minTimeSeconds - timeDivisionsCount = Math.floor(timeDelta/3600) // get number of hours and divide - timeDivisionsInterval = timeDivisionsCount > 11 ? 4 : 2 - timeDivisionsCount = timeDivisionsCount/timeDivisionsInterval - startTimeDivision = hrGraph.minTime.getHours() + 1 - labelsRepeater.model = graph.timeDivisionsCount } HrDataLoader { id: hrDataLoader } - Item { // labels column + VerticalLabels { // labels column id: markerParent width: parent.width/8 + startValue: 0 + endValue: hrGraph.maxValue anchors { left: parent.left top: hrGraph.top @@ -69,17 +43,6 @@ Item { topMargin: hrGraph.lineWidth/2 bottomMargin: anchors.topMargin } - Repeater { - model: graph.valueDivisionsCount - delegate: Label { - anchors.right: parent.right - property real value: graph.startValueDivision + graph.valueDivisionsInterval*index - text: value - font.pixelSize: Dims.w(5) - y: parent.height - (parent.height)*(value/graph.valuesDelta) - height/2 - verticalAlignment: Text.AlignVCenter - } - } } LineGraph { id: hrGraph @@ -92,9 +55,11 @@ Item { relativeMode: false lineWidth: 4 } - Item { //labels row + TimeLabels { id: labelsRow height: Dims.w(5) + startTime: hrGraph.minTime / 1000 + endTime: hrGraph.maxTime / 1000 anchors { bottom: parent.bottom left: hrGraph.left @@ -102,22 +67,5 @@ Item { rightMargin: hrGraph.lineWidth/2 leftMargin: anchors.rightMargin } - Repeater { - id: labelsRepeater - model: 0 - delegate: Label { - id: dowLabel - // anchors.horizontalCenter: parent.horizontalCenter - horizontalAlignment: Text.AlignHCenter - property var value: graph.startTimeDivision + index*graph.timeDivisionsInterval - text: value + ":00" - x: ((value*3600-graph.minTimeSeconds)/graph.timeDelta)*parent.width - onValueChanged: console.log("value",value, "mintimeseconds", graph.minTimeSeconds, "timeDelta", graph.timeDelta,"x",x,"text",text) - font.pixelSize: Dims.w(5) - } - } - } - function dateToDaySecs(date) { - return (date.getHours()*3600 + date.getMinutes()*60 + date.getSeconds()) } } diff --git a/src/graphs/TimeLabels.qml b/src/graphs/TimeLabels.qml new file mode 100644 index 0000000..bebeca5 --- /dev/null +++ b/src/graphs/TimeLabels.qml @@ -0,0 +1,149 @@ +/* + * Copyright (C) 2023 Arseniy Movshev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.15 +import org.asteroid.controls 1.0 + +Item { + id: root + property real startTime: 0 + property real endTime: 0 + property int minLabels: 3 //these min and max numbers of labels are only guidelines for the algorithm, and don't actually set hard limits. + property int maxLabels: 8 + property int startValueDivision + onStartTimeChanged: update() + onEndTimeChanged: update() + onMinLabelsChanged: update() + onMaxLabelsChanged: update() + Component.onCompleted: update() + function update() { + listModel.clear() + var delta = endTime - startTime + var interval = 0 + if (delta < 60 * maxLabels) { // check 1 minute + if (delta < 60 * minLabels) { + interval = 30 //label every 30s otherwise + } else { + interval = 60 + } + startValueDivision = interval * Math.ceil(startTime / interval) + // iterate and populate the list + var currentTime = startValueDivision + var i = 0; + var date + while (currentTime < endTime) { + date = new Date(currentTime*1000) + var value = date.getHours().toString() + ":" + date.getMinutes().toString() + var x = (currentTime - startTime) / delta + listModel.append({"value": value, "x": x}) + currentTime = currentTime + interval + i++ + } + + } else if (delta < 600 * maxLabels) { // check 10 minutes + if (delta < 600 * minLabels) { + interval = 300 //label every 5m otherwise + } else { + interval = 600 + } + startValueDivision = interval * Math.ceil(startTime / interval) + // iterate and populate the list + var currentTime = startValueDivision + var i = 0; + var date + while (currentTime < endTime) { + date = new Date(currentTime*1000) + var value = date.getHours().toString() + ":" + date.getMinutes().toString() + var x = (currentTime - startTime) / delta + listModel.append({"value": value, "x": x}) + currentTime = currentTime + interval + i++ + } + + } else if (delta < 7200 * maxLabels) { // check every 2 hours + if (delta > 3600 * maxLabels) { + interval = 7200 //label every 2h if 1h doesn't work - this is an ugly workaround so that a full day still gets some sort of divisions + } else if (delta < 3600 * minLabels) { + interval = 1800 //label every 30m otherwise + } else { + interval = 3600 + } + startValueDivision = interval * Math.ceil(startTime / interval) + // iterate and populate the list + var currentTime = startValueDivision + var i = 0; + var date + while (currentTime < endTime) { + date = new Date(currentTime*1000) + var value = date.getHours().toString() + ":" + date.getMinutes().toString() + var x = (currentTime - startTime) / delta + listModel.append({"value": value, "x": x}) + currentTime = currentTime + interval + i++ + } + + } else if (delta < 86400 * maxLabels) { // check days + if (delta < 86400 * minLabels) { + interval = 43200 //label every 12h otherwise + } else { + interval = 86400 + } + startValueDivision = interval * Math.ceil(startTime / interval) + // iterate and populate the list + var currentTime = startValueDivision + var i = 0; + var date + while (currentTime < endTime) { + date = new Date(currentTime*1000) + var value = date.getDate().toString() /* we should add am and pm here for 12h mode*/ + var x = ((currentTime - startTime) / delta) + listModel.append({"value": value, "x": x}) + currentTime = currentTime + interval + i++ + } + + } else if (delta < 604800 * maxLabels) { // check weeks + interval = 604800 + startValueDivision = interval * Math.ceil(startTime / interval) + // iterate and populate the list + var currentTime = startValueDivision + var i = 0; + var date + while (currentTime < endTime) { + date = new Date(currentTime*1000) + var value = date.getDate().toString() + var x = ((currentTime - startTime) / delta) + listModel.append({"value": value, "x": x}) + currentTime = currentTime + interval + i++ + } + } else { // handle months + console.log("viewing more than several days of data isn't implemented yet. please pester dodoradio about this.") + } + // this is slightly crude - we assume that the min and max label numbers will be set in such a way that halving gets us a reasonable interval. + // we also ignore everything over a number of weeks, as this needs a bit more thought that I don't want to deal with right now. this graph code is stalling too much other development. + } + Repeater { + model: ListModel { id: listModel } + delegate: Label { + text: model.value + font.pixelSize: Dims.w(5) + x: model.x*root.width - width/2 + verticalAlignment: Text.AlignVCenter + } + } +} diff --git a/src/graphs/VerticalLabels.qml b/src/graphs/VerticalLabels.qml new file mode 100644 index 0000000..5cb796b --- /dev/null +++ b/src/graphs/VerticalLabels.qml @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 Arseniy Movshev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import QtQuick 2.15 +import org.asteroid.controls 1.0 + +Item { + id: root + property real startValue: 0 + property real endValue: 0 + property int minLabels: 3 + property int maxLabels: 8 + property real valueDivisionsInterval: 0 + property real startValueDivision: 0 + property real valuesDelta + onStartValueChanged: update() + onEndValueChanged: update() + onMinLabelsChanged: update() + onMaxLabelsChanged: update() + + function update() { // this tries to guess how to generate nice looking labels + valuesDelta = endValue - startValue + var powTen = Math.pow(10,Math.trunc(Math.log10(valuesDelta))) + var interval = powTen + var numLabel = Math.floor(valuesDelta/interval) + if (numLabel > maxLabels) { + interval = powTen*2 + numLabel = valuesDelta/interval + if (numLabel > maxLabels) { + interval = powTen*5 + numLabel = valuesDelta/interval + if (numLabel > maxLabels) { + interval = powTen*10 + numLabel = valuesDelta/interval + } + } + } + if (numLabel < minLabels) { + interval = powTen/2 + numLabel = valuesDelta/interval + if (numLabel < minLabels) { + interval = powTen/5 + numLabel = valuesDelta/interval + if (numLabel < minLabels) { + interval = powTen/10 + numLabel = valuesDelta/interval + } + } + } + valueDivisionsInterval = interval + labelsRepeater.model = Math.round(numLabel) + 1 + startValueDivision = powTen*Math.trunc(startValue/powTen) + } + Repeater { + id: labelsRepeater + delegate: Label { + anchors.right: parent.right + property real value: root.startValueDivision + root.valueDivisionsInterval*index + text: value > 1000 ? value/1000 + "k" : value + font.pixelSize: Dims.w(5) + y: parent.height - (parent.height)*(value/root.valuesDelta) - height/2 + verticalAlignment: Text.AlignVCenter + } + } +} -- cgit v1.2.3-54-g00ecf