Files
pyGUI/src/pygui/ISC/Tabs/DataTab.qml
T

911 lines
39 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Controls.impl
import QtQuick.Layouts
import ISC
import ISC.Wafer
// ===== Data Tab (Compare Content Only) =====
// Per docs/design/navigation-mock.html: the Data tab shows only the Compare
// Runs content — the raw CSV table ("Inspect Table" mode in earlier drafts)
// is intentionally not displayed in the application.
// Layout: left column = aligned-curves chart with alignment scrubber + wafer
// overlap map; right side panel = run selection dropdowns, overlap settings,
// and DTW readout cards.
Item {
id: root
anchors.fill: parent
// Properties for DTW Comparison
property int activeBox: 1 // 1 for Reference (Run A), 2 for Session (Run B), 0 for inactive
property string compareFileA: ""
property string compareFileB: ""
property real warpingDistance: -1
property int frameOffset: 0
property real maxSensorDeviation: -1
property bool comparing: false
property var trendDataA: []
property var trendDataB: []
property string errorMessage: ""
// Wafer overlap properties
property var compareSensorLayout: []
property var compareSensorDiff: []
property string compareWaferShape: "round"
property real compareWaferSize: 300.0
readonly property bool hasSeries: trendDataA.length > 1 && trendDataB.length > 1
readonly property int seriesLen: Math.max(trendDataA.length, trendDataB.length)
function diffBands(diffs) {
return diffs.map(d => d > 1.0 ? "high" : d < -1.0 ? "low" : "in_range")
}
function shortName(path) {
return path ? path.split("/").pop() : ""
}
function clearResults() {
root.warpingDistance = -1
root.frameOffset = 0
root.maxSensorDeviation = -1
root.trendDataA = []
root.trendDataB = []
root.compareSensorLayout = []
root.compareSensorDiff = []
root.errorMessage = ""
}
function resetComparison() {
root.compareFileA = ""
root.compareFileB = ""
root.comparing = false
root.activeBox = 1
clearResults()
}
Component.onCompleted: file_browser.refreshFiles()
// Clicking a file in the left rail's browser populates the armed run slot
Connections {
target: streamController
function onLoadedFileChanged() {
if (streamController.loadedFile && root.activeBox > 0) {
if (root.activeBox === 1) {
root.compareFileA = streamController.loadedFile
root.activeBox = 2 // Auto-advance to box 2
} else if (root.activeBox === 2) {
root.compareFileB = streamController.loadedFile
root.activeBox = 0 // Finished selecting
}
}
}
function onComparisonResult(result) {
root.comparing = false
if (result && result.success) {
root.warpingDistance = result.distance
root.frameOffset = result.frame_offset !== undefined ? result.frame_offset : 0
root.maxSensorDeviation = result.max_sensor_deviation !== undefined ? result.max_sensor_deviation : -1
root.trendDataA = result.series_a || []
root.trendDataB = result.series_b || []
root.compareSensorLayout = result.sensor_layout || []
root.compareSensorDiff = result.sensor_diff || []
root.compareWaferShape = result.wafer_shape || "round"
root.compareWaferSize = result.wafer_size || 300.0
root.errorMessage = ""
scrubber.value = Math.floor(root.seriesLen / 2)
} else {
root.clearResults()
root.errorMessage = (result && result.error) ? result.error : "Comparison failed"
}
}
}
// ── Reusable pieces ────────────────────────────────────────────
component SectionTitle: Label {
font.pixelSize: Theme.fontXs
font.bold: true
font.letterSpacing: 1.2
color: Theme.sideMutedText
}
component PanelBox: Rectangle {
color: Theme.cardBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusMd
}
// Mini stat box under the alignment scrubber (mockup's subtle-box trio)
component ScrubStat: Rectangle {
id: scrubStat
property string label
property string value
property color valueColor: Theme.headingColor
Layout.fillWidth: true
implicitHeight: 38
radius: 4
color: Theme.cardWash
border.color: Theme.cardBorder
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.leftMargin: 6
anchors.rightMargin: 6
anchors.topMargin: 4
anchors.bottomMargin: 4
spacing: 0
Label {
text: scrubStat.label
font.pixelSize: 9
color: Theme.sideMutedText
}
Label {
text: scrubStat.value
font.pixelSize: Theme.fontSm
font.bold: true
font.family: Theme.codeFontFamily
color: scrubStat.valueColor
}
}
}
// Large labeled value card for the DTW readout panel
component ReadoutCard: Rectangle {
id: readoutCard
property string label
property string value
property color valueColor: Theme.headingColor
Layout.fillWidth: true
implicitHeight: 52
radius: Theme.radiusSm
color: Theme.subtleSectionBackground
border.color: Theme.cardBorder
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 2
Label {
text: readoutCard.label
font.pixelSize: 9
font.bold: true
font.letterSpacing: 0.2
color: Theme.sideMutedText
}
Label {
text: readoutCard.value
font.pixelSize: Theme.fontLg
font.bold: true
font.family: Theme.codeFontFamily
color: readoutCard.valueColor
}
}
}
// Run slot card: click to arm, then pick a file in the left rail's browser
component RunSlot: Rectangle {
id: slotBox
property int boxIndex: 1
property string title
property string filePath: ""
property color accent: Theme.primaryAccent
signal cleared()
Layout.fillWidth: true
implicitHeight: 58
radius: Theme.radiusSm
color: Theme.fieldBackground
border.width: root.activeBox === boxIndex ? 2 : 1
border.color: root.activeBox === boxIndex ? accent : Theme.cardBorder
Behavior on border.color { ColorAnimation { duration: Theme.durationFast } }
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: root.activeBox = slotBox.boxIndex
}
RowLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
Rectangle {
width: 8; height: 8; radius: 4
color: slotBox.accent
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Label {
text: slotBox.title
font.pixelSize: 9
font.bold: true
font.letterSpacing: 1.0
color: root.activeBox === slotBox.boxIndex ? slotBox.accent : Theme.sideMutedText
}
Label {
text: slotBox.filePath !== ""
? root.shortName(slotBox.filePath)
: (root.activeBox === slotBox.boxIndex ? "<- Select file from left panel..." : "Click to select")
font.pixelSize: Theme.fontXs
color: slotBox.filePath !== "" ? Theme.headingColor : Theme.bodyColor
opacity: slotBox.filePath !== "" ? 1.0 : 0.6
elide: Text.ElideMiddle
Layout.fillWidth: true
}
}
Button {
visible: slotBox.filePath !== ""
flat: true
implicitWidth: 24; implicitHeight: 24
onClicked: slotBox.cleared()
background: Rectangle {
radius: Theme.radiusSm
color: parent.hovered ? Theme.flatButtonHover : "transparent"
}
contentItem: IconImage {
source: "icons/x.svg"
width: 14; height: 14
sourceSize.width: 14
sourceSize.height: 14
color: Theme.bodyColor
anchors.centerIn: parent
}
}
}
}
// ── Main Layout ───────────────────────────────────────────────
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.panelPadding
spacing: Theme.rightPaneGap
// ── Title / Header ─────────────────────────────────────────
RowLayout {
Layout.fillWidth: true
spacing: 8
Label {
text: "Run Comparison (DTW)"
font.pixelSize: Theme.fontXl
font.bold: true
color: Theme.headingColor
Layout.fillWidth: true
elide: Text.ElideRight
}
Button {
id: resetBtn
flat: true
implicitWidth: 32
implicitHeight: 32
visible: root.compareFileA !== "" || root.compareFileB !== ""
onClicked: root.resetComparison()
background: Rectangle {
radius: Theme.radiusSm
color: resetBtn.hovered ? Theme.flatButtonHover : "transparent"
}
contentItem: IconImage {
source: "icons/rotate-ccw.svg"
width: 18; height: 18
sourceSize.width: 18
sourceSize.height: 18
color: Theme.bodyColor
anchors.centerIn: parent
}
ToolTip.visible: resetBtn.hovered
ToolTip.text: "Reset comparison"
}
}
// ── Error Banner ───────────────────────────────────────────
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: errorRow.implicitHeight + 16
visible: root.errorMessage !== ""
color: Theme.errorSurface
border.color: Theme.statusErrorColor
border.width: 1
radius: Theme.radiusSm
RowLayout {
id: errorRow
anchors.fill: parent
anchors.margins: 8
spacing: 8
IconImage {
source: "icons/triangle-alert.svg"
width: 16; height: 16
sourceSize.width: 16
sourceSize.height: 16
color: Theme.errorTextSoft
Layout.alignment: Qt.AlignTop
}
Label {
id: errorLabel
text: root.errorMessage
color: Theme.errorTextSoft
font.pixelSize: Theme.fontSm
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}
// ── Body: main column | side panel ─────────────────────────
RowLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 16
// --- Left Column: chart + scrubber, wafer overlap map ---
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumWidth: 350
spacing: 12
// Chart card with alignment scrubber
PanelBox {
Layout.fillWidth: true
Layout.preferredHeight: chartCol.implicitHeight + 24
ColumnLayout {
id: chartCol
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
SectionTitle {
text: "ALIGNED TEMP CURVE OVERLAY (DTW)"
Layout.fillWidth: true
}
Row {
spacing: 16
visible: root.hasSeries
Row {
spacing: 4
Rectangle { width: 12; height: 3; radius: 1.5; color: Theme.primaryAccent; anchors.verticalCenter: parent.verticalCenter }
Label { text: "Run A"; font.pixelSize: Theme.fontXs; color: Theme.bodyColor }
}
Row {
spacing: 4
Rectangle { width: 12; height: 3; radius: 1.5; color: Theme.themeSkill; anchors.verticalCenter: parent.verticalCenter }
Label { text: "Run B"; font.pixelSize: Theme.fontXs; color: Theme.bodyColor }
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 200
color: Theme.fieldBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
clip: true
Canvas {
id: overlayCanvas
anchors.fill: parent
visible: root.hasSeries
readonly property real padLeft: 50
readonly property real padRight: 16
readonly property real padTop: 16
readonly property real padBottom: 28
function drawGrid(ctx, minV, maxV, dataLen) {
var plotW = width - padLeft - padRight
var plotH = height - padTop - padBottom
var range = (maxV - minV) || 1
ctx.strokeStyle = Theme.chartGridLine
ctx.lineWidth = 1
ctx.fillStyle = Theme.chartAxisText
ctx.font = "10px sans-serif"
ctx.textAlign = "right"
var ySteps = 5
for (var yi = 0; yi <= ySteps; yi++) {
var yFrac = yi / ySteps
var yPx = padTop + plotH * yFrac
var yVal = maxV - yFrac * range
ctx.beginPath()
ctx.moveTo(padLeft, yPx)
ctx.lineTo(padLeft + plotW, yPx)
ctx.stroke()
ctx.fillText(yVal.toFixed(1), padLeft - 6, yPx + 3)
}
ctx.textAlign = "center"
var xSteps = Math.min(5, dataLen - 1)
for (var xi = 0; xi <= xSteps; xi++) {
var xFrac = xi / xSteps
var xPx = padLeft + plotW * xFrac
var xIdx = Math.round(xFrac * (dataLen - 1))
ctx.fillText(xIdx.toString(), xPx, height - 6)
}
ctx.save()
ctx.translate(12, padTop + plotH / 2)
ctx.rotate(-Math.PI / 2)
ctx.textAlign = "center"
ctx.fillText("°C", 0, 0)
ctx.restore()
ctx.textAlign = "center"
ctx.fillText("Frame", padLeft + plotW / 2, height - 2)
}
function drawSeries(ctx, series, minV, range, color) {
var plotW = width - padLeft - padRight
var plotH = height - padTop - padBottom
ctx.strokeStyle = color
ctx.lineWidth = 2
ctx.beginPath()
for (var i = 0; i < series.length; i++) {
var x = padLeft + (i / (series.length - 1)) * plotW
var y = padTop + plotH - ((series[i] - minV) / range) * plotH
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
}
function drawScrubMarker(ctx) {
if (root.seriesLen < 2) return
var plotW = width - padLeft - padRight
var x = padLeft + (scrubber.value / (root.seriesLen - 1)) * plotW
ctx.strokeStyle = Theme.primaryAccent
ctx.lineWidth = 1.5
ctx.setLineDash([3, 3])
ctx.beginPath()
ctx.moveTo(x, padTop)
ctx.lineTo(x, height - padBottom)
ctx.stroke()
ctx.setLineDash([])
}
onPaint: {
var ctx = getContext("2d")
ctx.reset()
if (!root.hasSeries) return
var all = root.trendDataA.concat(root.trendDataB)
var minV = Math.min.apply(null, all)
var maxV = Math.max.apply(null, all)
var padding = (maxV - minV) * 0.05
minV -= padding
maxV += padding
var range = (maxV - minV) || 1
drawGrid(ctx, minV, maxV, root.seriesLen)
drawSeries(ctx, root.trendDataA, minV, range, Theme.primaryAccent)
drawSeries(ctx, root.trendDataB, minV, range, Theme.themeSkill)
drawScrubMarker(ctx)
}
Connections {
target: root
function onTrendDataAChanged() { overlayCanvas.requestPaint() }
function onTrendDataBChanged() { overlayCanvas.requestPaint() }
}
Connections {
target: scrubber
function onValueChanged() { overlayCanvas.requestPaint() }
}
}
// Empty state
ColumnLayout {
anchors.centerIn: parent
visible: !root.hasSeries
spacing: 8
IconImage {
Layout.alignment: Qt.AlignHCenter
visible: !root.comparing
source: "icons/bar-chart.svg"
width: 32; height: 32
sourceSize.width: 32
sourceSize.height: 32
color: Theme.sideMutedText
}
Label {
Layout.alignment: Qt.AlignHCenter
text: root.comparing ? "Running DTW comparison…" : "Select two runs and run comparison"
color: Theme.bodyColor
font.pixelSize: Theme.fontSm
}
Label {
Layout.alignment: Qt.AlignHCenter
visible: !root.comparing
text: "Click a run box on the right, then select a file from the left panel."
color: Theme.sideMutedText
font.pixelSize: Theme.fontXs
}
}
}
// ── Alignment Timeline Scrubber ────────────
ColumnLayout {
Layout.fillWidth: true
spacing: 4
visible: root.hasSeries
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
RowLayout {
Layout.fillWidth: true
SectionTitle {
text: "ALIGNMENT TIMELINE SCRUBBER"
Layout.fillWidth: true
}
Label {
text: "Frame " + Math.round(scrubber.value) + " / " + Math.max(0, root.seriesLen - 1)
font.pixelSize: Theme.fontXs
font.bold: true
font.family: Theme.codeFontFamily
color: Theme.headingColor
}
}
Slider {
id: scrubber
Layout.fillWidth: true
from: 0
to: Math.max(1, root.seriesLen - 1)
stepSize: 1
value: 0
}
RowLayout {
id: scrubReadout
Layout.fillWidth: true
spacing: 8
readonly property int scrubIdx: Math.round(scrubber.value)
readonly property real tempA: root.trendDataA.length > 0
? root.trendDataA[Math.min(scrubIdx, root.trendDataA.length - 1)] : 0
readonly property real tempB: root.trendDataB.length > 0
? root.trendDataB[Math.min(scrubIdx, root.trendDataB.length - 1)] : 0
ScrubStat {
label: "Temp (Run A)"
value: scrubReadout.tempA.toFixed(1) + "°C"
valueColor: Theme.primaryAccent
}
ScrubStat {
label: "Temp (Run B)"
value: scrubReadout.tempB.toFixed(1) + "°C"
valueColor: Theme.themeSkill
}
ScrubStat {
label: "Difference"
value: Math.abs(scrubReadout.tempA - scrubReadout.tempB).toFixed(1) + "°C"
}
}
}
}
}
// Wafer Map Overlap View card
PanelBox {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumHeight: 220
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 8
RowLayout {
Layout.fillWidth: true
SectionTitle {
text: "WAFER MAP OVERLAP VIEW"
Layout.fillWidth: true
}
Label {
visible: root.compareFileA !== "" && root.compareFileB !== ""
text: (root.shortName(root.compareFileA).replace(".csv", "") + " vs "
+ root.shortName(root.compareFileB).replace(".csv", "")).toUpperCase()
font.pixelSize: Theme.fontXs
color: Theme.sideMutedText
elide: Text.ElideMiddle
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Theme.fieldBackground
border.color: Theme.cardBorder
border.width: 1
radius: Theme.radiusSm
clip: true
WaferMapItem {
id: overlapMapItem
anchors.fill: parent
visible: root.compareSensorLayout.length > 0 && enableOverlapCheck.checked
sensors: root.compareSensorLayout
values: root.compareSensorDiff
bands: root.diffBands(root.compareSensorDiff)
shape: root.compareWaferShape
size: root.compareWaferSize
target: 0
margin: 1.0
blend: blendSlider.value / 100
showLabels: true
ringColor: Theme.waferRingColor
axisColor: Theme.waferAxisColor
lowColor: Theme.sensorLow
inRangeColor: Theme.sensorInRange
highColor: Theme.sensorHigh
textColor: Theme.headingColor
}
ColumnLayout {
anchors.centerIn: parent
visible: root.compareSensorLayout.length === 0 || !enableOverlapCheck.checked
spacing: 4
IconImage {
Layout.alignment: Qt.AlignHCenter
source: "icons/map.svg"
width: 24; height: 24
sourceSize.width: 24
sourceSize.height: 24
color: Theme.sideMutedText
}
Label {
Layout.alignment: Qt.AlignHCenter
text: !enableOverlapCheck.checked ? "Overlap map disabled"
: "No sensor map available"
color: Theme.bodyColor
font.pixelSize: Theme.fontXs
}
}
}
}
}
}
// --- Right Column: run selection, overlap settings, readout ---
ColumnLayout {
Layout.fillWidth: false
Layout.preferredWidth: 280
Layout.minimumWidth: 280
Layout.maximumWidth: 280
Layout.fillHeight: true
spacing: 12
// Bento 1: Run Selection
PanelBox {
Layout.fillWidth: true
Layout.preferredHeight: runSelCol.implicitHeight + 24
ColumnLayout {
id: runSelCol
anchors.fill: parent
anchors.margins: 12
spacing: 10
SectionTitle { text: "RUN SELECTION" }
RunSlot {
title: "REFERENCE (RUN A)"
boxIndex: 1
accent: Theme.primaryAccent
filePath: root.compareFileA
onCleared: {
root.compareFileA = ""
root.activeBox = 1
root.clearResults()
}
}
RunSlot {
title: "SESSION (RUN B)"
boxIndex: 2
accent: Theme.themeSkill
filePath: root.compareFileB
onCleared: {
root.compareFileB = ""
root.activeBox = 2
root.clearResults()
}
}
Button {
id: compareBtn
Layout.fillWidth: true
Layout.preferredHeight: 32
enabled: root.compareFileA !== "" && root.compareFileB !== "" && !root.comparing
onClicked: {
root.comparing = true
root.clearResults()
streamController.compareFiles(root.compareFileA, root.compareFileB)
}
background: Rectangle {
radius: Theme.radiusSm
color: compareBtn.enabled
? (compareBtn.hovered ? Qt.lighter(Theme.primaryAccent, 1.1) : Theme.primaryAccent)
: Theme.buttonNeutralBackground
Behavior on color { ColorAnimation { duration: Theme.durationFast } }
}
contentItem: Row {
spacing: 8
anchors.centerIn: parent
Label {
anchors.verticalCenter: parent.verticalCenter
text: compareBtn.enabled
? (root.comparing ? "Comparing…" : "Run DTW Comparison")
: (root.comparing ? "Comparing…" : "Select both runs")
color: compareBtn.enabled ? Theme.tone100 : Theme.sideMutedText
font.pixelSize: Theme.fontXs
font.bold: true
}
BusyIndicator {
running: root.comparing
visible: root.comparing
width: 14; height: 14
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
// Bento 2: Overlap Map Settings
PanelBox {
Layout.fillWidth: true
Layout.preferredHeight: overlapCol.implicitHeight + 24
ColumnLayout {
id: overlapCol
anchors.fill: parent
anchors.margins: 12
spacing: 8
SectionTitle { text: "OVERLAP MAP SETTINGS" }
CheckBox {
id: enableOverlapCheck
Layout.fillWidth: true
checked: true
text: "Enable Overlap Map"
font.pixelSize: Theme.fontXs
contentItem: Label {
leftPadding: enableOverlapCheck.indicator.width + 6
text: enableOverlapCheck.text
font.pixelSize: Theme.fontXs
color: Theme.headingColor
verticalAlignment: Text.AlignVCenter
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 6
visible: enableOverlapCheck.checked
Rectangle {
Layout.fillWidth: true
height: 1
color: Theme.cardBorder
}
RowLayout {
Layout.fillWidth: true
spacing: 6
Label {
text: "Blend"
font.pixelSize: Theme.fontXs
color: Theme.bodyColor
Layout.preferredWidth: 40
}
Slider {
id: blendSlider
Layout.fillWidth: true
from: 0; to: 100; value: 50
stepSize: 1
}
Label {
text: Math.round(blendSlider.value) + "%"
font.pixelSize: Theme.fontXs
color: Theme.sideMutedText
Layout.preferredWidth: 30
}
}
// Diff color legend
Row {
Layout.fillWidth: true
spacing: 10
Row {
spacing: 3
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorLow; anchors.verticalCenter: parent.verticalCenter }
Label { text: "B cooler"; font.pixelSize: 9; color: Theme.sideMutedText }
}
Row {
spacing: 3
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorInRange; anchors.verticalCenter: parent.verticalCenter }
Label { text: "Match"; font.pixelSize: 9; color: Theme.sideMutedText }
}
Row {
spacing: 3
Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorHigh; anchors.verticalCenter: parent.verticalCenter }
Label { text: "B hotter"; font.pixelSize: 9; color: Theme.sideMutedText }
}
}
}
}
}
// Bento 3: DTW Comparison Readout
PanelBox {
Layout.fillWidth: true
Layout.preferredHeight: readoutCol.implicitHeight + 24
ColumnLayout {
id: readoutCol
anchors.fill: parent
anchors.margins: 12
spacing: 8
SectionTitle { text: "DTW COMPARISON READOUT" }
ReadoutCard {
label: "DTW ALIGNMENT DISTANCE"
value: root.warpingDistance >= 0 ? root.warpingDistance.toFixed(2) : "--"
valueColor: root.warpingDistance >= 0
? (root.warpingDistance < 50 ? Theme.metricGood : root.warpingDistance < 100 ? Theme.metricWarn : Theme.metricBad)
: Theme.sideMutedText
}
ReadoutCard {
label: "TEMPORAL FRAME OFFSET"
value: root.warpingDistance >= 0
? ((root.frameOffset >= 0 ? "+" : "") + root.frameOffset + " frames")
: "--"
valueColor: root.warpingDistance >= 0 ? Theme.headingColor : Theme.sideMutedText
}
ReadoutCard {
label: "MAX SENSOR DEVIATION"
value: root.maxSensorDeviation >= 0 ? root.maxSensorDeviation.toFixed(2) + "°C" : "--"
valueColor: root.maxSensorDeviation >= 0
? (root.maxSensorDeviation < 1.0 ? Theme.metricGood : root.maxSensorDeviation < 3.0 ? Theme.metricWarn : Theme.metricBad)
: Theme.sideMutedText
}
}
}
Item { Layout.fillHeight: true }
}
}
}
}