From 05a169f89b4269d6d6f9b66950e31f6833056274 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:52:43 +0000 Subject: [PATCH 1/6] Replace QtCharts in StatViewer with Canvas-based LineChart and LineChartLegend components Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> --- meshroom/ui/qml/Charts/LineChart.qml | 399 +++++++++++++++++++++ meshroom/ui/qml/Charts/LineChartLegend.qml | 110 ++++++ meshroom/ui/qml/Charts/qmldir | 2 + meshroom/ui/qml/GraphEditor/StatViewer.qml | 268 +++++--------- 4 files changed, 599 insertions(+), 180 deletions(-) create mode 100644 meshroom/ui/qml/Charts/LineChart.qml create mode 100644 meshroom/ui/qml/Charts/LineChartLegend.qml diff --git a/meshroom/ui/qml/Charts/LineChart.qml b/meshroom/ui/qml/Charts/LineChart.qml new file mode 100644 index 0000000000..b8cc781739 --- /dev/null +++ b/meshroom/ui/qml/Charts/LineChart.qml @@ -0,0 +1,399 @@ +import QtQuick + +/** + * LineChart is a generic Canvas-based 2D line chart. + * + * It renders one or more named, colored line series on a plot area with + * automatic axis scaling, grid lines, tick marks, and axis labels. + * No QtCharts dependency is required. + * + * Usage: + * LineChart { + * title: "CPU Usage" + * xAxisTitle: "Minutes" + * yAxisTitle: "%" + * xMin: 0; xMax: 60 + * yMin: 0; yMax: 100 + * textColor: "white" + * } + * + * // Add a series (returns the series index): + * var idx = chart.addSeries("CPU0", "#ff5722", [{x:0,y:10},{x:1,y:20}]) + * + * // Remove all series: + * chart.removeAllSeries() + */ + +Item { + id: root + + // ---- Chart labels ------------------------------------------------------- + + /// Text shown centered above the plot area + property string title: "" + /// Label drawn along the X axis + property string xAxisTitle: "" + /// Label drawn along the Y axis (rotated 90°) + property string yAxisTitle: "" + + // ---- Axis bounds -------------------------------------------------------- + + property real xMin: 0 + property real xMax: 1 + property real yMin: 0 + property real yMax: 100 + + // ---- Display settings --------------------------------------------------- + + property color textColor: palette.windowText + /// Approximate number of tick marks on each axis + property int suggestedTickCount: 5 + property int fontSize: 10 + + // ---- Series API --------------------------------------------------------- + + /// Read-only number of series currently in the chart + readonly property int count: _series.length + + /// Internal series storage – plain JS array of series descriptor objects: + /// { name, color, points:[{x,y}], visible, lineWidth } + property var _series: [] + + // Emitted when a series is added (index = position in _series) + signal seriesAdded(int index) + // Emitted when a series is removed (index = former position) + signal seriesRemoved(int index) + // Emitted whenever any series property (visibility, line width…) changes + signal seriesChanged() + + /** + * Add a series to the chart. + * @param name Display name for the legend + * @param seriesColor CSS color string or QML color value + * @param points Array of {x, y} objects + * @return Index of the new series + */ + function addSeries(name, seriesColor, points) { + var s = { + name: name, + color: seriesColor, + points: points || [], + visible: true, + lineWidth: 1.5 + } + var arr = _series.slice() + arr.push(s) + _series = arr + seriesAdded(_series.length - 1) + canvas.requestPaint() + return _series.length - 1 + } + + /// Remove all series from the chart + function removeAllSeries() { + var n = _series.length + _series = [] + for (var i = 0; i < n; i++) + seriesRemoved(i) + seriesChanged() + canvas.requestPaint() + } + + /// Return the series descriptor object at index i + function series(i) { + return _series[i] + } + + /// Show or hide the series at index i + function setSeriesVisible(i, vis) { + if (i < 0 || i >= _series.length) return + var arr = _series.slice() + arr[i] = _copyWith(arr[i], { visible: vis }) + _series = arr + seriesChanged() + canvas.requestPaint() + } + + /// Set visible state for all series at once + function setAllSeriesVisible(vis) { + var arr = _series.map(function(s) { return _copyWith(s, { visible: vis }) }) + _series = arr + seriesChanged() + canvas.requestPaint() + } + + /// Set the stroke width for the series at index i + function setSeriesLineWidth(i, w) { + if (i < 0 || i >= _series.length) return + var arr = _series.slice() + arr[i] = _copyWith(arr[i], { lineWidth: w }) + _series = arr + canvas.requestPaint() + } + + // ---- Helpers (private) -------------------------------------------------- + + /// Shallow-copy an object, overriding keys from overrides + function _copyWith(obj, overrides) { + var result = {} + for (var k in obj) result[k] = obj[k] + for (var ok in overrides) result[ok] = overrides[ok] + return result + } + + /** + * Compute a nice set of tick values for [minVal, maxVal]. + * Returns an array of evenly-spaced round numbers. + */ + function _niceTicks(minVal, maxVal, n) { + if (minVal >= maxVal) { + maxVal = minVal + 1 + } + var range = maxVal - minVal + var rough = range / Math.max(n - 1, 1) + var mag = Math.pow(10, Math.floor(Math.log(rough) / Math.LN10)) + var norm = rough / mag + var step + if (norm < 1.5) step = mag + else if (norm < 3.5) step = 2 * mag + else if (norm < 7.5) step = 5 * mag + else step = 10 * mag + + var start = Math.floor(minVal / step) * step + start = parseFloat(start.toPrecision(10)) + + var ticks = [] + var t = start + var maxIter = (n + 2) * 3 + var iter = 0 + while (iter < maxIter) { + iter++ + var rounded = parseFloat(t.toPrecision(10)) + if (rounded > maxVal + step * 0.01) break + if (rounded >= minVal - step * 0.01) + ticks.push(rounded) + t += step + t = parseFloat(t.toPrecision(10)) + } + return ticks + } + + /// Format a tick value compactly (no unnecessary trailing zeros) + function _fmtLabel(val) { + if (val === 0) return "0" + var abs = Math.abs(val) + if (abs >= 1000) return val.toFixed(0) + if (abs >= 100) return val.toFixed(0) + if (abs >= 10) return parseFloat(val.toFixed(1)).toString() + return parseFloat(val.toFixed(2)).toString() + } + + // ---- Visual ------------------------------------------------------------- + + SystemPalette { id: palette } + + Canvas { + id: canvas + anchors.fill: parent + antialiasing: true + + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + + Connections { + target: root + function onXMinChanged() { canvas.requestPaint() } + function onXMaxChanged() { canvas.requestPaint() } + function onYMinChanged() { canvas.requestPaint() } + function onYMaxChanged() { canvas.requestPaint() } + function onTextColorChanged() { canvas.requestPaint() } + function onTitleChanged() { canvas.requestPaint() } + function onXAxisTitleChanged(){ canvas.requestPaint() } + function onYAxisTitleChanged(){ canvas.requestPaint() } + } + + onPaint: { + var ctx = getContext("2d") + ctx.clearRect(0, 0, width, height) + + if (width <= 0 || height <= 0) return + + var w = width + var h = height + var fs = root.fontSize + + // ---- Compute axis ticks first (to measure Y label width) -------- + + var xMin = root.xMin, xMax = root.xMax + var yMin = root.yMin, yMax = root.yMax + if (xMax <= xMin) xMax = xMin + 1 + if (yMax <= yMin) yMax = yMin + 1 + + var yTicks = root._niceTicks(yMin, yMax, root.suggestedTickCount) + var xTicks = root._niceTicks(xMin, xMax, root.suggestedTickCount) + + ctx.font = fs + "px sans-serif" + var maxYLabelW = 0 + for (var ti = 0; ti < yTicks.length; ti++) { + var lw = ctx.measureText(root._fmtLabel(yTicks[ti])).width + if (lw > maxYLabelW) maxYLabelW = lw + } + + // ---- Layout margins --------------------------------------------- + + var hasTitle = root.title.length > 0 + var hasXTitle = root.xAxisTitle.length > 0 + var hasYTitle = root.yAxisTitle.length > 0 + + var marginLeft = maxYLabelW + 14 + (hasYTitle ? fs + 6 : 0) + var marginRight = 10 + var marginTop = hasTitle ? (fs + 2) * 2 + 4 : 10 + var marginBottom = fs + 10 + (hasXTitle ? fs + 6 : 0) + + var plotX = Math.floor(marginLeft) + var plotY = Math.floor(marginTop) + var plotW = Math.floor(w - marginLeft - marginRight) + var plotH = Math.floor(h - marginTop - marginBottom) + + if (plotW < 10 || plotH < 10) return + + // ---- Color helpers ---------------------------------------------- + + var tc = root.textColor + var tcCSS = "rgba(" + Math.round(tc.r*255) + "," + Math.round(tc.g*255) + "," + Math.round(tc.b*255) + "," + Math.min(tc.a, 1) + ")" + var gridCSS = "rgba(" + Math.round(tc.r*255) + "," + Math.round(tc.g*255) + "," + Math.round(tc.b*255) + ",0.15)" + var axisCSS = "rgba(" + Math.round(tc.r*255) + "," + Math.round(tc.g*255) + "," + Math.round(tc.b*255) + ",0.5)" + + // ---- Coordinate mapping ----------------------------------------- + + function mapX(x) { return plotX + (x - xMin) / (xMax - xMin) * plotW } + function mapY(y) { return plotY + (1.0 - (y - yMin) / (yMax - yMin)) * plotH } + + // ---- Draw title ------------------------------------------------- + + if (hasTitle) { + ctx.font = "bold " + (fs + 2) + "px sans-serif" + ctx.fillStyle = tcCSS + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.fillText(root.title, w / 2, marginTop / 2) + } + + // ---- Draw Y axis label (rotated) --------------------------------- + + if (hasYTitle) { + ctx.save() + ctx.font = fs + "px sans-serif" + ctx.fillStyle = tcCSS + ctx.textAlign = "center" + ctx.textBaseline = "middle" + ctx.translate(fs / 2 + 2, plotY + plotH / 2) + ctx.rotate(-Math.PI / 2) + ctx.fillText(root.yAxisTitle, 0, 0) + ctx.restore() + } + + // ---- Draw X axis label ------------------------------------------ + + if (hasXTitle) { + ctx.font = fs + "px sans-serif" + ctx.fillStyle = tcCSS + ctx.textAlign = "center" + ctx.textBaseline = "bottom" + ctx.fillText(root.xAxisTitle, plotX + plotW / 2, h - 2) + } + + // ---- Draw horizontal grid lines (at Y ticks) -------------------- + + ctx.strokeStyle = gridCSS + ctx.lineWidth = 1 + for (var yi = 0; yi < yTicks.length; yi++) { + var gy = mapY(yTicks[yi]) + if (gy < plotY - 0.5 || gy > plotY + plotH + 0.5) continue + ctx.beginPath() + ctx.moveTo(plotX, gy) + ctx.lineTo(plotX + plotW, gy) + ctx.stroke() + } + + // ---- Draw vertical grid lines (at X ticks) ---------------------- + + for (var xi = 0; xi < xTicks.length; xi++) { + var gx = mapX(xTicks[xi]) + if (gx < plotX - 0.5 || gx > plotX + plotW + 0.5) continue + ctx.beginPath() + ctx.moveTo(gx, plotY) + ctx.lineTo(gx, plotY + plotH) + ctx.stroke() + } + + // ---- Draw plot border ------------------------------------------- + + ctx.strokeStyle = axisCSS + ctx.lineWidth = 1 + ctx.beginPath() + ctx.rect(plotX, plotY, plotW, plotH) + ctx.stroke() + + // ---- Draw Y tick marks and labels -------------------------------- + + ctx.font = fs + "px sans-serif" + ctx.fillStyle = tcCSS + ctx.strokeStyle = axisCSS + ctx.lineWidth = 1 + + for (var yk = 0; yk < yTicks.length; yk++) { + var ty = mapY(yTicks[yk]) + if (ty < plotY - 0.5 || ty > plotY + plotH + 0.5) continue + ctx.textAlign = "right" + ctx.textBaseline = "middle" + ctx.fillText(root._fmtLabel(yTicks[yk]), plotX - 6, ty) + ctx.beginPath() + ctx.moveTo(plotX - 4, ty) + ctx.lineTo(plotX, ty) + ctx.stroke() + } + + // ---- Draw X tick marks and labels -------------------------------- + + for (var xk = 0; xk < xTicks.length; xk++) { + var tx = mapX(xTicks[xk]) + if (tx < plotX - 0.5 || tx > plotX + plotW + 0.5) continue + ctx.textAlign = "center" + ctx.textBaseline = "top" + ctx.fillText(root._fmtLabel(xTicks[xk]), tx, plotY + plotH + 5) + ctx.beginPath() + ctx.moveTo(tx, plotY + plotH) + ctx.lineTo(tx, plotY + plotH + 4) + ctx.stroke() + } + + // ---- Draw series lines (clipped to plot area) ------------------- + + ctx.save() + ctx.beginPath() + ctx.rect(plotX, plotY, plotW, plotH) + ctx.clip() + + for (var si = 0; si < root._series.length; si++) { + var s = root._series[si] + if (!s.visible || !s.points || s.points.length === 0) continue + + ctx.strokeStyle = s.color.toString() + ctx.lineWidth = s.lineWidth || 1.5 + ctx.lineJoin = "round" + ctx.lineCap = "round" + + var pts = s.points + ctx.beginPath() + ctx.moveTo(mapX(pts[0].x), mapY(pts[0].y)) + for (var pi = 1; pi < pts.length; pi++) { + ctx.lineTo(mapX(pts[pi].x), mapY(pts[pi].y)) + } + ctx.stroke() + } + + ctx.restore() + } + } +} diff --git a/meshroom/ui/qml/Charts/LineChartLegend.qml b/meshroom/ui/qml/Charts/LineChartLegend.qml new file mode 100644 index 0000000000..1d6338a5c6 --- /dev/null +++ b/meshroom/ui/qml/Charts/LineChartLegend.qml @@ -0,0 +1,110 @@ +import QtQuick +import QtQuick.Controls + +/** + * LineChartLegend is an interactive legend component for LineChart. + * + * It provides a labeled, colored CheckBox for each series in the associated + * LineChart, allowing the user to toggle series visibility. + * + * • Click – toggle the clicked series on/off + * • Ctrl + Click – show only the clicked series (solo mode) + * • Hover – highlight the hovered series, dim the others + * + * The component exposes a ButtonGroup so that an "ALL" master checkbox can + * display the aggregate check state of all legend items. + */ + +Flow { + id: root + + /// The LineChart instance whose series this legend represents + property var chartView: null + + /// Expose the internal ButtonGroup so callers can read its checkState + readonly property ButtonGroup buttonGroup: legendGroup + + ButtonGroup { + id: legendGroup + exclusive: false + } + + // ---- Internal model rebuilt from chartView whenever series change ------- + + ListModel { id: seriesModel } + + function _rebuild() { + seriesModel.clear() + if (!chartView) return + for (var i = 0; i < chartView.count; i++) { + var s = chartView.series(i) + if (!s) continue + seriesModel.append({ + seriesIndex: i, + seriesName: s.name, + seriesColor: s.color.toString(), + seriesVisible: s.visible + }) + } + } + + onChartViewChanged: { + seriesModel.clear() + if (chartView) { + chartView.seriesAdded.connect(_rebuild) + chartView.seriesRemoved.connect(_rebuild) + chartView.seriesChanged.connect(_rebuild) + _rebuild() + } + } + + // ---- Legend items ------------------------------------------------------- + + Repeater { + model: seriesModel + + ChartViewCheckBox { + id: legendItem + + ButtonGroup.group: legendGroup + + checked: model.seriesVisible + text: model.seriesName + color: model.seriesColor + + MouseArea { + anchors.fill: parent + hoverEnabled: true + + onEntered: { + if (!chartView) return + for (var i = 0; i < chartView.count; i++) { + if (chartView.series(i) && chartView.series(i).visible) { + chartView.setSeriesLineWidth( + i, i === model.seriesIndex ? 3.0 : 0.5) + } + } + } + + onExited: { + if (!chartView) return + for (var i = 0; i < chartView.count; i++) { + chartView.setSeriesLineWidth(i, 1.5) + } + } + + onClicked: function(mouse) { + if (!chartView) return + if (mouse.modifiers & Qt.ControlModifier) { + // Solo: hide everything except the clicked series + chartView.setAllSeriesVisible(false) + chartView.setSeriesVisible(model.seriesIndex, true) + } else { + var cur = chartView.series(model.seriesIndex) + if (cur) chartView.setSeriesVisible(model.seriesIndex, !cur.visible) + } + } + } + } + } +} diff --git a/meshroom/ui/qml/Charts/qmldir b/meshroom/ui/qml/Charts/qmldir index 0b50d1ed53..e02eabbfe0 100644 --- a/meshroom/ui/qml/Charts/qmldir +++ b/meshroom/ui/qml/Charts/qmldir @@ -3,3 +3,5 @@ module Charts ChartViewLegend 1.0 ChartViewLegend.qml ChartViewCheckBox 1.0 ChartViewCheckBox.qml InteractiveChartView 1.0 InteractiveChartView.qml +LineChart 1.0 LineChart.qml +LineChartLegend 1.0 LineChartLegend.qml diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index ed25d577ee..260a81a926 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -1,4 +1,3 @@ -import QtCharts import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -89,7 +88,6 @@ Item { id: reloadTimer interval: root.deltaTime * 60000; running: true; repeat: false onTriggered: readSourceFile() - } function readSourceFile() { @@ -122,7 +120,7 @@ Item { function resetCharts() { root.fileVersion = 0.0 - cpuLegend.clear() + root.gpuMaxAxis = 100 cpuChart.removeAllSeries() ramChart.removeAllSeries() gpuChart.removeAllSeries() @@ -158,57 +156,52 @@ Item { root.nbCores = nbCores root.cpuFrequency = getPropertyWithDefault(jsonObject.computer, "cpuFreq", -1) + root.nbReads = categories[0].length - 1 - root.nbReads = categories[0].length-1 - + // Build and add one series per CPU core for (var j = 0; j < nbCores; j++) { - var lineSerie = cpuChart.createSeries(ChartView.SeriesTypeLine, "CPU" + j, valueCpuX, valueCpuY) - - if (categories[j].length === 1) { - lineSerie.append(0, categories[j][0]) - lineSerie.append(root.deltaTime, categories[j][0]) + var cat = categories[j] + var corePoints = [] + if (cat.length === 1) { + corePoints = [{ x: 0, y: cat[0] }, { x: root.deltaTime, y: cat[0] }] } else { - var displayLength = Math.min(maxDisplayLength, categories[j].length) - var step = categories[j].length / displayLength - for (var kk = 0; kk < displayLength; kk += step) { - var k = Math.floor(kk * step) - lineSerie.append(k * root.deltaTime, categories[j][k]) + var displayLength = Math.min(maxDisplayLength, cat.length) + var step = cat.length / displayLength + for (var k = 0; k < displayLength; k++) { + var idx = Math.floor(k * step) + corePoints.push({ x: idx * root.deltaTime, y: cat[idx] }) } } - lineSerie.color = colors[j % colors.length] + cpuChart.addSeries("CPU" + j, colors[j % colors.length], corePoints) } - var averageLine = cpuChart.createSeries(ChartView.SeriesTypeLine, "AVERAGE", valueCpuX, valueCpuY) + // Compute and add the AVERAGE series + var avgDisplayLength = Math.min(maxDisplayLength, categories[0].length) + var avgStep = categories[0].length / avgDisplayLength var average = [] - - var displayLengthA = Math.min(maxDisplayLength, categories[0].length) - var stepA = categories[0].length / displayLengthA - for (var l = 0; l < displayLengthA; l += step) { + for (var l = 0; l < avgDisplayLength; l++) { average.push(0) } for (var m = 0; m < categories.length; m++) { var displayLengthB = Math.min(maxDisplayLength, categories[m].length) - var stepB = categories[0].length / displayLengthB - for (var nn = 0; nn < displayLengthB; nn++) { - var n = Math.floor(nn * stepB) - average[nn] += categories[m][n] + var stepB = categories[m].length / displayLengthB + for (var n = 0; n < displayLengthB; n++) { + average[n] += categories[m][Math.floor(n * stepB)] } } + var avgPoints = [] for (var q = 0; q < average.length; q++) { - average[q] = average[q] / (categories.length) - averageLine.append(q * root.deltaTime * stepA, average[q]) + average[q] = average[q] / categories.length + avgPoints.push({ x: q * root.deltaTime * avgStep, y: average[q] }) } - - averageLine.color = colors[colors.length - 1] + cpuChart.addSeries("AVERAGE", colors[colors.length - 1], avgPoints) } function hideOtherCpu(index) { - for (var i = 0; i < cpuChart.count; i++) { - cpuChart.series(i).visible = false - } - cpuChart.series(index).visible = true + cpuChart.setAllSeriesVisible(false) + cpuChart.setSeriesVisible(index, true) } @@ -231,21 +224,19 @@ Item { root.ramLabel = "RAM Max Peak: " } - var ramSerie = ramChart.createSeries(ChartView.SeriesTypeLine, root.ramLabel + root.ramTotal + "GB", valueRamX, valueRamY) - + var ramPoints = [] if (ram.length === 1) { - // Create 2 entries if we have only one input value to create a segment that can be display - ramSerie.append(0, ram[0]) - ramSerie.append(root.deltaTime, ram[0]) + // Create 2 entries if we have only one input value to create a segment that can be displayed + ramPoints = [{ x: 0, y: ram[0] }, { x: root.deltaTime, y: ram[0] }] } else { var displayLength = Math.min(maxDisplayLength, ram.length) var step = ram.length / displayLength - for(var ii = 0; ii < displayLength; ii++) { + for (var ii = 0; ii < displayLength; ii++) { var i = Math.floor(ii * step) - ramSerie.append(i * root.deltaTime, ram[i]) + ramPoints.push({ x: i * root.deltaTime, y: ram[i] }) } } - ramSerie.color = colors[10] + ramChart.addSeries(root.ramLabel + root.ramTotal + "GB", colors[10], ramPoints) } @@ -261,35 +252,35 @@ Item { var gpuUsed = getPropertyWithDefault(jsonObject.computer.curves, "gpuUsed", 0) var gpuTemperature = getPropertyWithDefault(jsonObject.computer.curves, "gpuTemperature", 0) - var gpuUsedSerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "GPU", valueGpuX, valueGpuY) - var gpuUsedMemorySerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "Memory", valueGpuX, valueGpuY) - var gpuTemperatureSerie = gpuChart.createSeries(ChartView.SeriesTypeLine, "Temperature", valueGpuX, valueGpuY) - var gpuMemoryRatio = root.gpuTotalMemory > 0 ? (100 / root.gpuTotalMemory) : 1 - if (gpuUsedMemory.length === 1) { - gpuUsedSerie.append(0, gpuUsed[0]) - gpuUsedSerie.append(1 * root.deltaTime, gpuUsed[0]) - - gpuUsedMemorySerie.append(0, gpuUsedMemory[0] * gpuMemoryRatio) - gpuUsedMemorySerie.append(1 * root.deltaTime, gpuUsedMemory[0] * gpuMemoryRatio) + var gpuUsedPoints = [] + var gpuMemPoints = [] + var gpuTempPoints = [] - gpuTemperatureSerie.append(0, gpuTemperature[0]) - gpuTemperatureSerie.append(1 * root.deltaTime, gpuTemperature[0]) + if (gpuUsedMemory.length === 1) { + gpuUsedPoints = [{ x: 0, y: gpuUsed[0] }, + { x: root.deltaTime, y: gpuUsed[0] }] + gpuMemPoints = [{ x: 0, y: gpuUsedMemory[0] * gpuMemoryRatio }, + { x: root.deltaTime, y: gpuUsedMemory[0] * gpuMemoryRatio }] + gpuTempPoints = [{ x: 0, y: gpuTemperature[0] }, + { x: root.deltaTime, y: gpuTemperature[0] }] root.gpuMaxAxis = Math.max(gpuMaxAxis, gpuTemperature[0]) } else { var displayLength = Math.min(maxDisplayLength, gpuUsedMemory.length) var step = gpuUsedMemory.length / displayLength - for (var ii = 0; ii < displayLength; ii += step) { - var i = Math.floor(ii*step) - gpuUsedSerie.append(i * root.deltaTime, gpuUsed[i]) - - gpuUsedMemorySerie.append(i * root.deltaTime, gpuUsedMemory[i] * gpuMemoryRatio) - - gpuTemperatureSerie.append(i * root.deltaTime, gpuTemperature[i]) + for (var ii = 0; ii < displayLength; ii++) { + var i = Math.floor(ii * step) + gpuUsedPoints.push({ x: i * root.deltaTime, y: gpuUsed[i] }) + gpuMemPoints.push({ x: i * root.deltaTime, y: gpuUsedMemory[i] * gpuMemoryRatio }) + gpuTempPoints.push({ x: i * root.deltaTime, y: gpuTemperature[i] }) root.gpuMaxAxis = Math.max(gpuMaxAxis, gpuTemperature[i]) } } + + gpuChart.addSeries("GPU", colors[0], gpuUsedPoints) + gpuChart.addSeries("Memory", colors[5], gpuMemPoints) + gpuChart.addSeries("Temperature", colors[15], gpuTempPoints) } @@ -356,14 +347,12 @@ Item { checkState: cpuLegend.buttonGroup.checkState leftPadding: 0 onClicked: { - var _checked = checked; - for (var i = 0; i < cpuChart.count; ++i) { - cpuChart.series(i).visible = _checked - } + var _checked = checked + cpuChart.setAllSeriesVisible(_checked) } } - ChartViewLegend { + LineChartLegend { id: cpuLegend Layout.fillWidth: true Layout.fillHeight: true @@ -372,147 +361,66 @@ Item { } } - InteractiveChartView { + LineChart { id: cpuChart Layout.fillWidth: true Layout.preferredHeight: width / 2 - margins.top: 0 - margins.bottom: 0 - antialiasing: true - - legend.visible: false - theme: ChartView.ChartThemeLight - backgroundColor: "transparent" - plotAreaColor: "transparent" - titleColor: textColor - visible: (root.fileVersion > 0.0) // Only visible if we have valid information + textColor: root.textColor title: "CPU: " + root.nbCores + " cores, " + root.cpuFrequency + "MHz" - - ValueAxis { - id: valueCpuY - min: 0 - max: 100 - titleText: "%" - color: textColor - gridLineColor: textColor - minorGridLineColor: textColor - shadesColor: textColor - shadesBorderColor: textColor - labelsColor: textColor - } - - ValueAxis { - id: valueCpuX - min: 0 - max: root.deltaTime * Math.max(1, root.nbReads) - titleText: "Minutes" - color: textColor - gridLineColor: textColor - minorGridLineColor: textColor - shadesColor: textColor - shadesBorderColor: textColor - labelsColor: textColor - } + xAxisTitle: "Minutes" + yAxisTitle: "%" + xMin: 0 + xMax: root.deltaTime * Math.max(1, root.nbReads) + yMin: 0 + yMax: 100 + + visible: (root.fileVersion > 0.0) } /************************** *** RAM UI *** **************************/ - InteractiveChartView { + LineChart { id: ramChart - margins.top: 0 - margins.bottom: 0 + Layout.fillWidth: true Layout.preferredHeight: width / 2 - antialiasing: true - legend.color: textColor - legend.labelColor: textColor - legend.visible: false - theme: ChartView.ChartThemeLight - backgroundColor: "transparent" - plotAreaColor: "transparent" - titleColor: textColor - - visible: (root.fileVersion > 0.0) // Only visible if we have valid information - title: root.ramLabel + root.ramTotal + "GB" - - ValueAxis { - id: valueRamY - min: 0 - max: 100 - titleText: "%" - color: textColor - gridLineColor: textColor - minorGridLineColor: textColor - shadesColor: textColor - shadesBorderColor: textColor - labelsColor: textColor - } - ValueAxis { - id: valueRamX - min: 0 - max: root.deltaTime * Math.max(1, root.nbReads) - titleText: "Minutes" - color: textColor - gridLineColor: textColor - minorGridLineColor: textColor - shadesColor: textColor - shadesBorderColor: textColor - labelsColor: textColor - } + textColor: root.textColor + title: root.ramLabel + root.ramTotal + "GB" + xAxisTitle: "Minutes" + yAxisTitle: "%" + xMin: 0 + xMax: root.deltaTime * Math.max(1, root.nbReads) + yMin: 0 + yMax: 100 + + visible: (root.fileVersion > 0.0) } /************************** *** GPU UI *** **************************/ - InteractiveChartView { + LineChart { id: gpuChart Layout.fillWidth: true - Layout.preferredHeight: width/2 - margins.top: 0 - margins.bottom: 0 - antialiasing: true - legend.color: textColor - legend.labelColor: textColor - theme: ChartView.ChartThemeLight - backgroundColor: "transparent" - plotAreaColor: "transparent" - titleColor: textColor - - visible: (root.fileVersion >= 2.0) // No GPU information was collected before stats 2.0 fileVersion - title: (root.gpuName || root.gpuTotalMemory) ? ("GPU: " + root.gpuName + ", " + root.gpuTotalMemory + "MB") : "No GPU" - - ValueAxis { - id: valueGpuY - min: 0 - max: root.gpuMaxAxis - titleText: "%, °C" - color: textColor - gridLineColor: textColor - minorGridLineColor: textColor - shadesColor: textColor - shadesBorderColor: textColor - labelsColor: textColor - } + Layout.preferredHeight: width / 2 - ValueAxis { - id: valueGpuX - min: 0 - max: root.deltaTime * Math.max(1, root.nbReads) - titleText: "Minutes" - color: textColor - gridLineColor: textColor - minorGridLineColor: textColor - shadesColor: textColor - shadesBorderColor: textColor - labelsColor: textColor - } + textColor: root.textColor + title: (root.gpuName || root.gpuTotalMemory) ? ("GPU: " + root.gpuName + ", " + root.gpuTotalMemory + "MB") : "No GPU" + xAxisTitle: "Minutes" + yAxisTitle: "%, °C" + xMin: 0 + xMax: root.deltaTime * Math.max(1, root.nbReads) + yMin: 0 + yMax: root.gpuMaxAxis + + visible: (root.fileVersion >= 2.0) } } } From fa9bc09cb8672331158d1e11159be00c3209b2df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:53:46 +0000 Subject: [PATCH 2/6] Address code review: rename ambiguous loop var, extract color helper function Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> --- meshroom/ui/qml/Charts/LineChart.qml | 9 ++++++--- meshroom/ui/qml/GraphEditor/StatViewer.qml | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/meshroom/ui/qml/Charts/LineChart.qml b/meshroom/ui/qml/Charts/LineChart.qml index b8cc781739..0669124477 100644 --- a/meshroom/ui/qml/Charts/LineChart.qml +++ b/meshroom/ui/qml/Charts/LineChart.qml @@ -260,9 +260,12 @@ Item { // ---- Color helpers ---------------------------------------------- var tc = root.textColor - var tcCSS = "rgba(" + Math.round(tc.r*255) + "," + Math.round(tc.g*255) + "," + Math.round(tc.b*255) + "," + Math.min(tc.a, 1) + ")" - var gridCSS = "rgba(" + Math.round(tc.r*255) + "," + Math.round(tc.g*255) + "," + Math.round(tc.b*255) + ",0.15)" - var axisCSS = "rgba(" + Math.round(tc.r*255) + "," + Math.round(tc.g*255) + "," + Math.round(tc.b*255) + ",0.5)" + function tcToCSS(alpha) { + return "rgba(" + Math.round(tc.r*255) + "," + Math.round(tc.g*255) + "," + Math.round(tc.b*255) + "," + alpha + ")" + } + var tcCSS = tcToCSS(Math.min(tc.a, 1)) + var gridCSS = tcToCSS(0.15) + var axisCSS = tcToCSS(0.5) // ---- Coordinate mapping ----------------------------------------- diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index 260a81a926..7222a5586d 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -179,7 +179,7 @@ Item { var avgDisplayLength = Math.min(maxDisplayLength, categories[0].length) var avgStep = categories[0].length / avgDisplayLength var average = [] - for (var l = 0; l < avgDisplayLength; l++) { + for (var avgIdx = 0; avgIdx < avgDisplayLength; avgIdx++) { average.push(0) } From e6fb7e909075b5c429643283565d0dbaf2b308b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 09:56:20 +0000 Subject: [PATCH 3/6] Fix Ctrl+click solo mode and add missing GPU legend Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> --- meshroom/ui/qml/Charts/LineChartLegend.qml | 4 +++- meshroom/ui/qml/GraphEditor/StatViewer.qml | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/meshroom/ui/qml/Charts/LineChartLegend.qml b/meshroom/ui/qml/Charts/LineChartLegend.qml index 1d6338a5c6..e09baddde8 100644 --- a/meshroom/ui/qml/Charts/LineChartLegend.qml +++ b/meshroom/ui/qml/Charts/LineChartLegend.qml @@ -96,9 +96,11 @@ Flow { onClicked: function(mouse) { if (!chartView) return if (mouse.modifiers & Qt.ControlModifier) { + // Capture index before setAllSeriesVisible triggers a model rebuild + var idx = model.seriesIndex // Solo: hide everything except the clicked series chartView.setAllSeriesVisible(false) - chartView.setSeriesVisible(model.seriesIndex, true) + chartView.setSeriesVisible(idx, true) } else { var cur = chartView.series(model.seriesIndex) if (cur) chartView.setSeriesVisible(model.seriesIndex, !cur.visible) diff --git a/meshroom/ui/qml/GraphEditor/StatViewer.qml b/meshroom/ui/qml/GraphEditor/StatViewer.qml index 7222a5586d..01d4af48af 100644 --- a/meshroom/ui/qml/GraphEditor/StatViewer.qml +++ b/meshroom/ui/qml/GraphEditor/StatViewer.qml @@ -405,6 +405,13 @@ Item { *** GPU UI *** **************************/ + LineChartLegend { + id: gpuLegend + Layout.fillWidth: true + Layout.leftMargin: 25 + chartView: gpuChart + } + LineChart { id: gpuChart From cc0627dd966be34084bc861dcb22fc08cd2b760a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:30:51 +0000 Subject: [PATCH 4/6] Replace QtCharts with Canvas-based LineChart in CameraResponseGraph Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> Agent-Logs-Url: https://github.com/alicevision/Meshroom/sessions/8dcbfe42-e6f0-4b11-803d-ae94dc80b532 --- meshroom/ui/components/csvData.py | 10 -- .../ui/qml/Viewer/CameraResponseGraph.qml | 122 ++++++------------ 2 files changed, 38 insertions(+), 94 deletions(-) diff --git a/meshroom/ui/components/csvData.py b/meshroom/ui/components/csvData.py index 715af2a6b5..935dc912a9 100644 --- a/meshroom/ui/components/csvData.py +++ b/meshroom/ui/components/csvData.py @@ -1,7 +1,6 @@ from meshroom.common.qt import QObjectListModel from PySide6.QtCore import QObject, Slot, Signal, Property -from PySide6 import QtCharts import csv import os @@ -112,14 +111,5 @@ def getLast(self): return "" return self._content[-1] - @Slot(QtCharts.QXYSeries) - def fillChartSerie(self, serie): - """Fill XYSerie used for displaying QML Chart.""" - if not serie: - return - serie.clear() - for index, value in enumerate(self._content): - serie.append(float(index), float(value)) - title = Property(str, lambda self: self._title, constant=True) content = Property("QStringList", lambda self: self._content, constant=True) diff --git a/meshroom/ui/qml/Viewer/CameraResponseGraph.qml b/meshroom/ui/qml/Viewer/CameraResponseGraph.qml index 992d7c9fcc..84ab0a5154 100644 --- a/meshroom/ui/qml/Viewer/CameraResponseGraph.qml +++ b/meshroom/ui/qml/Viewer/CameraResponseGraph.qml @@ -2,8 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import QtCharts - import Charts 1.0 import Controls 1.0 import DataObjects 1.0 @@ -34,101 +32,57 @@ FloatingPane { // Note: We need to use csvData.getNbColumns() slot instead of the csvData.nbColumns property to avoid a crash on linux. property bool crfReady: csvData && csvData.ready && (csvData.getNbColumns() >= 4) onCrfReadyChanged: { + responseChart.removeAllSeries() if (crfReady) { - redCurve.clear() - greenCurve.clear() - blueCurve.clear() - csvData.getColumn(1).fillChartSerie(redCurve) - csvData.getColumn(2).fillChartSerie(greenCurve) - csvData.getColumn(3).fillChartSerie(blueCurve) - } else { - redCurve.clear() - greenCurve.clear() - blueCurve.clear() + var xCol = csvData.getColumn(0).content + var curveColors = ["red", "green", "blue"] + for (var ci = 1; ci <= 3; ci++) { + var col = csvData.getColumn(ci) + var points = [] + for (var i = 0; i < col.content.length; i++) { + points.push({ x: parseFloat(xCol[i]), y: parseFloat(col.content[i]) }) + } + responseChart.addSeries(col.title, curveColors[ci - 1], points) + } } } - Item { - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenterOffset: -responseChart.width/2 - anchors.verticalCenterOffset: -responseChart.height/2 - InteractiveChartView { - id: responseChart - width: root.width > 400 ? 400 : (root.width < 350 ? 350 : root.width) - height: width * 0.75 + ColumnLayout { + anchors.fill: parent - title: "Camera Response Function (CRF)" - legend.visible: false - antialiasing: true + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 10 - ValueAxis { - id: valueAxisX - labelFormat: "%i" - titleText: "Camera Brightness" - min: crfReady ? csvData.getColumn(0).getFirst() : 0 - max: crfReady ? csvData.getColumn(0).getLast() : 1 - } - ValueAxis { - id: valueAxisY - titleText: "Normalized Radiance" - min: 0.0 - max: 1.0 + ChartViewCheckBox { + text: "ALL" + color: root.textColor + checkState: crfLegend.buttonGroup.checkState + leftPadding: 0 + onClicked: responseChart.setAllSeriesVisible(checked) } - // We cannot use a Repeater with these Components so we need to instantiate them one by one - LineSeries { - // Red curve - id: redCurve - axisX: valueAxisX - axisY: valueAxisY - name: crfReady ? csvData.getColumn(1).title : "" - color: name.toLowerCase() - } - LineSeries { - // Green curve - id: greenCurve - axisX: valueAxisX - axisY: valueAxisY - name: crfReady ? csvData.getColumn(2).title : "" - color: name.toLowerCase() - } - LineSeries { - // Blue curve - id: blueCurve - axisX: valueAxisX - axisY: valueAxisY - name: crfReady ? csvData.getColumn(3).title : "" - color: name.toLowerCase() + LineChartLegend { + id: crfLegend + Layout.fillWidth: true + chartView: responseChart } } - Item { - id: btnContainer - - anchors.bottom: responseChart.bottom - anchors.bottomMargin: 35 - anchors.left: responseChart.left - anchors.leftMargin: responseChart.width * 0.15 + LineChart { + id: responseChart - RowLayout { - ChartViewCheckBox { - text: "ALL" - color: textColor - checkState: legend.buttonGroup.checkState - onClicked: { - const _checked = checked - for (let i = 0; i < responseChart.count; ++i) { - responseChart.series(i).visible = _checked - } - } - } + Layout.fillWidth: true + Layout.preferredHeight: width * 0.75 - ChartViewLegend { - id: legend - chartView: responseChart - } - } + textColor: root.textColor + title: "Camera Response Function (CRF)" + xAxisTitle: "Camera Brightness" + yAxisTitle: "Normalized Radiance" + xMin: crfReady ? parseFloat(csvData.getColumn(0).getFirst()) : 0 + xMax: crfReady ? parseFloat(csvData.getColumn(0).getLast()) : 1 + yMin: 0.0 + yMax: 1.0 } } } From 93099f1af1ce65250c4f810a9e4f4578abbf9d39 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:48:08 +0000 Subject: [PATCH 5/6] Replace QtCharts with Canvas-based LineChart in SfmStatsView and SfmGlobalStats Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> Agent-Logs-Url: https://github.com/alicevision/Meshroom/sessions/c691a49d-fe73-4e50-92a4-4b5d1b6b3a46 --- meshroom/ui/qml/Viewer/SfmGlobalStats.qml | 380 +++++++--------------- meshroom/ui/qml/Viewer/SfmStatsView.qml | 308 +++++++----------- 2 files changed, 234 insertions(+), 454 deletions(-) diff --git a/meshroom/ui/qml/Viewer/SfmGlobalStats.qml b/meshroom/ui/qml/Viewer/SfmGlobalStats.qml index 74b735d902..174735fc50 100644 --- a/meshroom/ui/qml/Viewer/SfmGlobalStats.qml +++ b/meshroom/ui/qml/Viewer/SfmGlobalStats.qml @@ -1,4 +1,3 @@ -import QtCharts import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -28,264 +27,123 @@ FloatingPane { onWheel: {} } + // Colors assigned to the 6 statistical curves (Min, Max, Mean, Median, Q1, Q3) + readonly property var statColors: ["#4169e1", "#dc143c", "#228b22", "#ff8c00", "#9932cc", "#20b2aa"] - InteractiveChartView { - id: residualsPerViewChart - width: parent.width * 0.5 - height: parent.height * 0.5 - - title: "Residuals Per View" - legend.visible: false - antialiasing: true - - ValueAxis { - id: residualsPerViewValueAxisX - labelFormat: "%i" - titleText: "Ordered Views" - min: 0 - max: sfmDataStat.residualsPerViewMaxAxisX - } - ValueAxis { - id: residualsPerViewValueAxisY - titleText: "Reprojection Error (pix)" - min: 0 - max: sfmDataStat.residualsPerViewMaxAxisY - tickAnchor: 0 - tickInterval: 0.50 - tickCount: sfmDataStat.residualsPerViewMaxAxisY * 2 - } - LineSeries { - id: residualsMinPerViewLineSerie - axisX: residualsPerViewValueAxisX - axisY: residualsPerViewValueAxisY - name: "Min" - } - LineSeries { - id: residualsMaxPerViewLineSerie - axisX: residualsPerViewValueAxisX - axisY: residualsPerViewValueAxisY - name: "Max" - } - LineSeries { - id: residualsMeanPerViewLineSerie - axisX: residualsPerViewValueAxisX - axisY: residualsPerViewValueAxisY - name: "Mean" - } - LineSeries { - id: residualsMedianPerViewLineSerie - axisX: residualsPerViewValueAxisX - axisY: residualsPerViewValueAxisY - name: "Median" - } - LineSeries { - id: residualsFirstQuartilePerViewLineSerie - axisX: residualsPerViewValueAxisX - axisY: residualsPerViewValueAxisY - name: "Q1" - } - LineSeries { - id: residualsThirdQuartilePerViewLineSerie - axisX: residualsPerViewValueAxisX - axisY: residualsPerViewValueAxisY - name: "Q3" - } - } - - Item { - id: residualsPerViewBtnContainer - - Layout.fillWidth: true - anchors.bottom: residualsPerViewChart.bottom - anchors.bottomMargin: 35 - anchors.left: residualsPerViewChart.left - anchors.leftMargin: residualsPerViewChart.width * 0.25 - - RowLayout { - ChartViewCheckBox { - id: allObservations - text: "ALL" - color: textColor - checkState: residualsPerViewLegend.buttonGroup.checkState - onClicked: { - var _checked = checked; - for (var i = 0; i < residualsPerViewChart.count; ++i) { - residualsPerViewChart.series(i).visible = _checked - } + GridLayout { + anchors.fill: parent + columns: 2 + + // Residuals Per View chart + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 10 + ChartViewCheckBox { + text: "ALL" + color: textColor + leftPadding: 0 + checkState: residualsPerViewLegend.buttonGroup.checkState + onClicked: residualsPerViewChart.setAllSeriesVisible(checked) + } + LineChartLegend { + id: residualsPerViewLegend + Layout.fillWidth: true + chartView: residualsPerViewChart } } - - ChartViewLegend { - id: residualsPerViewLegend - chartView: residualsPerViewChart + LineChart { + id: residualsPerViewChart + Layout.fillWidth: true + Layout.fillHeight: true + textColor: root.textColor + title: "Residuals Per View" + xAxisTitle: "Ordered Views" + yAxisTitle: "Reprojection Error (pix)" + xMin: 0 + xMax: sfmDataStat.residualsPerViewMaxAxisX + yMin: 0 + yMax: sfmDataStat.residualsPerViewMaxAxisY } - } - } - - InteractiveChartView { - id: observationsLengthsPerViewChart - width: parent.width * 0.5 - height: parent.height * 0.5 - anchors.top: parent.top - anchors.topMargin: (parent.height) * 0.5 - title: "Observations Lengths Per View" - legend.visible: false - antialiasing: true - - ValueAxis { - id: observationsLengthsPerViewValueAxisX - labelFormat: "%i" - titleText: "Ordered Views" - min: 0 - max: sfmDataStat.observationsLengthsPerViewMaxAxisX - } - ValueAxis { - id: observationsLengthsPerViewValueAxisY - titleText: "Observations Lengths" - min: 0 - max: sfmDataStat.observationsLengthsPerViewMaxAxisY - tickAnchor: 0 - tickInterval: 0.50 - tickCount: sfmDataStat.observationsLengthsPerViewMaxAxisY * 2 - } - - LineSeries { - id: observationsLengthsMinPerViewLineSerie - axisX: observationsLengthsPerViewValueAxisX - axisY: observationsLengthsPerViewValueAxisY - name: "Min" - } - LineSeries { - id: observationsLengthsMaxPerViewLineSerie - axisX: observationsLengthsPerViewValueAxisX - axisY: observationsLengthsPerViewValueAxisY - name: "Max" - } - LineSeries { - id: observationsLengthsMeanPerViewLineSerie - axisX: observationsLengthsPerViewValueAxisX - axisY: observationsLengthsPerViewValueAxisY - name: "Mean" - } - LineSeries { - id: observationsLengthsMedianPerViewLineSerie - axisX: observationsLengthsPerViewValueAxisX - axisY: observationsLengthsPerViewValueAxisY - name: "Median" - } - LineSeries { - id: observationsLengthsFirstQuartilePerViewLineSerie - axisX: observationsLengthsPerViewValueAxisX - axisY: observationsLengthsPerViewValueAxisY - name: "Q1" - } - LineSeries { - id: observationsLengthsThirdQuartilePerViewLineSerie - axisX: observationsLengthsPerViewValueAxisX - axisY: observationsLengthsPerViewValueAxisY - name: "Q3" - } - } - - Item { - id: observationsLengthsPerViewBtnContainer - - Layout.fillWidth: true - anchors.bottom: observationsLengthsPerViewChart.bottom - anchors.bottomMargin: 35 - anchors.left: observationsLengthsPerViewChart.left - anchors.leftMargin: observationsLengthsPerViewChart.width * 0.25 - - RowLayout { - ChartViewCheckBox { - id: allModes - text: "ALL" - color: textColor - checkState: observationsLengthsPerViewLegend.buttonGroup.checkState - onClicked: { - var _checked = checked; - for (var i = 0; i < observationsLengthsPerViewChart.count; ++i) { - observationsLengthsPerViewChart.series(i).visible = _checked - } + // Landmarks Per View chart + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 10 + ChartViewCheckBox { + text: "ALL" + color: textColor + leftPadding: 0 + checkState: landmarksFeatTracksPerViewLegend.buttonGroup.checkState + onClicked: landmarksPerViewChart.setAllSeriesVisible(checked) + } + LineChartLegend { + id: landmarksFeatTracksPerViewLegend + Layout.fillWidth: true + chartView: landmarksPerViewChart } } - - ChartViewLegend { - id: observationsLengthsPerViewLegend - chartView: observationsLengthsPerViewChart + LineChart { + id: landmarksPerViewChart + Layout.fillWidth: true + Layout.fillHeight: true + textColor: root.textColor + title: "Landmarks Per View" + xAxisTitle: "Ordered Views" + yAxisTitle: "Number of Landmarks" + xMin: 0 + xMax: sfmDataStat.landmarksPerViewMaxAxisX + yMin: 0 + yMax: sfmDataStat.landmarksPerViewMaxAxisY } } - } - - InteractiveChartView { - id: landmarksPerViewChart - width: parent.width * 0.5 - height: parent.height * 0.5 - anchors.left: parent.left - anchors.leftMargin: (parent.width) * 0.5 - anchors.top: parent.top - - title: "Landmarks Per View" - legend.visible: false - antialiasing: true - ValueAxis { - id: landmarksPerViewValueAxisX - titleText: "Ordered Views" - min: 0.0 - max: sfmDataStat.landmarksPerViewMaxAxisX - } - ValueAxis { - id: landmarksPerViewValueAxisY - labelFormat: "%i" - titleText: "Number of Landmarks" - min: 0 - max: sfmDataStat.landmarksPerViewMaxAxisY - } - LineSeries { - id: landmarksPerViewLineSerie - axisX: landmarksPerViewValueAxisX - axisY: landmarksPerViewValueAxisY - name: "Landmarks" - } - LineSeries { - id: tracksPerViewLineSerie - axisX: landmarksPerViewValueAxisX - axisY: landmarksPerViewValueAxisY - name: "Tracks" - } - } - - Item { - id: landmarksFeatTracksPerViewBtnContainer - - Layout.fillWidth: true - anchors.bottom: landmarksPerViewChart.bottom - anchors.bottomMargin: 35 - anchors.left: landmarksPerViewChart.left - anchors.leftMargin: landmarksPerViewChart.width * 0.25 - - RowLayout { - ChartViewCheckBox { - id: allFeatures - text: "ALL" - color: textColor - checkState: landmarksFeatTracksPerViewLegend.buttonGroup.checkState - onClicked: { - var _checked = checked; - for (var i = 0; i < landmarksPerViewChart.count; ++i) { - landmarksPerViewChart.series(i).visible = _checked - } + // Observations Lengths Per View chart + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 10 + ChartViewCheckBox { + text: "ALL" + color: textColor + leftPadding: 0 + checkState: observationsLengthsPerViewLegend.buttonGroup.checkState + onClicked: observationsLengthsPerViewChart.setAllSeriesVisible(checked) + } + LineChartLegend { + id: observationsLengthsPerViewLegend + Layout.fillWidth: true + chartView: observationsLengthsPerViewChart } } - - ChartViewLegend { - id: landmarksFeatTracksPerViewLegend - chartView: landmarksPerViewChart + LineChart { + id: observationsLengthsPerViewChart + Layout.fillWidth: true + Layout.fillHeight: true + textColor: root.textColor + title: "Observations Lengths Per View" + xAxisTitle: "Ordered Views" + yAxisTitle: "Observations Lengths" + xMin: 0 + xMax: sfmDataStat.observationsLengthsPerViewMaxAxisX + yMin: 0 + yMax: sfmDataStat.observationsLengthsPerViewMaxAxisY } } + + // (empty fourth cell) + Item { Layout.fillWidth: true; Layout.fillHeight: true } } // Stats from the sfmData @@ -295,20 +153,26 @@ FloatingPane { mTracks: root.mTracks onAxisChanged: { - fillLandmarksPerViewSerie(landmarksPerViewLineSerie) - fillTracksPerViewSerie(tracksPerViewLineSerie) - fillResidualsMinPerViewSerie(residualsMinPerViewLineSerie) - fillResidualsMaxPerViewSerie(residualsMaxPerViewLineSerie) - fillResidualsMeanPerViewSerie(residualsMeanPerViewLineSerie) - fillResidualsMedianPerViewSerie(residualsMedianPerViewLineSerie) - fillResidualsFirstQuartilePerViewSerie(residualsFirstQuartilePerViewLineSerie) - fillResidualsThirdQuartilePerViewSerie(residualsThirdQuartilePerViewLineSerie) - fillObservationsLengthsMinPerViewSerie(observationsLengthsMinPerViewLineSerie) - fillObservationsLengthsMaxPerViewSerie(observationsLengthsMaxPerViewLineSerie) - fillObservationsLengthsMeanPerViewSerie(observationsLengthsMeanPerViewLineSerie) - fillObservationsLengthsMedianPerViewSerie(observationsLengthsMedianPerViewLineSerie) - fillObservationsLengthsFirstQuartilePerViewSerie(observationsLengthsFirstQuartilePerViewLineSerie) - fillObservationsLengthsThirdQuartilePerViewSerie(observationsLengthsThirdQuartilePerViewLineSerie) + landmarksPerViewChart.removeAllSeries() + landmarksPerViewChart.addSeries("Landmarks", root.statColors[0], sfmDataStat.getLandmarksPerViewPoints()) + landmarksPerViewChart.addSeries("Tracks", root.statColors[1], sfmDataStat.getTracksPerViewPoints()) + + residualsPerViewChart.removeAllSeries() + residualsPerViewChart.addSeries("Min", root.statColors[0], sfmDataStat.getResidualsMinPerViewPoints()) + residualsPerViewChart.addSeries("Max", root.statColors[1], sfmDataStat.getResidualsMaxPerViewPoints()) + residualsPerViewChart.addSeries("Mean", root.statColors[2], sfmDataStat.getResidualsMeanPerViewPoints()) + residualsPerViewChart.addSeries("Median", root.statColors[3], sfmDataStat.getResidualsMedianPerViewPoints()) + residualsPerViewChart.addSeries("Q1", root.statColors[4], sfmDataStat.getResidualsFirstQuartilePerViewPoints()) + residualsPerViewChart.addSeries("Q3", root.statColors[5], sfmDataStat.getResidualsThirdQuartilePerViewPoints()) + + observationsLengthsPerViewChart.removeAllSeries() + observationsLengthsPerViewChart.addSeries("Min", root.statColors[0], sfmDataStat.getObservationsLengthsMinPerViewPoints()) + observationsLengthsPerViewChart.addSeries("Max", root.statColors[1], sfmDataStat.getObservationsLengthsMaxPerViewPoints()) + observationsLengthsPerViewChart.addSeries("Mean", root.statColors[2], sfmDataStat.getObservationsLengthsMeanPerViewPoints()) + observationsLengthsPerViewChart.addSeries("Median", root.statColors[3], sfmDataStat.getObservationsLengthsMedianPerViewPoints()) + observationsLengthsPerViewChart.addSeries("Q1", root.statColors[4], sfmDataStat.getObservationsLengthsFirstQuartilePerViewPoints()) + observationsLengthsPerViewChart.addSeries("Q3", root.statColors[5], sfmDataStat.getObservationsLengthsThirdQuartilePerViewPoints()) } } } + diff --git a/meshroom/ui/qml/Viewer/SfmStatsView.qml b/meshroom/ui/qml/Viewer/SfmStatsView.qml index 859da48900..a2d9e46156 100644 --- a/meshroom/ui/qml/Viewer/SfmStatsView.qml +++ b/meshroom/ui/qml/Viewer/SfmStatsView.qml @@ -1,4 +1,3 @@ -import QtCharts import QtQuick import QtQuick.Controls import QtQuick.Layouts @@ -29,209 +28,120 @@ FloatingPane { onWheel: {} } - InteractiveChartView { - id: residualChart - width: parent.width * 0.5 - height: parent.height * 0.5 - - title: "Reprojection Errors" - legend.visible: false - antialiasing: true - - ValueAxis { - id: residualValueAxisX - titleText: "Reprojection Error" - min: 0.0 - max: viewStat.residualMaxAxisX - } - ValueAxis { - id: residualValueAxisY - labelFormat: "%i" - titleText: "Number of Points" - min: 0 - max: viewStat.residualMaxAxisY - } - LineSeries { - id: residualFullLineSerie - axisX: residualValueAxisX - axisY: residualValueAxisY - name: "Average on All Cameras" - } - LineSeries { - id: residualViewLineSerie - axisX: residualValueAxisX - axisY: residualValueAxisY - name: "Current" - } - } - - Item { - id: residualBtnContainer - - Layout.fillWidth: true - anchors.bottom: residualChart.bottom - anchors.bottomMargin: 35 - anchors.left: residualChart.left - anchors.leftMargin: residualChart.width * 0.15 - - RowLayout { - - ChartViewCheckBox { - id: allResiduals - text: "ALL" - color: textColor - checkState: residualLegend.buttonGroup.checkState - onClicked: { - var _checked = checked; - for (var i = 0; i < residualChart.count; ++i) { - residualChart.series(i).visible = _checked - } + GridLayout { + anchors.fill: parent + columns: 2 + + // Reprojection Errors chart + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 10 + ChartViewCheckBox { + text: "ALL" + color: textColor + leftPadding: 0 + checkState: residualLegend.buttonGroup.checkState + onClicked: residualChart.setAllSeriesVisible(checked) + } + LineChartLegend { + id: residualLegend + Layout.fillWidth: true + chartView: residualChart } } - - ChartViewLegend { - id: residualLegend - chartView: residualChart + LineChart { + id: residualChart + Layout.fillWidth: true + Layout.fillHeight: true + textColor: root.textColor + title: "Reprojection Errors" + xAxisTitle: "Reprojection Error" + yAxisTitle: "Number of Points" + xMin: 0 + xMax: viewStat.residualMaxAxisX + yMin: 0 + yMax: viewStat.residualMaxAxisY } } - } - InteractiveChartView { - id: observationsLengthsChart - width: parent.width * 0.5 - height: parent.height * 0.5 - anchors.top: parent.top - anchors.topMargin: (parent.height) * 0.5 - - legend.visible: false - title: "Observations Lengths" - - ValueAxis { - id: observationsLengthsvalueAxisX - labelFormat: "%i" - titleText: "Observations Length" - min: 2 - max: viewStat.observationsLengthsMaxAxisX - tickAnchor: 2 - tickInterval: 1 - tickCount: 5 - } - ValueAxis { - id: observationsLengthsvalueAxisY - labelFormat: "%i" - titleText: "Number of Points" - min: 0 - max: viewStat.observationsLengthsMaxAxisY - } - LineSeries { - id: observationsLengthsFullLineSerie - axisX: observationsLengthsvalueAxisX - axisY: observationsLengthsvalueAxisY - name: "All Cameras" - } - LineSeries { - id: observationsLengthsViewLineSerie - axisX: observationsLengthsvalueAxisX - axisY: observationsLengthsvalueAxisY - name: "Current" - } - } - - Item { - id: observationsLengthsBtnContainer - - Layout.fillWidth: true - anchors.bottom: observationsLengthsChart.bottom - anchors.bottomMargin: 35 - anchors.left: observationsLengthsChart.left - anchors.leftMargin: observationsLengthsChart.width * 0.25 - - RowLayout { - ChartViewCheckBox { - id: allObservations - text: "ALL" - color: textColor - checkState: observationsLengthsLegend.buttonGroup.checkState - onClicked: { - var _checked = checked; - for (var i = 0; i < observationsLengthsChart.count; ++i) { - observationsLengthsChart.series(i).visible = _checked - } + // Observations Scale chart + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 10 + ChartViewCheckBox { + text: "ALL" + color: textColor + leftPadding: 0 + checkState: observationsScaleLegend.buttonGroup.checkState + onClicked: observationsScaleChart.setAllSeriesVisible(checked) + } + LineChartLegend { + id: observationsScaleLegend + Layout.fillWidth: true + chartView: observationsScaleChart } } - - ChartViewLegend { - id: observationsLengthsLegend - chartView: observationsLengthsChart + LineChart { + id: observationsScaleChart + Layout.fillWidth: true + Layout.fillHeight: true + textColor: root.textColor + title: "Observations Scale" + xAxisTitle: "Scale" + yAxisTitle: "Number of Points" + xMin: 0 + xMax: viewStat.observationsScaleMaxAxisX + yMin: 0 + yMax: viewStat.observationsScaleMaxAxisY } } - } - - InteractiveChartView { - id: observationsScaleChart - width: parent.width * 0.5 - height: parent.height * 0.5 - anchors.left: parent.left - anchors.leftMargin: (parent.width) * 0.5 - anchors.top: parent.top - - legend.visible: false - title: "Observations Scale" - - ValueAxis { - id: observationsScaleValueAxisX - titleText: "Scale" - min: 0 - max: viewStat.observationsScaleMaxAxisX - } - ValueAxis { - id: observationsScaleValueAxisY - titleText: "Number of Points" - min: 0 - max: viewStat.observationsScaleMaxAxisY - } - LineSeries { - id: observationsScaleFullLineSerie - axisX: observationsScaleValueAxisX - axisY: observationsScaleValueAxisY - name: " Average on All Cameras" - } - LineSeries { - id: observationsScaleViewLineSerie - axisX: observationsScaleValueAxisX - axisY: observationsScaleValueAxisY - name: "Current" - } - } - Item { - id: observationsScaleBtnContainer - - Layout.fillWidth: true - anchors.bottom: observationsScaleChart.bottom - anchors.bottomMargin: 35 - anchors.left: observationsScaleChart.left - anchors.leftMargin: observationsScaleChart.width * 0.15 - - RowLayout { - ChartViewCheckBox { - id: allObservationsScales - text: "ALL" - color: textColor - checkState: observationsScaleLegend.buttonGroup.checkState - onClicked: { - var _checked = checked; - for (var i = 0; i < observationsScaleChart.count; ++i) { - observationsScaleChart.series(i).visible = _checked - } + // Observations Lengths chart + ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 10 + ChartViewCheckBox { + text: "ALL" + color: textColor + leftPadding: 0 + checkState: observationsLengthsLegend.buttonGroup.checkState + onClicked: observationsLengthsChart.setAllSeriesVisible(checked) + } + LineChartLegend { + id: observationsLengthsLegend + Layout.fillWidth: true + chartView: observationsLengthsChart } } - - ChartViewLegend { - id: observationsScaleLegend - chartView: observationsScaleChart + LineChart { + id: observationsLengthsChart + Layout.fillWidth: true + Layout.fillHeight: true + textColor: root.textColor + title: "Observations Lengths" + xAxisTitle: "Observations Length" + yAxisTitle: "Number of Points" + xMin: 2 + xMax: viewStat.observationsLengthsMaxAxisX + yMin: 0 + yMax: viewStat.observationsLengthsMaxAxisY } } + + // (empty fourth cell) + Item { Layout.fillWidth: true; Layout.fillHeight: true } } // Stats from a view the sfmData @@ -240,12 +150,18 @@ FloatingPane { msfmData: (root.visible && root.msfmData && root.msfmData.status === AliceVision.MSfMData.Ready) ? root.msfmData : null viewId: root.viewId onViewStatsChanged: { - fillResidualFullSerie(residualFullLineSerie) - fillResidualViewSerie(residualViewLineSerie) - fillObservationsLengthsFullSerie(observationsLengthsFullLineSerie) - fillObservationsLengthsViewSerie(observationsLengthsViewLineSerie) - fillObservationsScaleFullSerie(observationsScaleFullLineSerie) - fillObservationsScaleViewSerie(observationsScaleViewLineSerie) + residualChart.removeAllSeries() + residualChart.addSeries("Average on All Cameras", "#dc143c", viewStat.getResidualFullPoints()) + residualChart.addSeries("Current", "#00008b", viewStat.getResidualViewPoints()) + + observationsLengthsChart.removeAllSeries() + observationsLengthsChart.addSeries("All Cameras", "#dc143c", viewStat.getObservationsLengthsFullPoints()) + observationsLengthsChart.addSeries("Current", "#00008b", viewStat.getObservationsLengthsViewPoints()) + + observationsScaleChart.removeAllSeries() + observationsScaleChart.addSeries("Average on All Cameras", "#dc143c", viewStat.getObservationsScaleFullPoints()) + observationsScaleChart.addSeries("Current", "#00008b", viewStat.getObservationsScaleViewPoints()) } } } + From 7ff014b47b74dd4c492fbd67b2188a6cf4d91b3e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:42:41 +0000 Subject: [PATCH 6/6] Remove remaining QtCharts files, qmldir entries, and Docker addon references Co-authored-by: fabiencastan <153585+fabiencastan@users.noreply.github.com> Agent-Logs-Url: https://github.com/alicevision/Meshroom/sessions/d501c84e-b9f6-417c-a83a-b092bb236c2b --- docker/Dockerfile_rocky_deps | 2 +- docker/Dockerfile_ubuntu_deps | 2 +- meshroom/ui/qml/Charts/ChartViewLegend.qml | 103 ------------------ .../ui/qml/Charts/InteractiveChartView.qml | 27 ----- meshroom/ui/qml/Charts/qmldir | 2 - 5 files changed, 2 insertions(+), 134 deletions(-) delete mode 100644 meshroom/ui/qml/Charts/ChartViewLegend.qml delete mode 100644 meshroom/ui/qml/Charts/InteractiveChartView.qml diff --git a/docker/Dockerfile_rocky_deps b/docker/Dockerfile_rocky_deps index 4a456eec0c..d3ece99c5f 100644 --- a/docker/Dockerfile_rocky_deps +++ b/docker/Dockerfile_rocky_deps @@ -34,7 +34,7 @@ RUN chmod +x qt.run RUN ./qt.run --root /opt/Qt --verbose --email ${QT_CI_LOGIN} --password ${QT_CI_P} --accept-obligations \ --accept-licenses --default-answer --platform minimal --auto-answer installationErrorWithCancel=Ignore \ --no-force-installations --no-default-installations --confirm-command \ - install qt.qt6.683.linux_gcc_64 qt.qt6.683.addons.qtcharts qt.qt6.683.addons.qt3d + install qt.qt6.683.linux_gcc_64 qt.qt6.683.addons.qt3d RUN rm qt.run # Strip sections containing ".note.ABI.tag" from .so: https://github.com/Microsoft/WSL/issues/3023 diff --git a/docker/Dockerfile_ubuntu_deps b/docker/Dockerfile_ubuntu_deps index 77193f7813..4aac10a112 100644 --- a/docker/Dockerfile_ubuntu_deps +++ b/docker/Dockerfile_ubuntu_deps @@ -63,7 +63,7 @@ RUN chmod +x qt.run RUN ./qt.run --root /opt/Qt --verbose --email ${QT_CI_LOGIN} --password ${QT_CI_P} --accept-obligations \ --accept-licenses --default-answer --platform minimal --auto-answer installationErrorWithCancel=Ignore \ --no-force-installations --no-default-installations --confirm-command \ - install qt.qt6.683.linux_gcc_64 qt.qt6.683.addons.qtcharts qt.qt6.683.addons.qt3d + install qt.qt6.683.linux_gcc_64 qt.qt6.683.addons.qt3d RUN rm qt.run # Strip sections containing ".note.ABI.tag" from .so: https://github.com/Microsoft/WSL/issues/3023 diff --git a/meshroom/ui/qml/Charts/ChartViewLegend.qml b/meshroom/ui/qml/Charts/ChartViewLegend.qml deleted file mode 100644 index 576c635343..0000000000 --- a/meshroom/ui/qml/Charts/ChartViewLegend.qml +++ /dev/null @@ -1,103 +0,0 @@ -import QtQuick -import QtQuick.Controls -import QtCharts - -/** - * ChartViewLegend is an interactive legend component for ChartViews. - * It provides a CheckBox for each series that can control its visibility, - * and highlight on hovering. - */ - -Flow { - id: root - - // The ChartView to create the legend for - property ChartView chartView - // Currently hovered series - property var hoveredSeries: null - - readonly property ButtonGroup buttonGroup: ButtonGroup { - id: legendGroup - exclusive: false - } - - /// Shortcut function to clear legend - function clear() { - seriesModel.clear() - } - - // Update internal ListModel when ChartView's series change - Connections { - target: chartView - function onSeriesAdded(series) { - seriesModel.append({"series": series}) - } - function onSeriesRemoved(series) { - for (var i = 0; i < seriesModel.count; ++i) { - if (seriesModel.get(i)["series"] === series) { - seriesModel.remove(i) - return - } - } - } - } - - onChartViewChanged: { - clear() - for (var i = 0; i < chartView.count; ++i) - seriesModel.append({"series": chartView.series(i)}) - } - - Repeater { - // ChartView series cannot be accessed directly as a model. - // Use an intermediate ListModel populated with those series. - model: ListModel { - id: seriesModel - } - - ChartViewCheckBox { - ButtonGroup.group: legendGroup - - checked: series.visible - text: series.name - color: series.color - - onHoveredChanged: { - if (hovered && series.visible) - root.hoveredSeries = series - else - root.hoveredSeries = null - } - - // Hovered serie properties override - states: [ - State { - when: series && root.hoveredSeries === series - PropertyChanges { target: series; width: 5.0 } - }, - State { - when: series && root.hoveredSeries && root.hoveredSeries !== series - PropertyChanges { target: series; width: 0.2 } - } - ] - - MouseArea { - anchors.fill: parent - onClicked: function(mouse) { - if (mouse.modifiers & Qt.ControlModifier) - root.soloSeries(index) - else - series.visible = !series.visible - } - } - } - } - - /// Hide all series but the one at index 'idx' - function soloSeries(idx) { - for (var i = 0; i < seriesModel.count; i++) { - chartView.series(i).visible = false - } - chartView.series(idx).visible = true - } -} diff --git a/meshroom/ui/qml/Charts/InteractiveChartView.qml b/meshroom/ui/qml/Charts/InteractiveChartView.qml deleted file mode 100644 index 2434c0cbe3..0000000000 --- a/meshroom/ui/qml/Charts/InteractiveChartView.qml +++ /dev/null @@ -1,27 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import QtCharts - -ChartView { - id: root - antialiasing: true - - Rectangle { - id: plotZone - x: root.plotArea.x - y: root.plotArea.y - width: root.plotArea.width - height: root.plotArea.height - color: "transparent" - - MouseArea { - anchors.fill: parent - - property double degreeToScale: 1.0 / 120.0 // Default mouse scroll is 15 degree - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - onClicked: { - root.zoomReset() - } - } - } -} diff --git a/meshroom/ui/qml/Charts/qmldir b/meshroom/ui/qml/Charts/qmldir index e02eabbfe0..054e25e644 100644 --- a/meshroom/ui/qml/Charts/qmldir +++ b/meshroom/ui/qml/Charts/qmldir @@ -1,7 +1,5 @@ module Charts -ChartViewLegend 1.0 ChartViewLegend.qml ChartViewCheckBox 1.0 ChartViewCheckBox.qml -InteractiveChartView 1.0 InteractiveChartView.qml LineChart 1.0 LineChart.qml LineChartLegend 1.0 LineChartLegend.qml