├── .clang-format ├── .gitignore ├── LICENSE ├── README.md ├── doc ├── intensity-calculation.svg ├── miters1.png ├── pathologically-sharp-spikes.svg ├── screenshot-weather.png ├── screenshot.png └── vertex-shader.svg ├── examples ├── GraphStack.qml ├── HoverCursor.qml ├── InfluxWeatherGraph.qml ├── LabeledSensorGraph.qml ├── LineGraphWithHoverFeedback.qml ├── README ├── hardcoded-data.qml ├── memory.qml ├── pool-pump-current-desktop-widget.qml ├── sensor-graphs.qml ├── sensor-summary-desktop-widget.qml ├── sensors.qml ├── summary.qml ├── temperatures.qml └── weather-summary-desktop-widget.qml ├── graph.pro ├── graph.qrc ├── influxdb.cpp ├── influxdb.h ├── linegraph.cpp ├── linegraph.h ├── linegraphmodel.cpp ├── linegraphmodel.h ├── linenode.cpp ├── linenode_p.h ├── lmsensors.cpp ├── lmsensors.h ├── main.cpp ├── org └── ecloud │ └── charts │ └── qmldir ├── plugin.cpp └── shaders ├── LineNode.fsh └── LineNode.vsh /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | # BasedOnStyle: LLVM 4 | AccessModifierOffset: -4 5 | AlignAfterOpenBracket: true 6 | AlignConsecutiveAssignments: false 7 | AlignEscapedNewlinesLeft: false 8 | AlignOperands: true 9 | AlignTrailingComments: true 10 | AllowAllParametersOfDeclarationOnNextLine: true 11 | AllowShortBlocksOnASingleLine: false 12 | AllowShortCaseLabelsOnASingleLine: false 13 | AllowShortFunctionsOnASingleLine: All 14 | AllowShortIfStatementsOnASingleLine: false 15 | AllowShortLoopsOnASingleLine: false 16 | AlwaysBreakAfterDefinitionReturnType: None 17 | AlwaysBreakBeforeMultilineStrings: false 18 | AlwaysBreakTemplateDeclarations: false 19 | BinPackArguments: true 20 | BinPackParameters: true 21 | BreakBeforeBinaryOperators: None 22 | BreakBeforeBraces: Linux 23 | BreakBeforeTernaryOperators: true 24 | BreakConstructorInitializersBeforeComma: false 25 | ColumnLimit: 132 26 | CommentPragmas: '^ IWYU pragma:' 27 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 28 | ConstructorInitializerIndentWidth: 4 29 | ContinuationIndentWidth: 4 30 | Cpp11BracedListStyle: true 31 | DerivePointerAlignment: false 32 | DisableFormat: false 33 | ExperimentalAutoDetectBinPacking: false 34 | ForEachMacros: [ foreach, Q_FOREACH, BOOST_FOREACH ] 35 | IndentCaseLabels: false 36 | IndentWidth: 4 37 | IndentWrappedFunctionNames: false 38 | KeepEmptyLinesAtTheStartOfBlocks: true 39 | MacroBlockBegin: '' 40 | MacroBlockEnd: '' 41 | MaxEmptyLinesToKeep: 1 42 | NamespaceIndentation: None 43 | ObjCBlockIndentWidth: 2 44 | ObjCSpaceAfterProperty: false 45 | ObjCSpaceBeforeProtocolList: true 46 | PenaltyBreakBeforeFirstCallParameter: 19 47 | PenaltyBreakComment: 300 48 | PenaltyBreakFirstLessLess: 120 49 | PenaltyBreakString: 1000 50 | PenaltyExcessCharacter: 1000000 51 | PenaltyReturnTypeOnItsOwnLine: 60 52 | PointerAlignment: Right 53 | SpaceAfterCStyleCast: false 54 | SpaceBeforeAssignmentOperators: true 55 | SpaceBeforeParens: ControlStatements 56 | SpaceInEmptyParentheses: false 57 | SpacesBeforeTrailingComments: 1 58 | SpacesInAngles: false 59 | SpacesInContainerLiterals: true 60 | SpacesInCStyleCastParentheses: false 61 | SpacesInParentheses: false 62 | SpacesInSquareBrackets: false 63 | Standard: Cpp11 64 | TabWidth: 4 65 | UseTab: Never 66 | ... 67 | 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | 3 | *.slo 4 | *.lo 5 | *.o 6 | *.a 7 | *.la 8 | *.lai 9 | *.so 10 | *.dll 11 | *.dylib 12 | 13 | # Qt-es 14 | 15 | /.qmake.cache 16 | /.qmake.stash 17 | *.pro.user 18 | *.pro.user.* 19 | *.qbs.user 20 | *.qbs.user.* 21 | *.moc 22 | moc_*.cpp 23 | qrc_*.cpp 24 | ui_*.h 25 | Makefile* 26 | *build-* 27 | 28 | # QtCreator 29 | 30 | *.autosave 31 | 32 | # QtCtreator Qml 33 | *.qmlproject.user 34 | *.qmlproject.user.* 35 | 36 | # QtCtreator CMake 37 | CMakeLists.txt.user 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qqchart 2 | GPU-powered line charts in Qt Quick 3 | 4 | 5 | 6 | 7 | The holy grail is to be able to send raw data to the GPU and have it 8 | draw a line graph, without needing much preparation on the CPU side 9 | (other than having it in time-ordered form, which comes naturally). 10 | 11 | Of course it's still pretty far from that. So far I have to pad out 12 | a vertex array to send over, and then the GPU can rearrange the vertices 13 | to achieve an antialiased line rendering. 14 | 15 | But I can monitor my system sensors with a pretty low CPU usage, anyway. 16 | Can also graph data from InfluxDB; I'm using that for a weather station, 17 | among other things. 18 | 19 | To try it out: 20 | ``` 21 | qmake; make 22 | qml -I . examples/sensor-summary-desktop-widget.qml 23 | ``` 24 | 25 | To install: 26 | ``` 27 | make install 28 | ``` 29 | 30 | You can then use it in your own QML scripts: 31 | ``` 32 | import org.ecloud.charts 1.0 33 | ``` 34 | 35 | Issues 36 | 37 | - [ ] data with sharp spikes renders "ghostly", shimmering, coming and going... 38 | 39 | Incomplete features 40 | 41 | - [ ] labels, grids and tickmarks (see the axis-labels branch) 42 | 43 | -------------------------------------------------------------------------------- /doc/intensity-calculation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | image/svg+xmlOpenclipartt = -0.5 91 | t = +0.5 106 | opacity = cos(ᴨt) 121 | 132 | 133 | -------------------------------------------------------------------------------- /doc/miters1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ec1oud/qqchart/905a55ab94e1025fe9a7b6c0282942f55188122c/doc/miters1.png -------------------------------------------------------------------------------- /doc/pathologically-sharp-spikes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 29 | 34 | 35 | 43 | 48 | 49 | 57 | 62 | 63 | 72 | 77 | 78 | 86 | 91 | 92 | 100 | 105 | 106 | 115 | 120 | 121 | 130 | 135 | 136 | 144 | 149 | 150 | 158 | 163 | 164 | 172 | 177 | 178 | 179 | 201 | 203 | 204 | 206 | image/svg+xml 207 | 209 | 210 | 211 | 212 | 213 | 218 | 224 | 230 | 236 | 242 | 248 | 254 | OKnot too sharp 269 | too sharp:the intersectionof the strokes isbeyond thestrokes themselves 300 | corrected (compromise):lines are thinneron each sideto prevent overlap;but in extreme cases,thinness becomes noticeable too 335 | 340 | 345 | 350 | 355 | 360 | 365 | 370 | 377 | 378 | 379 | -------------------------------------------------------------------------------- /doc/screenshot-weather.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ec1oud/qqchart/905a55ab94e1025fe9a7b6c0282942f55188122c/doc/screenshot-weather.png -------------------------------------------------------------------------------- /doc/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ec1oud/qqchart/905a55ab94e1025fe9a7b6c0282942f55188122c/doc/screenshot.png -------------------------------------------------------------------------------- /doc/vertex-shader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 29 | 34 | 35 | 43 | 48 | 49 | 58 | 63 | 64 | 72 | 77 | 78 | 86 | 91 | 92 | 101 | 106 | 107 | 116 | 121 | 122 | 130 | 135 | 136 | 144 | 149 | 150 | 158 | 163 | 164 | 172 | 177 | 178 | 186 | 191 | 192 | 201 | 206 | 207 | 216 | 221 | 222 | 231 | 236 | 237 | 245 | 250 | 251 | 260 | 265 | 266 | 274 | 279 | 280 | 289 | 294 | 295 | 296 | 314 | 316 | 317 | 319 | image/svg+xml 320 | 322 | 323 | 324 | 325 | 326 | 331 | 336 | 342 | 348 | 354 | 360 | i = 0t = -1 375 | i = 1t = +1 390 | 398 | 406 | i = 2t = -1 421 | i = 3t = +1 436 | 442 | 448 | 453 | 458 | 463 | 468 | 475 | lineToward 486 | lineAway 497 | 504 | normal 515 | averageTangent 526 | 533 | miter 547 | 553 | 559 | 564 | 569 | 576 | miterLength 587 | 593 | 599 | 606 | miterLength 617 | 623 | 629 | 635 | 641 | 642 | 643 | -------------------------------------------------------------------------------- /examples/GraphStack.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.4 3 | 4 | import org.ecloud.charts 1.0 5 | 6 | Item { 7 | id: root 8 | property alias color: graph.color 9 | property alias antialiasing: graph.antialiasing 10 | property real timeScale: 1 11 | property LineGraphModel model: null 12 | 13 | function newSample(i) { 14 | // return (Math.sin(i / 20.0 * Math.PI * 2) + 1) * 0.4; 15 | // return (Math.sin(i / 20.0 * Math.PI * 2) + 1) * 0.4 + Math.random() * 0.05; 16 | return Math.sin(i / 20.0 * Math.PI * 2) * 0.95 + Math.random() * 0.05; 17 | } 18 | 19 | property int offset: 25; 20 | 21 | LineGraph { 22 | id: graph 23 | anchors.fill: parent 24 | model: root.model 25 | minValue: -1 + offsetSlider.value 26 | maxValue: 1 + offsetSlider.value 27 | lineWidth: widthSlider.value 28 | color: "lightsteelblue" 29 | fillColorBelow: fillBelowCb.checked ? "#22FF2222" : "transparent" 30 | fillColorAbove: fillAboveCb.checked ? "#222222FF" : "transparent" 31 | warningMinColor: "yellow" 32 | warningMaxColor: "orange" 33 | wireframe: false 34 | visible: fillCb.checked 35 | opacity: 0.8 36 | timeSpan: 40 * root.timeScale 37 | } 38 | 39 | LineGraph { 40 | id: wireframe 41 | anchors.fill: graph 42 | model: root.model 43 | minValue: -1 + offsetSlider.value 44 | maxValue: 1 + offsetSlider.value 45 | lineWidth: widthSlider.value 46 | color: graph.visible ? "black" : "white" 47 | fillColorBelow: fillBelowCb.checked ? "#F22" : "transparent" 48 | fillColorAbove: fillAboveCb.checked ? "#22F" : "transparent" 49 | warningMinColor: "black" 50 | warningMaxColor: "black" 51 | wireframe: true 52 | timeSpan: graph.timeSpan 53 | visible: wireframeCb.checked 54 | } 55 | 56 | LineGraph { 57 | id: plainLine 58 | anchors.fill: graph 59 | model: root.model 60 | minValue: -1 + offsetSlider.value 61 | maxValue: 1 + offsetSlider.value 62 | lineWidth: 0 63 | color: "red" 64 | warningMinColor: "red" 65 | warningMaxColor: "red" 66 | wireframe: true 67 | timeSpan: graph.timeSpan 68 | visible: originalLineCb.checked 69 | } 70 | 71 | Timer { 72 | id: timer 73 | interval: 500 74 | repeat: true 75 | running: timerRun.checked 76 | onTriggered: model.appendSample(root.newSample(++root.offset)) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /examples/HoverCursor.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | 3 | MouseArea { 4 | id: hoverCursor 5 | anchors.fill: parent 6 | hoverEnabled: true 7 | property real hoverX: containsMouse ? rect.x : -1 8 | property real timeSpan: -1 9 | property date lastSampleTime: new Date() 10 | Rectangle { 11 | id: rect 12 | width: 1 13 | height: parent.height 14 | color: "red" 15 | x: visible ? Math.min(parent.mouseX, parent.width - 10) : -1 16 | visible: parent.containsMouse 17 | Rectangle { 18 | color: "#AAFFDDDD" 19 | border.color: "red" 20 | anchors.fill: text 21 | anchors.margins: -2 22 | anchors.leftMargin: 0 23 | anchors.topMargin: 2 24 | rotation: -90 25 | transformOrigin: Item.TopLeft 26 | } 27 | Text { 28 | id: text 29 | color: "#882222" 30 | visible: hoverCursor.timeSpan > 0 31 | property date cursorTime: new Date(hoverCursor.lastSampleTime.getTime() + (hoverCursor.timeSpan * (hoverCursor.mouseX - hoverCursor.width) / hoverCursor.width * 1000)) 32 | text: cursorTime.toISOString().slice(0, 10) + " " + cursorTime.toISOString().slice(11, 16) 33 | rotation: -90 34 | transformOrigin: Item.TopLeft 35 | y: parent.height / 2 + width / 2 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /examples/InfluxWeatherGraph.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import org.ecloud.charts 1.0 3 | 4 | Rectangle { 5 | id: root 6 | width: 580 7 | height: 360 8 | color: "transparent" 9 | border.color: "#6e4b4d" 10 | antialiasing: true 11 | radius: 20 12 | property int timespanHours : 240 13 | property bool currentVisible: false 14 | InfluxQuery { 15 | id: query 16 | server: "http://localhost:8086" 17 | database: "weather" 18 | measurement: "uradmonitor" 19 | fields: ["temperature", "pressure", "humidity"] 20 | //wherePairs: [{"stationId": "41000008"}] 21 | wherePairs: [{"stationId": "820000ED"}] 22 | timeConstraint: "> now() - " + root.timespanHours + "h" 23 | updateIntervalMs: sampleInterval * 1000 24 | sampleInterval: timespanHours * 3600 / root.width * 2 25 | Component.onCompleted: sampleAllValues() 26 | } 27 | ListView { 28 | id: list 29 | anchors.fill: parent 30 | anchors.margins: 8 31 | model: query.values 32 | spacing: 4 33 | clip: true 34 | 35 | delegate: Item { 36 | width: list.width 37 | property int visibleRows: Math.min(4, list.model.length) 38 | property bool isTemperature: label === "temperature" 39 | property bool isPressure: label === "pressure" 40 | height: (list.height - 4 * (visibleRows - 1)) / visibleRows 41 | Text { 42 | anchors.verticalCenter: parent.verticalCenter 43 | anchors.right: parent.right 44 | color: "darkgrey" 45 | styleColor: "black" 46 | style: Text.Outline 47 | font.pointSize: 24 48 | font.bold: true 49 | text: model.currentValue.toFixed(isPressure ? 0 : 2) + model.unit 50 | visible: currentVisible 51 | } 52 | LineGraphWithHoverFeedback { 53 | id: graph 54 | model: modelData 55 | anchors.fill: parent 56 | timeSpan: root.timespanHours * 3600 57 | color: isTemperature ? "green" : "yellow" 58 | minValue: model.minValue // continuously, along with autoScale 59 | maxValue: model.maxValue 60 | warningMinColor: isTemperature ? "cyan" : "orange" 61 | warningMaxColor: "orange" 62 | lineWidth: isTemperature ? 2 : 1 63 | hoverX: hoverCursor.mouseX 64 | Component.onCompleted: { 65 | model.clipValues = false // because we will auto-scale 66 | if (isTemperature) 67 | model.additiveCorrection = -5 // -11 // known-inaccurate sensor 68 | } 69 | onSamplesChanged: model.autoScale() 70 | } 71 | Text { 72 | text: label + ": last " + (root.timespanHours > 24 ? 73 | (root.timespanHours / 24) + " days" : 74 | root.timespanHours + " hours") 75 | font.bold: true 76 | color: "black" 77 | } 78 | Text { 79 | anchors { 80 | right: parent.right 81 | top: parent.top 82 | } 83 | horizontalAlignment: Text.Right 84 | text: maxValue 85 | color: "black" 86 | } 87 | Text { 88 | anchors { 89 | right: parent.right 90 | bottom: parent.bottom 91 | } 92 | horizontalAlignment: Text.Right 93 | text: minValue 94 | color: "black" 95 | } 96 | } 97 | } 98 | HoverCursor { 99 | id: hoverCursor 100 | anchors.margins: 8 101 | timeSpan: query1.values[0].timeSpan 102 | lastSampleTime: query1.lastSampleTime 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /examples/LabeledSensorGraph.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import org.ecloud.charts 1.0 3 | 4 | LineGraphWithHoverFeedback { 5 | id: root 6 | property int labelLine: 0 7 | timeSpan: 600 8 | height: 100 9 | x: 4 10 | width: parent.width - 8 11 | property alias lineHeight: label.font.pixelSize 12 | Text { 13 | id: label 14 | color: parent.color 15 | text: model.label + (model.chipName === "" ? "" : " (" + model.chipName + ")") 16 | y: labelLine * font.pixelSize 17 | font.weight: Font.Bold 18 | } 19 | Row { 20 | anchors.left: parent.horizontalCenter 21 | spacing: 10 22 | Text { 23 | color: "white" 24 | text: model.currentValue.toFixed(2) + " " + model.unit 25 | y: labelLine * font.pixelSize 26 | font.weight: Font.Bold 27 | } 28 | Text { 29 | color: "darkgrey" 30 | text: "(max " + Math.round(model.maxSampleValue) + " / " + Math.round(root.maxValue) + ")" 31 | y: labelLine * font.pixelSize 32 | font.weight: Font.Bold 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/LineGraphWithHoverFeedback.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import org.ecloud.charts 1.0 3 | 4 | LineGraph { 5 | id: root 6 | property int hoverX: -1 7 | property var hoverSample: null 8 | 9 | function updateHover() { 10 | if (hoverX < 0) { 11 | hoverSample = null 12 | } else { 13 | hoverSample = sampleNearestX(hoverX) 14 | } 15 | } 16 | 17 | onHoverXChanged: updateHover() 18 | onSamplesChanged: updateHover() 19 | 20 | Rectangle { 21 | id: pointFeedback 22 | x: hoverSample ? hoverSample.x - width / 2 : -100 23 | y: hoverSample ? hoverSample.y - height / 2 : -100 24 | color: "red" // root.color 25 | width: 7 26 | height: 7 27 | radius: width / 2 28 | border.color: "white" 29 | antialiasing: true 30 | // visible: hoverSample 31 | 32 | Rectangle { 33 | id: labelBackground 34 | color: "#55555533" 35 | border.color: root.color 36 | width: valueLabel.implicitWidth * 1.2 37 | height: valueLabel.implicitHeight * 1.5 38 | radius: height * 0.2 39 | anchors.right: parent.horizontalCenter 40 | anchors.bottom: parent.bottom 41 | anchors.bottomMargin: 10 42 | Text { 43 | id: valueLabel 44 | anchors.centerIn: parent 45 | text: hoverSample ? hoverSample.value.toFixed(2) : "" 46 | color: "white" 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/README: -------------------------------------------------------------------------------- 1 | QML2_IMPORT_PATH=.. qml example.qml 2 | -------------------------------------------------------------------------------- /examples/hardcoded-data.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.0 2 | import QtQuick.Controls 1.4 3 | import QtQuick.Controls.Styles 1.4 4 | 5 | import org.ecloud.charts 1.0 6 | 7 | Rectangle { 8 | width: 990 9 | height: 480 10 | color: "black" 11 | 12 | GraphStack { 13 | id: graphs 14 | anchors.fill: parent 15 | anchors.margins: 20 16 | anchors.bottomMargin: 100 17 | antialiasing: aaCb.checked 18 | timeScale: hZoomSlider.value 19 | color: "lightsteelblue" 20 | model: LineGraphModel { 21 | label: "example data" 22 | } 23 | Component.onCompleted: { 24 | var time = new Date() 25 | time = new Date(time.getTime() - 1000 * 30) 26 | model.appendSample(0.3, time) 27 | time = new Date(time.getTime() + 1000) 28 | model.appendSample(0.5, time) 29 | 30 | // upward spikes 31 | time = new Date(time.getTime() + 50) 32 | model.appendSample(0, time) 33 | time = new Date(time.getTime() + 100) 34 | model.appendSample(0, time) 35 | time = new Date(time.getTime() + 50) 36 | model.appendSample(1, time) 37 | time = new Date(time.getTime() + 20) 38 | model.appendSample(1, time) 39 | time = new Date(time.getTime() + 50) 40 | model.appendSample(0, time) 41 | time = new Date(time.getTime() + 100) 42 | model.appendSample(0, time) 43 | 44 | time = new Date(time.getTime() + 50) 45 | model.appendSample(0, time) 46 | time = new Date(time.getTime() + 100) 47 | model.appendSample(0, time) 48 | time = new Date(time.getTime() + 50) 49 | model.appendSample(1, time) 50 | time = new Date(time.getTime() + 50) 51 | model.appendSample(1, time) 52 | time = new Date(time.getTime() + 50) 53 | model.appendSample(0, time) 54 | time = new Date(time.getTime() + 200) 55 | model.appendSample(0, time) 56 | 57 | // downward spikes 58 | time = new Date(time.getTime() + 50) 59 | model.appendSample(1, time) 60 | time = new Date(time.getTime() + 100) 61 | model.appendSample(1, time) 62 | 63 | time = new Date(time.getTime() + 50) 64 | model.appendSample(0, time) 65 | time = new Date(time.getTime() + 20) 66 | model.appendSample(0, time) 67 | time = new Date(time.getTime() + 50) 68 | model.appendSample(1, time) 69 | time = new Date(time.getTime() + 100) 70 | model.appendSample(1, time) 71 | 72 | time = new Date(time.getTime() + 50) 73 | model.appendSample(0, time) 74 | time = new Date(time.getTime() + 50) 75 | model.appendSample(0, time) 76 | time = new Date(time.getTime() + 50) 77 | model.appendSample(1, time) 78 | time = new Date(time.getTime() + 100) 79 | model.appendSample(1, time) 80 | 81 | // "square" waves 82 | time = new Date(time.getTime() + 40) 83 | model.appendSample(0, time) 84 | time = new Date(time.getTime() + 1000) 85 | model.appendSample(0, time) 86 | time = new Date(time.getTime() + 40) 87 | model.appendSample(1, time) 88 | time = new Date(time.getTime() + 1000) 89 | model.appendSample(1, time) 90 | time = new Date(time.getTime() + 100) 91 | model.appendSample(0, time) 92 | time = new Date(time.getTime() + 1000) 93 | model.appendSample(0, time) 94 | time = new Date(time.getTime() + 100) 95 | model.appendSample(1, time) 96 | time = new Date(time.getTime() + 1000) 97 | model.appendSample(1, time) 98 | time = new Date(time.getTime() + 100) 99 | model.appendSample(0, time) 100 | time = new Date(time.getTime() + 1000) 101 | model.appendSample(0, time) 102 | time = new Date(time.getTime() + 100) 103 | model.appendSample(1, time) 104 | time = new Date(time.getTime() + 1000) 105 | model.appendSample(1, time) 106 | time = new Date(time.getTime() + 100) 107 | model.appendSample(0, time) 108 | time = new Date(time.getTime() + 1000) 109 | model.appendSample(0, time) 110 | time = new Date(time.getTime() + 100) 111 | model.appendSample(1, time) 112 | time = new Date(time.getTime() + 1000) 113 | model.appendSample(0, time) 114 | 115 | // backtrack 116 | time = new Date(time.getTime() - 500) 117 | model.appendSample(0.5, time) 118 | time = new Date(time.getTime() + 700) 119 | model.appendSample(0, time) 120 | time = new Date(time.getTime() + 100) 121 | model.appendSample(0.2, time) 122 | time = new Date(time.getTime() + 100) 123 | model.appendSample(0, time) 124 | time = new Date(time.getTime() + 100) 125 | model.appendSample(1, time) 126 | time = new Date(time.getTime() + 1850) 127 | model.appendSample(0, time) 128 | time = new Date(time.getTime() + 1000) 129 | model.appendSample(1, time) 130 | time = new Date(time.getTime() + 1000) 131 | model.appendSample(0.9, time) 132 | time = new Date(time.getTime() + 1000) 133 | model.appendSample(1.01, time) 134 | time = new Date(time.getTime() + 1000) 135 | model.appendSample(0.98, time) 136 | time = new Date(time.getTime() + 1000) 137 | model.appendSample(0.9, time) 138 | time = new Date(time.getTime() + 1000) 139 | model.appendSample(0.2, time) 140 | time = new Date(time.getTime() + 1000) 141 | model.appendSample(0.02, time) 142 | time = new Date(time.getTime() + 1000) 143 | model.appendSample(0, time) 144 | time = new Date(time.getTime() + 1000) 145 | model.appendSample(0.01, time) 146 | time = new Date(time.getTime() + 1000) 147 | model.appendSample(0.03, time) 148 | time = new Date(time.getTime() + 1000) 149 | model.appendSample(0.02, time) 150 | time = new Date(time.getTime() + 1000) 151 | model.appendSample(0.04, time) 152 | time = new Date(time.getTime() + 1000) 153 | model.appendSample(0.07, time) 154 | time = new Date(time.getTime() + 1000) 155 | model.appendSample(0.05, time) 156 | time = new Date(time.getTime() + 1000) 157 | model.appendSample(0.06, time) 158 | time = new Date(time.getTime() + 1000) 159 | model.appendSample(0.18, time) 160 | time = new Date(time.getTime() + 1000) 161 | model.appendSample(0.09, time) 162 | time = new Date(time.getTime() + 1000) 163 | model.appendSample(0.1, time) 164 | time = new Date(time.getTime() + 1000) 165 | model.appendSample(0.08, time) 166 | time = new Date(time.getTime() + 1000) 167 | model.appendSample(-0.06, time) 168 | time = new Date(time.getTime() + 1000) 169 | model.appendSample(0.04, time) 170 | time = new Date(time.getTime() + 1000) 171 | model.appendSample(0.02, time) 172 | time = new Date(time.getTime() + 1000) 173 | model.appendSample(0, time) 174 | time = new Date(time.getTime() + 1000) 175 | model.appendSample(0.1, time) 176 | } 177 | 178 | } 179 | 180 | Rectangle { 181 | anchors.fill: graphs 182 | color: "transparent" 183 | border.color: "#40444444" 184 | border.width: 2 185 | Flow { 186 | anchors.top: parent.bottom 187 | anchors.margins: 6 188 | width: parent.width 189 | spacing: 20 190 | opacity: 0.5 191 | Column { 192 | width: 300 193 | Slider { 194 | id: widthSlider 195 | width: 300 196 | minimumValue: 1 197 | maximumValue: 25 198 | value: 1.3 199 | } 200 | Text { 201 | text: "line width " + widthSlider.value.toFixed(2) 202 | color: "white" 203 | } 204 | } 205 | Column { 206 | width: 300 207 | Slider { 208 | id: offsetSlider 209 | width: 300 210 | minimumValue: -1 211 | maximumValue: +1 212 | } 213 | Text { 214 | text: "y offset " + offsetSlider.value.toFixed(2) 215 | color: "white" 216 | } 217 | } 218 | Column { 219 | width: 300 220 | Slider { 221 | id: hZoomSlider 222 | width: 300 223 | minimumValue: 1 224 | maximumValue: 50 225 | } 226 | Text { 227 | text: "time scale " + hZoomSlider.value.toFixed(2) 228 | color: "white" 229 | } 230 | } 231 | Component { 232 | id: whiteCheckboxStyle 233 | CheckBoxStyle { 234 | label: Text { 235 | text: control.text 236 | color: "white" 237 | } 238 | } 239 | } 240 | 241 | CheckBox { 242 | id: fillCb 243 | text: "fill" 244 | style: whiteCheckboxStyle 245 | checked: true 246 | } 247 | CheckBox { 248 | id: aaCb 249 | text: "antialiasing" 250 | style: whiteCheckboxStyle 251 | checked: true 252 | } 253 | CheckBox { 254 | id: originalLineCb 255 | text: "actual samples" 256 | style: whiteCheckboxStyle 257 | // checked: true 258 | } 259 | CheckBox { 260 | id: wireframeCb 261 | text: "wireframe" 262 | style: whiteCheckboxStyle 263 | // checked: true 264 | } 265 | CheckBox { 266 | id: timerRun 267 | text: "periodic update" 268 | style: whiteCheckboxStyle 269 | } 270 | CheckBox { 271 | id: fillBelowCb 272 | text: "fill below" 273 | style: whiteCheckboxStyle 274 | } 275 | CheckBox { 276 | id: fillAboveCb 277 | text: "fill above" 278 | style: whiteCheckboxStyle 279 | } 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /examples/memory.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import QtQuick.Window 2.1 3 | import QtGraphicalEffects 1.0 4 | import org.ecloud.charts 1.0 5 | 6 | Window { 7 | width: 480 8 | height: 320 9 | //x: 1920 - width - 4 10 | y: 1080 - height 11 | color: "transparent" 12 | flags: Qt.FramelessWindowHint | Qt.WindowStaysOnBottomHint 13 | visible: true 14 | 15 | Rectangle { 16 | id: content 17 | anchors.fill: parent 18 | visible: false 19 | color: "transparent" 20 | border.color: "#6e4b4d" 21 | antialiasing: true 22 | radius: 20 23 | Repeater { 24 | model: LmSensors.filtered(Sensor.Memory) 25 | LabeledSensorGraph { 26 | model: modelData 27 | color: Qt.rgba(0.3 + index * 0.33, 1 - index * 0.27, 0.7 + index * 0.11, 1) 28 | labelLine: index + 2 29 | lineWidth: 1 30 | visible: modelData.maxValue > 0 31 | } 32 | } 33 | } 34 | DropShadow { 35 | source: content 36 | anchors.fill: content 37 | horizontalOffset: 1 38 | verticalOffset: 1 39 | radius: 8.0 40 | samples: 7 41 | color: "#80000000" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/pool-pump-current-desktop-widget.qml: -------------------------------------------------------------------------------- 1 | #!/bin/env qml 2 | 3 | import QtQuick 2.12 4 | import QtQuick.Window 2.12 5 | import Qt.labs.settings 1.0 6 | import org.ecloud.charts 1.0 7 | 8 | Window { 9 | id: window 10 | width: 740 11 | height: 360 12 | color: "transparent" 13 | flags: Qt.FramelessWindowHint | Qt.WindowStaysOnBottomHint 14 | visible: true 15 | 16 | Settings { 17 | id: settings 18 | category: "PoolPumpDesktopWidget" 19 | property alias x: window.x 20 | property alias y: window.y 21 | property string server 22 | property string user 23 | property string password 24 | } 25 | 26 | property int timespanHours : 720 27 | InfluxQuery { 28 | id: query1 29 | server: settings.server 30 | user: settings.user 31 | password: settings.password 32 | ignoreSslErrors: true // such as self-signed certificate or hostname not matching 33 | // graph for data from https://github.com/ec1oud/devcron/blob/master/statistics/cs5490-current.lua 34 | database: "homeauto" 35 | measurement: "current_max_hourly" 36 | fields: ["max_current"] 37 | wherePairs: [{"device": "poolpump"}] 38 | timeConstraint: "> now() - " + window.timespanHours + "h" 39 | updateIntervalMs: sampleInterval * 1000 40 | sampleInterval: 3600 41 | Component.onCompleted: { 42 | sampleAllValues() 43 | values[0].maxValue = 3.5 44 | } 45 | } 46 | LineGraphWithHoverFeedback { 47 | id: graph 48 | model: query1.values[0] 49 | anchors.fill: parent 50 | anchors.margins: 10 51 | timeSpan: query1.values[0].timeSpan 52 | color: "lightsteelblue" 53 | // fillColorBelow: "lightsteelblue" 54 | warningMaxColor: "lightsteelblue" 55 | minValue: 0 56 | maxValue: 3.5 57 | lineWidth: 1 58 | hoverX: hoverCursor.hoverX 59 | HoverCursor { 60 | id: hoverCursor 61 | timeSpan: query1.values[0].timeSpan 62 | lastSampleTime: query1.lastSampleTime 63 | } 64 | } 65 | Text { 66 | text: "Pool pump current: last " + (window.timespanHours > 24 ? 67 | (window.timespanHours / 24) + " days" : 68 | window.timespanHours + " hours") + 69 | " as of " + query1.lastSampleTime 70 | font.bold: true 71 | color: "black" 72 | } 73 | Text { 74 | anchors { 75 | right: parent.right 76 | top: parent.top 77 | rightMargin: 10 78 | } 79 | horizontalAlignment: Text.Right 80 | text: query1.values[0].maxValue 81 | color: "black" 82 | } 83 | Text { 84 | anchors { 85 | right: parent.right 86 | bottom: parent.bottom 87 | rightMargin: 10 88 | } 89 | horizontalAlignment: Text.Right 90 | text: query1.values[0].minValue 91 | color: "black" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /examples/sensor-graphs.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import org.ecloud.charts 1.0 3 | 4 | Rectangle { 5 | width: 600 6 | height: 640 7 | color: "black" 8 | ListView { 9 | id: list 10 | anchors.fill: parent 11 | anchors.margins: 6 12 | model: LmSensors.sensors 13 | delegate: Rectangle { 14 | width: parent.width 15 | height: 75 16 | border.color: "#111" 17 | color: "transparent" 18 | LineGraph { 19 | id: graph 20 | model: modelData 21 | anchors.fill: parent 22 | timeSpan: width 23 | } 24 | Text { 25 | text: label + " (" + chipName + "):\nscale " + parent.width / parent.timeSpan + 26 | " min " + minSampleValue.toFixed(2) + " max " + maxSampleValue.toFixed(2) + 27 | " norm " + normalMinValue.toFixed(2) + ".." + normalMaxValue.toFixed(2) 28 | color: "grey" 29 | } 30 | Text { 31 | text: currentValue.toFixed(2) + " " + unit 32 | anchors.horizontalCenter: parent.horizontalCenter 33 | color: "white" 34 | } 35 | Text { 36 | anchors { 37 | right: parent.right 38 | top: parent.top 39 | } 40 | horizontalAlignment: Text.Right 41 | text: maxValue 42 | color: "grey" 43 | } 44 | Text { 45 | anchors { 46 | right: parent.right 47 | bottom: parent.bottom 48 | } 49 | horizontalAlignment: Text.Right 50 | text: minValue 51 | color: "grey" 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/sensor-summary-desktop-widget.qml: -------------------------------------------------------------------------------- 1 | #!/bin/env qml 2 | 3 | import QtQuick 2.5 4 | import QtQuick.Window 2.1 5 | import QtGraphicalEffects 1.0 6 | import Qt.labs.settings 1.0 7 | import org.ecloud.charts 1.0 8 | 9 | Window { 10 | id: window 11 | width: 580 12 | height: 360 13 | color: "transparent" 14 | flags: Qt.FramelessWindowHint | Qt.WindowStaysOnBottomHint 15 | visible: true 16 | 17 | Settings { 18 | category: "SensorSummaryDesktopWidget" 19 | property alias x: window.x 20 | property alias y: window.y 21 | property alias width: window.width 22 | property alias height: window.height 23 | } 24 | 25 | Rectangle { 26 | id: content 27 | anchors.fill: parent 28 | visible: false 29 | color: "transparent" 30 | border.color: "#6e4b4d" 31 | antialiasing: true 32 | radius: 20 33 | Item { 34 | id: bottom 35 | anchors.fill: parent 36 | anchors.topMargin: cpuFrequencyRepeater.implicitHeight 37 | 38 | Repeater { 39 | id: batteries 40 | model: LmSensors.filtered(Sensor.Energy) 41 | LabeledSensorGraph { 42 | model: modelData 43 | color: "lightgreen" 44 | labelLine: index + 1 45 | lineWidth: 1 46 | //visible: modelData.maxSampleValue > 0 47 | hoverX: hoverCursor.hoverX - 4 48 | } 49 | } 50 | Repeater { 51 | id: fans 52 | model: LmSensors.filtered(Sensor.Fan) 53 | LabeledSensorGraph { 54 | model: modelData 55 | color: Qt.rgba(0.3 + index * 0.03, 0.2 + index * 0.17, 1, 1) 56 | labelLine: batteries.count + index + 2 57 | lineWidth: 3 58 | //visible: modelData.maxSampleValue > 0 59 | hoverX: hoverCursor.hoverX - 4 60 | } 61 | } 62 | Repeater { 63 | model: LmSensors.filtered(Sensor.Temperature, "Core 0") 64 | LabeledSensorGraph { 65 | model: modelData 66 | color: "#F85" 67 | labelLine: 0 68 | minValue: 25 69 | maxValue: 85 70 | hoverX: hoverCursor.hoverX - 4 71 | } 72 | } 73 | Repeater { 74 | model: LmSensors.filtered(Sensor.Cpu) 75 | LabeledSensorGraph { 76 | model: modelData 77 | color: "wheat" 78 | labelLine: batteries.count + 1 79 | hoverX: hoverCursor.hoverX - 4 80 | } 81 | } 82 | Repeater { 83 | model: LmSensors.filtered(Sensor.Memory, "Memory Used") 84 | LabeledSensorGraph { 85 | model: modelData 86 | color: "cyan" 87 | labelLine: batteries.count + fans.count + 2 88 | lineWidth: 2 89 | hoverX: hoverCursor.hoverX - 4 90 | } 91 | } 92 | Repeater { 93 | model: LmSensors.filtered(Sensor.Memory, "Memory Cached") 94 | LabeledSensorGraph { 95 | model: modelData 96 | color: "darkgreen" 97 | labelLine: batteries.count + fans.count + 3 98 | lineWidth: 1 99 | hoverX: hoverCursor.hoverX - 4 100 | } 101 | } 102 | Repeater { 103 | model: LmSensors.filtered(Sensor.Memory, "Swap Free") 104 | LabeledSensorGraph { 105 | model: modelData 106 | color: "midnightblue" 107 | labelLine: batteries.count + fans.count + 4 108 | lineWidth: 3 109 | visible: modelData.maxValue > 0 110 | hoverX: hoverCursor.hoverX - 4 111 | } 112 | } 113 | } 114 | Item { 115 | anchors.top: parent.top 116 | anchors.topMargin: 4 117 | width: parent.width 118 | height: 100 119 | Repeater { 120 | id: cpuFrequencyRepeater 121 | model: LmSensors.filtered(Sensor.Frequency) 122 | implicitHeight: count * 15.5 123 | LabeledSensorGraph { 124 | y: lineHeight * index 125 | height: lineHeight / 3 126 | model: modelData 127 | warningMinColor: "saddlebrown" 128 | color: Qt.rgba(0, 0.2 + index * 0.07, 0.1, 1) 129 | warningMaxColor: "cyan" 130 | lineWidth: 1 131 | } 132 | onItemAdded: model[index].downsampleInterval = 15 133 | } 134 | } 135 | Rectangle { 136 | anchors.bottom: bottom.top 137 | anchors.bottomMargin: 2 138 | anchors.margins: 8 139 | anchors.left: parent.left 140 | anchors.right: parent.right 141 | height: 1 142 | color: content.border.color 143 | } 144 | } 145 | DropShadow { 146 | source: content 147 | anchors.fill: content 148 | horizontalOffset: 1 149 | verticalOffset: 1 150 | radius: 8.0 151 | samples: 7 152 | color: "#80000000" 153 | } 154 | 155 | HoverCursor { id: hoverCursor } 156 | } 157 | -------------------------------------------------------------------------------- /examples/sensors.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import org.ecloud.charts 1.0 3 | 4 | ListView { 5 | width: 200 6 | height: 200 7 | model: LmSensors.sensors 8 | delegate: Text { text: label + ": " + currentValue.toFixed(2) } 9 | } 10 | -------------------------------------------------------------------------------- /examples/summary.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import org.ecloud.charts 1.0 3 | 4 | Rectangle { 5 | width: 480 6 | height: 320 7 | color: "black" 8 | 9 | Repeater { 10 | model: LmSensors.filtered(Sensor.Fan) 11 | LabeledSensorGraph { 12 | model: modelData 13 | color: Qt.rgba(0.3 + index * 0.03, 0.2 + index * 0.17, 1, 1) 14 | labelLine: index + 2 15 | lineWidth: 3 16 | visible: model.maxSampleValue > 0 17 | hoverX: hoverCursor.hoverX - 10 18 | } 19 | } 20 | Repeater { 21 | model: LmSensors.filtered(Sensor.Temperature, "Core 0") 22 | LabeledSensorGraph { 23 | model: modelData 24 | color: "#F85" 25 | labelLine: 0 26 | minValue: 25 27 | maxValue: 85 28 | hoverX: hoverCursor.hoverX - 10 29 | } 30 | } 31 | Repeater { 32 | model: LmSensors.filtered(Sensor.Cpu) 33 | LabeledSensorGraph { 34 | model: modelData 35 | color: "wheat" 36 | labelLine: 1 37 | hoverX: hoverCursor.hoverX - 10 38 | } 39 | } 40 | 41 | HoverCursor { id: hoverCursor } 42 | 43 | // possible but unnecessary: 44 | // Component.onCompleted: LmSensors.setDownsampleInterval(2) 45 | } 46 | -------------------------------------------------------------------------------- /examples/temperatures.qml: -------------------------------------------------------------------------------- 1 | import QtQuick 2.5 2 | import org.ecloud.charts 1.0 3 | 4 | Rectangle { 5 | width: 480 6 | height: 320 7 | color: "black" 8 | property var colors: ["grey", "cyan", "magenta", "violet", "steelblue", "wheat", "beige", "brown"] 9 | Repeater { 10 | id: list 11 | anchors.fill: parent 12 | anchors.margins: 6 13 | model: LmSensors.filtered(Sensor.Temperature) 14 | LineGraph { 15 | model: modelData 16 | anchors.fill: parent 17 | color: colors[index] 18 | Text { 19 | color: parent.color 20 | text: modelData.label + " (" + modelData.chipName + ")" 21 | y: index * font.pixelSize 22 | } 23 | Text { 24 | color: "white" 25 | text: modelData.currentValue.toFixed(2) + " " + modelData.unit 26 | anchors.horizontalCenter: parent.horizontalCenter 27 | y: index * font.pixelSize 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/weather-summary-desktop-widget.qml: -------------------------------------------------------------------------------- 1 | #!/bin/env qml 2 | 3 | import QtQuick 2.5 4 | import QtQuick.Window 2.1 5 | import Qt.labs.settings 1.0 6 | 7 | Window { 8 | id: window 9 | width: 580 10 | height: 728 11 | color: "transparent" 12 | flags: Qt.FramelessWindowHint | Qt.WindowStaysOnBottomHint 13 | visible: true 14 | 15 | Settings { 16 | category: "WeatherSummaryDesktopWidget" 17 | property alias x: window.x 18 | property alias y: window.y 19 | property alias width: window.width 20 | property alias height: window.height 21 | } 22 | 23 | Column { 24 | id: column 25 | spacing: 8 26 | anchors.fill: parent 27 | InfluxWeatherGraph { 28 | timespanHours: 72 29 | height: (window.height - column.spacing) / 2 30 | currentVisible: true 31 | } 32 | InfluxWeatherGraph { 33 | timespanHours: 720 // a month 34 | height: (window.height - column.spacing) / 2 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /graph.pro: -------------------------------------------------------------------------------- 1 | QT += qml quick network 2 | TARGET = org/ecloud/charts/chartsplugin 3 | TEMPLATE = lib 4 | 5 | SOURCES += plugin.cpp \ 6 | influxdb.cpp \ 7 | linegraph.cpp \ 8 | linegraphmodel.cpp \ 9 | linenode.cpp \ 10 | lmsensors.cpp 11 | 12 | HEADERS += \ 13 | influxdb.h \ 14 | linegraph.h \ 15 | linegraphmodel.h \ 16 | linenode_p.h \ 17 | lmsensors.h 18 | 19 | RESOURCES += graph.qrc 20 | 21 | LIBS += -lsensors 22 | 23 | QMAKE_CXXFLAGS += -std=c++14 24 | 25 | OTHER_FILES += examples/*.qml org/ecloud/charts/qmldir 26 | 27 | target.path = $$[QT_INSTALL_QML]/org/ecloud/charts 28 | qmldir.path = $$[QT_INSTALL_QML]/org/ecloud/charts 29 | qmldir.files = org/ecloud/charts/qmldir 30 | INSTALLS += target qmldir 31 | -------------------------------------------------------------------------------- /graph.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | shaders/LineNode.vsh 4 | shaders/LineNode.fsh 5 | 6 | 7 | -------------------------------------------------------------------------------- /influxdb.cpp: -------------------------------------------------------------------------------- 1 | #include "influxdb.h" 2 | 3 | #include 4 | 5 | Q_LOGGING_CATEGORY(lcInflux, "org.ecloud.charts.model.influxdb") 6 | 7 | InfluxValueSeries::InfluxValueSeries(QString fieldName, QObject *parent) 8 | : LineGraphModel(parent) 9 | { 10 | setLabel(fieldName); 11 | setTimeSpan(INT_MAX); 12 | setDownsampleMethod(NoDownsample); // influx will do that for us 13 | } 14 | 15 | void InfluxValueSeries::setAdditiveCorrection(qreal additiveCorrection) 16 | { 17 | if (m_additiveCorrection == additiveCorrection) 18 | return; 19 | 20 | m_additiveCorrection = additiveCorrection; 21 | emit additiveCorrectionChanged(); 22 | } 23 | 24 | void InfluxValueSeries::setMultiplicativeCorrection(qreal multiplicativeCorrection) 25 | { 26 | if (m_multiplicativeCorrection == multiplicativeCorrection) 27 | return; 28 | 29 | m_multiplicativeCorrection = multiplicativeCorrection; 30 | emit multiplicativeCorrectionChanged(); 31 | } 32 | 33 | void InfluxValueSeries::finagle(qreal &time, qreal &value) 34 | { 35 | Q_UNUSED(time); 36 | value = value * m_multiplicativeCorrection + m_additiveCorrection; 37 | } 38 | 39 | void appendItems(QQmlListProperty *property, InfluxValueSeries *item) 40 | { 41 | Q_UNUSED(property); 42 | Q_UNUSED(item); 43 | // Do nothing; can't actually append 44 | } 45 | 46 | int itemSize(QQmlListProperty *property) { return static_cast *>(property->data)->size(); } 47 | 48 | InfluxValueSeries *itemAt(QQmlListProperty *property, int index) 49 | { 50 | return static_cast *>(property->data)->at(index); 51 | } 52 | 53 | void clearitemPtr(QQmlListProperty *property) { return static_cast *>(property->data)->clear(); } 54 | 55 | QQmlListProperty InfluxQuery::values() 56 | { 57 | return QQmlListProperty(this, &m_values, &appendItems, &itemSize, &itemAt, &clearitemPtr); 58 | } 59 | 60 | void InfluxQuery::setServer(QUrl server) 61 | { 62 | server.setPath(QLatin1String("/query")); 63 | if (m_server == server) 64 | return; 65 | m_server = server; 66 | emit serverChanged(); 67 | } 68 | 69 | void InfluxQuery::setUser(QString user) 70 | { 71 | if (m_user == user) 72 | return; 73 | 74 | m_user = user; 75 | emit userChanged(); 76 | } 77 | 78 | void InfluxQuery::setPassword(QString password) 79 | { 80 | if (m_password == password) 81 | return; 82 | 83 | m_password = password; 84 | emit passwordChanged(); 85 | } 86 | 87 | void InfluxQuery::setIgnoreSslErrors(bool ignoreSslErrors) 88 | { 89 | if (m_ignoreSslErrors == ignoreSslErrors) 90 | return; 91 | 92 | m_ignoreSslErrors = ignoreSslErrors; 93 | emit ignoreSslErrorsChanged(); 94 | } 95 | 96 | void InfluxQuery::setDatabase(QString db) 97 | { 98 | if (m_database == db) 99 | return; 100 | 101 | m_initialized = false; 102 | m_database = db; 103 | emit databaseChanged(); 104 | } 105 | 106 | void InfluxQuery::setMeasurement(QString measurement) 107 | { 108 | if (m_measurement == measurement) 109 | return; 110 | 111 | m_initialized = false; 112 | m_measurement = measurement; 113 | emit measurementChanged(); 114 | } 115 | 116 | void InfluxQuery::setFields(QStringList fields) 117 | { 118 | if (m_fields == fields) 119 | return; 120 | 121 | m_initialized = false; 122 | m_fields = fields; 123 | emit fieldsChanged(); 124 | } 125 | 126 | void InfluxQuery::setWherePairs(QJsonArray wherePairs) 127 | { 128 | m_initialized = false; 129 | m_wherePairs = wherePairs; 130 | emit wherePairsChanged(); 131 | } 132 | 133 | void InfluxQuery::setTimeConstraint(QString timeConstraint) 134 | { 135 | if (m_timeConstraint == timeConstraint) 136 | return; 137 | 138 | m_initialized = false; 139 | m_timeConstraint = timeConstraint; 140 | emit timeConstraintChanged(); 141 | } 142 | 143 | void InfluxQuery::setSampleInterval(int sampleInterval) 144 | { 145 | if (m_sampleInterval == sampleInterval) 146 | return; 147 | 148 | m_sampleInterval = sampleInterval; 149 | emit sampleIntervalChanged(); 150 | } 151 | 152 | void InfluxQuery::onAuthenticationRequired(QNetworkReply *reply, QAuthenticator *authenticator) 153 | { 154 | authenticator->setUser(m_user); 155 | authenticator->setPassword(m_password); 156 | } 157 | 158 | void InfluxQuery::setUpdateIntervalMs(int updateIntervalMs) 159 | { 160 | if (m_updateIntervalMs == updateIntervalMs) 161 | return; 162 | 163 | m_updateIntervalMs = updateIntervalMs; 164 | if (m_timerId > 0) 165 | killTimer(m_timerId); 166 | m_timerId = startTimer(m_updateIntervalMs); 167 | emit updateIntervalMsChanged(); 168 | } 169 | 170 | void InfluxQuery::init() 171 | { 172 | // SELECT temperature,pressure FROM \"uradmonitor\" WHERE "stationId" = '41000008' AND time > now() - 10h" 173 | // or 174 | // SELECT mean(temperature),mean(pressure) FROM uradmonitor WHERE "stationId" = '41000008' AND time > now() - 240h GROUP BY time(10m) 175 | m_queryString = QLatin1String("SELECT "); 176 | int i = 0; 177 | for (auto f : m_fields) { 178 | if (i++) 179 | m_queryString += QLatin1Char(','); 180 | if (m_sampleInterval) 181 | m_queryString += QString(QLatin1String("mean(%1)")).arg(f); 182 | else 183 | m_queryString += f; 184 | } 185 | m_queryString += QLatin1String(" FROM ") + m_measurement; 186 | QStringList whereAnds; 187 | for (const QJsonValue &wpair : m_wherePairs) { 188 | const QJsonObject o = wpair.toObject(); 189 | for (const QString &k : o.keys()) 190 | whereAnds << QString(QLatin1String("%1 = '%2'")).arg(k).arg(o.value(k).toString()); 191 | } 192 | m_queryString += QLatin1String(" WHERE "); 193 | //qDebug() << whereAnds; 194 | if (!whereAnds.isEmpty()) 195 | m_queryString += whereAnds.join(QLatin1String(" AND ")); 196 | 197 | // only get the full range initially; otherwise just get incremental updates 198 | if (m_lastUpdate.isNull()) { 199 | if (!m_timeConstraint.isEmpty()) { 200 | if (!whereAnds.isEmpty()) 201 | m_queryString += QLatin1String(" AND "); 202 | m_queryString += QLatin1String("time ") + m_timeConstraint; 203 | } 204 | } else { 205 | int secsSinceLast = m_lastUpdate.secsTo(QDateTime::currentDateTime()); 206 | if (!whereAnds.isEmpty()) 207 | m_queryString += QLatin1String("AND "); 208 | m_queryString += QString(QLatin1String("time > now() - %1s")).arg(secsSinceLast); 209 | } 210 | 211 | if (m_sampleInterval) 212 | m_queryString += QString(QLatin1String(" GROUP BY time(%1s)")).arg(m_sampleInterval); 213 | 214 | qCDebug(lcInflux) << m_queryString; 215 | 216 | QUrlQuery quq; 217 | quq.addQueryItem(QLatin1String("db"), m_database); 218 | quq.addQueryItem(QLatin1String("q"), m_queryString); 219 | m_queryUrl = m_server; 220 | m_queryUrl.setQuery(quq); 221 | qCDebug(lcInflux) << m_queryUrl; 222 | if (m_lastUpdate.isNull()) { 223 | m_values.clear(); 224 | for (const QString & field : m_fields) { 225 | InfluxValueSeries *v = new InfluxValueSeries(field); 226 | // initial min/max; we'll refine it later from influx 227 | if (field == QLatin1String("temperature")) { 228 | v->setMinValue(-20); 229 | v->setNormalMinValue(0); 230 | v->setMaxValue(40); 231 | v->setNormalMaxValue(25); 232 | v->setUnit(QString::fromUtf8("°C")); 233 | } else if (field == QLatin1String("pressure")) { 234 | v->setMinValue(95000); 235 | v->setNormalMinValue(98000); 236 | v->setMaxValue(105000); 237 | v->setNormalMaxValue(103000); 238 | v->setUnit(QLatin1String("hPa")); 239 | v->setMultiplier(0.01); 240 | } else if (field == QLatin1String("humidity")) { 241 | v->setMinValue(0); 242 | v->setNormalMinValue(50); 243 | v->setMaxValue(100); 244 | v->setNormalMaxValue(90); 245 | v->setUnit(QLatin1String("%")); 246 | } else if (field == QLatin1String("moisture")) { 247 | v->setMinValue(0); 248 | v->setNormalMinValue(40); 249 | v->setMaxValue(100); 250 | v->setNormalMaxValue(90); 251 | v->setUnit(QLatin1String("%")); 252 | } 253 | m_values.append(v); 254 | } 255 | emit valuesChanged(); 256 | } 257 | 258 | m_influxReq = QNetworkRequest(m_queryUrl); 259 | m_initialized = m_lastUpdate.isValid(); 260 | m_lastUpdate = QDateTime::currentDateTime(); 261 | emit initializedChanged(); 262 | } 263 | 264 | InfluxQuery::InfluxQuery(QObject *parent) : QObject(parent) 265 | { 266 | connect(&m_nam, &QNetworkAccessManager::authenticationRequired, 267 | this, &InfluxQuery::onAuthenticationRequired); 268 | } 269 | 270 | bool InfluxQuery::sampleAllValues() 271 | { 272 | if (!m_initialized) 273 | init(); 274 | 275 | m_netReply = m_nam.get(m_influxReq); 276 | if (m_ignoreSslErrors) 277 | m_netReply->ignoreSslErrors(); 278 | connect(m_netReply, &QNetworkReply::finished, this, &InfluxQuery::networkFinished); 279 | connect(m_netReply, SIGNAL(error(QNetworkReply::NetworkError)), 280 | this, SLOT(networkError(QNetworkReply::NetworkError))); 281 | 282 | return true; 283 | } 284 | 285 | void InfluxQuery::networkError(QNetworkReply::NetworkError e) 286 | { 287 | qCWarning(lcInflux) << e; 288 | m_netReply->disconnect(); 289 | m_netReply->deleteLater(); 290 | m_netReply = nullptr; 291 | } 292 | 293 | void InfluxQuery::networkFinished() 294 | { 295 | QJsonParseError err; 296 | QJsonDocument jdoc = QJsonDocument::fromJson(m_netReply->readAll(), &err); 297 | if (err.error == QJsonParseError::NoError) { 298 | // qDebug() << jdoc.object().value(QLatin1String("results")).toArray(); 299 | QJsonObject o = jdoc.object().value(QLatin1String("results")).toArray().first().toObject(); 300 | QJsonArray arr = o.value("series").toArray().first().toObject().value("values").toArray(); 301 | QDateTime first; 302 | int count = 0; 303 | for (auto o : arr) { 304 | QJsonArray samples = o.toArray(); 305 | m_lastSampleTime = QDateTime::fromString(samples.takeAt(0).toString(), Qt::ISODateWithMs); 306 | if (first.isNull()) 307 | first = m_lastSampleTime; 308 | ++count; 309 | // qDebug() << last.toString() << samples; 310 | for (int i = 0; i < m_values.count(); ++i) { 311 | qreal val = samples.takeAt(0).toDouble(qQNaN()); 312 | m_values[i]->appendSampleMs(val, m_lastSampleTime); 313 | } 314 | } 315 | int timeSpan = int(qAbs(first.secsTo(m_lastSampleTime))); 316 | m_lastUpdate = QDateTime::currentDateTime(); 317 | qCDebug(lcInflux) << "for" << m_fields << "got" << count << "samples from" << first << "to" << m_lastSampleTime << "timespan" << timeSpan; 318 | if (!m_initialized) { 319 | for (int i = 0; i < m_values.count(); ++i) { 320 | m_values[i]->setTimeSpan(timeSpan); 321 | qCDebug(lcInflux) << "value range of" << m_fields.at(i) << m_values.at(i)->minSampleValue() << m_values.at(i)->maxSampleValue(); 322 | } 323 | } 324 | emit valuesUpdated(); 325 | } else { 326 | qCWarning(lcInflux) << err.errorString(); 327 | } 328 | m_netReply->disconnect(); 329 | m_netReply->deleteLater(); 330 | m_netReply = nullptr; 331 | } 332 | -------------------------------------------------------------------------------- /influxdb.h: -------------------------------------------------------------------------------- 1 | #ifndef INFLUXDB_H 2 | #define INFLUXDB_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include "linegraphmodel.h" 15 | 16 | class InfluxValueSeries : public LineGraphModel 17 | { 18 | Q_OBJECT 19 | Q_PROPERTY(qreal additiveCorrection READ additiveCorrection WRITE setAdditiveCorrection NOTIFY additiveCorrectionChanged) 20 | Q_PROPERTY(qreal multiplicativeCorrection READ multiplicativeCorrection WRITE setMultiplicativeCorrection NOTIFY multiplicativeCorrectionChanged) 21 | 22 | public: 23 | explicit InfluxValueSeries(QString fieldName, QObject *parent = 0); 24 | 25 | qreal additiveCorrection() const { return m_additiveCorrection; } 26 | void setAdditiveCorrection(qreal additiveCorrection); 27 | 28 | qreal multiplicativeCorrection() const { return m_multiplicativeCorrection; } 29 | void setMultiplicativeCorrection(qreal multiplicativeCorrection); 30 | 31 | signals: 32 | void additiveCorrectionChanged(); 33 | void multiplicativeCorrectionChanged(); 34 | 35 | protected: 36 | virtual void finagle(qreal &time, qreal &value); 37 | 38 | private: 39 | qreal m_additiveCorrection = 0; 40 | qreal m_multiplicativeCorrection = 1; 41 | }; 42 | 43 | 44 | // curl -GET 'http://localhost:8086/query' --data-urlencode "db=weather" --data-urlencode "q=SELECT temperature,pressure FROM \"uradmonitor\" WHERE "stationId" = '41000008' AND time > now() - 10h" 45 | 46 | class InfluxQuery : public QObject 47 | { 48 | Q_OBJECT 49 | // input 50 | Q_PROPERTY(QUrl server READ server WRITE setServer NOTIFY serverChanged) 51 | Q_PROPERTY(QString user READ user WRITE setUser NOTIFY userChanged) 52 | Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged) 53 | Q_PROPERTY(bool ignoreSslErrors READ ignoreSslErrors WRITE setIgnoreSslErrors NOTIFY ignoreSslErrorsChanged) 54 | Q_PROPERTY(QString database READ database WRITE setDatabase NOTIFY databaseChanged) 55 | Q_PROPERTY(QString measurement READ measurement WRITE setMeasurement NOTIFY measurementChanged) 56 | Q_PROPERTY(QStringList fields READ fields WRITE setFields NOTIFY fieldsChanged) 57 | Q_PROPERTY(QJsonArray wherePairs READ wherePairs WRITE setWherePairs NOTIFY wherePairsChanged) 58 | Q_PROPERTY(QString timeConstraint READ timeConstraint WRITE setTimeConstraint NOTIFY timeConstraintChanged) // e.g. "> now() - 12h" 59 | Q_PROPERTY(int updateIntervalMs READ updateIntervalMs WRITE setUpdateIntervalMs NOTIFY updateIntervalMsChanged) 60 | Q_PROPERTY(int sampleInterval READ sampleInterval WRITE setSampleInterval NOTIFY sampleIntervalChanged) // seconds between graphed samples 61 | 62 | // output 63 | Q_PROPERTY(bool initialized READ initialized NOTIFY initializedChanged) 64 | Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged) 65 | Q_PROPERTY(QQmlListProperty values READ values NOTIFY valuesChanged) 66 | Q_PROPERTY(QDateTime lastUpdate READ lastUpdate NOTIFY valuesUpdated) // time when the query ran 67 | Q_PROPERTY(QDateTime lastSampleTime READ lastSampleTime NOTIFY valuesUpdated) // timestamp of the last datapoint in values 68 | 69 | public: 70 | explicit InfluxQuery(QObject *parent = nullptr); 71 | 72 | Q_INVOKABLE bool sampleAllValues(); 73 | 74 | bool initialized() { return m_initialized; } 75 | QString errorMessage() { return m_errorMessage; } 76 | 77 | int updateIntervalMs() const { return m_updateIntervalMs; } 78 | void setUpdateIntervalMs(int updateIntervalMs); 79 | 80 | QQmlListProperty values(); 81 | 82 | QDateTime lastUpdate() { return m_lastUpdate; } 83 | QDateTime lastSampleTime() const { return m_lastSampleTime; } 84 | 85 | QUrl server() const { return m_server; } 86 | void setServer(QUrl server); 87 | 88 | QString user() const { return m_user;} 89 | void setUser(QString user); 90 | 91 | QString password() const { return m_password; } 92 | void setPassword(QString password); 93 | 94 | bool ignoreSslErrors() const { return m_ignoreSslErrors; } 95 | void setIgnoreSslErrors(bool ignoreSslErrors); 96 | 97 | QString database() const { return m_database; } 98 | void setDatabase(QString database); 99 | 100 | QString measurement() const { return m_measurement; } 101 | void setMeasurement(QString measurement); 102 | 103 | QStringList fields() const { return m_fields; } 104 | void setFields(QStringList fields); 105 | 106 | QJsonArray wherePairs() const { return m_wherePairs; } 107 | void setWherePairs(QJsonArray wherePairs); 108 | 109 | QString timeConstraint() const { return m_timeConstraint; } 110 | void setTimeConstraint(QString timeConstraint); 111 | 112 | int sampleInterval() const { return m_sampleInterval; } 113 | void setSampleInterval(int sampleInterval); 114 | 115 | signals: 116 | void valuesChanged(); // that means values property definition, not actual data! 117 | void valuesUpdated(); // actual data points 118 | void initializedChanged(); 119 | void errorMessageChanged(); 120 | void updateIntervalMsChanged(); 121 | void serverChanged(); 122 | void userChanged(); 123 | void passwordChanged(); 124 | void ignoreSslErrorsChanged(); 125 | void databaseChanged(); 126 | void measurementChanged(); 127 | void fieldsChanged(); 128 | void wherePairsChanged(); 129 | void timeConstraintChanged(); 130 | void sampleIntervalChanged(); 131 | 132 | protected: 133 | void timerEvent(QTimerEvent *) Q_DECL_OVERRIDE { sampleAllValues(); } 134 | 135 | protected slots: 136 | void onAuthenticationRequired(QNetworkReply *reply, QAuthenticator *authenticator); 137 | void networkError(QNetworkReply::NetworkError e); 138 | void networkFinished(); 139 | 140 | private: 141 | void init(); 142 | 143 | private: 144 | QList m_values; 145 | QString m_errorMessage; 146 | QUrl m_server; 147 | QString m_user; 148 | QString m_password; 149 | QString m_database; 150 | QString m_measurement; 151 | QStringList m_fields; 152 | QJsonArray m_wherePairs; 153 | QString m_timeConstraint; 154 | QString m_queryString; 155 | bool m_initialized = false; 156 | bool m_ignoreSslErrors = false; 157 | int m_updateIntervalMs = -1; 158 | int m_timerId = 0; 159 | QUrl m_queryUrl; 160 | QDateTime m_lastUpdate; 161 | QDateTime m_lastSampleTime; 162 | 163 | QNetworkAccessManager m_nam; 164 | QNetworkRequest m_influxReq; 165 | QNetworkReply *m_netReply = nullptr; 166 | int m_sampleInterval = 0; 167 | }; 168 | 169 | #endif // INFLUXDB_H 170 | -------------------------------------------------------------------------------- /linegraph.cpp: -------------------------------------------------------------------------------- 1 | #include "linegraph.h" 2 | #include "linenode_p.h" 3 | #include 4 | #include 5 | 6 | LineGraph::LineGraph() 7 | { 8 | setFlag(ItemHasContents, true); 9 | setAntialiasing(true); 10 | } 11 | 12 | void LineGraph::registerMetaType() 13 | { 14 | qmlRegisterType("LineGraph", 1, 0, "LineGraph"); 15 | } 16 | 17 | void LineGraph::appendSample(qreal value) 18 | { 19 | m_model->appendSampleMs(value); 20 | } 21 | 22 | void LineGraph::removeFirstSample() 23 | { 24 | m_model->removeFirstSample(); 25 | } 26 | 27 | void LineGraph::setLineWidth(qreal lineWidth) 28 | { 29 | if (m_lineWidth == lineWidth) 30 | return; 31 | 32 | m_lineWidth = lineWidth; 33 | m_propertiesChanged = true; 34 | emit lineWidthChanged(); 35 | update(); 36 | } 37 | 38 | void LineGraph::setColor(QColor color) 39 | { 40 | if (m_color == color) 41 | return; 42 | 43 | m_color = color; 44 | m_propertiesChanged = true; 45 | emit colorChanged(); 46 | update(); 47 | } 48 | 49 | void LineGraph::setFillColorBelow(QColor fillColorBelow) 50 | { 51 | if (m_fillColorBelow == fillColorBelow) 52 | return; 53 | 54 | m_fillColorBelow = fillColorBelow; 55 | m_propertiesChanged = true; 56 | emit fillColorBelowChanged(); 57 | update(); 58 | } 59 | 60 | void LineGraph::setFillColorAbove(QColor fillColorAbove) 61 | { 62 | if (m_fillColorAbove == fillColorAbove) 63 | return; 64 | 65 | m_fillColorAbove = fillColorAbove; 66 | m_propertiesChanged = true; 67 | emit fillColorAboveChanged(); 68 | update(); 69 | } 70 | 71 | void LineGraph::setWarningMinColor(QColor warningMinColor) 72 | { 73 | if (m_warningMinColor == warningMinColor) 74 | return; 75 | 76 | m_warningMinColor = warningMinColor; 77 | m_propertiesChanged = true; 78 | emit warningMinColorChanged(); 79 | update(); 80 | } 81 | 82 | void LineGraph::setWarningMaxColor(QColor warningMaxColor) 83 | { 84 | if (m_warningMaxColor == warningMaxColor) 85 | return; 86 | 87 | m_warningMaxColor = warningMaxColor; 88 | m_propertiesChanged = true; 89 | emit warningMaxColorChanged(); 90 | update(); 91 | } 92 | 93 | void LineGraph::setMinValue(qreal minValue) 94 | { 95 | if (m_model) 96 | m_model->setMinValue(minValue); 97 | 98 | m_geometryChanged = true; 99 | m_propertiesChanged = true; 100 | emit minValueChanged(); 101 | update(); 102 | } 103 | 104 | void LineGraph::setMaxValue(qreal maxValue) 105 | { 106 | if (m_model) 107 | m_model->setMaxValue(maxValue); 108 | 109 | m_geometryChanged = true; 110 | m_propertiesChanged = true; 111 | emit maxValueChanged(); 112 | update(); 113 | } 114 | 115 | void LineGraph::setTimeSpan(qreal timeSpan) 116 | { 117 | if (m_timeSpan == timeSpan) 118 | return; 119 | 120 | m_timeSpan = timeSpan; 121 | m_geometryChanged = true; 122 | emit timeSpanChanged(); 123 | if (m_model) 124 | m_model->setDownsampleInterval(timeSpan / width()); 125 | update(); 126 | } 127 | 128 | qreal LineGraph::xAtTime(qreal time) 129 | { 130 | return width() - (m_model->maxSampleTime() - time) / m_timeSpan * width(); 131 | } 132 | 133 | QJSValue LineGraph::sampleNearestX(qreal x) 134 | { 135 | static QJSValue nullJS(QJSValue::NullValue); 136 | QJSEngine *engine = qmlEngine(this); 137 | 138 | qreal time = m_model->maxSampleTime() - m_timeSpan + x / width() * m_timeSpan; 139 | LineNode::LineVertex v = m_model->sampleNearest(time); 140 | if (qIsNaN(v.y)) 141 | return nullJS; 142 | 143 | qreal vscale = height() / (m_model->maxValue() - m_model->minValue()); 144 | QJSValue ret = engine->newObject(); 145 | ret.setProperty(QLatin1String("time"), v.x); // TODO add m_timeOffset 146 | ret.setProperty(QLatin1String("value"), v.y); 147 | ret.setProperty(QLatin1String("x"), xAtTime(v.x)); 148 | ret.setProperty(QLatin1String("y"), height() - vscale * (v.y - m_model->minValue())); 149 | return ret; 150 | } 151 | 152 | void LineGraph::setModel(LineGraphModel *model) 153 | { 154 | if (m_model == model) 155 | return; 156 | 157 | if (m_model) { 158 | disconnect(m_model, &LineGraphModel::samplesChanged, this, &LineGraph::updateVertices); 159 | disconnect(m_model, &LineGraphModel::minValueChanged, this, &LineGraph::minValueChanged); 160 | disconnect(m_model, &LineGraphModel::maxValueChanged, this, &LineGraph::maxValueChanged); 161 | } 162 | m_model = model; 163 | connect(model, &LineGraphModel::samplesChanged, this, &LineGraph::updateVertices); 164 | connect(model, &LineGraphModel::minValueChanged, this, &LineGraph::minValueChanged); 165 | connect(model, &LineGraphModel::maxValueChanged, this, &LineGraph::maxValueChanged); 166 | if (m_model) 167 | m_model->setDownsampleInterval(m_timeSpan / width()); 168 | emit modelChanged(); 169 | updateVertices(); 170 | } 171 | 172 | void LineGraph::setWireframe(bool wireframe) 173 | { 174 | if (m_wireframe == wireframe) 175 | return; 176 | 177 | m_wireframe = wireframe; 178 | m_propertiesChanged = true; 179 | emit wireframeChanged(); 180 | } 181 | 182 | void LineGraph::geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry) 183 | { 184 | m_geometryChanged = true; 185 | if (m_model) 186 | m_model->setDownsampleInterval(m_timeSpan / width()); 187 | update(); 188 | QQuickItem::geometryChanged(newGeometry, oldGeometry); 189 | } 190 | 191 | void LineGraph::updateVertices() 192 | { 193 | m_samplesChanged = true; 194 | update(); 195 | } 196 | 197 | 198 | class GraphNode : public QSGNode 199 | { 200 | public: 201 | LineNode *line; 202 | }; 203 | 204 | 205 | QSGNode *LineGraph::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) 206 | { 207 | if (!m_model) 208 | return oldNode; 209 | 210 | GraphNode *n = static_cast(oldNode); 211 | 212 | QRectF rect = boundingRect(); 213 | 214 | if (rect.isEmpty()) { 215 | delete n; 216 | return 0; 217 | } 218 | if (!n) { 219 | n = new GraphNode(); 220 | n->line = new LineNode(); 221 | n->appendChildNode(n->line); 222 | } 223 | if (m_propertiesChanged) { 224 | float fillDirection = 0; 225 | QColor fillColor = m_color; 226 | if (m_fillColorAbove != Qt::transparent) { 227 | fillDirection = -1; 228 | fillColor = m_fillColorAbove; 229 | } 230 | if (m_fillColorBelow != Qt::transparent) { 231 | fillDirection = 1; 232 | fillColor = m_fillColorBelow; 233 | } 234 | n->line->setHeight(height()); 235 | n->line->setLineWidth(m_lineWidth); 236 | n->line->setColor(fillColor); 237 | n->line->setWarningMinColor(m_warningMinColor); 238 | n->line->setWarningMaxColor(m_warningMaxColor); 239 | n->line->setWarningMinValue(m_model->normalMinValue()); 240 | n->line->setWarningMaxValue(m_model->normalMaxValue()); 241 | n->line->setFillDirection(fillDirection); 242 | n->line->setMinValue(m_model->minValue()); 243 | n->line->setMaxValue(m_model->maxValue()); 244 | n->line->setWireframe(m_wireframe); 245 | } 246 | n->line->setSpread(antialiasing() && !m_wireframe ? 1.0 : 0.0); 247 | 248 | if ((m_geometryChanged || m_samplesChanged) && !m_model->vertices()->isEmpty()) 249 | n->line->updateGeometry(rect, m_model->vertices(), width() / m_timeSpan); 250 | 251 | m_geometryChanged = false; 252 | m_samplesChanged = false; 253 | m_propertiesChanged = false; 254 | 255 | emit samplesChanged(); 256 | 257 | return n; 258 | } 259 | -------------------------------------------------------------------------------- /linegraph.h: -------------------------------------------------------------------------------- 1 | #ifndef LINEGRAPH_H 2 | #define LINEGRAPH_H 3 | 4 | #include 5 | #include "linegraphmodel.h" 6 | 7 | class LineGraph : public QQuickItem 8 | { 9 | Q_OBJECT 10 | Q_PROPERTY(LineGraphModel * model READ model WRITE setModel NOTIFY modelChanged) 11 | Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) 12 | Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) 13 | Q_PROPERTY(QColor fillColorBelow READ fillColorBelow WRITE setFillColorBelow NOTIFY fillColorBelowChanged) 14 | Q_PROPERTY(QColor fillColorAbove READ fillColorAbove WRITE setFillColorAbove NOTIFY fillColorAboveChanged) 15 | Q_PROPERTY(QColor warningMinColor READ warningMinColor WRITE setWarningMinColor NOTIFY warningMinColorChanged) 16 | Q_PROPERTY(QColor warningMaxColor READ warningMaxColor WRITE setWarningMaxColor NOTIFY warningMaxColorChanged) 17 | Q_PROPERTY(qreal minValue READ minValue WRITE setMinValue NOTIFY minValueChanged) // convenience 18 | Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged) // convenience 19 | Q_PROPERTY(qreal timeSpan READ timeSpan WRITE setTimeSpan NOTIFY timeSpanChanged) // in seconds 20 | Q_PROPERTY(bool wireframe READ wireframe WRITE setWireframe NOTIFY wireframeChanged) 21 | 22 | public: 23 | LineGraph(); 24 | 25 | LineGraphModel * model() const { return m_model; } 26 | void setModel(LineGraphModel * model); 27 | 28 | qreal lineWidth() const { return m_lineWidth; } 29 | void setLineWidth(qreal lineWidth); 30 | 31 | QColor color() const { return m_color; } 32 | void setColor(QColor color); 33 | 34 | QColor fillColorBelow() const { return m_fillColorBelow; } 35 | void setFillColorBelow(QColor fillColorBelow); 36 | 37 | QColor fillColorAbove() const { return m_fillColorAbove; } 38 | void setFillColorAbove(QColor fillColorAbove); 39 | 40 | QColor warningMinColor() const { return m_warningMinColor; } 41 | void setWarningMinColor(QColor warningMinColor); 42 | 43 | QColor warningMaxColor() const { return m_warningMaxColor; } 44 | void setWarningMaxColor(QColor warningMaxColor); 45 | 46 | qreal minValue() const { return m_model ? m_model->minValue() : 0; } 47 | void setMinValue(qreal minValue); 48 | 49 | qreal maxValue() const { return m_model ? m_model->maxValue() : 0; } 50 | void setMaxValue(qreal maxValue); 51 | 52 | qreal timeSpan() const { return m_timeSpan; } 53 | void setTimeSpan(qreal timeSpan); 54 | 55 | bool wireframe() const { return m_wireframe; } 56 | void setWireframe(bool wireframe); 57 | 58 | Q_INVOKABLE QJSValue sampleNearestX(qreal x); 59 | Q_INVOKABLE qreal xAtTime(qreal time); 60 | 61 | static void registerMetaType(); 62 | 63 | protected: 64 | QSGNode *updatePaintNode(QSGNode *, UpdatePaintNodeData *); 65 | void geometryChanged(const QRectF &newGeometry, const QRectF &oldGeometry); 66 | void updateVertices(); 67 | 68 | public slots: 69 | void appendSample(qreal value); // convenience 70 | void removeFirstSample(); // convenience 71 | 72 | signals: 73 | void modelChanged(); 74 | void lineWidthChanged(); 75 | void colorChanged(); 76 | void fillColorBelowChanged(); 77 | void fillColorAboveChanged(); 78 | void warningMinColorChanged(); 79 | void warningMaxColorChanged(); 80 | void minValueChanged(); 81 | void maxValueChanged(); 82 | void timeSpanChanged(); 83 | void samplesChanged(); // the model has samplesChanged too, but this occurs at a max of once per frame 84 | void wireframeChanged(); 85 | 86 | protected: 87 | LineGraphModel *m_model = 0; 88 | bool m_samplesChanged = false; 89 | bool m_geometryChanged = false; 90 | bool m_propertiesChanged = true; 91 | bool m_wireframe = false; 92 | QColor m_color = Qt::cyan; 93 | QColor m_fillColorBelow = Qt::transparent; 94 | QColor m_fillColorAbove = Qt::transparent; 95 | QColor m_warningMinColor = Qt::yellow; 96 | QColor m_warningMaxColor = Qt::red; 97 | qreal m_lineWidth = 1.3; 98 | qreal m_timeSpan = 60; 99 | }; 100 | 101 | #endif // LINEGRAPH_H 102 | -------------------------------------------------------------------------------- /linegraphmodel.cpp: -------------------------------------------------------------------------------- 1 | #include "linegraphmodel.h" 2 | #include 3 | #include 4 | 5 | Q_LOGGING_CATEGORY(lcLineGraphModel, "org.ecloud.charts.model") 6 | 7 | const qint64 LineGraphModel::m_timeOffset(LineGraphModel::timeNowMs()); 8 | 9 | /*! 10 | \qmltype LineGraphModel 11 | \instantiates LineGraphModel 12 | \brief A model to store vertices for LineGraph to render 13 | \inherits QObject 14 | 15 | A LineGraphModel stores a time-value series of samples in a form 16 | which is useful both for in-memory storage and later querying, 17 | and for direct rendering: that is, a vector of vertex structs 18 | ready to be sent to the line graph vertex shader. 19 | 20 | It's required to append samples in time order, with the oldest first. 21 | */ 22 | LineGraphModel::LineGraphModel(QObject *parent) : QObject(parent) 23 | { 24 | } 25 | 26 | double LineGraphModel::triangleArea( 27 | const LineGraphModel::TimeValue &a, const LineGraphModel::TimeValue &b, const LineGraphModel::TimeValue &c) 28 | { 29 | // www.mathopenref.com/coordtrianglearea.html 30 | return qAbs( (a.time * (b.value - c.value) + 31 | b.time * (c.value - a.value) + 32 | c.time * (a.value - b.value)) / 2); 33 | } 34 | 35 | LineGraphModel::TimeValue LineGraphModel::largestTriangle(const TimeValue &previous, const QVector ¤t, const TimeValue &next) 36 | { 37 | if (current.count() == 1) 38 | return current.first(); 39 | 40 | int chosen = 0; 41 | double chosenArea = triangleArea(previous, current.first(), next); 42 | for (int i = 1; i < current.size(); ++i) { 43 | if (triangleArea(previous, current[i], next) > chosenArea) 44 | chosen = i; 45 | } 46 | 47 | return current[chosen]; 48 | } 49 | 50 | /*! 51 | Returns the average time and value of the given series. 52 | */ 53 | LineGraphModel::TimeValue LineGraphModel::average(const QVector &tvv) 54 | { 55 | TimeValue ret = {0, 0}; 56 | for (const TimeValue &tv : tvv) { 57 | ret.time += tv.time; 58 | ret.value += tv.value; 59 | } 60 | ret.time /= tvv.count(); 61 | ret.value /= tvv.count(); 62 | return ret; 63 | } 64 | 65 | LineGraphModel::TimeValue LineGraphModel::endVertex(int fromLast) 66 | { 67 | int i = (m_vertices.count() / LineNode::verticesPerSample - fromLast) * LineNode::verticesPerSample; 68 | TimeValue ret; 69 | ret.time = m_vertices[i].x /* + m_timeOffset */; 70 | ret.value = m_vertices[i].y; 71 | return ret; 72 | } 73 | 74 | /*! 75 | Append the given sample \a value taken at the given \a time, 76 | in seconds since startup. 77 | */ 78 | void LineGraphModel::appendSample(qreal value, qreal time) 79 | { 80 | TimeValue tv { time, value }; 81 | // On currentBucket overflow: 82 | // finalize the vertex from m_previousBucket 83 | // current becomes previous 84 | bool modify = !m_currentBucket.isEmpty(); 85 | if (m_downsampleMethod == LargestTriangleThreeBuckets) { 86 | if (!m_currentBucket.isEmpty() && time > m_currentBucket.first().time + m_downsampleBucketInterval) { 87 | if (m_vertices.count() > LineNode::verticesPerSample * 2) { 88 | TimeValue previousVtx = largestTriangle(endVertex(2), m_previousBucket, tv); 89 | modifyEndVertices(previousVtx.time, previousVtx.value, 1); 90 | } 91 | m_previousBucket = m_currentBucket; 92 | m_currentBucket.clear(); 93 | TimeValue previousVtx = average(m_previousBucket); 94 | modifyEndVertices(previousVtx.time, previousVtx.value); 95 | modify = false; 96 | } 97 | m_currentBucket.append(tv); 98 | // qCDebug(lcLineGraphModel) << "buckets" << m_previousBucket.count() << m_currentBucket.count(); 99 | } else { 100 | modify = false; 101 | } 102 | if (modify) 103 | modifyEndVertices(time, value); 104 | else 105 | appendVertices(time, value); 106 | } 107 | 108 | /*! 109 | Append the given sample \a value taken at the given \a timestamp, 110 | in milliseconds since the epoch. 111 | */ 112 | void LineGraphModel::appendSampleMs(qreal value, qint64 timestamp) 113 | { 114 | appendSample(value, (timestamp - m_timeOffset) / 1000.0); 115 | } 116 | 117 | void LineGraphModel::removeFirstSample() 118 | { 119 | qWarning("poorly tested, maybe wrong"); // removing from the bucket(s) depends on m_downsampleMethod if we even need to do that 120 | m_vertices.remove(0, LineNode::verticesPerSample); 121 | } 122 | 123 | /*! 124 | Get the LineVertex instance which is nearest the given \a time, 125 | which is in seconds, beginning with zero from the first sample recorded. 126 | */ 127 | LineNode::LineVertex LineGraphModel::sampleNearest(qreal time) 128 | { 129 | LineNode::LineVertex proto; 130 | proto.x = time; 131 | auto found = std::upper_bound(m_vertices.begin(), m_vertices.end(), proto, [](const auto &lhs, const auto &rhs) { 132 | return lhs.x < rhs.x; 133 | }); 134 | if (found == m_vertices.end()) { 135 | proto.x = -1; 136 | proto.y = qQNaN(); 137 | return proto; 138 | } 139 | //qDebug() << Q_FUNC_INFO << time << "found" << found->t << found->x << found->y; 140 | return *found; 141 | } 142 | 143 | /** 144 | Returns a "nice" number approximately equal to \a range 145 | Rounds the number if \a round = true; takes the ceiling if \a round = false. 146 | 147 | From http://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks 148 | */ 149 | qreal LineGraphModel::niceNum(qreal range, bool round) 150 | { 151 | qreal exponent; /** exponent of range */ 152 | qreal fraction; /** fractional part of range */ 153 | qreal niceFraction; /** nice, rounded fraction */ 154 | 155 | exponent = floor(log10(range)); 156 | fraction = range / pow(10.f, exponent); 157 | 158 | if (round) { 159 | if (fraction < 1.5) 160 | niceFraction = 1; 161 | else if (fraction < 3) 162 | niceFraction = 2; 163 | else if (fraction < 7) 164 | niceFraction = 5; 165 | else 166 | niceFraction = 10; 167 | } else { 168 | if (fraction <= 1) 169 | niceFraction = 1; 170 | else if (fraction <= 2) 171 | niceFraction = 2; 172 | else if (fraction <= 5) 173 | niceFraction = 5; 174 | else 175 | niceFraction = 10; 176 | } 177 | 178 | return niceFraction * pow(10, exponent); 179 | } 180 | 181 | void LineGraphModel::autoScale() 182 | { 183 | qreal range = niceNum(m_maxSampleValue - m_minSampleValue, false); 184 | qreal tickSpacing = niceNum(range / (m_maxTicks - 1), true); 185 | setMinValue(floor(m_minSampleValue / tickSpacing) * tickSpacing); 186 | setMaxValue(ceil(m_maxSampleValue / tickSpacing) * tickSpacing); 187 | qCDebug(lcLineGraphModel) << Q_FUNC_INFO << m_minSampleValue << m_maxSampleValue 188 | << "tickSpacing" << tickSpacing << "autoscaled:" << minValue() << maxValue(); 189 | } 190 | 191 | void LineGraphModel::setNormalMinValue(qreal normalMinValue) 192 | { 193 | if (m_normalMinValue == normalMinValue) 194 | return; 195 | 196 | m_normalMinValue = normalMinValue; 197 | emit normalMinValueChanged(); 198 | } 199 | 200 | void LineGraphModel::setNormalMaxValue(qreal normalMaxValue) 201 | { 202 | if (m_normalMaxValue == normalMaxValue) 203 | return; 204 | 205 | m_normalMaxValue = normalMaxValue; 206 | emit normalMaxValueChanged(); 207 | } 208 | 209 | void LineGraphModel::setLabel(QString label) 210 | { 211 | if (m_label == label) 212 | return; 213 | 214 | m_label = label; 215 | emit labelChanged(); 216 | } 217 | 218 | void LineGraphModel::setUnit(QString unit) 219 | { 220 | if (m_unit == unit) 221 | return; 222 | 223 | m_unit = unit; 224 | emit unitChanged(); 225 | } 226 | 227 | void LineGraphModel::setTimeSpan(int timeSpan) 228 | { 229 | if (m_timeSpan == timeSpan) 230 | return; 231 | 232 | m_timeSpan = timeSpan; 233 | emit timeSpanChanged(); 234 | } 235 | 236 | void LineGraphModel::setDownsampleInterval(qreal downsampleInterval) 237 | { 238 | if (m_downsampleBucketInterval == downsampleInterval) 239 | return; 240 | 241 | m_downsampleBucketInterval = downsampleInterval; 242 | emit downsampleIntervalChanged(); 243 | } 244 | 245 | void LineGraphModel::setDownsampleMethod(LineGraphModel::DownsampleMethod downsampleMethod) 246 | { 247 | if (m_downsampleMethod == downsampleMethod) 248 | return; 249 | 250 | m_downsampleMethod = downsampleMethod; 251 | emit downsampleMethodChanged(); 252 | } 253 | 254 | qreal LineGraphModel::currentValue() const 255 | { 256 | if (m_vertices.length()) 257 | return m_vertices.last().y; 258 | else 259 | return 0; 260 | } 261 | 262 | void LineGraphModel::setMinValue(qreal minValue) 263 | { 264 | if (m_minValue == minValue) 265 | return; 266 | 267 | m_minValue = minValue; 268 | emit minValueChanged(); 269 | } 270 | 271 | void LineGraphModel::setMaxValue(qreal maxValue) 272 | { 273 | if (m_maxValue == maxValue) 274 | return; 275 | 276 | m_maxValue = maxValue; 277 | emit maxValueChanged(); 278 | } 279 | 280 | void LineGraphModel::setClipValues(bool clipValues) 281 | { 282 | if (m_clipValues == clipValues) 283 | return; 284 | 285 | m_clipValues = clipValues; 286 | emit clipValuesChanged(); 287 | } 288 | 289 | const QVector *LineGraphModel::vertices() 290 | { 291 | return &m_vertices; 292 | } 293 | 294 | /*! 295 | Append vertices representing the given sample \a value 296 | taken at the given \a time, in seconds since startup. 297 | */ 298 | void LineGraphModel::appendVertices(qreal time, qreal value) 299 | { 300 | // qDebug() << m_label << time << value << "already have samples:" << m_vertices.size(); 301 | if (m_clipValues) { 302 | if (value < m_minValue) 303 | value = m_minValue; 304 | if (value > m_maxValue) 305 | value = m_maxValue; 306 | } 307 | value *= m_multiplier; 308 | finagle(time, value); 309 | if (value > m_maxSampleValue) { 310 | m_maxSampleValue = value; 311 | emit maxSampleValueChanged(); 312 | } 313 | if (value < m_minSampleValue) { 314 | m_minSampleValue = value; 315 | emit minSampleValueChanged(); 316 | } 317 | int i = m_vertices.size() - LineNode::verticesPerSample; 318 | if (i >= 0) { 319 | m_vertices[i].nextX = time; 320 | m_vertices[i++].nextY = value; 321 | m_vertices[i].nextX = time; 322 | m_vertices[i++].nextY = value; 323 | m_vertices[i].nextX = time; 324 | m_vertices[i++].nextY = value; 325 | m_vertices[i].nextX = time; 326 | m_vertices[i++].nextY = value; 327 | } 328 | i = m_vertices.size(); 329 | m_vertices.resize(i + LineNode::verticesPerSample); 330 | float tp = i > 0 ? m_vertices[i - 1].x : time; 331 | float samplePrev = i > 0 ? m_vertices[i - 1].y : value; 332 | m_vertices[i++].set(0, -1, time, value, tp, samplePrev, time + 0.01, value); 333 | m_vertices[i++].set(1, 1, time, value, tp, samplePrev, time + 0.01, value); 334 | m_vertices[i++].set(2, -1, time, value, tp, samplePrev, time + 0.01, value); 335 | m_vertices[i++].set(3, 1, time, value, tp, samplePrev, time + 0.01, value); 336 | Q_ASSERT(i == m_vertices.size()); 337 | 338 | qint64 earliestTimeAllowed = m_vertices.last().x - m_timeSpan; 339 | while (m_vertices.size() > 2 && m_vertices[1].x < earliestTimeAllowed) 340 | m_vertices.remove(0, 4); 341 | emit samplesChanged(); 342 | } 343 | 344 | void LineGraphModel::modifyEndVertices(qreal time, qreal value, int fromLast) 345 | { 346 | // qDebug() << m_label << time << value << "already have samples:" << m_vertices.size(); 347 | value *= m_multiplier; 348 | finagle(time, value); 349 | int i = (m_vertices.length() / LineNode::verticesPerSample - fromLast - 1) * LineNode::verticesPerSample; 350 | Q_ASSERT(i >= 0); 351 | qreal tp = time - 0.01; 352 | qreal samplePrev = value; 353 | if (i >= LineNode::verticesPerSample) { 354 | int j = i - LineNode::verticesPerSample; 355 | tp = m_vertices[j].x; 356 | samplePrev = m_vertices[j].y; 357 | m_vertices[j].nextX = time; 358 | m_vertices[j++].nextY = value; 359 | m_vertices[j].nextX = time; 360 | m_vertices[j++].nextY = value; 361 | m_vertices[j].nextX = time; 362 | m_vertices[j++].nextY = value; 363 | m_vertices[j].nextX = time; 364 | m_vertices[j++].nextY = value; 365 | } 366 | qreal tn = time + 0.001; 367 | qreal sampleNext = value; 368 | if (fromLast > 0) { 369 | int j = i + LineNode::verticesPerSample; 370 | tn = m_vertices[j].x; 371 | sampleNext = m_vertices[j].y; 372 | m_vertices[j].prevX = time; 373 | m_vertices[j++].prevY = value; 374 | m_vertices[j].prevX = time; 375 | m_vertices[j++].prevY = value; 376 | m_vertices[j].prevX = time; 377 | m_vertices[j++].prevY = value; 378 | m_vertices[j].prevX = time; 379 | m_vertices[j++].prevY = value; 380 | } 381 | m_vertices[i++].set(0, -1, time, value, tp, samplePrev, tn, sampleNext); 382 | m_vertices[i++].set(1, 1, time, value, tp, samplePrev, tn, sampleNext); 383 | m_vertices[i++].set(2, -1, time, value, tp, samplePrev, tn, sampleNext); 384 | m_vertices[i++].set(3, 1, time, value, tp, samplePrev, tn, sampleNext); 385 | emit samplesChanged(); 386 | } 387 | -------------------------------------------------------------------------------- /linegraphmodel.h: -------------------------------------------------------------------------------- 1 | #ifndef LINEGRAPHMODEL_H 2 | #define LINEGRAPHMODEL_H 3 | 4 | #include 5 | #include 6 | #include "linenode_p.h" 7 | 8 | class LineGraphModel : public QObject 9 | { 10 | Q_OBJECT 11 | Q_PROPERTY(int timeSpan READ timeSpan WRITE setTimeSpan NOTIFY timeSpanChanged) 12 | Q_PROPERTY(qreal downsampleInterval READ downsampleInterval WRITE setDownsampleInterval NOTIFY downsampleIntervalChanged) 13 | Q_PROPERTY(DownsampleMethod downsampleMethod READ downsampleMethod WRITE setDownsampleMethod NOTIFY downsampleMethodChanged) 14 | Q_PROPERTY(qreal currentValue READ currentValue NOTIFY samplesChanged) 15 | Q_PROPERTY(qreal minValue READ minValue WRITE setMinValue NOTIFY minValueChanged) 16 | Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged) 17 | Q_PROPERTY(bool clipValues READ clipValues WRITE setClipValues NOTIFY clipValuesChanged) 18 | Q_PROPERTY(qreal normalMinValue READ normalMinValue WRITE setNormalMinValue NOTIFY normalMinValueChanged) 19 | Q_PROPERTY(qreal normalMaxValue READ normalMaxValue WRITE setNormalMaxValue NOTIFY normalMaxValueChanged) 20 | Q_PROPERTY(qreal minSampleValue READ minSampleValue NOTIFY minSampleValueChanged) 21 | Q_PROPERTY(qreal maxSampleValue READ maxSampleValue NOTIFY maxSampleValueChanged) 22 | Q_PROPERTY(QString label READ label WRITE setLabel NOTIFY labelChanged) 23 | Q_PROPERTY(QString unit READ unit WRITE setUnit NOTIFY unitChanged) 24 | 25 | public: 26 | enum DownsampleMethod { NoDownsample = 0, LargestTriangleThreeBuckets }; 27 | 28 | explicit LineGraphModel(QObject *parent = 0); 29 | 30 | int timeSpan() const { return m_timeSpan; } 31 | void setTimeSpan(int timeSpan); 32 | 33 | qreal downsampleInterval() const { return m_downsampleBucketInterval; } 34 | void setDownsampleInterval(qreal downsampleInterval); 35 | 36 | DownsampleMethod downsampleMethod() const { return m_downsampleMethod; } 37 | void setDownsampleMethod(DownsampleMethod downsampleMethod); 38 | 39 | qreal currentValue() const; 40 | 41 | qreal minValue() const { return m_minValue; } 42 | void setMinValue(qreal minValue); 43 | 44 | qreal maxValue() const { return m_maxValue; } 45 | void setMaxValue(qreal maxValue); 46 | 47 | bool clipValues() const { return m_clipValues; } 48 | void setClipValues(bool clipValues); 49 | 50 | qreal minSampleValue() const { return m_minSampleValue; } 51 | qreal maxSampleValue() const { return m_maxSampleValue; } 52 | qreal minSampleTime() const { return m_vertices.isEmpty() ? -1 : m_vertices.first().x; } // in seconds 53 | qreal maxSampleTime() const { return m_vertices.isEmpty() ? -1 : m_vertices.last().x; } 54 | 55 | QString label() const { return m_label; } 56 | void setLabel(QString label); 57 | 58 | QString unit() { return m_unit; } 59 | void setUnit(QString unit); 60 | 61 | void setMultiplier(qreal m) { m_multiplier = m; } 62 | 63 | Q_INVOKABLE LineNode::LineVertex sampleNearest(qreal time); 64 | Q_INVOKABLE void autoScale(); 65 | 66 | qreal normalMinValue() const { return m_normalMinValue; } 67 | void setNormalMinValue(qreal normalMinValue); 68 | 69 | qreal normalMaxValue() const { return m_normalMaxValue; } 70 | void setNormalMaxValue(qreal normalMaxValue); 71 | 72 | static qreal niceNum(qreal range, bool round); 73 | 74 | signals: 75 | void timeSpanChanged(); 76 | void downsampleIntervalChanged(); 77 | void downsampleMethodChanged(); 78 | void samplesChanged(); 79 | void minSampleValueChanged(); 80 | void maxSampleValueChanged(); 81 | void minValueChanged(); 82 | void maxValueChanged(); 83 | void normalMinValueChanged(); 84 | void normalMaxValueChanged(); 85 | void labelChanged(); 86 | void unitChanged(); 87 | void clipValuesChanged(); 88 | 89 | public slots: 90 | void appendSample(qreal value, qreal time); // time in seconds 91 | void appendSampleMs(qreal value, qint64 timestamp = timeNowMs()); 92 | void appendSampleMs(qreal value, QDateTime timestamp) { appendSampleMs(value, timestamp.toMSecsSinceEpoch()); } 93 | void removeFirstSample(); 94 | 95 | protected: 96 | struct TimeValue { 97 | qreal time; 98 | qreal value; 99 | }; 100 | 101 | static qint64 timeNowMs() { return QDateTime::currentMSecsSinceEpoch(); } 102 | 103 | protected: 104 | virtual const QVector *vertices(); 105 | void appendVertices(qreal time, qreal value); 106 | void modifyEndVertices(qreal time, qreal value, int fromLast = 0); 107 | TimeValue endVertex(int fromLast = 0); 108 | static double triangleArea(const LineGraphModel::TimeValue &a, const LineGraphModel::TimeValue &b, const LineGraphModel::TimeValue &c); 109 | static LineGraphModel::TimeValue largestTriangle(const TimeValue &previous, const QVector ¤t, const TimeValue &next); 110 | static LineGraphModel::TimeValue average(const QVector &tvv); 111 | virtual void finagle(qreal &time, qreal &value) { Q_UNUSED(time); Q_UNUSED(value); } 112 | 113 | protected: 114 | QVector m_currentBucket; 115 | QVector m_previousBucket; 116 | QVector m_vertices; 117 | int m_timeSpan = 1000; // in seconds 118 | DownsampleMethod m_downsampleMethod = LargestTriangleThreeBuckets; 119 | bool m_clipValues = true; 120 | int m_maxTicks = 10; // TODO adjust based on view size 121 | qreal m_downsampleBucketInterval = 0; // time in seconds: duration of one bucket 122 | qreal m_minValue = 0; 123 | qreal m_maxValue = 1; 124 | qreal m_normalMinValue = 0; 125 | qreal m_normalMaxValue = 1; 126 | qreal m_minSampleValue = qInf(); // seen for all time, even if it's no longer in m_vertices 127 | qreal m_maxSampleValue = -qInf(); 128 | qreal m_multiplier = 1; 129 | QString m_label; 130 | QString m_unit; 131 | 132 | static const qint64 m_timeOffset; 133 | 134 | friend class LineGraph; 135 | }; 136 | 137 | #endif // LINEGRAPHMODEL_H 138 | -------------------------------------------------------------------------------- /linenode.cpp: -------------------------------------------------------------------------------- 1 | #include "linenode_p.h" 2 | 3 | #include 4 | 5 | #include 6 | 7 | class LineShader : public QSGSimpleMaterialShader 8 | { 9 | QSG_DECLARE_SIMPLE_SHADER(LineShader, LineNode::LineMaterial) 10 | 11 | public: 12 | LineShader() { 13 | setShaderSourceFile(QOpenGLShader::Vertex, ":/shaders/LineNode.vsh"); 14 | setShaderSourceFile(QOpenGLShader::Fragment, ":/shaders/LineNode.fsh"); 15 | } 16 | 17 | QList attributes() const { return QList() << "pos" << "prevNext"; } 18 | 19 | void updateState(const LineNode::LineMaterial *m, const LineNode::LineMaterial *) { 20 | program()->setUniformValue(id_height, m->height); 21 | program()->setUniformValue(id_lineWidth, GLfloat(m->aa ? m->lineWidth * 1.7 : m->lineWidth)); 22 | program()->setUniformValue(id_warningBelowMinimum, m->warningMinValue); 23 | program()->setUniformValue(id_warningAboveMaximum, m->warningMaxValue); 24 | program()->setUniformValue(id_fillDirection, m->fillDirection); 25 | program()->setUniformValue(id_normalColor, m->color); 26 | program()->setUniformValue(id_warningMinColor, m->warningMinColor); 27 | program()->setUniformValue(id_warningMaxColor, m->warningMaxColor); 28 | program()->setUniformValue(id_dataScalingTransform, m->dataTransform.toGenericMatrix<2, 2>()); 29 | program()->setUniformValue(id_dataOffset, m->dataTransform.column(3).toVector2D()); 30 | program()->setUniformValue(id_aa, m->fillDirection == 0 ? m->aa : 0); 31 | //qDebug() << "colors" << m->color << m->warningMinColor << m->warningMaxColor; 32 | } 33 | 34 | void resolveUniforms() { 35 | id_height = program()->uniformLocation("height"); 36 | id_lineWidth = program()->uniformLocation("lineWidth"); 37 | id_warningBelowMinimum = program()->uniformLocation("warningBelowMinimum"); 38 | id_warningAboveMaximum = program()->uniformLocation("warningAboveMaximum"); 39 | id_fillDirection = program()->uniformLocation("fillDirection"); 40 | id_normalColor = program()->uniformLocation("normalColor"); 41 | id_warningMinColor = program()->uniformLocation("warningMinColor"); 42 | id_warningMaxColor = program()->uniformLocation("warningMaxColor"); 43 | id_dataScalingTransform = program()->uniformLocation("dataScalingTransform"); 44 | id_dataOffset = program()->uniformLocation("dataOffset"); 45 | id_aa = program()->uniformLocation("aa"); 46 | } 47 | 48 | private: 49 | int id_height; 50 | int id_lineWidth; 51 | int id_warningBelowMinimum; 52 | int id_warningAboveMaximum; 53 | int id_fillDirection; 54 | int id_normalColor; 55 | int id_warningMinColor; 56 | int id_warningMaxColor; 57 | int id_dataScalingTransform; 58 | int id_dataOffset; 59 | int id_aa; 60 | }; 61 | 62 | static const QSGGeometry::AttributeSet &attributes() 63 | { 64 | static QSGGeometry::Attribute attr[] = { 65 | QSGGeometry::Attribute::create(0, 4, GL_FLOAT), 66 | QSGGeometry::Attribute::create(1, 4, GL_FLOAT) 67 | }; 68 | static QSGGeometry::AttributeSet set = { 2, 8 * sizeof(float), attr }; 69 | return set; 70 | } 71 | 72 | LineNode::LineNode() 73 | : m_geometry(attributes(), 0) 74 | { 75 | setGeometry(&m_geometry); 76 | 77 | m_material = LineShader::createMaterial(); 78 | m_material->setFlag(QSGMaterial::Blending); 79 | setMaterial(m_material); 80 | setFlag(OwnsMaterial); 81 | } 82 | 83 | void LineNode::updateGeometry(const QRectF &bounds, const QVector *v, qreal timeScale) 84 | { 85 | float vscale = bounds.height() / (m_maxValue - m_minValue); 86 | // TODO only adjust the x offset each frame 87 | QMatrix4x4 &matrix = m_material->state()->dataTransform; 88 | matrix.setToIdentity(); 89 | matrix.scale(timeScale, -vscale); 90 | matrix.translate(bounds.width() / timeScale - v->last().x, -m_maxValue); 91 | //qDebug() << v->size() << timeScale << vscale << matrix << v->first().x << v->first().y << v->last().x << v->last().y; 92 | 93 | m_geometry.setDrawingMode(m_wireframe ? GL_LINE_STRIP : GL_TRIANGLE_STRIP); 94 | if (m_geometry.vertexCount() != v->size()) 95 | m_geometry.allocate(v->size()); 96 | // TODO limit on left side to stay in bounds 97 | memcpy(m_geometry.vertexData(), v->constData(), sizeof(LineVertex) * v->size()); 98 | markDirty(QSGNode::DirtyGeometry); 99 | } 100 | 101 | void LineNode::setHeight(float height) 102 | { 103 | m_material->state()->height = height; 104 | markDirty(QSGNode::DirtyMaterial); 105 | } 106 | 107 | void LineNode::setLineWidth(float width) 108 | { 109 | m_material->state()->lineWidth = width; 110 | markDirty(QSGNode::DirtyMaterial); 111 | } 112 | 113 | void LineNode::setColor(QColor color) 114 | { 115 | m_material->state()->color = color; 116 | markDirty(QSGNode::DirtyMaterial); 117 | } 118 | 119 | void LineNode::setWarningMinColor(QColor color) 120 | { 121 | m_material->state()->warningMinColor = color; 122 | markDirty(QSGNode::DirtyMaterial); 123 | } 124 | 125 | void LineNode::setWarningMaxColor(QColor color) 126 | { 127 | m_material->state()->warningMaxColor = color; 128 | markDirty(QSGNode::DirtyMaterial); 129 | } 130 | 131 | void LineNode::setMinValue(qreal v) 132 | { 133 | m_minValue = v; 134 | markDirty(QSGNode::DirtyMaterial); 135 | } 136 | 137 | void LineNode::setMaxValue(qreal v) 138 | { 139 | m_maxValue = v; 140 | markDirty(QSGNode::DirtyMaterial); 141 | } 142 | 143 | void LineNode::setWarningMinValue(qreal v) 144 | { 145 | m_material->state()->warningMinValue = v; 146 | markDirty(QSGNode::DirtyMaterial); 147 | } 148 | 149 | void LineNode::setWarningMaxValue(qreal v) 150 | { 151 | m_material->state()->warningMaxValue = v; 152 | markDirty(QSGNode::DirtyMaterial); 153 | } 154 | 155 | void LineNode::setFillDirection(qreal v) 156 | { 157 | m_material->state()->fillDirection = v; 158 | markDirty(QSGNode::DirtyMaterial); 159 | } 160 | 161 | void LineNode::setSpread(qreal v) 162 | { 163 | m_material->state()->aa = v; 164 | markDirty(QSGNode::DirtyMaterial); 165 | } 166 | 167 | void LineNode::setWireframe(bool v) 168 | { 169 | m_wireframe = v; 170 | } 171 | -------------------------------------------------------------------------------- /linenode_p.h: -------------------------------------------------------------------------------- 1 | #ifndef LINENODE_H 2 | #define LINENODE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | class LineNode : public QSGGeometryNode 9 | { 10 | public: 11 | LineNode(); 12 | 13 | struct LineVertex { 14 | float x; // time (assuming your x axis is that); first sample has x=0 15 | float y; // original sample value 16 | float i; // incrementing index 17 | float t; // -1 or 1 to inset or outset by half-line-width and achieve antialiasing 18 | float prevX; // previous/next values are for the shader to calculate slopes, miter angles etc. 19 | float prevY; 20 | float nextX; 21 | float nextY; 22 | inline void set(int ii, float tt, float xx, float yy, float px, float py, float nx, float ny) { 23 | x = xx; y = yy; i = ii; t = tt; 24 | //qDebug() << "x" << xx << "y" << yy << "i" << ii << "t" << tt; 25 | prevX = px; prevY = py; nextX = nx; nextY = ny; 26 | } 27 | }; 28 | 29 | static const int verticesPerSample = 4; 30 | 31 | void updateGeometry(const QRectF &bounds, const QVector *v, qreal timeScale); 32 | void setHeight(float height); 33 | void setLineWidth(float width); 34 | void setColor(QColor color); 35 | void setWarningMinColor(QColor color); 36 | void setWarningMaxColor(QColor color); 37 | void setMinValue(qreal v); 38 | void setMaxValue(qreal v); 39 | void setWarningMinValue(qreal v); 40 | void setWarningMaxValue(qreal v); 41 | void setFillDirection(qreal v); 42 | void setSpread(qreal v); 43 | void setWireframe(bool v); 44 | 45 | struct LineMaterial 46 | { 47 | QColor color; 48 | QColor warningMinColor; 49 | QColor warningMaxColor; 50 | float height; 51 | float lineWidth; 52 | float warningMinValue; 53 | float warningMaxValue; 54 | float fillDirection; 55 | float aa; 56 | QMatrix4x4 dataTransform; 57 | }; 58 | 59 | private: 60 | QSGGeometry m_geometry; 61 | QSGSimpleMaterial *m_material; 62 | qreal m_minValue = 0; 63 | qreal m_maxValue = 1; 64 | bool m_wireframe; 65 | 66 | friend class LineShader; 67 | friend class LineGraph; 68 | }; 69 | 70 | #endif // LINENODE_H 71 | -------------------------------------------------------------------------------- /lmsensors.cpp: -------------------------------------------------------------------------------- 1 | #include "lmsensors.h" 2 | #include 3 | 4 | LmSensors::LmSensors(QObject *parent) : QObject(parent) 5 | { 6 | m_initialized = init(); 7 | // throw away first sample - tends to be wrong 8 | for (Sensor *item : m_sensors) 9 | item->sample(); 10 | m_timerId = startTimer(m_updateIntervalMs); 11 | } 12 | 13 | void appendItems(QQmlListProperty *property, Sensor *item) 14 | { 15 | Q_UNUSED(property); 16 | Q_UNUSED(item); 17 | // Do nothing. can't add to a directory using this method 18 | } 19 | 20 | int itemSize(QQmlListProperty *property) { return static_cast *>(property->data)->size(); } 21 | 22 | Sensor *itemAt(QQmlListProperty *property, int index) 23 | { 24 | return static_cast *>(property->data)->at(index); 25 | } 26 | 27 | void clearitemPtr(QQmlListProperty *property) { return static_cast *>(property->data)->clear(); } 28 | 29 | QQmlListProperty LmSensors::sensors() 30 | { 31 | return QQmlListProperty(this, &m_sensors, &appendItems, &itemSize, &itemAt, &clearitemPtr); 32 | } 33 | 34 | bool LmSensors::init() 35 | { 36 | #define BUF_SIZE 200 37 | static char buf[BUF_SIZE]; 38 | 39 | int chip_nr, a; 40 | const sensors_chip_name *chip; 41 | const sensors_subfeature *sub; 42 | const sensors_feature *feature; 43 | const char *adap = NULL; 44 | 45 | // add CPU load 46 | 47 | Sensor *new_item = new Sensor(Sensor::Cpu); 48 | new_item->m_index = m_sensors.count(); 49 | new_item->setLabel(tr("CPU Load")); 50 | new_item->m_adapter = "proc-stat"; 51 | new_item->setMinValue(0); 52 | new_item->setMaxValue(100); 53 | new_item->m_unit = "%"; 54 | m_sensors.append(new_item); 55 | 56 | // add memory metrics 57 | 58 | qreal memoryTotal = Sensor(Sensor::MemoryTotal).sample(); 59 | 60 | new_item = new Sensor(Sensor::MemoryFree); 61 | new_item->m_index = m_sensors.count(); 62 | new_item->setLabel(tr("Memory Free")); 63 | new_item->m_adapter = "proc-stat"; 64 | new_item->setMinValue(0); 65 | new_item->setMaxValue(memoryTotal); 66 | new_item->setNormalMinValue(memoryTotal * 0.02); 67 | new_item->setNormalMaxValue(memoryTotal); 68 | new_item->m_unit = "KB"; 69 | m_sensors.append(new_item); 70 | 71 | new_item = new Sensor(Sensor::MemoryUsed); 72 | new_item->m_index = m_sensors.count(); 73 | new_item->setLabel(tr("Memory Used")); 74 | new_item->m_adapter = "proc-stat"; 75 | new_item->setMinValue(0); 76 | new_item->setMaxValue(memoryTotal); 77 | new_item->setNormalMinValue(0); 78 | new_item->setNormalMaxValue(memoryTotal * 0.98); 79 | new_item->m_unit = "KB"; 80 | m_sensors.append(new_item); 81 | 82 | new_item = new Sensor(Sensor::MemoryCache); 83 | new_item->m_index = m_sensors.count(); 84 | new_item->setLabel(tr("Memory Cached")); 85 | new_item->m_adapter = "proc-stat"; 86 | new_item->setMinValue(0); 87 | new_item->setMaxValue(memoryTotal); 88 | new_item->setNormalMinValue(0); 89 | new_item->setNormalMaxValue(memoryTotal); 90 | new_item->m_unit = "KB"; 91 | m_sensors.append(new_item); 92 | 93 | qreal swapTotal = Sensor(Sensor::SwapTotal).sample(); 94 | 95 | new_item = new Sensor(Sensor::SwapFree); 96 | new_item->m_index = m_sensors.count(); 97 | new_item->setLabel(tr("Swap Free")); 98 | new_item->m_adapter = "proc-stat"; 99 | new_item->setMinValue(0); 100 | new_item->setMaxValue(swapTotal); 101 | new_item->setNormalMinValue(swapTotal * 0.02); 102 | new_item->setNormalMaxValue(swapTotal); 103 | new_item->m_unit = "KB"; 104 | m_sensors.append(new_item); 105 | 106 | new_item = new Sensor(Sensor::SwapUsed); 107 | new_item->m_index = m_sensors.count(); 108 | new_item->setLabel(tr("Swap Used")); 109 | new_item->m_adapter = "proc-stat"; 110 | new_item->setMinValue(0); 111 | new_item->setMaxValue(swapTotal); 112 | new_item->setNormalMinValue(0); 113 | new_item->setNormalMaxValue(swapTotal * 0.98); 114 | new_item->m_unit = "KB"; 115 | m_sensors.append(new_item); 116 | 117 | // add lm-sensors 118 | 119 | if (int err = sensors_init(NULL)) { 120 | m_errorMessage = sensors_strerror(err); 121 | emit errorMessageChanged(); 122 | emit sensorsChanged(); 123 | return false; 124 | } else { 125 | chip_nr = 0; 126 | 127 | while ((chip = sensors_get_detected_chips(NULL, &chip_nr))) { 128 | if (sensors_snprintf_chip_name(buf, BUF_SIZE, chip) < 0) 129 | sprintf(buf, "%i", chip_nr); 130 | 131 | adap = sensors_get_adapter_name(&chip->bus); 132 | // if(adap) qDebug() << " " << adap; 133 | a = 0; 134 | while ((feature = sensors_get_features(chip, &a))) { 135 | // qDebug() << " " << sensors_get_label(chip, feature) << "type" << feature->type; 136 | Sensor *new_item = new Sensor(); 137 | 138 | sub = sensors_get_subfeature(chip, feature, (sensors_subfeature_type)(((int)feature->type) << 8)); 139 | 140 | new_item->m_index = m_sensors.count(); 141 | new_item->setLabel(QLatin1String(sensors_get_label(chip, feature))); 142 | if (adap) 143 | new_item->m_adapter = adap; 144 | new_item->m_chip = chip; 145 | new_item->m_chipId = chip_nr; 146 | new_item->m_chipName = buf; 147 | new_item->m_feature = feature; 148 | new_item->m_subfeature = sub; 149 | new_item->setMinValue(0); 150 | new_item->setMaxValue(100); 151 | 152 | const sensors_subfeature *limitSub = nullptr; 153 | 154 | switch (new_item->m_feature->type) { 155 | case SENSORS_FEATURE_IN: 156 | new_item->m_type = Sensor::SensorType::Input; 157 | // fallthrough 158 | case SENSORS_FEATURE_VID: 159 | if (new_item->m_type == Sensor::SensorType::Unknown) 160 | new_item->m_type = Sensor::SensorType::Vid; 161 | new_item->setMinValue(0); 162 | new_item->setMaxValue(15); 163 | new_item->m_unit = "V"; 164 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_IN_MIN); 165 | if (limitSub) 166 | sensors_get_value(chip, limitSub->number, &new_item->m_normalMinValue); 167 | else 168 | qDebug() << "no min limit" << new_item->chipName() << new_item->m_feature->name << new_item->m_subfeature->name; 169 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_IN_MAX); 170 | if (limitSub) 171 | sensors_get_value(chip, limitSub->number, &new_item->m_normalMaxValue); 172 | else 173 | qDebug() << "no max limit" << new_item->chipName() << new_item->m_feature->name << new_item->m_subfeature->name; 174 | break; 175 | case SENSORS_FEATURE_FAN: 176 | new_item->m_type = Sensor::SensorType::Fan; 177 | new_item->setMinValue(0); 178 | new_item->setMaxValue(7200); 179 | new_item->setNormalMaxValue(5500); 180 | new_item->m_unit = "RPM"; 181 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_FAN_MIN); 182 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMinValue); 183 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_FAN_MAX); 184 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMaxValue); 185 | break; 186 | case SENSORS_FEATURE_TEMP: 187 | new_item->m_type = Sensor::SensorType::Temperature; 188 | new_item->m_unit = "°C"; 189 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_TEMP_MIN); 190 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMinValue); 191 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_TEMP_MAX); 192 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMaxValue); 193 | break; 194 | case SENSORS_FEATURE_POWER: 195 | new_item->m_type = Sensor::SensorType::Power; 196 | new_item->m_unit = "W"; 197 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_POWER_MAX); 198 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMaxValue); 199 | break; 200 | case SENSORS_FEATURE_ENERGY: 201 | new_item->m_type = Sensor::SensorType::Energy; 202 | new_item->m_unit = "J"; 203 | break; 204 | case SENSORS_FEATURE_CURR: 205 | new_item->m_type = Sensor::SensorType::Current; 206 | new_item->m_unit = "A"; 207 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_CURR_MIN); 208 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMinValue); 209 | limitSub = sensors_get_subfeature(chip, feature, SENSORS_SUBFEATURE_CURR_MAX); 210 | if (limitSub) sensors_get_value(chip, limitSub->number, &new_item->m_normalMaxValue); 211 | break; 212 | case SENSORS_FEATURE_HUMIDITY: 213 | new_item->m_type = Sensor::SensorType::Humidity; 214 | new_item->m_unit = "%"; 215 | break; 216 | case SENSORS_FEATURE_INTRUSION: 217 | new_item->m_type = Sensor::SensorType::Intrusion; 218 | new_item->setMinValue(0); 219 | new_item->setMaxValue(12); 220 | break; 221 | default:; 222 | } 223 | // qDebug() << "range of" << new_item->m_label << new_item->m_normalMinValue << new_item->m_normalMaxValue; 224 | 225 | m_sensors.append(new_item); 226 | } 227 | } 228 | } 229 | 230 | // take inventory of batteries and power adapters 231 | QDir powerSupplyDir(QLatin1String("/sys/class/power_supply")); 232 | for (const QString &powerSupply : powerSupplyDir.entryList()) { 233 | if (powerSupply.startsWith(QLatin1String("."))) 234 | continue; 235 | 236 | bool isBattery = powerSupply.toLower().startsWith(QLatin1String("bat")); 237 | QDir dir(powerSupplyDir); 238 | dir.cd(powerSupply); 239 | if (isBattery) { 240 | for (QString metric : dir.entryList(QStringList() << "*_now")) { 241 | QString fullMetric(metric); 242 | fullMetric.replace(QLatin1String("_now"), QLatin1String("_full")); 243 | qreal fullAmount = readRealFile(dir.absoluteFilePath(fullMetric)) / 1000000; 244 | if (qIsNaN(fullAmount)) 245 | continue; 246 | // OK we're good: we have a means of reading the "now" value, and have already read the "full" value 247 | new_item = new Sensor(Sensor::Energy); 248 | new_item->m_file = new QFile(dir.absoluteFilePath(metric)); 249 | new_item->m_index = m_sensors.count(); 250 | new_item->setLabel(powerSupply); 251 | new_item->m_adapter = "power_supply"; 252 | new_item->setMinValue(0); 253 | new_item->setMaxValue(fullAmount); 254 | new_item->setNormalMinValue(fullAmount * 0.02); 255 | new_item->setNormalMaxValue(fullAmount); 256 | new_item->m_unit = "Ah"; // guessing uAh from the size of the number... so we divided down above 257 | new_item->m_scale = 1 / 1000000.0; 258 | m_sensors.append(new_item); 259 | } 260 | } else if (powerSupply.toLower().startsWith(QLatin1String("ac"))) { 261 | // AC adapter 262 | new_item = new Sensor(Sensor::Connected); 263 | new_item->m_file = new QFile(dir.absoluteFilePath(QLatin1String("online"))); 264 | new_item->m_index = m_sensors.count(); 265 | new_item->setLabel(powerSupply); 266 | new_item->m_adapter = "power_supply"; 267 | new_item->setMinValue(0); 268 | new_item->setMaxValue(1); 269 | new_item->setNormalMinValue(0); 270 | new_item->setNormalMaxValue(1); 271 | new_item->m_unit = "connected"; 272 | m_sensors.append(new_item); 273 | } 274 | } 275 | 276 | // find relevant cpufreq files 277 | QStringList curFreqs = find(QLatin1String("/sys/devices/system/cpu"), QStringList() << QLatin1String("scaling_cur_freq")); 278 | curFreqs.sort(); 279 | QRegularExpression reCpu("cpu(\\d+)"); 280 | bool hasPolicyFiles = false; 281 | bool skipPolicyFiles = false; 282 | for (const QString &cf : curFreqs) 283 | if (cf.contains("policy")) 284 | hasPolicyFiles = true; 285 | else 286 | skipPolicyFiles = true; 287 | for (const QString &cf : curFreqs) { 288 | // prefer /sys/devices/system/cpu/cpu2/cpufreq/scaling_cur_freq over /sys/devices/system/cpu/cpufreq/policy2/scaling_cur_freq, iff both exist 289 | if (skipPolicyFiles && cf.contains(QLatin1String("policy"))) 290 | continue; 291 | QString minF(cf); minF.replace(QLatin1String("_cur"), QLatin1String("_min")); 292 | QString maxF(cf); maxF.replace(QLatin1String("_cur"), QLatin1String("_max")); 293 | qreal min = readRealFile(minF) / 1000; // kHz to MHz 294 | qreal max = readRealFile(maxF) / 1000; 295 | if (!(qIsNaN(min) || qIsNaN(max))) { 296 | new_item = new Sensor(Sensor::Frequency); 297 | new_item->m_file = new QFile(cf); 298 | new_item->m_index = m_sensors.count(); 299 | { 300 | QRegularExpressionMatch match = reCpu.match(cf); 301 | if (match.hasMatch()) 302 | new_item->setLabel(QString(QLatin1String("CPU %1")).arg(match.captured(0))); 303 | else 304 | new_item->setLabel(QLatin1String("CPU")); 305 | } 306 | new_item->m_adapter = "CPU"; 307 | new_item->setMinValue(min); 308 | new_item->setMaxValue(max); 309 | new_item->setNormalMinValue(min); 310 | new_item->setNormalMaxValue(max * 0.98); 311 | new_item->m_unit = "MHz"; 312 | new_item->m_scale = 1 / 1000.0; 313 | m_sensors.append(new_item); 314 | } 315 | } 316 | 317 | emit sensorsChanged(); 318 | return true; 319 | } 320 | 321 | bool LmSensors::sampleAllValues() 322 | { 323 | qint64 timestamp = LineGraphModel::timeNowMs(); 324 | for (Sensor *item : m_sensors) 325 | item->recordSample(timestamp); 326 | return true; 327 | } 328 | 329 | void LmSensors::setDownsampleInterval(qreal downsampleInterval) 330 | { 331 | for (Sensor *item : m_sensors) 332 | item->setDownsampleInterval(downsampleInterval); 333 | } 334 | 335 | QList LmSensors::filtered(int t, const QString substring) 336 | { 337 | QList ret; 338 | QVector types; 339 | if (t == Sensor::SensorType::Memory) 340 | types << Sensor::MemoryFree << Sensor::MemoryUsed << Sensor::MemoryCache << Sensor::SwapFree << Sensor::SwapUsed; 341 | else 342 | types << static_cast(t); 343 | for (Sensor * item : m_sensors) 344 | if ((t == Sensor::SensorType::Unknown || types.contains(item->type())) && 345 | (substring.isEmpty() || item->label().contains(substring) || 346 | item->chipName().contains(substring) || item->adapter().contains(substring))) 347 | ret << item; 348 | //qDebug() << "found" << ret.count() << "of type" << type; 349 | return ret; 350 | } 351 | 352 | qreal LmSensors::readRealFile(const QString &path) 353 | { 354 | QFile f(path); 355 | qreal val = qQNaN(); 356 | if (f.open(QFile::ReadOnly)) { 357 | bool ok = false; 358 | val = f.readAll().trimmed().toDouble(&ok); 359 | f.close(); 360 | if (!ok) 361 | val = qQNaN(); 362 | } 363 | return val; 364 | } 365 | 366 | QStringList LmSensors::find(const QString &dir, const QStringList &nameFilters) 367 | { 368 | QDirIterator it(dir, nameFilters, QDir::NoFilter, QDirIterator::Subdirectories); 369 | QStringList ret; 370 | while (it.hasNext()) 371 | ret << it.next(); 372 | return ret; 373 | } 374 | 375 | void LmSensors::setUpdateIntervalMs(int updateIntervalMs) 376 | { 377 | if (m_updateIntervalMs == updateIntervalMs) 378 | return; 379 | 380 | m_updateIntervalMs = updateIntervalMs; 381 | killTimer(m_timerId); 382 | m_timerId = startTimer(m_updateIntervalMs); 383 | emit updateIntervalMsChanged(); 384 | } 385 | 386 | Sensor::Sensor(SensorType type, QObject *parent) : LineGraphModel(parent) 387 | { 388 | m_type = type; 389 | m_maxValue = 100; // good for temperatures and percentages at least 390 | m_normalMaxValue = 100; 391 | } 392 | 393 | bool Sensor::recordSample(qint64 timestamp) 394 | { 395 | qreal val = sample(); 396 | if (val == 65535) 397 | return false; 398 | appendSampleMs(val, timestamp); 399 | return true; 400 | } 401 | 402 | void Sensor::getMemoryMetric(const char *metric, qreal &val) 403 | { 404 | QFile data("/proc/meminfo"); 405 | QLatin1String metricStr(metric); 406 | if (data.open(QFile::ReadOnly)) { 407 | QTextStream in(&data); 408 | QString line; 409 | 410 | do { 411 | line = in.readLine(1024); 412 | } while (!line.startsWith(metricStr)); 413 | 414 | QString afterColon = line.mid(line.indexOf(':') + 1).trimmed(); 415 | int spaceIdx = afterColon.indexOf(' '); 416 | QString beforeUnits = afterColon.left(spaceIdx); 417 | m_unit = afterColon.mid(spaceIdx + 1); 418 | bool ok = false; 419 | val = beforeUnits.toDouble(&ok); 420 | if (!ok) 421 | val = 0; 422 | else if (val > 10000 && m_unit.toLower() == QLatin1String("kb")) { 423 | val /= 1000; 424 | m_unit = QLatin1String("MB"); 425 | } 426 | } else { 427 | val = 0; 428 | } 429 | } 430 | 431 | void Sensor::getCPULoad(qreal &val) 432 | { 433 | // http://stackoverflow.com/questions/3017162/how-to-get-total-cpu-usage-in-linux-c 434 | // http://www.linuxhowtos.org/System/procstat.htm 435 | 436 | // cpu 63536 963 11961 946741 2090 0 107 0 0 0 437 | 438 | QFile data("/proc/stat"); 439 | if (data.open(QFile::ReadOnly)) { 440 | QTextStream in(&data); 441 | QStringList list; 442 | qint64 total_jiffies = 0, work_jiffies = 0; 443 | 444 | do { 445 | list = in.readLine(1024).split(" ", QString::SkipEmptyParts); 446 | } while (list.at(0) != "cpu"); 447 | 448 | for (int x = 1; x < list.length(); x++) 449 | total_jiffies += list.at(x).toInt(); 450 | for (int x = 1; x < 4; x++) 451 | work_jiffies += list.at(x).toInt(); 452 | 453 | if (m_totalJiffies) 454 | val = qreal(work_jiffies - m_workJiffies) / qreal(total_jiffies - m_totalJiffies) * 100.; 455 | else 456 | val = 0; 457 | 458 | m_workJiffies = work_jiffies; 459 | m_totalJiffies = total_jiffies; 460 | } else { 461 | val = 0; 462 | } 463 | } 464 | 465 | qreal Sensor::sample() 466 | { 467 | qreal valTotal = 0; 468 | qreal val = 0; 469 | 470 | switch (m_type) { 471 | case Sensor::MemoryTotal: 472 | getMemoryMetric("MemTotal", val); 473 | break; 474 | case Sensor::MemoryFree: 475 | getMemoryMetric("MemFree", val); 476 | break; 477 | case Sensor::MemoryUsed: 478 | getMemoryMetric("MemTotal", valTotal); 479 | getMemoryMetric("MemFree", val); 480 | val = valTotal - val; 481 | break; 482 | case Sensor::MemoryCache: 483 | getMemoryMetric("Cached", val); 484 | break; 485 | case Sensor::SwapTotal: 486 | getMemoryMetric("SwapTotal", val); 487 | break; 488 | case Sensor::SwapFree: 489 | getMemoryMetric("SwapFree", val); 490 | break; 491 | case Sensor::SwapUsed: 492 | getMemoryMetric("SwapTotal", valTotal); 493 | getMemoryMetric("SwapFree", val); 494 | val = valTotal - val; 495 | break; 496 | case Sensor::SensorType::Cpu: 497 | getCPULoad(val); 498 | break; 499 | case Sensor::Connected: 500 | case Sensor::Energy: 501 | case Sensor::Frequency: 502 | if (m_file && m_file->open(QFile::ReadOnly)) { 503 | val = m_file->readAll().trimmed().toDouble(); 504 | m_file->close(); 505 | } 506 | break; 507 | default: // LM sensors 508 | sensors_get_value(m_chip, m_subfeature->number, &val); 509 | break; 510 | } 511 | val *= m_scale; 512 | return val; 513 | } 514 | -------------------------------------------------------------------------------- /lmsensors.h: -------------------------------------------------------------------------------- 1 | #ifndef LMSENSORS_H 2 | #define LMSENSORS_H 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | // LM-Sensors Library Header 16 | #include 17 | #include 18 | #include "linegraphmodel.h" 19 | 20 | class Sensor : public LineGraphModel 21 | { 22 | Q_OBJECT 23 | Q_PROPERTY(SensorType type READ type CONSTANT) 24 | Q_PROPERTY(QString adapter READ adapter CONSTANT) 25 | Q_PROPERTY(QString chipName READ chipName CONSTANT) 26 | Q_PROPERTY(int chipId READ chipId CONSTANT) 27 | 28 | public: 29 | enum SensorType { Unknown = 0, Cpu, 30 | Memory = 0x100, MemoryFree, MemoryUsed, MemoryCache, MemoryTotal, SwapFree, SwapUsed, SwapTotal, 31 | Input = 0x200, Fan, Temperature, Power, Energy, Current, Humidity, Vid, Intrusion, Connected, Frequency }; 32 | Q_ENUM(SensorType) 33 | 34 | explicit Sensor(SensorType type = Unknown, QObject *parent = 0); 35 | 36 | Q_INVOKABLE qreal sample(); 37 | 38 | SensorType type() { return m_type; } 39 | QString adapter() { return m_adapter; } 40 | QString chipName() { return m_chipName; } 41 | int chipId() { return m_chipId; } 42 | 43 | private: 44 | bool recordSample(qint64 timestamp); 45 | void getMemoryMetric(const char *metric, qreal &val); 46 | void getCPULoad(qreal &val); 47 | 48 | private: 49 | int m_index = -1; 50 | int m_chipId = 0; 51 | SensorType m_type = SensorType::Unknown; 52 | const sensors_chip_name *m_chip = 0; 53 | const sensors_feature *m_feature = 0; 54 | const sensors_subfeature *m_subfeature = 0; 55 | qint64 m_totalJiffies = 0; 56 | qint64 m_workJiffies = 0; 57 | qreal m_scale = 1; 58 | QString m_adapter; 59 | QString m_chipName; 60 | QFile *m_file = nullptr; 61 | 62 | friend class LmSensors; 63 | }; 64 | 65 | 66 | class LmSensors : public QObject 67 | { 68 | Q_OBJECT 69 | Q_PROPERTY(bool initialized READ initialized NOTIFY sensorsChanged) 70 | Q_PROPERTY(QString errorMessage READ errorMessage NOTIFY errorMessageChanged) 71 | Q_PROPERTY(QQmlListProperty sensors READ sensors NOTIFY sensorsChanged) 72 | Q_PROPERTY(int updateIntervalMs READ updateIntervalMs WRITE setUpdateIntervalMs NOTIFY updateIntervalMsChanged) 73 | 74 | public: 75 | 76 | explicit LmSensors(QObject *parent = 0); 77 | 78 | Q_INVOKABLE bool sampleAllValues(); 79 | Q_INVOKABLE qint64 timestamp() { return (QDateTime().currentDateTime().toMSecsSinceEpoch()); } 80 | Q_INVOKABLE void setDownsampleInterval(qreal downsampleInterval); 81 | 82 | bool initialized() { return m_initialized; } 83 | QString errorMessage() { return m_errorMessage; } 84 | 85 | int updateIntervalMs() const { return m_updateIntervalMs; } 86 | void setUpdateIntervalMs(int updateIntervalMs); 87 | 88 | QQmlListProperty sensors(); 89 | 90 | Q_INVOKABLE QList filtered(int type, const QString substring = QString()); // int is really Sensor::SensorType 91 | static qreal readRealFile(const QString &path); 92 | 93 | signals: 94 | void sensorsChanged(); 95 | void errorMessageChanged(); 96 | void updateIntervalMsChanged(); 97 | 98 | protected: 99 | void timerEvent(QTimerEvent *) Q_DECL_OVERRIDE { sampleAllValues(); } 100 | static QStringList find(const QString &dir, const QStringList &nameFilters); 101 | 102 | private: 103 | bool init(); 104 | 105 | private: 106 | QList m_sensors; 107 | QString m_errorMessage; 108 | bool m_initialized; 109 | int m_updateIntervalMs = 1000; 110 | int m_timerId = -1; 111 | }; 112 | 113 | //QML_DECLARE_TYPE(LmSensors *) 114 | //QML_DECLARE_TYPE(Sensor *) 115 | 116 | 117 | #endif // LMSENSORS_H 118 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "LineGraph.h" 5 | #include 6 | 7 | int main(int argc, char *argv[]) 8 | { 9 | QGuiApplication a(argc, argv); 10 | 11 | LineGraph::registerMetaType(); 12 | 13 | QQuickView view; 14 | view.resize(1190, 900); 15 | view.setResizeMode(QQuickView::SizeRootObjectToView); 16 | view.setSource(QUrl("qrc:///main.qml")); 17 | view.show(); 18 | 19 | return a.exec(); 20 | } 21 | -------------------------------------------------------------------------------- /org/ecloud/charts/qmldir: -------------------------------------------------------------------------------- 1 | module org.ecloud.charts 2 | plugin chartsplugin 3 | classname ChartsPlugin 4 | typeinfo plugins.qmltypes 5 | -------------------------------------------------------------------------------- /plugin.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "influxdb.h" 9 | #include "lmsensors.h" 10 | #include "linegraph.h" 11 | 12 | Q_LOGGING_CATEGORY(lcRegistration, "org.ecloud.charts.registration") 13 | 14 | static const char *ModuleName = "org.ecloud.charts"; 15 | 16 | static QObject *LmSensorsSingleton(QQmlEngine *engine, QJSEngine *scriptEngine) 17 | { 18 | Q_UNUSED(engine) 19 | Q_UNUSED(scriptEngine) 20 | 21 | LmSensors *ret = new LmSensors(); 22 | return ret; 23 | } 24 | 25 | class ChartsPlugin : public QQmlExtensionPlugin 26 | { 27 | Q_OBJECT 28 | Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface/1.0") 29 | 30 | public: 31 | ChartsPlugin() : QQmlExtensionPlugin() { } 32 | 33 | virtual void initializeEngine(QQmlEngine *engine, const char * uri) { 34 | Q_UNUSED(engine) 35 | qCDebug(lcRegistration) << uri; 36 | } 37 | 38 | virtual void registerTypes(const char *uri) { 39 | qCDebug(lcRegistration) << uri; 40 | Q_ASSERT(uri == QLatin1String(ModuleName)); 41 | qmlRegisterType(uri, 1, 0, "LineGraph"); 42 | qmlRegisterType(uri, 1, 0, "LineGraphModel"); 43 | qmlRegisterSingletonType(uri, 1, 0, "LmSensors", LmSensorsSingleton); 44 | qmlRegisterType(uri, 1, 0, "Sensor"); 45 | qmlRegisterType(uri, 1, 0, "InfluxQuery"); 46 | qmlRegisterUncreatableType(uri, 1, 0, "InfluxValueSeries", "InfluxValueSeries is only available from InfluxQuery"); 47 | // qmlRegisterType(uri, 1, 0, "InfluxValueSeries"); 48 | } 49 | }; 50 | 51 | QT_END_NAMESPACE 52 | 53 | #include "plugin.moc" 54 | -------------------------------------------------------------------------------- /shaders/LineNode.fsh: -------------------------------------------------------------------------------- 1 | uniform lowp float qt_Opacity; 2 | uniform lowp float aa; 3 | 4 | varying lowp float vT; 5 | varying lowp vec4 vColor; 6 | 7 | #define PI 3.14159265359 8 | 9 | void main(void) 10 | { 11 | lowp float tt = abs(aa - 1.0) + aa * cos(vT * PI); 12 | 13 | gl_FragColor = vColor * qt_Opacity * tt; 14 | } 15 | -------------------------------------------------------------------------------- /shaders/LineNode.vsh: -------------------------------------------------------------------------------- 1 | #version 120 2 | 3 | // per-vertex givens 4 | attribute highp vec4 pos; // x, y, i, t; x and y are time and value, in real-world units 5 | attribute highp vec4 prevNext; // time and value of the previous datapoint, and the next one 6 | 7 | // constants passed in from LineNode 8 | uniform lowp float height; // total height in pixels 9 | uniform lowp float lineWidth; // in pixels 10 | uniform lowp float warningBelowMinimum; // change to warningMinColor if datapoint > warningBelowMinimum 11 | uniform lowp float warningAboveMaximum; // change to warningMaxColor if datapoint > warningAboveMaximum 12 | uniform lowp float fillDirection; // -1 to fill above, +1 to fill below, 0 to stroke 13 | uniform lowp vec4 normalColor; // color to stroke or fill, if data is within range 14 | uniform lowp vec4 warningMinColor; 15 | uniform lowp vec4 warningMaxColor; 16 | uniform highp mat2 dataScalingTransform;// transform to go from real-world units to pixels 17 | uniform highp vec2 dataOffset; // offset in pixels, to locate the y axis and to put the latest sample at the right 18 | uniform highp mat4 qt_Matrix; // Qt's usual transform to go from Item to Window coordinates 19 | 20 | // variables which will be passed through to the frag shader, and automatically interpolated between vertices 21 | varying lowp float vT; // distance: 0 is on the line; goes to +/-0.5 outwards across the stroke 22 | varying lowp vec4 vColor; // color of the vertex 23 | 24 | void main(void) 25 | { 26 | float i = pos.z; // vertex index from 0 to 3 (there are 4 vertices per datapoint) 27 | float t = pos.w; // -1 for even (lower) vertices, +1 for odd (upper) vertices (redundant, since we could calculate it from i) 28 | vec2 posPx = dataScalingTransform * pos.xy; // the data point, transformed to pixel units, in 2D (we don't need 3D) 29 | float oddMult = mod(i, 2.0); // will be 1 if i is odd, 0 if it's even 30 | float evenMult = mod(i + 1.0, 2.0); // will be 0 if i is odd, 1 if it's even 31 | vec2 offset = vec2(0.); // how much we will shift the vertex from its on-line position 32 | 33 | if (fillDirection == 0.) { 34 | // we are stroking: need to calculate the miter or knee 35 | vec2 prev = dataScalingTransform * prevNext.xy; 36 | vec2 next = dataScalingTransform * prevNext.zw; 37 | vec2 lineToward = normalize(posPx - prev); 38 | vec2 lineAway = normalize(next - posPx); 39 | vec2 normal = vec2(lineAway.y, -lineAway.x); 40 | vec2 averageTangent = (prev == posPx) ? lineAway : normalize(lineToward + lineAway); 41 | vec2 miter = normalize(vec2(-averageTangent.y, averageTangent.x)); 42 | float halfLineWidth = lineWidth / 2.0; 43 | float miterLength = halfLineWidth / dot(normal, miter); 44 | 45 | if (dot(lineToward, lineAway) >= 0) { 46 | // angle is right or obtuse: OK to use ordinary miter 47 | offset = -t * miterLength * miter; 48 | } else { 49 | // angle is acute: make a beveled miter to avoid overshooting too far 50 | vec2 upToCap = miter * halfLineWidth * t; 51 | vec2 capDeviation = averageTangent * halfLineWidth; 52 | float innerMiterLimit = max(-lineWidth, min(posPx.x - prev.x, lineWidth)) * 8.; 53 | miterLength = max(-innerMiterLimit, min(miterLength, innerMiterLimit)); 54 | if (lineToward.y > 0) { 55 | capDeviation *= sign(i - 2.0) * mod(i - 2.0, 2.0); // lower knee 56 | offset = oddMult * (upToCap + capDeviation) + evenMult * (-t * miterLength * miter); 57 | } else { 58 | capDeviation *= sign(i - 1.0) * mod(i - 1.0, 2.0); // upper knee 59 | offset = evenMult * (upToCap + capDeviation) + oddMult * (-t * miterLength * miter); 60 | } 61 | } 62 | } 63 | 64 | posPx += dataOffset; 65 | 66 | if (fillDirection != 0.) { 67 | // we are filling space above or below the line 68 | // odd vertices stay in place, while even vertices are moved to the top or bottom 69 | posPx.y *= oddMult; 70 | if (fillDirection > 0) 71 | offset.y = evenMult * height * fillDirection; 72 | } 73 | 74 | // apply the calculated offset to the vertex, and then apply Qt's usual 75 | // transform to go from Item coordinates to Window coordinates (and in 3D) 76 | gl_Position = qt_Matrix * vec4(posPx + offset, 0, 1.0); 77 | 78 | vT = t * 0.5; 79 | vColor = pos.y > warningAboveMaximum ? warningMaxColor : 80 | pos.y < warningBelowMinimum ? warningMinColor : normalColor; 81 | } 82 | --------------------------------------------------------------------------------