diff --git a/src/pygui/ISC/Tabs/DataTab.qml b/src/pygui/ISC/Tabs/DataTab.qml index 2b83b08..b489931 100644 --- a/src/pygui/ISC/Tabs/DataTab.qml +++ b/src/pygui/ISC/Tabs/DataTab.qml @@ -4,19 +4,22 @@ import QtQuick.Layouts import ISC import ISC.Wafer -// ===== Data Tab (Compare Mode Only) ===== -// Focuses exclusively on comparing two wafer runs side-by-side using DTW. -// Clicking Box 1 or Box 2 activates selection mode, and clicking a file in the -// left rail's file browser populates that slot. +// ===== 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: [] @@ -29,43 +32,57 @@ Item { 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 resetComparison() { - root.compareFileA = "" - root.compareFileB = "" + function shortName(path) { + return path ? path.split("/").pop() : "" + } + + // Badge colors per wafer type, matching the mockup's file-row chips + function badgeFg(t) { + return t === "Z" ? "#a78bfa" : t === "B" ? "#93c5fd" : t === "A" ? "#6ee7b7" : "#fde047" + } + function badgeBg(t) { + return t === "Z" ? Qt.rgba(139 / 255, 92 / 255, 246 / 255, 0.15) + : t === "B" ? Qt.rgba(59 / 255, 130 / 255, 246 / 255, 0.15) + : t === "A" ? Qt.rgba(16 / 255, 185 / 255, 129 / 255, 0.15) + : Qt.rgba(245 / 255, 158 / 255, 11 / 255, 0.15) + } + + function clearResults() { root.warpingDistance = -1 + root.frameOffset = 0 root.maxSensorDeviation = -1 - root.comparing = false root.trendDataA = [] root.trendDataB = [] root.compareSensorLayout = [] root.compareSensorDiff = [] root.errorMessage = "" - root.activeBox = 1 } - // Connections to handle clicked/selected files on left rail file browser + function resetComparison() { + root.compareFileA = "" + root.compareFileB = "" + root.comparing = false + runACombo.currentIndex = -1 + runBCombo.currentIndex = -1 + clearResults() + } + + Component.onCompleted: file_browser.refreshFiles() + 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 || [] @@ -74,18 +91,220 @@ Item { root.compareWaferShape = result.wafer_shape || "round" root.compareWaferSize = result.wafer_size || 300.0 root.errorMessage = "" + scrubber.value = Math.floor(root.seriesLen / 2) } else { - root.warpingDistance = -1 - root.maxSensorDeviation = -1 - root.trendDataA = [] - root.trendDataB = [] - root.compareSensorLayout = [] - root.compareSensorDiff = [] + 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: Qt.rgba(1, 1, 1, 0.02) + 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 selector dropdown fed by the left-rail file browser model + component RunSelector: ComboBox { + id: selector + property string placeholder: "Select CSV…" + + Layout.fillWidth: true + implicitHeight: 36 + model: file_browser.files + currentIndex: -1 + + readonly property var currentFile: (currentIndex >= 0 && currentIndex < file_browser.files.length) + ? file_browser.files[currentIndex] : null + + background: Rectangle { + radius: Theme.radiusXs + color: Theme.fieldBackground + border.color: selector.popup.visible ? Theme.fieldBorderFocus : Theme.fieldBorder + border.width: 1 + } + + contentItem: RowLayout { + spacing: 8 + + Rectangle { + Layout.leftMargin: 8 + width: 16; height: 16; radius: 4 + visible: selector.currentFile !== null + color: root.badgeBg(selector.currentFile ? selector.currentFile.waferType : "") + Label { + anchors.centerIn: parent + text: selector.currentFile ? selector.currentFile.waferType : "" + font.pixelSize: 9 + font.bold: true + color: root.badgeFg(selector.currentFile ? selector.currentFile.waferType : "") + } + } + Label { + Layout.fillWidth: true + Layout.leftMargin: selector.currentFile === null ? 8 : 0 + text: selector.currentFile ? selector.currentFile.baseName : selector.placeholder + font.pixelSize: Theme.fontSm + font.weight: selector.currentFile ? Font.Medium : Font.Normal + color: selector.currentFile ? Theme.fieldText : Theme.fieldPlaceholder + elide: Text.ElideMiddle + } + } + + indicator: Label { + x: selector.width - width - 8 + y: (selector.height - height) / 2 + text: "▾" + font.pixelSize: Theme.fontSm + color: Theme.sideMutedText + } + + delegate: ItemDelegate { + id: fileDelegate + width: selector.width + highlighted: selector.highlightedIndex === index + + background: Rectangle { + radius: Theme.radiusXs + color: fileDelegate.highlighted ? Theme.buttonNeutralHover : "transparent" + } + contentItem: RowLayout { + spacing: 8 + Rectangle { + width: 20; height: 20; radius: 4 + color: root.badgeBg(modelData.waferType) + Label { + anchors.centerIn: parent + text: modelData.waferType + font.pixelSize: Theme.fontXs + font.bold: true + color: root.badgeFg(modelData.waferType) + } + } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + Label { + Layout.fillWidth: true + text: modelData.baseName + font.pixelSize: Theme.fontSm + font.weight: Font.Medium + color: Theme.headingColor + elide: Text.ElideMiddle + } + Label { + Layout.fillWidth: true + text: modelData.date || modelData.chamber || "" + visible: text !== "" + font.pixelSize: 9 + color: Theme.sideMutedText + elide: Text.ElideRight + } + } + } + } + + popup: Popup { + y: selector.height + 4 + width: selector.width + implicitHeight: Math.min(contentItem.implicitHeight + 8, 220) + padding: 4 + + background: Rectangle { + radius: Theme.radiusXs + color: Theme.cardBackground + border.color: Theme.cardBorder + border.width: 1 + } + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: selector.popup.visible ? selector.delegateModel : null + currentIndex: selector.highlightedIndex + ScrollIndicator.vertical: ScrollIndicator {} + } + } + } + // ── Main Layout ─────────────────────────────────────────────── ColumnLayout { anchors.fill: parent @@ -106,7 +325,6 @@ Item { elide: Text.ElideRight } - // Reset button Button { id: resetBtn flat: true @@ -119,15 +337,13 @@ Item { radius: Theme.radiusSm color: resetBtn.hovered ? Qt.rgba(1, 1, 1, 0.08) : "transparent" } - contentItem: Label { - text: "\u21BA" + text: "↺" font.pixelSize: Theme.fontLg color: Theme.bodyColor horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter } - ToolTip.visible: resetBtn.hovered ToolTip.text: "Reset comparison" } @@ -139,219 +355,6 @@ Item { } } - // ── Selection Boxes (Box 1 and Box 2) ────────────────────── - RowLayout { - Layout.fillWidth: true - spacing: 16 - - // --- Box 1: Reference CSV (Run A) --- - Rectangle { - id: box1 - Layout.fillWidth: true - Layout.preferredHeight: 70 - radius: Theme.radiusMd - color: Theme.cardBackground - border.width: root.activeBox === 1 ? 2 : 1 - border.color: root.activeBox === 1 ? Theme.primaryAccent : Theme.cardBorder - - Behavior on border.color { ColorAnimation { duration: Theme.durationFast } } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: root.activeBox = 1 - } - - RowLayout { - anchors.fill: parent - anchors.margins: 14 - spacing: 10 - - // Color indicator dot - Rectangle { - width: 10; height: 10; radius: 5 - color: Theme.primaryAccent - Layout.alignment: Qt.AlignVCenter - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - Label { - text: "REFERENCE (RUN A)" - font.pixelSize: Theme.fontXs - font.bold: true - font.letterSpacing: 1.0 - color: root.activeBox === 1 ? Theme.primaryAccent : Theme.sideMutedText - } - Label { - text: root.compareFileA !== "" - ? root.compareFileA.split("/").pop() - : (root.activeBox === 1 ? "← Select file from left panel..." : "Click to select") - font.pixelSize: Theme.fontSm - color: root.compareFileA !== "" ? Theme.headingColor : Theme.bodyColor - opacity: root.compareFileA !== "" ? 1.0 : 0.6 - elide: Text.ElideMiddle - Layout.fillWidth: true - } - } - - Button { - text: "✕" - visible: root.compareFileA !== "" - flat: true - implicitWidth: 24; implicitHeight: 24 - onClicked: { - root.compareFileA = "" - root.activeBox = 1 - root.warpingDistance = -1 - root.trendDataA = [] - root.trendDataB = [] - root.compareSensorLayout = [] - root.compareSensorDiff = [] - } - background: Rectangle { - radius: Theme.radiusSm - color: parent.hovered ? Qt.rgba(1, 1, 1, 0.08) : "transparent" - } - contentItem: Label { - text: "✕" - color: Theme.bodyColor - font.pixelSize: Theme.fontSm - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - } - } - } - - // --- Box 2: Session CSV (Run B) --- - Rectangle { - id: box2 - Layout.fillWidth: true - Layout.preferredHeight: 70 - radius: Theme.radiusMd - color: Theme.cardBackground - border.width: root.activeBox === 2 ? 2 : 1 - border.color: root.activeBox === 2 ? Theme.themeSkill : Theme.cardBorder - - Behavior on border.color { ColorAnimation { duration: Theme.durationFast } } - - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: root.activeBox = 2 - } - - RowLayout { - anchors.fill: parent - anchors.margins: 14 - spacing: 10 - - // Color indicator dot - Rectangle { - width: 10; height: 10; radius: 5 - color: Theme.themeSkill - Layout.alignment: Qt.AlignVCenter - } - - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - Label { - text: "SESSION (RUN B)" - font.pixelSize: Theme.fontXs - font.bold: true - font.letterSpacing: 1.0 - color: root.activeBox === 2 ? Theme.themeSkill : Theme.sideMutedText - } - Label { - text: root.compareFileB !== "" - ? root.compareFileB.split("/").pop() - : (root.activeBox === 2 ? "← Select file from left panel..." : "Click to select") - font.pixelSize: Theme.fontSm - color: root.compareFileB !== "" ? Theme.headingColor : Theme.bodyColor - opacity: root.compareFileB !== "" ? 1.0 : 0.6 - elide: Text.ElideMiddle - Layout.fillWidth: true - } - } - - Button { - text: "✕" - visible: root.compareFileB !== "" - flat: true - implicitWidth: 24; implicitHeight: 24 - onClicked: { - root.compareFileB = "" - root.activeBox = 2 - root.warpingDistance = -1 - root.trendDataA = [] - root.trendDataB = [] - root.compareSensorLayout = [] - root.compareSensorDiff = [] - } - background: Rectangle { - radius: Theme.radiusSm - color: parent.hovered ? Qt.rgba(1, 1, 1, 0.08) : "transparent" - } - contentItem: Label { - text: "✕" - color: Theme.bodyColor - font.pixelSize: Theme.fontSm - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - } - } - } - } - } - - // ── Compare Button ───────────────────────────────────────── - Button { - id: compareBtn - Layout.fillWidth: true - Layout.preferredHeight: 40 - enabled: root.compareFileA !== "" && root.compareFileB !== "" && !root.comparing - onClicked: { - root.comparing = true - root.warpingDistance = -1 - root.maxSensorDeviation = -1 - root.trendDataA = [] - root.trendDataB = [] - root.errorMessage = "" - 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") - : "SELECT BOTH RUNS ABOVE" - color: compareBtn.enabled ? Theme.tone100 : Theme.sideMutedText - font.pixelSize: Theme.fontSm - font.bold: true - font.letterSpacing: 0.8 - } - BusyIndicator { - running: root.comparing - visible: root.comparing - width: 16; height: 16 - anchors.verticalCenter: parent.verticalCenter - } - } - } - // ── Error Banner ─────────────────────────────────────────── Rectangle { Layout.fillWidth: true @@ -373,320 +376,273 @@ Item { } } - // ── DTW Chart + Overlap Wafer Map Row ────────────────────── + // ── Body: main column | side panel ───────────────────────── RowLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: 16 - // --- Left Column: Aligned Curves Chart --- - Rectangle { + // --- Left Column: chart + scrubber, wafer overlap map --- + ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true Layout.minimumWidth: 350 - color: Theme.cardBackground - border.color: Theme.cardBorder - border.width: 1 - radius: Theme.radiusMd - - ColumnLayout { - anchors.fill: parent - anchors.margins: 16 - spacing: 10 - - RowLayout { - Layout.fillWidth: true - Label { - text: "Aligned Temperature Curves (DTW)" - font.pixelSize: Theme.fontSm - font.bold: true - color: Theme.headingColor - Layout.fillWidth: true - elide: Text.ElideRight - } - - // Legend - Row { - spacing: 16 - visible: root.trendDataA.length > 1 - - 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.fillHeight: true - color: Theme.fieldBackground - border.color: Theme.cardBorder - border.width: 1 - radius: Theme.radiusSm - clip: true - - readonly property bool hasSeries: root.trendDataA.length > 1 && root.trendDataB.length > 1 - - Canvas { - id: overlayCanvas - anchors.fill: parent - anchors.margins: 0 - visible: parent.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 = Qt.rgba(1, 1, 1, 0.08) - ctx.lineWidth = 1 - ctx.fillStyle = Qt.rgba(1, 1, 1, 0.35) - ctx.font = "10px sans-serif" - ctx.textAlign = "right" - - // Y-axis gridlines + labels - 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) - } - - // X-axis labels - 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) - } - - // Y-axis title - ctx.save() - ctx.translate(12, padTop + plotH / 2) - ctx.rotate(-Math.PI / 2) - ctx.textAlign = "center" - ctx.fillText("°C", 0, 0) - ctx.restore() - - // X-axis title - 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() - } - - onPaint: { - var ctx = getContext("2d") - ctx.reset() - if (!parent.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, Math.max(root.trendDataA.length, root.trendDataB.length)) - drawSeries(ctx, root.trendDataA, minV, range, Theme.primaryAccent) - drawSeries(ctx, root.trendDataB, minV, range, Theme.themeSkill) - } - - Connections { - target: root - function onTrendDataAChanged() { overlayCanvas.requestPaint() } - function onTrendDataBChanged() { overlayCanvas.requestPaint() } - } - } - - // Empty state - ColumnLayout { - anchors.centerIn: parent - visible: !parent.hasSeries - spacing: 8 - - Label { - Layout.alignment: Qt.AlignHCenter - text: root.comparing ? "" : "📊" - font.pixelSize: 32 - } - Label { - Layout.alignment: Qt.AlignHCenter - text: root.comparing ? "Running DTW comparison…" : "Select two files and run comparison" - color: Theme.bodyColor - font.pixelSize: Theme.fontSm - } - Label { - Layout.alignment: Qt.AlignHCenter - visible: !root.comparing - text: "Click Box 1, then a file. Click Box 2, then a file." - color: Theme.sideMutedText - font.pixelSize: Theme.fontXs - } - } - } - } - } - - // --- Right Column: Wafer Map Overlap & Readouts --- - ColumnLayout { - Layout.fillWidth: false - Layout.preferredWidth: 280 - Layout.minimumWidth: 280 - Layout.maximumWidth: 280 - Layout.fillHeight: true spacing: 12 - // ── DTW Readout Cards ───────────────────────────────── - Rectangle { + // Chart card with alignment scrubber + PanelBox { Layout.fillWidth: true - Layout.preferredHeight: dtwReadoutCol.implicitHeight + 24 - color: Theme.cardBackground - border.color: Theme.cardBorder - border.width: 1 - radius: Theme.radiusMd + Layout.preferredHeight: chartCol.implicitHeight + 24 ColumnLayout { - id: dtwReadoutCol + id: chartCol anchors.fill: parent anchors.margins: 12 spacing: 8 - Label { - text: "COMPARISON RESULTS" - font.pixelSize: Theme.fontXs - font.bold: true - font.letterSpacing: 1.2 - color: Theme.sideMutedText + RowLayout { Layout.fillWidth: true - } - - // DTW Distance - Rectangle { - Layout.fillWidth: true - height: 52 - color: Theme.panelBackground - border.color: Theme.cardBorder - border.width: 1 - radius: Theme.radiusSm - - ColumnLayout { - anchors.fill: parent - anchors.margins: 8 - spacing: 2 - Label { - text: "DTW Alignment Distance" - font.pixelSize: Theme.fontXs - color: Theme.bodyColor + 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 } } - Label { - text: root.warpingDistance >= 0 ? root.warpingDistance.toFixed(2) : "--" - font.pixelSize: Theme.fontLg - font.bold: true - color: root.warpingDistance >= 0 - ? (root.warpingDistance < 50 ? "#6ee7b7" : root.warpingDistance < 100 ? "#fde047" : "#ef4444") - : Theme.sideMutedText + 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 } } } } - // Max Sensor Deviation Rectangle { Layout.fillWidth: true - height: 52 - color: Theme.panelBackground + Layout.preferredHeight: 200 + color: Theme.fieldBackground border.color: Theme.cardBorder border.width: 1 radius: Theme.radiusSm + clip: true - ColumnLayout { + Canvas { + id: overlayCanvas anchors.fill: parent - anchors.margins: 8 - spacing: 2 - Label { - text: "Max Sensor Deviation" - font.pixelSize: Theme.fontXs - color: Theme.bodyColor + 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 = Qt.rgba(1, 1, 1, 0.08) + ctx.lineWidth = 1 + ctx.fillStyle = Qt.rgba(1, 1, 1, 0.35) + 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) } - Label { - text: root.maxSensorDeviation >= 0 ? root.maxSensorDeviation.toFixed(2) + "\u00B0C" : "--" - font.pixelSize: Theme.fontLg - font.bold: true - color: root.maxSensorDeviation >= 0 - ? (root.maxSensorDeviation < 1.0 ? "#6ee7b7" : root.maxSensorDeviation < 3.0 ? "#fde047" : "#ef4444") - : Theme.sideMutedText + + 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() } } } - } - // Match Quality Badge - Rectangle { - Layout.fillWidth: true - height: 28 - visible: root.warpingDistance >= 0 - radius: Theme.radiusSm - color: root.warpingDistance < 50 ? Qt.rgba(0.43, 0.91, 0.72, 0.12) - : root.warpingDistance < 100 ? Qt.rgba(0.99, 0.88, 0.28, 0.12) - : Qt.rgba(0.94, 0.27, 0.27, 0.12) - - Label { + // Empty state + ColumnLayout { anchors.centerIn: parent - text: root.warpingDistance < 50 ? "✓ Excellent Match" - : root.warpingDistance < 100 ? "⚠ Moderate Deviation" - : "✗ Poor Match" - font.pixelSize: Theme.fontXs - font.bold: true - color: root.warpingDistance < 50 ? "#6ee7b7" - : root.warpingDistance < 100 ? "#fde047" - : "#ef4444" + visible: !root.hasSeries + spacing: 8 + + Label { + Layout.alignment: Qt.AlignHCenter + text: root.comparing ? "" : "📊" + font.pixelSize: 32 + } + 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: "Pick Run A and Run B in the panel on the right." + 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 Overlap View ──────────────────────────────── - Rectangle { + // Wafer Map Overlap View card + PanelBox { Layout.fillWidth: true Layout.fillHeight: true - color: Theme.cardBackground - border.color: Theme.cardBorder - border.width: 1 - radius: Theme.radiusMd + Layout.minimumHeight: 220 ColumnLayout { anchors.fill: parent @@ -695,20 +651,17 @@ Item { RowLayout { Layout.fillWidth: true - Label { - text: "SENSOR DEVIATION MAP" - font.pixelSize: Theme.fontXs - font.bold: true - font.letterSpacing: 1.2 - color: Theme.sideMutedText + SectionTitle { + text: "WAFER MAP OVERLAP VIEW" Layout.fillWidth: true } - CheckBox { - id: enableOverlapCheck - checked: true - implicitWidth: 18; implicitHeight: 18 - ToolTip.visible: hovered - ToolTip.text: "Toggle overlap heatmap" + 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 } } @@ -744,7 +697,7 @@ Item { ColumnLayout { anchors.centerIn: parent - visible: root.compareSensorLayout.length === 0 + visible: root.compareSensorLayout.length === 0 || !enableOverlapCheck.checked spacing: 4 Label { Layout.alignment: Qt.AlignHCenter @@ -753,58 +706,239 @@ Item { } Label { Layout.alignment: Qt.AlignHCenter - text: "No sensor map available" + text: !enableOverlapCheck.checked ? "Overlap map disabled" + : "No sensor map available" color: Theme.bodyColor font.pixelSize: Theme.fontXs } } } + } + } + } - // Blend slider - RowLayout { + // --- 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" } + + ColumnLayout { Layout.fillWidth: true - spacing: 6 + spacing: 4 Label { - text: "Blend" - font.pixelSize: Theme.fontXs - color: Theme.bodyColor - } - Slider { - id: blendSlider - Layout.fillWidth: true - from: 0; to: 100; value: 50 - stepSize: 1 - } - Label { - text: Math.round(blendSlider.value) + "%" - font.pixelSize: Theme.fontXs + text: "Reference CSV (Run A)" + font.pixelSize: 9 color: Theme.sideMutedText - Layout.preferredWidth: 30 + } + RunSelector { + id: runACombo + onActivated: function (index) { + root.compareFileA = file_browser.files[index].fileName + root.clearResults() + } } } - // Color legend - Row { + ColumnLayout { Layout.fillWidth: true - spacing: 8 - Row { - spacing: 3 - Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorLow; anchors.verticalCenter: parent.verticalCenter } - Label { text: "Cool"; font.pixelSize: 9; color: Theme.sideMutedText } + spacing: 4 + Label { + text: "Session CSV (Run B)" + 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 } + RunSelector { + id: runBCombo + onActivated: function (index) { + root.compareFileB = file_browser.files[index].fileName + root.clearResults() + } } - Row { - spacing: 3 - Rectangle { width: 8; height: 8; radius: 4; color: Theme.sensorHigh; anchors.verticalCenter: parent.verticalCenter } - Label { text: "Hot"; font.pixelSize: 9; color: Theme.sideMutedText } + } + + 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 ? "#6ee7b7" : root.warpingDistance < 100 ? "#fde047" : "#ef4444") + : 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 ? "#6ee7b7" : root.maxSensorDeviation < 3.0 ? "#fde047" : "#ef4444") + : Theme.sideMutedText + } + } + } + + Item { Layout.fillHeight: true } } } } diff --git a/src/pygui/backend/controllers/session_controller.py b/src/pygui/backend/controllers/session_controller.py index b3cca55..2477350 100644 --- a/src/pygui/backend/controllers/session_controller.py +++ b/src/pygui/backend/controllers/session_controller.py @@ -357,6 +357,10 @@ class SessionController(QObject): result = compare_runs(series_a, series_b) + # Mean temporal shif along the DTW path: positive means run B + # reaches the same profile features lather than run A. + path = result["warping_path"] + frame_offset = round(sum(j - i for i, j in path) / len(path)) if path else 0 # Max sensor deviation: walk the DTW-aligned frame pairs and take # the largest per-sensor abs difference across all sensors, not # just the sensor[0] series used for alignment. @@ -419,6 +423,7 @@ class SessionController(QObject): "success": True, "distance": result["distance"], "warping_path": result["warping_path"][:50], # limit path size + "frame_offset": frame_offset, "max_sensor_deviation": max_sensor_deviation, "series_a": series_a, "series_b": series_b, diff --git a/tests/test_session_controller.py b/tests/test_session_controller.py index ec24039..efa1ecd 100644 --- a/tests/test_session_controller.py +++ b/tests/test_session_controller.py @@ -63,7 +63,8 @@ def test_compare_files_integration(controller): assert "distance" in args assert "series_a" in args assert "series_b" in args - + assert args["frame_offset"] == 0 + def test_recording_toggle(controller, tmp_path): csv_path = str(tmp_path / "rec" / "live_test.csv") controller.startRecording(csv_path, "SN123")