6import QGroundControl.Controls
8/// Shared chart base for the Log Viewer.
10/// Provides: GraphsView with QGC-themed GraphsTheme, a HH:MM:SS x-axis
11/// labelDelegate, zoom/cursor interaction, position marker line, and
12/// LogViewerCursorPopup.
14/// Derived charts (LogViewerChart, LogViewerAltChart) embed this as an Item,
15/// connect to zoomRangeSet / cursorPositionSet to refresh their series data,
16/// and supply popup extra rows as declarative children (forwarded via the
17/// default property alias to the popup's extraContent).
21 // -------------------------------------------------------------------------
23 // -------------------------------------------------------------------------
24 required property var logParser
26 // -------------------------------------------------------------------------
28 // -------------------------------------------------------------------------
29 property bool xAxisShowLocalTime: false
30 property string yAxisTitle: ""
31 property real popupYOffset: 0 ///< added to popup y (alt chart offsets slightly)
33 // -------------------------------------------------------------------------
34 // Read-only aliases to child items — derived charts access these
35 // -------------------------------------------------------------------------
36 readonly property alias graphsView: _chart
37 readonly property alias xAxis: _xAxis
38 readonly property alias yAxis: _yAxis
39 readonly property alias colorBlockWidth: _popup.colorBlockWidth
40 readonly property real popupWidth: _popup.implicitWidth
42 // -------------------------------------------------------------------------
43 // Default property — children declared inside LogViewerBaseChart { } go
44 // into the popup's extraContent column (chart-specific popup rows).
45 // -------------------------------------------------------------------------
46 default property alias content: _popup.extraContent
48 // -------------------------------------------------------------------------
50 // -------------------------------------------------------------------------
52 /// Emitted only on user-driven cursor interaction.
53 /// Do NOT re-emit this when handling an external sync (setSharedCursor).
54 signal cursorMoved(real t)
56 /// Emitted only on user-driven zoom.
57 /// Do NOT re-emit this when handling an external sync (setSharedZoom).
58 signal zoomApplied(real minX, real maxX)
60 /// Emitted whenever the zoom range changes (user-driven or external sync).
61 /// Derived charts connect here to refresh their series data.
62 signal zoomRangeSet(real minX, real maxX)
64 /// Emitted whenever the cursor position changes (user-driven or external sync).
65 /// Derived charts connect here to update popup values (e.g. _markerAltValue).
66 signal cursorPositionSet(real t)
68 // -------------------------------------------------------------------------
69 // State — writable by base, readable by derived charts
70 // -------------------------------------------------------------------------
71 property bool markerVisible: false
72 property real markerPixelX: 0
73 property real markerXValue: 0
74 property real fullMinX: 0
75 property real fullMaxX: 1
76 property real zoomMinX: 0
77 property real zoomMaxX: 1
79 // -------------------------------------------------------------------------
81 // -------------------------------------------------------------------------
83 /// Set the full and zoom range (called on log load).
84 /// Emits zoomRangeSet so derived charts can refresh their series.
85 function initRange(minX, maxX) {
86 // Small margin each side so data doesn't touch the edges; timestamps are time since boot so never below 0
87 const pad = (maxX - minX) * 0.025
88 const paddedMinX = Math.max(0, minX - pad)
89 const paddedMaxX = maxX + pad
94 _xAxis.min = paddedMinX
95 _xAxis.max = paddedMaxX
96 zoomRangeSet(paddedMinX, paddedMaxX)
99 /// User-driven zoom — also emits zoomApplied for cross-chart sync.
100 function applyZoomRange(minX, maxX) {
101 _applyZoomInternal(minX, maxX)
102 zoomApplied(minX, maxX)
105 /// External zoom sync — does not re-emit zoomApplied.
106 function setSharedZoom(minX, maxX) {
107 _applyZoomInternal(minX, maxX)
110 /// Place the cursor at axis value t and emit cursorPositionSet.
111 /// Does NOT emit cursorMoved — use for both external sync and initial placement.
112 function setCursor(t) {
113 if (_xAxis.max <= _xAxis.min) return
115 markerPixelX = _axisXToPixel(t)
120 /// External cursor sync — delegates to setCursor (no cursorMoved emitted).
121 function setSharedCursor(t) {
125 function resetZoom() {
126 applyZoomRange(fullMinX, fullMaxX)
129 function clearCursor() {
130 markerVisible = false
133 // -------------------------------------------------------------------------
135 // -------------------------------------------------------------------------
136 function _pixelToAxisX(pixelX) {
137 const plotX = _chart.plotArea.x
138 const plotW = _chart.plotArea.width
139 if (plotW <= 0 || _xAxis.max <= _xAxis.min) return _xAxis.min
140 const ratio = (Math.max(plotX, Math.min(plotX + plotW, pixelX)) - plotX) / plotW
141 return _xAxis.min + ratio * (_xAxis.max - _xAxis.min)
144 function _axisXToPixel(axisX) {
145 const plotX = _chart.plotArea.x
146 const plotW = _chart.plotArea.width
147 if (plotW <= 0 || _xAxis.max <= _xAxis.min) return plotX
148 const ratio = (axisX - _xAxis.min) / (_xAxis.max - _xAxis.min)
149 return plotX + Math.max(0, Math.min(plotW, ratio * plotW))
152 function _applyZoomInternal(minX, maxX) {
153 if (maxX <= minX) return
158 if (markerVisible && (markerXValue < minX || markerXValue > maxX)) {
159 markerXValue = (minX + maxX) / 2
161 markerPixelX = _axisXToPixel(markerXValue)
162 zoomRangeSet(minX, maxX)
165 function _updateCursorFromInteraction(pixelX) {
166 if (_xAxis.max <= _xAxis.min) return
167 const plotX = _chart.plotArea.x
168 const plotW = _chart.plotArea.width
170 markerPixelX = Math.max(plotX, Math.min(plotX + plotW, pixelX))
171 markerXValue = _pixelToAxisX(markerPixelX)
172 cursorPositionSet(markerXValue)
173 cursorMoved(markerXValue)
177 const plotMidX = _chart.plotArea.x + _chart.plotArea.width / 2
178 if (markerPixelX < plotMidX) {
179 const rightX = _chart.plotArea.x + _chart.plotArea.width - _popup.width
180 return Math.max(0, Math.min(rightX, control.width - _popup.width))
182 return _chart.plotArea.x + _popup._margin
186 // -------------------------------------------------------------------------
188 // -------------------------------------------------------------------------
189 QGCPalette { id: qgcPal }
200 colorScheme: qgcPal.globalTheme === QGCPalette.Light ? GraphsTheme.ColorScheme.Light : GraphsTheme.ColorScheme.Dark
201 backgroundColor: qgcPal.windowShadeDark
202 backgroundVisible: true
203 plotAreaBackgroundColor: qgcPal.windowShadeDark
204 grid.mainColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.25)
205 grid.subColor: Qt.rgba(qgcPal.text.r, qgcPal.text.g, qgcPal.text.b, 0.12)
207 labelBackgroundVisible: false
208 labelTextColor: qgcPal.text
209 axisXLabelFont.family: ScreenTools.fixedFontFamily
210 axisXLabelFont.pointSize: ScreenTools.smallFontPointSize
211 axisYLabelFont.family: ScreenTools.fixedFontFamily
212 axisYLabelFont.pointSize: ScreenTools.smallFontPointSize
217 titleText: xAxisShowLocalTime ? qsTr("Time (local)") : qsTr("Elapsed")
222 labelDelegate: Component {
224 property string text: "" // raw seconds value, assigned by axis
227 anchors.centerIn: parent
228 color: _chart.theme.labelTextColor
229 font: _chart.theme.axisXLabelFont
230 horizontalAlignment: Text.AlignHCenter
232 const secs = parseFloat(parent.text)
233 if (isNaN(secs)) { return parent.text }
234 if (xAxisShowLocalTime) {
235 const st = logParser.startTime
236 if (st && !isNaN(st.getTime()) && st.getTime() > 0) {
237 const use12h = Qt.locale().timeFormat(Locale.ShortFormat).indexOf("a") >= 0
238 || Qt.locale().timeFormat(Locale.ShortFormat).indexOf("A") >= 0
239 return Qt.formatTime(new Date(st.getTime() + secs * 1000),
240 use12h ? "h:mm:ss AP" : "HH:mm:ss")
243 const wholeSecs = Math.floor(secs)
244 const hh = Math.floor(wholeSecs / 3600)
245 const mm = Math.floor((wholeSecs % 3600) / 60)
246 const ss = wholeSecs % 60
248 return hh + ":" + String(mm).padStart(2, "0") + ":" + String(ss).padStart(2, "0")
250 return mm + ":" + String(ss).padStart(2, "0")
261 titleText: yAxisTitle
267 // Recalculate marker pixel position when the chart is resized or laid out.
270 function onPlotAreaChanged() {
271 if (control.markerVisible) {
272 control.markerPixelX = control._axisXToPixel(control.markerXValue)
277 // Zoom selection rectangle
281 color: Qt.rgba(1, 1, 1, 0.2)
282 border.color: qgcPal.buttonHighlight
289 acceptedButtons: Qt.LeftButton
290 enabled: _xAxis.max > _xAxis.min
293 property real _dragStartX: 0
294 property bool _isDraggingCursor: false
295 property bool _isDraggingZoom: false
296 property bool _wasDoubleClick: false
298 onPressed: (mouse) => {
299 _isDraggingCursor = false
300 _isDraggingZoom = false
301 _wasDoubleClick = false
302 if (mouse.modifiers & Qt.ShiftModifier) {
303 // Shift pressed — start cursor drag
304 _isDraggingCursor = true
305 control._updateCursorFromInteraction(mouse.x)
307 // Plain press — start zoom selection
308 _dragStartX = mouse.x
309 _zoomRect.x = mouse.x
310 _zoomRect.y = _chart.plotArea.y
312 _zoomRect.height = _chart.plotArea.height
313 _zoomRect.visible = false // only show once drag is meaningful
317 onPositionChanged: (mouse) => {
319 if (_isDraggingCursor) {
320 control._updateCursorFromInteraction(mouse.x)
322 // Plain drag — update zoom selection rect
323 const left = Math.min(_dragStartX, mouse.x)
324 const right = Math.max(_dragStartX, mouse.x)
326 _zoomRect.width = Math.max(0, right - left)
327 if (_zoomRect.width >= ScreenTools.defaultFontPixelWidth * 0.5) {
328 _isDraggingZoom = true
329 _zoomRect.visible = true
334 onReleased: (mouse) => {
335 if (_isDraggingCursor) {
336 _isDraggingCursor = false
339 _zoomRect.visible = false
340 if (_isDraggingZoom) {
341 _isDraggingZoom = false
342 const leftX = control._pixelToAxisX(_zoomRect.x)
343 const rightX = control._pixelToAxisX(_zoomRect.x + _zoomRect.width)
344 control.applyZoomRange(Math.min(leftX, rightX), Math.max(leftX, rightX))
345 } else if (!_wasDoubleClick) {
346 // Tap — place cursor (suppressed on the second click of a double-click)
347 control._updateCursorFromInteraction(mouse.x)
351 onDoubleClicked: (mouse) => {
352 // onDoubleClicked fires before onReleased for the second click,
353 // so this flag prevents onReleased from repositioning the cursor.
354 _wasDoubleClick = true
356 const centerT = (control.fullMinX + control.fullMaxX) / 2
357 control.setCursor(centerT)
358 control.cursorMoved(centerT)
362 // Position marker line
364 visible: markerVisible
368 height: _chart.plotArea.height
373 // Cursor popup — chart-specific rows are injected via default property alias
374 LogViewerCursorPopup {
376 visible: markerVisible
378 y: _chart.plotArea.y + popupYOffset
380 cursorXValue: markerXValue
381 logParser: control.logParser
382 xAxisShowLocalTime: control.xAxisShowLocalTime
383 zoomMinX: control.zoomMinX
384 zoomMaxX: control.zoomMaxX
385 plotAreaWidth: _chart.plotArea.width