nanovna-saver/src/NanoVNASaver/Charts/Chart.py

352 wiersze
11 KiB
Python
Czysty Zwykły widok Historia

# NanoVNASaver
2020-06-25 17:52:30 +00:00
#
# A python program to view and export Touchstone data from a NanoVNA
2020-06-25 17:52:30 +00:00
# Copyright (C) 2019, 2020 Rune B. Broberg
2021-06-30 05:21:14 +00:00
# Copyright (C) 2020,2021 NanoVNA-Saver Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging
2021-06-26 21:08:56 +00:00
from dataclasses import dataclass, field, replace
2022-09-15 05:53:08 +00:00
from typing import List, Set, Tuple, ClassVar, Any, Optional
2023-03-12 07:02:58 +00:00
from PyQt6 import QtWidgets, QtGui, QtCore
from PyQt6.QtCore import pyqtSignal, Qt
from PyQt6.QtGui import QColor, QColorConstants, QAction
from NanoVNASaver import Defaults
from NanoVNASaver.RFTools import Datapoint
from NanoVNASaver.Marker.Widget import Marker
2021-06-26 21:08:56 +00:00
logger = logging.getLogger(__name__)
2021-07-06 15:01:20 +00:00
2021-06-26 21:08:56 +00:00
@dataclass
2021-06-27 13:08:01 +00:00
class ChartColors: # pylint: disable=too-many-instance-attributes
2023-03-12 07:02:58 +00:00
background: QColor = field(
default_factory=lambda: QColor(QColorConstants.White)
)
foreground: QColor = field(
2023-03-12 07:02:58 +00:00
default_factory=lambda: QColor(QColorConstants.LightGray)
2023-03-08 08:40:39 +00:00
)
reference: QColor = field(default_factory=lambda: QColor(0, 0, 255, 64))
reference_secondary: QColor = field(
2023-03-08 08:40:39 +00:00
default_factory=lambda: QColor(0, 0, 192, 48)
)
2023-03-12 07:02:58 +00:00
sweep: QColor = field(
default_factory=lambda: QColor(QColorConstants.DarkYellow)
)
sweep_secondary: QColor = field(
2023-03-12 07:02:58 +00:00
default_factory=lambda: QColor(QColorConstants.DarkMagenta)
2023-03-08 08:40:39 +00:00
)
swr: QColor = field(default_factory=lambda: QColor(255, 0, 0, 128))
2023-03-12 07:02:58 +00:00
text: QColor = field(default_factory=lambda: QColor(QColorConstants.Black))
bands: QColor = field(default_factory=lambda: QColor(128, 128, 128, 48))
2021-06-26 21:08:56 +00:00
2022-09-15 05:53:08 +00:00
2021-06-26 22:55:43 +00:00
@dataclass
class ChartDimensions:
height: int = 200
height_min: int = 200
width: int = 200
width_min: int = 200
line: int = 1
point: int = 2
2022-09-15 05:53:08 +00:00
2021-06-27 08:59:07 +00:00
@dataclass
class ChartDragBox:
2022-09-15 05:53:08 +00:00
pos: Tuple[int] = (-1, -1)
2021-06-27 08:59:07 +00:00
pos_start: Tuple[int] = (0, 0)
state: bool = False
move_x: int = -1
move_y: int = -1
2022-09-15 05:53:08 +00:00
2021-06-27 08:59:07 +00:00
@dataclass
class ChartFlags:
draw_lines: bool = False
is_popout: bool = False
2021-06-26 22:55:43 +00:00
2021-07-06 07:25:20 +00:00
2021-07-05 19:09:43 +00:00
class ChartMarker(QtWidgets.QWidget):
2021-07-06 15:01:20 +00:00
def __init__(self, qp: QtGui.QPaintDevice):
2021-07-05 19:09:43 +00:00
super().__init__()
self.qp = qp
2021-07-06 07:25:20 +00:00
def draw(self, x: int, y: int, color: QtGui.QColor, text: str = ""):
2022-09-15 15:37:41 +00:00
offset = int(Defaults.cfg.chart.marker_size // 2)
if Defaults.cfg.chart.marker_at_tip:
2021-07-06 15:01:20 +00:00
y -= offset
2021-07-05 19:09:43 +00:00
pen = QtGui.QPen(color)
self.qp.setPen(pen)
qpp = QtGui.QPainterPath()
2021-07-06 15:01:20 +00:00
qpp.moveTo(x, y + offset)
qpp.lineTo(x - offset, y - offset)
qpp.lineTo(x + offset, y - offset)
qpp.lineTo(x, y + offset)
2021-07-05 19:09:43 +00:00
if Defaults.cfg.chart.marker_filled:
2021-07-05 19:09:43 +00:00
self.qp.fillPath(qpp, color)
else:
self.qp.drawPath(qpp)
if text and Defaults.cfg.chart.marker_label:
2021-07-06 15:01:20 +00:00
text_width = self.qp.fontMetrics().horizontalAdvance(text)
2023-03-08 08:40:39 +00:00
self.qp.drawText(x - int(text_width // 2), y - 3 - offset, text)
2021-07-05 19:09:43 +00:00
2021-06-27 08:59:07 +00:00
class Chart(QtWidgets.QWidget):
2021-06-26 22:55:43 +00:00
bands: ClassVar[Any] = None
popoutRequested: ClassVar[Any] = pyqtSignal(object)
2021-07-06 07:25:20 +00:00
color: ClassVar[ChartColors] = ChartColors()
def __init__(self, name):
super().__init__()
self.name = name
2023-03-08 08:40:39 +00:00
self.sweepTitle = ""
2021-06-27 08:59:07 +00:00
self.leftMargin = 30
self.rightMargin = 20
self.bottomMargin = 20
self.topMargin = 30
2021-06-26 22:55:43 +00:00
self.dim = ChartDimensions()
2021-06-27 08:59:07 +00:00
self.dragbox = ChartDragBox()
self.flag = ChartFlags()
self.draggedMarker = None
2021-06-26 21:08:56 +00:00
self.data: List[Datapoint] = []
self.reference: List[Datapoint] = []
2021-06-27 08:59:07 +00:00
2021-06-26 21:08:56 +00:00
self.markers: List[Marker] = []
self.swrMarkers: Set[float] = set()
2023-03-12 07:02:58 +00:00
self.action_popout = QAction("Popout chart")
2022-09-15 05:53:08 +00:00
self.action_popout.triggered.connect(
2023-03-08 08:40:39 +00:00
lambda: self.popoutRequested.emit(self)
)
self.addAction(self.action_popout)
2019-09-29 18:30:25 +00:00
2023-03-12 07:02:58 +00:00
self.action_save_screenshot = QAction("Save image")
2021-06-26 21:08:56 +00:00
self.action_save_screenshot.triggered.connect(self.saveScreenshot)
self.addAction(self.action_save_screenshot)
2023-03-12 07:02:58 +00:00
self.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
2019-10-13 15:35:32 +00:00
def setReference(self, data):
self.reference = data
self.update()
def resetReference(self):
self.reference = []
self.update()
def setData(self, data):
self.data = data
self.update()
def setMarkers(self, markers):
self.markers = markers
def setBands(self, bands):
self.bands = bands
def setLineThickness(self, thickness):
2021-06-26 22:55:43 +00:00
self.dim.line = thickness
self.update()
def setPointSize(self, size):
2021-06-26 22:55:43 +00:00
self.dim.point = size
self.update()
2019-10-29 11:47:30 +00:00
def setMarkerSize(self, size):
Defaults.cfg.chart.marker_size = size
2019-10-29 11:47:30 +00:00
self.update()
def setSweepTitle(self, title):
self.sweepTitle = title
self.update()
def getActiveMarker(self) -> Marker:
if self.draggedMarker is not None:
return self.draggedMarker
return next(
(
m
for m in self.markers
if m.isMouseControlledRadioButton.isChecked()
),
None,
)
2022-09-15 05:53:08 +00:00
def getNearestMarker(self, x, y) -> Optional[Marker]:
if not self.data:
return None
shortest = 10**6
nearest = None
for m in self.markers:
mx, my = self.getPosition(self.data[m.location])
2021-07-05 09:37:48 +00:00
distance = abs(complex(x - mx, y - my))
if distance < shortest:
shortest = distance
nearest = m
return nearest
2021-06-22 20:07:36 +00:00
def getPosition(self, d: Datapoint) -> Tuple[int, int]:
return self.getXPosition(d), self.getYPosition(d)
def setDrawLines(self, draw_lines):
2021-06-27 08:59:07 +00:00
self.flag.draw_lines = draw_lines
self.update()
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
2023-03-12 07:02:58 +00:00
if event.buttons() == Qt.MouseButton.RightButton:
event.ignore()
return
2023-03-12 07:02:58 +00:00
if event.buttons() == Qt.MouseButton.MiddleButton:
2019-11-16 11:07:32 +00:00
# Drag event
event.accept()
self.dragbox.move_x = event.position().x()
self.dragbox.move_y = event.position().y()
2019-11-16 11:07:32 +00:00
return
2023-03-12 07:02:58 +00:00
if event.modifiers() == Qt.KeyboardModifier.ControlModifier:
2019-11-16 11:07:32 +00:00
event.accept()
2021-06-27 08:59:07 +00:00
self.dragbox.state = True
self.dragbox.pos_start = (
event.position().x(),
event.position().y(),
)
return
2023-03-12 07:02:58 +00:00
if event.modifiers() == Qt.KeyboardModifier.ShiftModifier:
self.draggedMarker = self.getNearestMarker(
event.position().x(), event.position().y()
)
self.mouseMoveEvent(event)
2021-07-05 09:37:48 +00:00
def mouseReleaseEvent(self, a0: QtGui.QMouseEvent):
self.draggedMarker = None
2021-06-27 08:59:07 +00:00
if self.dragbox.state:
2022-09-15 05:53:08 +00:00
self.zoomTo(
2022-09-15 19:05:07 +00:00
self.dragbox.pos_start[0],
self.dragbox.pos_start[1],
2023-03-12 07:02:58 +00:00
a0.position().x(),
a0.position().y(),
2023-03-08 08:40:39 +00:00
)
2021-06-27 08:59:07 +00:00
self.dragbox.state = False
self.dragbox.pos = (-1, -1)
self.dragbox.pos_start = (0, 0)
self.update()
2022-05-28 19:48:13 +00:00
def wheelEvent(self, a0: QtGui.QWheelEvent) -> None:
delta = a0.angleDelta().y()
if not delta or (not self.data and not self.reference):
a0.ignore()
return
modifiers = a0.modifiers()
2023-03-12 07:02:58 +00:00
zoom_x = modifiers != Qt.KeyboardModifier.ShiftModifier
zoom_y = modifiers != Qt.KeyboardModifier.ControlModifier
2022-05-28 19:48:13 +00:00
rate = -delta / 120
# zooming in 10% increments and 9% complementary
divisor = 10 if delta > 0 else 9
factor_x = rate * self.dim.width / divisor if zoom_x else 0
factor_y = rate * self.dim.height / divisor if zoom_y else 0
2023-03-12 07:02:58 +00:00
abs_x = max(0, a0.position().x() - self.leftMargin)
abs_y = max(0, a0.position().y() - self.topMargin)
2022-05-28 19:48:13 +00:00
ratio_x = abs_x / self.dim.width
ratio_y = abs_y / self.dim.height
self.zoomTo(
int(self.leftMargin + ratio_x * factor_x),
int(self.topMargin + ratio_y * factor_y),
int(self.leftMargin + self.dim.width - (1 - ratio_x) * factor_x),
2023-03-08 08:40:39 +00:00
int(self.topMargin + self.dim.height - (1 - ratio_y) * factor_y),
2022-05-28 19:48:13 +00:00
)
a0.accept()
def zoomTo(self, x1, y1, x2, y2):
2021-07-05 09:37:48 +00:00
raise NotImplementedError()
2019-09-29 18:30:25 +00:00
def saveScreenshot(self):
logger.info("Saving %s to file...", self.name)
2021-06-27 13:08:01 +00:00
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
2023-03-08 08:40:39 +00:00
parent=self,
caption="Save image",
filter="PNG (*.png);;All files (*.*)",
)
2019-09-29 18:30:25 +00:00
logger.debug("Filename: %s", filename)
2021-06-27 13:08:01 +00:00
if not filename:
return
if not QtCore.QFileInfo(filename).suffix():
filename += ".png"
self.grab().save(filename)
2019-09-29 18:30:25 +00:00
def copy(self):
new_chart = self.__class__(self.name)
new_chart.data = self.data
new_chart.reference = self.reference
2021-06-27 08:59:07 +00:00
new_chart.dim = replace(self.dim)
new_chart.flag = replace(self.flag)
new_chart.markers = self.markers
2019-10-13 15:35:32 +00:00
new_chart.swrMarkers = self.swrMarkers
new_chart.bands = self.bands
2021-06-27 08:59:07 +00:00
new_chart.resize(self.width(), self.height())
2021-06-26 22:55:43 +00:00
new_chart.setPointSize(self.dim.point)
new_chart.setLineThickness(self.dim.line)
return new_chart
2019-10-13 15:35:32 +00:00
def addSWRMarker(self, swr: float):
self.swrMarkers.add(swr)
self.update()
def removeSWRMarker(self, swr: float):
try:
self.swrMarkers.remove(swr)
except KeyError:
logger.debug("KeyError from %s", self.name)
finally:
self.update()
def clearSWRMarkers(self):
self.swrMarkers.clear()
self.update()
2022-05-28 19:48:13 +00:00
@staticmethod
2023-03-08 08:40:39 +00:00
def drawMarker(
x: int, y: int, qp: QtGui.QPainter, color: QtGui.QColor, number: int = 0
):
2021-07-06 15:01:20 +00:00
cmarker = ChartMarker(qp)
2022-05-28 19:48:13 +00:00
cmarker.draw(x, y, color, f"{number}")
2019-10-29 13:21:22 +00:00
def drawTitle(self, qp: QtGui.QPainter, position: QtCore.QPoint = None):
2021-07-06 15:01:20 +00:00
qp.setPen(Chart.color.text)
2021-06-27 13:08:01 +00:00
if position is None:
qf = QtGui.QFontMetricsF(self.font())
width = qf.boundingRect(self.sweepTitle).width()
2022-09-15 05:53:08 +00:00
position = QtCore.QPointF(self.width() / 2 - width / 2, 15)
2021-06-27 13:08:01 +00:00
qp.drawText(position, self.sweepTitle)
2021-07-06 07:25:20 +00:00
def update(self):
pal = self.palette()
2023-03-12 07:02:58 +00:00
pal.setColor(QtGui.QPalette.ColorRole.Window, Chart.color.background)
2021-07-06 07:25:20 +00:00
self.setPalette(pal)
super().update()