refactor(qml): extract sidebar panels and data tab comparison cards to modular components
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Controls.impl
|
||||
import QtQuick.Layouts
|
||||
import ISC
|
||||
|
||||
// Curve overlay chart card with the alignment timeline scrubber.
|
||||
PanelBox {
|
||||
id: card
|
||||
|
||||
required property var trendDataA
|
||||
required property var trendDataB
|
||||
required property int seriesLen
|
||||
required property bool hasSeries
|
||||
required property bool comparing
|
||||
|
||||
// Scrubber position — the internal Slider is private; consumers
|
||||
// read/write frameIndex and call focusScrubber().
|
||||
property alias frameIndex: scrubber.value
|
||||
|
||||
function focusScrubber() { scrubber.forceActiveFocus() }
|
||||
|
||||
implicitHeight: chartCol.implicitHeight + 24
|
||||
|
||||
ColumnLayout {
|
||||
id: chartCol
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 8
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
SectionTitle {
|
||||
text: "CURVE OVERLAY"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Row {
|
||||
spacing: 16
|
||||
visible: card.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: card.hasSeries
|
||||
|
||||
readonly property real padLeft: 50
|
||||
readonly property real padRight: 16
|
||||
readonly property real padTop: 24
|
||||
readonly property real padBottom: 36
|
||||
|
||||
// Nice tick step (1/2/5 × 10^k) — same scheme as TrendChartItem,
|
||||
// so labels land on round numbers instead of raw data min/max.
|
||||
function niceStep(rawStep) {
|
||||
var mag = Math.pow(10, Math.floor(Math.log10(rawStep)))
|
||||
return [1, 2, 5, 10].map(function (s) { return s * mag })
|
||||
.find(function (s) { return s >= rawStep })
|
||||
}
|
||||
|
||||
function drawGrid(ctx, minV, maxV, yStep, 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 " + Theme.uiFontFamily
|
||||
ctx.textAlign = "right"
|
||||
|
||||
var ySteps = Math.max(1, Math.round(range / yStep))
|
||||
var yDec = Math.max(0, -Math.floor(Math.log10(yStep)))
|
||||
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(yDec), padLeft - 6, yPx + 3)
|
||||
}
|
||||
|
||||
ctx.textAlign = "center"
|
||||
var xMax = dataLen - 1
|
||||
if (xMax > 0) {
|
||||
var xStep = niceStep(xMax / 5 || 1)
|
||||
for (var xIdx = 0; xIdx <= xMax; xIdx += xStep) {
|
||||
var xPx = padLeft + (xIdx / xMax) * plotW
|
||||
ctx.fillText(xIdx.toString(), xPx, height - 20)
|
||||
}
|
||||
}
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(14, padTop + plotH / 2)
|
||||
ctx.rotate(-Math.PI / 2)
|
||||
ctx.textAlign = "center"
|
||||
ctx.font = "12px " + Theme.uiFontFamily
|
||||
ctx.fillText("Celcius (°C)", 0, 0)
|
||||
ctx.restore()
|
||||
|
||||
ctx.textAlign = "center"
|
||||
ctx.font = "12px " + Theme.uiFontFamily
|
||||
ctx.fillText("Frame", padLeft + plotW / 2, height - 4)
|
||||
}
|
||||
|
||||
function drawSeries(ctx, series, minV, range, color, dataLen) {
|
||||
// x is mapped against the shared frame domain (dataLen), not the
|
||||
// series' own length — otherwise a shorter run's line stretches to
|
||||
// fill the whole plot instead of stopping where its data ends.
|
||||
if (series.length < 2) return
|
||||
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 / (dataLen - 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 (card.seriesLen < 2) return
|
||||
var plotW = width - padLeft - padRight
|
||||
var x = padLeft + (scrubber.value / (card.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 (!card.hasSeries) return
|
||||
var all = card.trendDataA.concat(card.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
|
||||
// Snap bounds to nice-step multiples -> stable round-number axis
|
||||
var yStep = niceStep((maxV - minV) / 5 || 1)
|
||||
minV = Math.floor(minV / yStep) * yStep
|
||||
maxV = Math.ceil(maxV / yStep) * yStep
|
||||
var range = (maxV - minV) || 1
|
||||
|
||||
drawGrid(ctx, minV, maxV, yStep, card.seriesLen)
|
||||
drawSeries(ctx, card.trendDataA, minV, range, Theme.primaryAccent, card.seriesLen)
|
||||
drawSeries(ctx, card.trendDataB, minV, range, Theme.themeSkill, card.seriesLen)
|
||||
drawScrubMarker(ctx)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: card
|
||||
function onTrendDataAChanged() { overlayCanvas.requestPaint() }
|
||||
function onTrendDataBChanged() { overlayCanvas.requestPaint() }
|
||||
}
|
||||
Connections {
|
||||
target: scrubber
|
||||
function onValueChanged() { overlayCanvas.requestPaint() }
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent
|
||||
visible: !card.hasSeries
|
||||
spacing: 8
|
||||
|
||||
IconImage {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: !card.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: card.comparing ? "Running DTW comparison…" : "Select two runs and run comparison"
|
||||
color: Theme.bodyColor
|
||||
font.pixelSize: Theme.fontSm
|
||||
}
|
||||
Label {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
visible: !card.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: card.hasSeries
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 1
|
||||
color: Theme.cardBorder
|
||||
}
|
||||
|
||||
SectionTitle {
|
||||
text: "TIMELINE SCRUBBER"
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 8
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: scrubber
|
||||
Layout.fillWidth: true
|
||||
leftPadding: overlayCanvas.padLeft
|
||||
rightPadding: overlayCanvas.padRight
|
||||
from: 0
|
||||
to: Math.max(1, card.seriesLen - 1)
|
||||
stepSize: 1
|
||||
value: 0
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user