Improve ac charts

This commit is contained in:
Michael Zanetti 2023-05-02 01:15:52 +02:00
parent 0e5619b4f9
commit edbda7abda
3 changed files with 971 additions and 728 deletions

View File

@ -71,12 +71,6 @@ Item {
onEntriesAdded: { onEntriesAdded: {
print("**** entries added", index, entries.length, "entries in series:", valueSeries.count, "in model", logsModel.count) print("**** entries added", index, entries.length, "entries in series:", valueSeries.count, "in model", logsModel.count)
// if (valueSeries.count == 0) {
// print("adding zero item", new Date())
// valueSeries.insert(0, new Date(), 0)
// zeroSeries.ensureValue(new Date())
// }
for (var i = 0; i < entries.length; i++) { for (var i = 0; i < entries.length; i++) {
var entry = entries[i] var entry = entries[i]
// print("entry", entry.timestamp, entry.source, JSON.stringify(entry.values)) // print("entry", entry.timestamp, entry.source, JSON.stringify(entry.values))
@ -92,14 +86,6 @@ Item {
valueSeries.insert(insertIdx, entry.timestamp.getTime() - 500, !value) valueSeries.insert(insertIdx, entry.timestamp.getTime() - 500, !value)
valueSeries.insert(insertIdx+1, entry.timestamp, value) valueSeries.insert(insertIdx+1, entry.timestamp, value)
// valueSeries.removePoints(0, 1);
// if (insertIdx == 0) {
// // first index, we'll have to update the "now" value
// valueSeries.insert(0, entry.timestamp.getTime() + 2000, value)
// zeroSeries.ensureValue(new Date(entry.timestamp.getTime() + 2000))
// }
} else { } else {
var value = entry.values[root.stateType.name] var value = entry.values[root.stateType.name]
if (value == null) { if (value == null) {
@ -127,9 +113,12 @@ Item {
onEntriesRemoved: { onEntriesRemoved: {
print("removing:", index, count, valueSeries.count) print("removing:", index, count, valueSeries.count)
if (root.stateType.type.toLowerCase() == "bool") { if (root.stateType.type.toLowerCase() == "bool") {
valueSeries.removePoints((index * 2) /*+ 1*/, count * 2) valueSeries.removePoints(index * 2, count * 2)
if (valueSeries.count == 1) {
valueSeries.removePoints(0, 1);
}
} else { } else {
valueSeries.removePoints(index /*+ 1*/, count) valueSeries.removePoints(index, count)
} }
zeroSeries.shrink() zeroSeries.shrink()

View File

@ -31,9 +31,10 @@ Page {
id: d id: d
property date now: new Date() property date now: new Date()
property int sampleRate: NewLogsModel.SampleRate15Mins property int sampleRate: selectionTabs.currentValue.sampleRate
property int range: 60 * 24 readonly property int range: selectionTabs.currentValue.range
readonly property int visibleValues: range / sampleRate
readonly property var startTime: { readonly property var startTime: {
var date = new Date(now); var date = new Date(now);
@ -49,6 +50,50 @@ Page {
} }
ColumnLayout {
anchors.fill: parent
SelectionTabs {
id: selectionTabs
Layout.fillWidth: true
Layout.leftMargin: Style.smallMargins
Layout.rightMargin: Style.smallMargins
currentIndex: 1
model: ListModel {
ListElement {
modelData: qsTr("Hours")
sampleRate: NewLogsModel.SampleRate1Min
range: 180 // 3 Hours: 3 * 60
}
ListElement {
modelData: qsTr("Days")
sampleRate: NewLogsModel.SampleRate15Mins
range: 1440 // 1 Day: 24 * 60
}
ListElement {
modelData: qsTr("Weeks")
sampleRate: NewLogsModel.SampleRate1Hour
range: 10080 // 7 Days: 7 * 24 * 60
}
ListElement {
modelData: qsTr("Months")
sampleRate: NewLogsModel.SampleRate3Hours
range: 43200 // 30 Days: 30 * 24 * 60
}
}
onTabSelected: {
d.now = new Date()
// logsModel.clear()
print("*** tab selected")
// logsModel.fetchLogs()
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ChartView { ChartView {
id: chartView id: chartView
anchors.fill: parent anchors.fill: parent
@ -57,13 +102,51 @@ Page {
margins.left: 0 margins.left: 0
margins.right: 0 margins.right: 0
margins.top: 0 margins.top: 0
margins.bottom: Style.smallIconSize + Style.margins margins.bottom: 0
legend.visible: false legend.visible: false
legend.alignment: Qt.AlignBottom legend.alignment: Qt.AlignBottom
legend.font: Style.extraSmallFont legend.font: Style.extraSmallFont
legend.labelColor: Style.foregroundColor legend.labelColor: Style.foregroundColor
property int busyCounter: 0
ActivityIndicator {
anchors.centerIn: parent
visible: chartView.busyCounter > 0
opacity: .5
}
// Label {
// anchors.centerIn: parent
// visible: !logsModel.busy && logsModel.count == 0
// text: qsTr("No data")
// font: Style.smallFont
// opacity: .5
// }
Label {
x: chartView.x + chartView.plotArea.x + (chartView.plotArea.width - width) / 2
y: chartView.y + chartView.plotArea.y + Style.smallMargins
text: {
switch (d.sampleRate) {
case NewLogsModel.SampleRate1Min:
return d.startTime.toLocaleDateString(Qt.locale(), Locale.LongFormat)
case NewLogsModel.SampleRate15Mins:
case NewLogsModel.SampleRate1Hour:
case NewLogsModel.SampleRate3Hours:
case NewLogsModel.SampleRate1Day:
case NewLogsModel.SampleRate1Week:
case NewLogsModel.SampleRate1Month:
case NewLogsModel.SampleRate1Year:
return d.startTime.toLocaleDateString(Qt.locale(), Locale.ShortFormat) + " - " + d.endTime.toLocaleDateString(Qt.locale(), Locale.ShortFormat)
}
}
font: Style.smallFont
opacity: ((new Date().getTime() - d.now.getTime()) / d.sampleRate / 60000) > d.visibleValues ? .5 : 0
Behavior on opacity { NumberAnimation {} }
}
ValueAxis { ValueAxis {
id: temperatureAxis id: temperatureAxis
min: 0 min: 0
@ -166,27 +249,27 @@ Page {
min: d.startTime min: d.startTime
max: d.endTime max: d.endTime
format: { format: {
// switch (selectionTabs.currentValue.sampleRate) { switch (selectionTabs.currentValue.sampleRate) {
// case EnergyLogs.SampleRate1Min: case NewLogsModel.SampleRate1Min:
// case EnergyLogs.SampleRate15Mins: case NewLogsModel.SampleRate15Mins:
return "hh:mm" return "hh:mm"
// case EnergyLogs.SampleRate1Hour: case NewLogsModel.SampleRate1Hour:
// case EnergyLogs.SampleRate3Hours: case NewLogsModel.SampleRate3Hours:
// case EnergyLogs.SampleRate1Day: case NewLogsModel.SampleRate1Day:
// return "dd.MM." return "dd.MM."
// } }
} }
tickCount: { tickCount: {
// switch (selectionTabs.currentValue.sampleRate) { switch (selectionTabs.currentValue.sampleRate) {
// case EnergyLogs.SampleRate1Min: case NewLogsModel.SampleRate1Min:
// case EnergyLogs.SampleRate15Mins: case NewLogsModel.SampleRate15Mins:
// return root.width > 500 ? 13 : 7 return root.width > 500 ? 13 : 7
// case EnergyLogs.SampleRate1Hour: case NewLogsModel.SampleRate1Hour:
// return 7 return 7
// case EnergyLogs.SampleRate3Hours: case NewLogsModel.SampleRate3Hours:
// case EnergyLogs.SampleRate1Day: case NewLogsModel.SampleRate1Day:
return root.width > 500 ? 12 : 6 return root.width > 500 ? 12 : 6
// } }
} }
labelsFont: Style.extraSmallFont labelsFont: Style.extraSmallFont
gridVisible: false gridVisible: false
@ -211,6 +294,13 @@ Page {
startTime: new Date(d.startTime.getTime() - d.range * 60000) startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000) endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate sampleRate: d.sampleRate
onBusyChanged: {
if (busy) {
chartView.busyCounter++
} else {
chartView.busyCounter--
}
}
onEntriesAdded: { onEntriesAdded: {
for (var i = 0; i < entries.length; i++) { for (var i = 0; i < entries.length; i++) {
@ -261,6 +351,14 @@ Page {
startTime: new Date(d.startTime.getTime() - d.range * 60000) startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000) endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate sampleRate: d.sampleRate
onBusyChanged: {
if (busy) {
chartView.busyCounter++
} else {
chartView.busyCounter--
}
}
onEntriesAdded: { onEntriesAdded: {
for (var i = 0; i < entries.length; i++) { for (var i = 0; i < entries.length; i++) {
var entry = entries[i] var entry = entries[i]
@ -311,6 +409,13 @@ Page {
startTime: new Date(d.startTime.getTime() - d.range * 60000) startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000) endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate sampleRate: d.sampleRate
onBusyChanged: {
if (busy) {
chartView.busyCounter++
} else {
chartView.busyCounter--
}
}
onEntriesAdded: { onEntriesAdded: {
for (var i = 0; i < entries.length; i++) { for (var i = 0; i < entries.length; i++) {
@ -358,6 +463,13 @@ Page {
startTime: new Date(d.startTime.getTime() - d.range * 60000) startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000) endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate sampleRate: d.sampleRate
onBusyChanged: {
if (busy) {
chartView.busyCounter++
} else {
chartView.busyCounter--
}
}
onEntriesAdded: { onEntriesAdded: {
for (var i = 0; i < entries.length; i++) { for (var i = 0; i < entries.length; i++) {
var entry = entries[i] var entry = entries[i]
@ -392,7 +504,8 @@ Page {
} }
Repeater { Repeater {
// model: zoneWrapper.windowSensors id: windowOpenRepeater
model: zoneWrapper.windowSensors
delegate: Item { delegate: Item {
id: closableDelegate id: closableDelegate
readonly property Thing thing: zoneWrapper.windowSensors.get(index) readonly property Thing thing: zoneWrapper.windowSensors.get(index)
@ -405,21 +518,78 @@ Page {
id: closableLowerSeries id: closableLowerSeries
XYPoint {x: dateTimeAxis.min.getTime(); y: 0} XYPoint {x: dateTimeAxis.min.getTime(); y: 0}
XYPoint {x: dateTimeAxis.max.getTime(); y: 0} XYPoint {x: dateTimeAxis.max.getTime(); y: 0}
function ensureValue(timestamp) {
if (count == 0) {
append(timestamp, 0)
} else if (count == 1) {
if (timestamp.getTime() < at(0).x) {
insert(0, timestamp, 0)
} else {
append(timestamp, 0)
}
} else {
if (timestamp.getTime() < at(0).x) {
remove(0)
insert(0, timestamp, 0)
} else if (timestamp.getTime() > at(1).x) {
remove(1)
append(timestamp, 0)
}
}
}
function shrink() {
clear();
if (logsModel.count > 0) {
ensureValue(logsModel.get(0).timestamp)
ensureValue(logsModel.get(logsModel.count-1).timestamp)
}
}
} }
readonly property NewLogsModel logsModel: NewLogsModel { readonly property NewLogsModel logsModel: NewLogsModel {
id: logsModelNg engine: _engine
engine: typeIds.length ? _engine : null
source: "state-" + thing.id + "-closed" source: "state-" + thing.id + "-closed"
startTime: new Date(d.startTime.getTime() - d.range * 60000) startTime: new Date(d.startTime.getTime() - d.range * 60000)
endTime: new Date(d.endTime.getTime() + d.range * 60000) endTime: new Date(d.endTime.getTime() + d.range * 60000)
sampleRate: d.sampleRate property bool haveGeneratedLast: false
onBusyChanged: {
if (busy) {
chartView.busyCounter++
} else {
chartView.busyCounter--
}
}
onEntriesAdded: {
for (var i = 0; i < entries.length; i++) {
var entry = entries[i]
var value = entry.values["closed"]
if (value == null) {
value = false;
} }
BoolSeriesAdapter { closableLowerSeries.ensureValue(entry.timestamp)
logsModel: closableDelegate.logsModel
xySeries: closableUpperSeries // for booleans, we'll insert the opposite value right before the new one so the position is doubled
inverted: true var insertIdx = (index + i) * 2
closableUpperSeries.insert(insertIdx, entry.timestamp.getTime() - 500, value)
closableUpperSeries.insert(insertIdx+1, entry.timestamp, !value)
}
var last = closableUpperSeries.at(closableUpperSeries.count-1);
if (last.x < d.endTime) {
closableUpperSeries.append(d.endTime, last.y)
haveGeneratedLast = true
}
}
onEntriesRemoved: {
closableUpperSeries.removePoints(index * 2, count * 2)
if (haveGeneratedLast) {
closableUpperSeries.removePoints(series.count - 1, 1)
haveGeneratedLast = false
}
}
Component.onCompleted: fetchLogs()
} }
Component.onCompleted: { Component.onCompleted: {
@ -442,7 +612,8 @@ Page {
} }
Repeater { Repeater {
// model: zoneWrapper.thermostats.count id: heatingRepeater
model: zoneWrapper.thermostats.count
delegate: Item { delegate: Item {
id: heatingDelegate id: heatingDelegate
readonly property Thing thing: zoneWrapper.thermostats.get(index) readonly property Thing thing: zoneWrapper.thermostats.get(index)
@ -454,32 +625,82 @@ Page {
} }
LineSeries { LineSeries {
id: heatingLowerSeries id: heatingLowerSeries
XYPoint {x: dateTimeAxis.max.getTime(); y: 0}
XYPoint {x: dateTimeAxis.min.getTime(); y: 0} XYPoint {x: dateTimeAxis.min.getTime(); y: 0}
XYPoint {x: dateTimeAxis.max.getTime(); y: 0}
function ensureValue(timestamp) {
if (count == 0) {
append(timestamp, 0)
} else if (count == 1) {
if (timestamp.getTime() < at(0).x) {
insert(0, timestamp, 0)
} else {
append(timestamp, 0)
}
} else {
if (timestamp.getTime() < at(0).x) {
remove(0)
insert(0, timestamp, 0)
} else if (timestamp.getTime() > at(1).x) {
remove(1)
append(timestamp, 0)
}
}
}
function shrink() {
clear();
if (logsModel.count > 0) {
ensureValue(logsModel.get(0).timestamp)
ensureValue(logsModel.get(logsModel.count-1).timestamp)
}
}
} }
readonly property LogsModel logsModel: LogsModel { readonly property NewLogsModel logsModel: NewLogsModel {
objectName: "heat: " + thing.name objectName: "heat: " + thing.name
engine: typeIds.length > 0 ? _engine : null engine: _engine
thingId: thing ? thing.id : "" source: "state-" + thing.id + "-heatingOn"
typeIds: { startTime: new Date(d.startTime.getTime() - d.range * 60000)
var ret = []; endTime: new Date(d.endTime.getTime() + d.range * 60000)
var heatingOnStateType = thing.thingClass.stateTypes.findByName("heatingOn") property bool haveGeneratedLast: false
if (heatingOnStateType) { onBusyChanged: {
ret.push(heatingOnStateType.id) if (busy) {
chartView.busyCounter++
} else {
chartView.busyCounter--
} }
return ret;
} }
sourceFilter: LogsModel.SourceStates onEntriesAdded: {
live: true for (var i = 0; i < entries.length; i++) {
// graphSeries: heatingUpperSeries var entry = entries[i]
viewStartTime: dateTimeAxis.min var value = entry.values["heatingOn"]
if (value == null) {
value = false;
} }
BoolSeriesAdapter { heatingLowerSeries.ensureValue(entry.timestamp)
logsModel: heatingDelegate.logsModel
xySeries: heatingUpperSeries // for booleans, we'll insert the opposite value right before the new one so the position is doubled
var insertIdx = (index + i) * 2
heatingUpperSeries.insert(insertIdx, entry.timestamp.getTime() - 500, !value)
heatingUpperSeries.insert(insertIdx+1, entry.timestamp, value)
}
var last = heatingUpperSeries.at(heatingUpperSeries.count-1);
if (last.x < d.endTime) {
heatingUpperSeries.append(d.endTime, last.y)
heatingLowerSeries.ensureValue(d.endTime)
haveGeneratedLast = true
}
}
onEntriesRemoved: {
heatingUpperSeries.removePoints(index * 2, count * 2)
if (haveGeneratedLast) {
heatingUpperSeries.removePoints(series.count - 1, 1)
haveGeneratedLast = false
}
heatingLowerSeries.shrink()
}
Component.onCompleted: fetchLogs()
} }
Component.onCompleted: { Component.onCompleted: {
@ -488,10 +709,6 @@ Page {
series.upperSeries = heatingUpperSeries series.upperSeries = heatingUpperSeries
series.color = Style.red series.color = Style.red
series.opacity = 0.1 series.opacity = 0.1
// series.opacity = Qt.binding(function() {
// return d.selectedSeries == null || d.selectedSeries == series ? 1 : 0.3
// })
series.borderWidth = 0; series.borderWidth = 0;
series.borderColor = series.color series.borderColor = series.color
} }
@ -502,7 +719,6 @@ Page {
} }
} }
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
@ -587,15 +803,24 @@ Page {
interval: 300 interval: 300
repeat: false repeat: false
onTriggered: { onTriggered: {
// for (var i = 0; i < consumersRepeater.count; i++) { for (var i = 0; i < thermostatsRepeater.count; i++) {
// if (consumersRepeater.itemAt(i).logs.fetchingData) { thermostatsRepeater.itemAt(i).logsModel.fetchLogs()
// wheelStopTimer.start() }
// return; for (var i = 0; i < tempRepeater.count; i++) {
// } tempRepeater.itemAt(i).logsModel.fetchLogs()
// } }
for (var i = 0; i < humidityRepeater.count; i++) {
// powerBalanceLogs.fetchLogs() humidityRepeater.itemAt(i).logsModel.fetchLogs()
// logsLoader.fetchLogs() }
for (var i = 0; i < vocRepeater.count; i++) {
vocRepeater.itemAt(i).logsModel.fetchLogs()
}
for (var i = 0; i < windowOpenRepeater.count; i++) {
windowOpenRepeater.itemAt(i).logsModel.fetchLogs()
}
for (var i = 0; i < heatingRepeater.count; i++) {
heatingRepeater.itemAt(i).logsModel.fetchLogs()
}
} }
} }
@ -735,19 +960,27 @@ Page {
} }
} }
} }
}
GridLayout {
RowLayout {
id: legend id: legend
anchors { left: parent.left; bottom: parent.bottom; right: parent.right } Layout.fillWidth: true
anchors.leftMargin: chartView.plotArea.x Layout.leftMargin: chartView.plotArea.x
height: Style.smallIconSize Layout.rightMargin: Style.smallMargins
anchors.margins: Style.margins Layout.bottomMargin: Style.smallMargins
columns: Math.min(Math.max(1, width / 100),
thermostatsRepeater.count
+ tempRepeater.count
+ humidityRepeater.count
+ vocRepeater.count
+ (windowOpenRepeater.count > 0 ? 1 : 0)
+ (heatingRepeater.count > 0 ? 1 : 0))
Repeater { Repeater {
model: thermostatsRepeater.count model: thermostatsRepeater.count
delegate: LegendDelegate { delegate: LegendDelegate {
thing: thermostatsRepeater.itemAt(index).thing Layout.fillWidth: true
text: thermostatsRepeater.itemAt(index).thing.name
iconName: app.interfaceToIcon("thermostat") iconName: app.interfaceToIcon("thermostat")
color: app.interfaceToColor("temperaturesensor") color: app.interfaceToColor("temperaturesensor")
} }
@ -755,7 +988,8 @@ Page {
Repeater { Repeater {
model: tempRepeater.count model: tempRepeater.count
delegate: LegendDelegate { delegate: LegendDelegate {
thing: tempRepeater.itemAt(index).thing Layout.fillWidth: true
text: tempRepeater.itemAt(index).thing.name
iconName: app.interfaceToIcon("temperaturesensor") iconName: app.interfaceToIcon("temperaturesensor")
color: app.interfaceToColor("temperaturesensor") color: app.interfaceToColor("temperaturesensor")
} }
@ -763,7 +997,8 @@ Page {
Repeater { Repeater {
model: humidityRepeater.count model: humidityRepeater.count
delegate: LegendDelegate { delegate: LegendDelegate {
thing: humidityRepeater.itemAt(index).thing Layout.fillWidth: true
text: humidityRepeater.itemAt(index).thing.name
iconName: app.interfaceToIcon("humiditysensor") iconName: app.interfaceToIcon("humiditysensor")
color: app.interfaceToColor("humiditysensor") color: app.interfaceToColor("humiditysensor")
} }
@ -771,11 +1006,24 @@ Page {
Repeater { Repeater {
model: vocRepeater.count model: vocRepeater.count
delegate: LegendDelegate { delegate: LegendDelegate {
thing: vocRepeater.itemAt(index).thing Layout.fillWidth: true
text: vocRepeater.itemAt(index).thing.name
iconName: app.interfaceToIcon("vocsensor") iconName: app.interfaceToIcon("vocsensor")
color: app.interfaceToColor("vocsensor") color: app.interfaceToColor("vocsensor")
} }
} }
LegendDelegate {
Layout.fillWidth: true
color: Qt.rgba(Style.green.r, Style.green.g, Style.green.b, 0.1)
text: qsTr("Window open")
}
LegendDelegate {
Layout.fillWidth: true
color: Qt.rgba(Style.red.r, Style.red.g, Style.red.b, 0.1)
text: qsTr("Heating on")
}
} }
} }
}

View File

@ -9,31 +9,37 @@ import QtCharts 2.3
Item { Item {
id: root id: root
property Thing thing: null implicitHeight: Style.smallIconSize + Style.smallMargins
property string iconName: "" property alias text: textLabel.text
property alias iconName: icon.name
property color color: "white" property color color: "white"
Layout.fillWidth: true
Layout.fillHeight: true
// opacity: selfProductionConsumptionSeries.opacity
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
anchors.topMargin: -Style.smallMargins anchors.topMargin: -Style.smallMargins
anchors.bottomMargin: -Style.smallMargins anchors.bottomMargin: -Style.smallMargins
// onClicked: d.selectSeries(selfProductionConsumptionSeries)
} }
Row { Row {
anchors.centerIn: parent anchors.centerIn: parent
spacing: Style.smallMargins spacing: Style.smallMargins
ColorIcon { ColorIcon {
name: root.iconName id: icon
size: Style.smallIconSize size: Style.smallIconSize
color: root.color color: root.color
visible: root.iconName != ""
} }
Rectangle {
width: Style.smallIconSize
height: Style.smallIconSize
color: root.color
visible: root.iconName == ""
}
Label { Label {
id: textLabel
width: parent.parent.width - x width: parent.parent.width - x
elide: Text.ElideRight elide: Text.ElideRight
visible: root.width > 60 visible: root.width > 60
text: root.thing.name
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
font: Style.smallFont font: Style.smallFont
} }