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 } } } } }