From 65d983d5e3ab09771a641b50f42f3bfed1fd54d4 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Fri, 26 Oct 2018 12:11:25 +0200 Subject: [PATCH] more work on qtcharts --- libnymea-app-core/models/logsmodelng.cpp | 44 ++- nymea-app/resources.qrc | 2 + nymea-app/ui/customviews/GenericTypeGraph.qml | 252 ++++++++++++++++++ .../ui/customviews/GenericTypeGraphPre110.qml | 84 ++++++ nymea-app/ui/devicepages/StateLogPage.qml | 232 +--------------- 5 files changed, 385 insertions(+), 229 deletions(-) create mode 100644 nymea-app/ui/customviews/GenericTypeGraph.qml create mode 100644 nymea-app/ui/customviews/GenericTypeGraphPre110.qml diff --git a/libnymea-app-core/models/logsmodelng.cpp b/libnymea-app-core/models/logsmodelng.cpp index eccfafba..cfb02fa4 100644 --- a/libnymea-app-core/models/logsmodelng.cpp +++ b/libnymea-app-core/models/logsmodelng.cpp @@ -213,9 +213,39 @@ void LogsModelNg::logsReply(const QVariantMap &data) for (int i = 0; i < newBlock.count(); i++) { LogEntry *entry = newBlock.at(i); m_list.insert(offset + i, entry); - qDebug() << "Adding line series point:" << i << entry->timestamp().toSecsSinceEpoch() << entry->value().toReal(); + if (m_graphSeries) { - m_graphSeries->insert(offset + i, QPointF(entry->timestamp().toMSecsSinceEpoch(), entry->value().toReal())); + Device *dev = m_engine->deviceManager()->devices()->getDevice(entry->deviceId()); + if (dev && dev->deviceClass()->stateTypes()->getStateType(entry->typeId())->type() == "Bool") { + // We don't want bools painting triangles, add a toggle point to keep lines straight + if (i > 0) { + LogEntry *newerEntry = newBlock.at(i - 1); + if (newerEntry->value().toBool() != entry->value().toBool()) { + qDebug() << "Adding bool line series point:" << (newerEntry->timestamp().addSecs(-1)) << newerEntry->timestamp().addSecs(-1).toMSecsSinceEpoch() << (entry->value().toBool() ? 1 : 0) << "(correction)"; + m_graphSeries->append(QPointF(newerEntry->timestamp().addSecs(-1).toMSecsSinceEpoch(), entry->value().toBool() ? 1 : 0)); + } + } + if (m_graphSeries->count() == 0) { + qDebug() << "Adding bool line series point:" << QDateTime::currentDateTime() << QDateTime::currentDateTime().toMSecsSinceEpoch() - 1 << (entry->value().toBool() ? 1 : 0) << "(beginning)"; + m_graphSeries->append(QPointF(QDateTime::currentDateTime().toMSecsSinceEpoch(), 1));// entry->value().toBool() ? 1 : 0)); + m_graphSeries->append(QPointF(QDateTime::currentDateTime().toMSecsSinceEpoch(), entry->value().toBool() ? 1 : 0)); + } + qDebug() << "Adding bool line series point:" << entry->timestamp() << entry->timestamp().toMSecsSinceEpoch() << (entry->value().toBool() ? 1 : 0); + m_graphSeries->append(QPointF(entry->timestamp().toMSecsSinceEpoch(), entry->value().toBool() ? 1 : 0)); + } else { +// if (i > 0) { +// LogEntry *newerEntry = newBlock.at(i - 1); +// if (newerEntry->value() != entry->value()) { +// qDebug() << "Adding line series point:" << (offset + i) << newerEntry->timestamp().toMSecsSinceEpoch() - 1 << (entry->value().toReal()) << "(correction)"; +// m_graphSeries->append(QPointF(newerEntry->timestamp().toMSecsSinceEpoch() - 1, entry->value().toReal())); +// } +// } + if (m_graphSeries->count() == 0) { + m_graphSeries->insert(0, QPointF(QDateTime::currentDateTime().toMSecsSinceEpoch(), entry->value().toReal())); + } + qDebug() << "Adding line series point:" << (offset + i) << entry->timestamp().toMSecsSinceEpoch() << (entry->value().toReal()); + m_graphSeries->append(QPointF(entry->timestamp().toMSecsSinceEpoch(), entry->value().toReal())); + } } if (!newMin.isValid() || newMin > entry->value()) { newMin = entry->value().toReal(); @@ -236,7 +266,7 @@ void LogsModelNg::logsReply(const QVariantMap &data) emit maxValueChanged(); } - if (m_list.count() > 0 && m_list.last()->timestamp() > m_viewStartTime && canFetchMore()) { + if (m_viewStartTime.isValid() && m_list.count() > 0 && m_list.last()->timestamp() > m_viewStartTime && canFetchMore()) { fetchMore(); } } @@ -246,8 +276,8 @@ void LogsModelNg::fetchMore(const QModelIndex &parent) Q_UNUSED(parent) qDebug() << "fetchMore called"; - if (!m_engine->jsonRpcClient()) { - qWarning() << "Cannot update. JsonRpcClient not set"; + if (!m_engine) { + qWarning() << "Cannot update. Engine not set"; return; } if (m_busy) { @@ -294,8 +324,8 @@ void LogsModelNg::fetchMore(const QModelIndex &parent) bool LogsModelNg::canFetchMore(const QModelIndex &parent) const { Q_UNUSED(parent) - qDebug() << "canFetchMore" << m_canFetchMore; - return m_canFetchMore; + qDebug() << "canFetchMore" << (m_engine && m_canFetchMore); + return m_engine && m_canFetchMore; } void LogsModelNg::newLogEntryReceived(const QVariantMap &data) diff --git a/nymea-app/resources.qrc b/nymea-app/resources.qrc index 0489361d..f855504b 100644 --- a/nymea-app/resources.qrc +++ b/nymea-app/resources.qrc @@ -127,5 +127,7 @@ translations/nymea-app-de_DE.qm translations/nymea-app-en_US.qm ../LICENSE + ui/customviews/GenericTypeGraphPre110.qml + ui/customviews/GenericTypeGraph.qml diff --git a/nymea-app/ui/customviews/GenericTypeGraph.qml b/nymea-app/ui/customviews/GenericTypeGraph.qml new file mode 100644 index 00000000..c5eeebdc --- /dev/null +++ b/nymea-app/ui/customviews/GenericTypeGraph.qml @@ -0,0 +1,252 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" +import "../customviews" +import QtCharts 2.2 + +Item { + id: root + + property var device: null + property var stateType: null + readonly property var deviceClass: engine.deviceManager.deviceClasses.getDeviceClass(device.deviceClassId); + readonly property bool hasConnectable: deviceClass.interfaces.indexOf("connectable") >= 0 + readonly property var connectedStateType: hasConnectable ? deviceClass.stateTypes.findByName("connected") : null + + LogsModelNg { + id: logsModelNg + engine: _engine + deviceId: root.device.id + typeIds: [root.stateType.id] + live: true + graphSeries: lineSeries1 + viewStartTime: xAxis.min + } + + LogsModelNg { + id: connectedLogsModel + engine: root.hasConnectable ? _engine : null // don't even try to poll if we don't have a connectable interface + deviceId: root.device.id + typeIds: [root.connectedStateType.id] + live: true + graphSeries: connectedLineSeries + viewStartTime: xAxis.min + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + RowLayout { + Layout.alignment: Qt.AlignHCenter + HeaderButton { + imageSource: "../images/zoom-out.svg" + onClicked: { + var diff = xAxis.max.getTime() - xAxis.min.getTime() + var newTime = new Date(xAxis.min.getTime() - (diff / 4)) + xAxis.min = newTime; + } + } + + Label { + Layout.preferredWidth: 100 + horizontalAlignment: Text.AlignHCenter + text: { + var timeDiff = (xAxis.max.getTime() - xAxis.min.getTime()) / 1000; + if (timeDiff < 60) { + return qsTr("%1 seconds").arg(Math.round(timeDiff)); + } + timeDiff = timeDiff / 60 + if (timeDiff < 60) { + return qsTr("%1 minutes").arg(Math.round(timeDiff)); + } + timeDiff = timeDiff / 60 + if (timeDiff < 48) { + return qsTr("%1 hours").arg(Math.round(timeDiff)); + } + timeDiff = timeDiff / 24; + if (timeDiff < 14) { + return qsTr("%1 days").arg(Math.round(timeDiff)); + } + timeDiff = timeDiff / 7 + if (timeDiff < 5) { + return qsTr("%1 weeks").arg(Math.round(timeDiff)); + } + timeDiff * timeDiff * 7 / 30 + if (timeDiff < 24) { + return qsTr("%1 months").arg(Math.round(timeDiff)); + } + timeDiff = timeDiff * 30 / 356 + return qsTr("%1 years").arg(Math.round(timeDiff)) + } + } + + HeaderButton { + imageSource: "../images/zoom-in.svg" + onClicked: { + var diff = xAxis.max.getTime() - xAxis.min.getTime() + var newTime = new Date(xAxis.min.getTime() + (diff / 4)) + xAxis.min = newTime; + } + } + } + + ChartView { + id: chartView + Layout.fillWidth: true + Layout.fillHeight: true + margins.top: 0 + margins.bottom: 0 + margins.left: 0 + margins.right: 0 + backgroundColor: Material.background + legend.labelColor: app.foregroundColor + + animationDuration: 300 + animationOptions: ChartView.SeriesAnimations + + ValueAxis { + id: yAxis + min: logsModelNg.minValue + max: logsModelNg.maxValue + labelsFont.pixelSize: app.smallFont + labelsColor: app.foregroundColor + tickCount: chartView.height / 40 + color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .2) + gridLineColor: color + } + + ValueAxis { + id: connectedAxis + min: 0 + max: 1 + visible: false + } + + DateTimeAxis { + id: xAxis + gridVisible: false + color: Qt.rgba(app.foregroundColor.r, app.foregroundColor.g, app.foregroundColor.b, .2) + tickCount: chartView.width / 70 + labelsFont.pixelSize: app.smallFont + labelsColor: app.foregroundColor + titleText: { + if (xAxis.min.getYear() === xAxis.max.getYear() + && xAxis.min.getMonth() === xAxis.max.getMonth() + && xAxis.min.getDate() === xAxis.max.getDate()) { + return Qt.formatDate(xAxis.min) + } + return Qt.formatDate(xAxis.min) + " - " + Qt.formatDate(xAxis.max) + } + titleBrush: app.foregroundColor + format: { + var timeDiff = (xAxis.max.getTime() - xAxis.min.getTime()) / 1000 + if (timeDiff < 60) { // one minute + return "mm:ss" + } + if (timeDiff < 60 * 60) { // one hour + return "hh:mm" + } + if (timeDiff < 60 * 60 * 24 * 2) { // two day + return "hh:mm" + } + if (timeDiff < 60 * 60 * 24 * 7) { // one week + return "ddd hh:mm" + } + if (timeDiff < 60 * 60 * 24 * 7 * 30) { // one month + return "dd.MM." + } + return "MMM yy" + } + + min: { + var date = new Date(); + date.setHours(date.getHours() - 6); + return date; + } + max: new Date() + } + + AreaSeries { + axisX: xAxis + axisY: connectedAxis + name: qsTr("Not connected") + visible: root.hasConnectable + upperSeries: LineSeries { + XYPoint {x: xAxis.min.getTime(); y: 1} + XYPoint {x: xAxis.max.getTime(); y: 1} + } + + lowerSeries: LineSeries { + id: connectedLineSeries + } + color: "#55ff0000" + } + + AreaSeries { + axisX: xAxis + axisY: yAxis + name: root.stateType.displayName + borderColor: app.accentColor + borderWidth: 4 + upperSeries: LineSeries { + id: lineSeries1 + } + color: Qt.rgba(app.accentColor.r, app.accentColor.g, app.accentColor.b, .3) + } + + + MouseArea { + anchors.fill: parent + property int lastX: 0 + property int lastY: 0 + + function scrollRightLimited(dx) { + chartView.animationOptions = ChartView.NoAnimation + var now = new Date() + // if we're already at the limit, don't even start scrolling + if (dx < 0 || xAxis.max < now) { + chartView.scrollRight(dx) + } + // figure out if we scrolled too far + var overshoot = xAxis.max.getTime() - now.getTime() +// print("overshoot is:", overshoot, "oldMax", xAxis.max, "newMax", now, "oldMin", xAxis.min, "newMin", new Date(xAxis.min.getTime() - overshoot)) + if (overshoot > 0) { + var range = xAxis.max - xAxis.min + xAxis.max = now + xAxis.min = new Date(xAxis.max.getTime() - range) + } + chartView.animationOptions = ChartView.SeriesAnimations + } + + function zoomInLimited(dy) { + chartView.animationOptions = ChartView.NoAnimation + var oldMax = xAxis.max; + chartView.scrollRight(dy); + var timeDiff = xAxis.max.getTime() - oldMax.getTime() + xAxis.min = new Date(xAxis.min.getTime() - timeDiff * 2) + chartView.animationOptions = ChartView.SeriesAnimations + } + + onPressed: { + lastX = mouse.x + lastY = mouse.y + } + + onWheel: { + scrollRightLimited(-wheel.pixelDelta.x) +// zoomInLimited(wheel.pixelDelta.y) + } + + onPositionChanged: { + if (lastX !== mouse.x) { + scrollRightLimited(lastX - mouseX) + lastX = mouse.x + } + } + } + } + } +} diff --git a/nymea-app/ui/customviews/GenericTypeGraphPre110.qml b/nymea-app/ui/customviews/GenericTypeGraphPre110.qml new file mode 100644 index 00000000..1a66b780 --- /dev/null +++ b/nymea-app/ui/customviews/GenericTypeGraphPre110.qml @@ -0,0 +1,84 @@ +import QtQuick 2.9 +import QtQuick.Controls 2.2 +import QtQuick.Controls.Material 2.2 +import QtQuick.Layouts 1.1 +import Nymea 1.0 +import "../components" +import "../customviews" + +ColumnLayout { + id: root + + property var device: null + property var stateType: null + + TabBar { + id: zoomTabBar + Layout.fillWidth: true + TabButton { + text: qsTr("6 h") + property int avg: ValueLogsProxyModel.AverageQuarterHour + property date startTime: { + var date = new Date(); + date.setHours(new Date().getHours() - 6) + date.setMinutes(0) + date.setSeconds(0) + return date; + } + } + TabButton { + text: qsTr("24 h") + property int avg: ValueLogsProxyModel.AverageHourly + property date startTime: { + var date = new Date(); + date.setHours(new Date().getHours() - 24); + date.setMinutes(0) + date.setSeconds(0) + return date; + } + } + TabButton { + text: qsTr("7 d") + property int avg: ValueLogsProxyModel.AverageDayTime + property date startTime: { + var date = new Date(); + date.setDate(new Date().getDate() - 7); + date.setHours(0) + date.setMinutes(0) + date.setSeconds(0) + return date; + } + } + } + + Graph { + Layout.fillWidth: true + Layout.fillHeight: true + mode: settings.graphStyle + color: app.accentColor + + Timer { + id: updateTimer + interval: 10 + repeat: false + onTriggered: { + graphModel.update() + } + } + + model: ValueLogsProxyModel { + id: graphModel + deviceId: root.device.id + typeIds: [stateType.id] + average: zoomTabBar.currentItem.avg + startTime: zoomTabBar.currentItem.startTime + Component.onCompleted: updateTimer.start(); + onAverageChanged: updateTimer.start() + onStartTimeChanged: updateTimer.start(); + engine: _engine + + // Live doesn't work yet with ValueLogsProxyModel + // live: true + } + } +} diff --git a/nymea-app/ui/devicepages/StateLogPage.qml b/nymea-app/ui/devicepages/StateLogPage.qml index 3da7fb25..0420b84c 100644 --- a/nymea-app/ui/devicepages/StateLogPage.qml +++ b/nymea-app/ui/devicepages/StateLogPage.qml @@ -5,7 +5,6 @@ import QtQuick.Layouts 1.1 import Nymea 1.0 import "../components" import "../customviews" -import QtCharts 2.2 Page { id: root @@ -24,7 +23,7 @@ Page { } header: GuhHeader { - text: qsTr("History") + text: qsTr("History for %1").arg(root.stateType.displayName) onBackPressed: pageStack.pop() } @@ -45,8 +44,6 @@ Page { deviceId: root.device.id typeIds: [root.stateType.id] live: true - graphSeries: lineSeries1 - viewStartTime: xAxis.min } ColumnLayout { @@ -62,9 +59,6 @@ Page { TabButton { text: qsTr("Graph") } - TabButton { - text: qsTr("Graph NG") - } } SwipeView { @@ -96,225 +90,19 @@ Page { } } - ColumnLayout { + Loader { + id: graphLoader width: swipeView.width height: swipeView.height - TabBar { - id: zoomTabBar - Layout.fillWidth: true - TabButton { - text: qsTr("6 h") - property int avg: ValueLogsProxyModel.AverageQuarterHour - property date startTime: { - var date = new Date(); - date.setHours(new Date().getHours() - 6) - date.setMinutes(0) - date.setSeconds(0) - return date; - } - } - TabButton { - text: qsTr("24 h") - property int avg: ValueLogsProxyModel.AverageHourly - property date startTime: { - var date = new Date(); - date.setHours(new Date().getHours() - 24); - date.setMinutes(0) - date.setSeconds(0) - return date; - } - } - TabButton { - text: qsTr("7 d") - property int avg: ValueLogsProxyModel.AverageDayTime - property date startTime: { - var date = new Date(); - date.setDate(new Date().getDate() - 7); - date.setHours(0) - date.setMinutes(0) - date.setSeconds(0) - return date; - } + Component.onCompleted: { + var source; + if (engine.jsonRpcClient.ensureServerVersion("1.10")) { + source = Qt.resolvedUrl("../customviews/GenericTypeGraph.qml"); + } else { + source = Qt.resolvedUrl("../customviews/GenericTypeGraphPre110.qml"); } + setSource(source, {device: root.device, stateType: root.stateType}) } - - Graph { - Layout.fillWidth: true - Layout.fillHeight: true - mode: settings.graphStyle - color: app.accentColor - - Timer { - id: updateTimer - interval: 10 - repeat: false - onTriggered: { - graphModel.update() - } - } - - model: ValueLogsProxyModel { - id: graphModel - deviceId: root.device.id - typeIds: [stateType.id] - average: zoomTabBar.currentItem.avg - startTime: zoomTabBar.currentItem.startTime - Component.onCompleted: updateTimer.start(); - onAverageChanged: updateTimer.start() - onStartTimeChanged: updateTimer.start(); - engine: _engine - - // Live doesn't work yet with ValueLogsProxyModel - // live: true - } - } - } - - - Item { - width: swipeView.width - height: swipeView.height - - ColumnLayout { - anchors.fill: parent - RowLayout { - Layout.alignment: Qt.AlignRight - HeaderButton { - imageSource: "../images/zoom-in.svg" - onClicked: { - var diff = xAxis.max.getTime() - xAxis.min.getTime() - var newTime = new Date(xAxis.min.getTime() + (diff / 4)) - xAxis.min = newTime; - } - } - HeaderButton { - imageSource: "../images/zoom-out.svg" - onClicked: { - var diff = xAxis.max.getTime() - xAxis.min.getTime() - var newTime = new Date(xAxis.min.getTime() - (diff / 4)) - xAxis.min = newTime; - } - } - } - - ChartView { - id: chartView - Layout.fillWidth: true - Layout.fillHeight: true - margins.top: 0 - margins.bottom: 0 - margins.left: 0 - margins.right: 0 - backgroundColor: Material.background - animationDuration: 300 - animationOptions: ChartView.SeriesAnimations - - ValueAxis { - id: yAxis - min: logsModelNg.minValue - max: logsModelNg.maxValue - labelsFont.pixelSize: app.smallFont - tickCount: chartView.height / 40 - } - - DateTimeAxis { - id: xAxis - gridVisible: false - tickCount: chartView.width / 70 - labelsFont.pixelSize: app.smallFont - format: { - var timeDiff = (xAxis.max.getTime() - xAxis.min.getTime()) / 1000 - if (timeDiff < 60) { // one minute - return "mm:ss" - } - if (timeDiff < 60 * 60) { // one hour - return "hh:mm" - } - if (timeDiff < 60 * 60 * 24 * 2) { // two day - return "hh:mm" - } - if (timeDiff < 60 * 60 * 24 * 7) { // one week - return "ddd hh:mm" - } - if (timeDiff < 60 * 60 * 24 * 7 * 30) { // one month - return "dd.MM." - } - return "MMM yy" - } - - min: { - var date = new Date(); - date.setHours(date.getHours() - 6); - return date; - } - max: new Date() - } - - AreaSeries { - axisX: xAxis - axisY: yAxis - name: root.stateType.displayName - borderColor: app.accentColor - borderWidth: 4 - upperSeries: LineSeries { - id: lineSeries1 - width: 4 - } - color: Qt.rgba(app.accentColor.r, app.accentColor.g, app.accentColor.b, .3) - } - - MouseArea { - anchors.fill: parent - property int lastX: 0 - property int lastY: 0 - - function scrollRightLimited(dx) { - chartView.animationOptions = ChartView.NoAnimation - var now = new Date() - // if we're already at the limit, don't even start scrolling - if (dx < 0 || xAxis.max < now) { - chartView.scrollRight(dx) - } - // figure out if we scrolled too far - var overshoot = xAxis.max.getTime() - now.getTime() - print("overshoot is:", overshoot, "oldMax", xAxis.max, "newMax", now, "oldMin", xAxis.min, "newMin", new Date(xAxis.min.getTime() - overshoot)) - if (overshoot > 0) { - var range = xAxis.max - xAxis.min - xAxis.max = now - xAxis.min = new Date(xAxis.max.getTime() - range) - } - chartView.animationOptions = ChartView.SeriesAnimations - } - - function zoomInLimited(dy) { - chartView.animationOptions = ChartView.NoAnimation - var oldMax = xAxis.max; - chartView.scrollRight(dy); - var timeDiff = xAxis.max.getTime() - oldMax.getTime() - xAxis.min = new Date(xAxis.min.getTime() - timeDiff * 2) - chartView.animationOptions = ChartView.SeriesAnimations - } - - onPressed: { - lastX = mouse.x - lastY = mouse.y - } - - onWheel: { - scrollRightLimited(-wheel.pixelDelta.x) -// zoomInLimited(wheel.pixelDelta.y) - } - - onPositionChanged: { - if (lastX !== mouse.x) { - scrollRightLimited(lastX - mouseX) - lastX = mouse.x - } - } - } - } - } - } } }