Улучшить две шкалы, навигацию и формулы графиков
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
"""Local polynomial representation of the curves actually drawn on a plot."""
|
"""Local polynomial representation of the curves actually drawn on a plot."""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from functools import lru_cache
|
||||||
import math
|
import math
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ class FormulaPiece:
|
|||||||
+ f'{number(left)} ≤ x ≤ {number(self.right - origin)} {unit}').rstrip()
|
+ f'{number(left)} ≤ x ≤ {number(self.right - origin)} {unit}').rstrip()
|
||||||
|
|
||||||
|
|
||||||
def linear_piece(timestamps, values, x):
|
def linear_piece(timestamps, values, x, window=None):
|
||||||
"""A source polyline has a linear equation, not an inferred analytic model."""
|
"""A source polyline has a linear equation, not an inferred analytic model."""
|
||||||
previous = None
|
previous = None
|
||||||
for stamp, value in zip(timestamps, values):
|
for stamp, value in zip(timestamps, values):
|
||||||
@@ -48,11 +49,39 @@ def linear_piece(timestamps, values, x):
|
|||||||
if previous is not None:
|
if previous is not None:
|
||||||
t0, y0 = previous
|
t0, y0 = previous
|
||||||
if t0 <= x <= stamp and stamp > t0:
|
if t0 <= x <= stamp and stamp > t0:
|
||||||
|
if window is not None:
|
||||||
|
left, right = max(t0, window[0]), min(stamp, window[1])
|
||||||
|
if not left <= x <= right or left >= right:
|
||||||
|
return None
|
||||||
|
slope = (value - y0) / (stamp - t0)
|
||||||
|
return FormulaPiece(left, right, (y0 + slope * (left - t0), slope * (right - left)))
|
||||||
return FormulaPiece(t0, stamp, (y0, value - y0))
|
return FormulaPiece(t0, stamp, (y0, value - y0))
|
||||||
previous = stamp, value
|
previous = stamp, value
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=32)
|
||||||
|
def window_curve(curve, left, right):
|
||||||
|
"""Reconstruct a formula using only source samples in the focused window."""
|
||||||
|
from dataclasses import replace
|
||||||
|
from .plot_processing import process
|
||||||
|
request = curve.request
|
||||||
|
points = tuple((x, y) for x, y in request.series.points if left <= x <= right)
|
||||||
|
if points == request.series.points:
|
||||||
|
return curve
|
||||||
|
if len({x for x, _ in points}) < 2:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return process(replace(request, series=replace(request.series, points=points), x_range=(left, right)))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def window_piece(curve, x, left, right):
|
||||||
|
local = window_curve(curve, left, right)
|
||||||
|
return local.piece_at(x) if local is not None and left <= x <= right else None
|
||||||
|
|
||||||
|
|
||||||
def polynomial_coefficients(nodes, values):
|
def polynomial_coefficients(nodes, values):
|
||||||
"""Convert evaluations of an existing polynomial to power coefficients."""
|
"""Convert evaluations of an existing polynomial to power coefficients."""
|
||||||
divided = list(values)
|
divided = list(values)
|
||||||
|
|||||||
@@ -14,16 +14,16 @@ except ImportError:
|
|||||||
|
|
||||||
|
|
||||||
DRAG_HINT = ('ЛКМ рядом с маркером — двигать маркер; ЛКМ на свободном поле — '
|
DRAG_HINT = ('ЛКМ рядом с маркером — двигать маркер; ЛКМ на свободном поле — '
|
||||||
'перемещение по X; Shift + ЛКМ — перемещение по Y. '
|
'перемещение по X; Ctrl + ЛКМ или Shift + ЛКМ — перемещение по Y. '
|
||||||
'Щелчок мимо маркеров — одиночный курсор.')
|
'Щелчок мимо маркеров — одиночный курсор.')
|
||||||
|
|
||||||
|
|
||||||
def vertical_pan_requested(event):
|
def vertical_pan_requested(event):
|
||||||
return bool(event.modifiers() & Qt.ShiftModifier)
|
return bool(event.modifiers() & (Qt.ShiftModifier | Qt.ControlModifier))
|
||||||
|
|
||||||
|
|
||||||
def cursor_hit(plot, position, value, event, radius=8):
|
def cursor_hit(plot, position, value, event, radius=8):
|
||||||
"""Shift always pans; a cursor can only be grabbed near its visible line."""
|
"""Ctrl/Shift always pan; cursors are grabbed near their visible lines."""
|
||||||
return (not vertical_pan_requested(event) and value is not None
|
return (not vertical_pan_requested(event) and value is not None
|
||||||
and plot.core is not None and plot.has_plot_data
|
and plot.core is not None and plot.has_plot_data
|
||||||
and plot.visible_bounds.left <= value <= plot.visible_bounds.right
|
and plot.visible_bounds.left <= value <= plot.visible_bounds.right
|
||||||
@@ -34,6 +34,19 @@ def cursor_hit(plot, position, value, event, radius=8):
|
|||||||
class PlotDragInteraction:
|
class PlotDragInteraction:
|
||||||
"""Capture a hit marker or pan one axis, chosen at the start of the drag."""
|
"""Capture a hit marker or pan one axis, chosen at the start of the drag."""
|
||||||
|
|
||||||
|
def _capture_y_pan(self):
|
||||||
|
return self.viewport
|
||||||
|
|
||||||
|
def _apply_y_pan(self, fraction, initial):
|
||||||
|
if self.core and not self.lock_y:
|
||||||
|
self.viewport = initial.transform(self.core, pan_y=fraction)
|
||||||
|
|
||||||
|
def _navigation_pan_y(self, fraction):
|
||||||
|
self._apply_y_pan(fraction, self._capture_y_pan())
|
||||||
|
self._refresh_controls()
|
||||||
|
self.update()
|
||||||
|
self.view_changed.emit()
|
||||||
|
|
||||||
def mousePressEvent(self, event):
|
def mousePressEvent(self, event):
|
||||||
if event.button() != Qt.LeftButton or not self.core:
|
if event.button() != Qt.LeftButton or not self.core:
|
||||||
return super().mousePressEvent(event)
|
return super().mousePressEvent(event)
|
||||||
@@ -45,6 +58,7 @@ class PlotDragInteraction:
|
|||||||
self.setFocus(Qt.MouseFocusReason)
|
self.setFocus(Qt.MouseFocusReason)
|
||||||
self._press = QPointF(pos)
|
self._press = QPointF(pos)
|
||||||
self._press_viewport = self.viewport
|
self._press_viewport = self.viewport
|
||||||
|
self._press_y_pan = self._capture_y_pan()
|
||||||
self._press_markers = self.markers
|
self._press_markers = self.markers
|
||||||
self._press_bounds = self.visible_bounds
|
self._press_bounds = self.visible_bounds
|
||||||
self._pan_vertical = vertical_pan_requested(event)
|
self._pan_vertical = vertical_pan_requested(event)
|
||||||
@@ -80,10 +94,11 @@ class PlotDragInteraction:
|
|||||||
self.markers = self._press_markers.drag(self.core, marker,
|
self.markers = self._press_markers.drag(self.core, marker,
|
||||||
delta.y() if marker.horizontal else delta.x(),
|
delta.y() if marker.horizontal else delta.x(),
|
||||||
values.height() if marker.horizontal else rect.width(), self._press_bounds)
|
values.height() if marker.horizontal else rect.width(), self._press_bounds)
|
||||||
|
elif self._pan_vertical:
|
||||||
|
self._apply_y_pan(delta.y() / values.height(), self._press_y_pan)
|
||||||
else:
|
else:
|
||||||
self.viewport = self._press_viewport.transform(self.core,
|
self.viewport = self._press_viewport.transform(self.core,
|
||||||
pan_x=delta.x() / rect.width() if not self._pan_vertical and not self.lock_x else 0,
|
pan_x=delta.x() / rect.width() if not self.lock_x else 0)
|
||||||
pan_y=delta.y() / values.height() if self._pan_vertical and not self.lock_y else 0)
|
|
||||||
self._refresh_controls()
|
self._refresh_controls()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,20 @@ except ImportError:
|
|||||||
from protocan.plot import Bounds, Viewport
|
from protocan.plot import Bounds, Viewport
|
||||||
|
|
||||||
|
|
||||||
def split_amplitudes(series, minimum_ratio=10):
|
def small_amplitude(values, threshold=30):
|
||||||
"""Split at the largest decade gap; zero-valued channels join small ones."""
|
magnitude = max((abs(v) for v in values if math.isfinite(v)), default=None)
|
||||||
magnitudes = sorted((max((abs(v) for v in s.values if math.isfinite(v)), default=0), s.key)
|
return magnitude is not None and magnitude <= threshold
|
||||||
for s in series if s.visible and not s.discrete and len(s.values))
|
|
||||||
positive = [(m, key) for m, key in magnitudes if m > 0]
|
|
||||||
gaps = [(positive[i + 1][0] / positive[i][0], positive[i][0])
|
def split_amplitudes(series, threshold=30):
|
||||||
for i in range(len(positive) - 1)]
|
"""Channels with finite peak magnitude <= 30 use the right amplitude axis."""
|
||||||
if not gaps or max(gaps)[0] < minimum_ratio:
|
result = set()
|
||||||
return set()
|
for channel in series:
|
||||||
threshold = max(gaps)[1]
|
if not channel.visible or channel.discrete:
|
||||||
return {key for magnitude, key in magnitudes if magnitude <= threshold}
|
continue
|
||||||
|
if small_amplitude(channel.values, threshold):
|
||||||
|
result.add(channel.key)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
class DualAxisPlot:
|
class DualAxisPlot:
|
||||||
@@ -33,6 +36,7 @@ class DualAxisPlot:
|
|||||||
self.right_axis_keys = set()
|
self.right_axis_keys = set()
|
||||||
self.right_data_bounds = Bounds(0, 1, 0, 1)
|
self.right_data_bounds = Bounds(0, 1, 0, 1)
|
||||||
self.right_viewport = Viewport()
|
self.right_viewport = Viewport()
|
||||||
|
self.right_axis_limits = None
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def _sync_bounds(self):
|
def _sync_bounds(self):
|
||||||
@@ -43,7 +47,7 @@ class DualAxisPlot:
|
|||||||
super()._sync_bounds()
|
super()._sync_bounds()
|
||||||
|
|
||||||
def set_plot_bounds(self, bounds, has_data=True, x_origin=0):
|
def set_plot_bounds(self, bounds, has_data=True, x_origin=0):
|
||||||
if self.right_axis_keys:
|
if self.right_axis_keys and self.has_left_axis():
|
||||||
groups = [[], []]
|
groups = [[], []]
|
||||||
for series in self._series:
|
for series in self._series:
|
||||||
if series.visible and not series.discrete:
|
if series.visible and not series.discrete:
|
||||||
@@ -57,13 +61,50 @@ class DualAxisPlot:
|
|||||||
bounds, self.right_data_bounds = ranges
|
bounds, self.right_data_bounds = ranges
|
||||||
super().set_plot_bounds(bounds, has_data, x_origin)
|
super().set_plot_bounds(bounds, has_data, x_origin)
|
||||||
|
|
||||||
|
def has_left_axis(self):
|
||||||
|
return any(s.visible and not s.discrete and s.values and s.key not in self.right_axis_keys
|
||||||
|
for s in self._series)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def right_visible_bounds(self):
|
def right_visible_bounds(self):
|
||||||
|
if not self.has_left_axis():
|
||||||
|
return self.visible_bounds
|
||||||
right = (self.right_data_bounds.visible(self.core, self.right_viewport)
|
right = (self.right_data_bounds.visible(self.core, self.right_viewport)
|
||||||
if self.core else self.right_data_bounds)
|
if self.core else self.right_data_bounds)
|
||||||
left = self.visible_bounds
|
left = self.visible_bounds
|
||||||
|
if self.lock_y and self.right_axis_limits is not None:
|
||||||
|
return Bounds(left.left, left.right, *self.right_axis_limits)
|
||||||
return Bounds(left.left, left.right, right.bottom, right.top)
|
return Bounds(left.left, left.right, right.bottom, right.top)
|
||||||
|
|
||||||
|
def _axis_editor_values(self):
|
||||||
|
fields = super()._axis_editor_values()
|
||||||
|
if not self.right_axis_keys:
|
||||||
|
return fields
|
||||||
|
if not self.has_left_axis():
|
||||||
|
return fields[:2] + [("Y справа min", fields[2][1]),
|
||||||
|
("Y справа max", fields[3][1])]
|
||||||
|
right = self.right_visible_bounds
|
||||||
|
return fields[:2] + [("Y слева min", fields[2][1]),
|
||||||
|
("Y слева max", fields[3][1]),
|
||||||
|
("Y справа min", right.bottom),
|
||||||
|
("Y справа max", right.top)]
|
||||||
|
|
||||||
|
def _apply_axis_editor_values(self, values):
|
||||||
|
if len(values) == 6:
|
||||||
|
# Validate both ranges before changing either one.
|
||||||
|
Bounds(*values[:4]).validated(self.core)
|
||||||
|
right = Bounds(*values[:2], *values[4:]).validated(self.core)
|
||||||
|
self.right_axis_limits = (right.bottom, right.top)
|
||||||
|
super()._apply_axis_editor_values(values[:4])
|
||||||
|
|
||||||
|
def _lock_axis(self, axis, locked):
|
||||||
|
if axis == 'y' and locked and self.right_axis_limits is None:
|
||||||
|
bounds = self.right_visible_bounds
|
||||||
|
self.right_axis_limits = (bounds.bottom, bounds.top)
|
||||||
|
super()._lock_axis(axis, locked)
|
||||||
|
if axis == 'y' and not locked:
|
||||||
|
self.right_axis_limits = None
|
||||||
|
|
||||||
def series_bounds(self, series):
|
def series_bounds(self, series):
|
||||||
return self.right_visible_bounds if series.key in self.right_axis_keys else self.visible_bounds
|
return self.right_visible_bounds if series.key in self.right_axis_keys else self.visible_bounds
|
||||||
|
|
||||||
@@ -80,6 +121,15 @@ class DualAxisPlot:
|
|||||||
self.right_viewport = Viewport()
|
self.right_viewport = Viewport()
|
||||||
super().reset_viewport()
|
super().reset_viewport()
|
||||||
|
|
||||||
|
def _capture_y_pan(self):
|
||||||
|
return super()._capture_y_pan(), self.right_viewport
|
||||||
|
|
||||||
|
def _apply_y_pan(self, fraction, initial):
|
||||||
|
left, right = initial
|
||||||
|
super()._apply_y_pan(fraction, left)
|
||||||
|
if self.core and not self.lock_y and self.right_axis_keys and self.has_left_axis():
|
||||||
|
self.right_viewport = right.transform(self.core, pan_y=fraction)
|
||||||
|
|
||||||
def reset_interaction(self):
|
def reset_interaction(self):
|
||||||
self.right_viewport = Viewport()
|
self.right_viewport = Viewport()
|
||||||
super().reset_interaction()
|
super().reset_interaction()
|
||||||
@@ -98,7 +148,7 @@ class DualAxisPlot:
|
|||||||
if ticks and self.core and not self.lock_y:
|
if ticks and self.core and not self.lock_y:
|
||||||
factor = 1.25 ** max(-8, min(8, ticks / 120))
|
factor = 1.25 ** max(-8, min(8, ticks / 120))
|
||||||
self.setFocus(Qt.MouseFocusReason)
|
self.setFocus(Qt.MouseFocusReason)
|
||||||
if self.right_axis_keys and pos.x() >= rect.center().x():
|
if self.right_axis_keys and self.has_left_axis() and pos.x() >= rect.center().x():
|
||||||
self.right_viewport = self.right_viewport.transform(
|
self.right_viewport = self.right_viewport.transform(
|
||||||
self.core, 1, factor, focus_y=(pos.y() - rect.top()) / rect.height())
|
self.core, 1, factor, focus_y=(pos.y() - rect.top()) / rect.height())
|
||||||
self.update()
|
self.update()
|
||||||
@@ -111,14 +161,34 @@ class DualAxisPlot:
|
|||||||
super().paint_axes_and_markers(painter)
|
super().paint_axes_and_markers(painter)
|
||||||
if not self.right_axis_keys or not self.core:
|
if not self.right_axis_keys or not self.core:
|
||||||
return
|
return
|
||||||
rect, bounds = self.value_rect(), self.right_visible_bounds
|
paint_right_axis(painter, self.core, self.value_rect(), self.right_visible_bounds)
|
||||||
painter.save()
|
|
||||||
painter.setPen(QColor('#a4b8cd'))
|
|
||||||
step = self.core.tick_step(bounds.top - bounds.bottom, rect.height())
|
def paint_right_axis(painter, core, rect, bounds):
|
||||||
first = math.ceil(bounds.bottom / step)
|
painter.save()
|
||||||
count = min(100, max(0, math.floor(bounds.top / step) - first + 1))
|
painter.setPen(QColor('#a4b8cd'))
|
||||||
for index in range(count):
|
step = core.tick_step(bounds.top - bounds.bottom, rect.height() * 2)
|
||||||
value = (first + index) * step
|
first = math.ceil(bounds.bottom / step)
|
||||||
y = rect.top() + bounds.fraction(self.core, value, True) * rect.height()
|
count = min(100, max(0, math.floor(bounds.top / step) - first + 1))
|
||||||
painter.drawText(QPointF(rect.right() + 7, y + 4), f'{value:.4g}')
|
for index in range(count):
|
||||||
painter.restore()
|
value = (first + index) * step
|
||||||
|
y = rect.top() + bounds.fraction(core, value, True) * rect.height()
|
||||||
|
painter.drawText(QPointF(rect.right() + 7, y + 4), f'{value:.4g}')
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
|
||||||
|
class SingleAmplitudeAxis:
|
||||||
|
"""Apply the same side policy to a canvas exposing amplitude_values()."""
|
||||||
|
def has_left_axis(self):
|
||||||
|
return not small_amplitude(self.amplitude_values())
|
||||||
|
|
||||||
|
def plot_rect(self):
|
||||||
|
rect = super().plot_rect()
|
||||||
|
if not self.has_left_axis():
|
||||||
|
rect.setWidth(max(1, rect.width() - 72))
|
||||||
|
return rect
|
||||||
|
|
||||||
|
def paint_axes_and_markers(self, painter):
|
||||||
|
super().paint_axes_and_markers(painter)
|
||||||
|
if self.core and not self.has_left_axis():
|
||||||
|
paint_right_axis(painter, self.core, self.value_rect(), self.visible_bounds)
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ except ImportError:
|
|||||||
from PySide2.QtWidgets import QScrollArea
|
from PySide2.QtWidgets import QScrollArea
|
||||||
|
|
||||||
NAVIGATION_HINT = ('Колесо и +/− — масштаб X; Ctrl + колесо и Ctrl + +/− — масштаб Y. '
|
NAVIGATION_HINT = ('Колесо и +/− — масштаб X; Ctrl + колесо и Ctrl + +/− — масштаб Y. '
|
||||||
'ЛКМ на свободном поле — перемещение; ←/→ — перемещение по X; Home/End — начало/конец.')
|
'ЛКМ — перемещение по X; Ctrl + ЛКМ — по Y; '
|
||||||
|
'←/→ — перемещение по X; ↑/↓ — по Y; Home/End — начало/конец.')
|
||||||
|
|
||||||
|
|
||||||
class PlotNavigation:
|
class PlotNavigation:
|
||||||
@@ -29,6 +30,8 @@ class PlotNavigation:
|
|||||||
bool(modifiers & Qt.ControlModifier))
|
bool(modifiers & Qt.ControlModifier))
|
||||||
elif key in (Qt.Key_Left, Qt.Key_Right) and not (modifiers & Qt.ControlModifier):
|
elif key in (Qt.Key_Left, Qt.Key_Right) and not (modifiers & Qt.ControlModifier):
|
||||||
self._navigation_pan(-.1 if key == Qt.Key_Left else .1)
|
self._navigation_pan(-.1 if key == Qt.Key_Left else .1)
|
||||||
|
elif key in (Qt.Key_Up, Qt.Key_Down):
|
||||||
|
self._navigation_pan_y(-.1 if key == Qt.Key_Up else .1)
|
||||||
elif key in (Qt.Key_Home, Qt.Key_End):
|
elif key in (Qt.Key_Home, Qt.Key_End):
|
||||||
self._navigation_edge(key == Qt.Key_End)
|
self._navigation_edge(key == Qt.Key_End)
|
||||||
else:
|
else:
|
||||||
@@ -80,6 +83,12 @@ class ScrollPlotNavigation(PlotNavigation):
|
|||||||
bar.setValue(round(bar.value() + fraction * bar.pageStep()))
|
bar.setValue(round(bar.value() + fraction * bar.pageStep()))
|
||||||
self.navigation_panned()
|
self.navigation_panned()
|
||||||
|
|
||||||
|
def _navigation_pan_y(self, fraction):
|
||||||
|
scroll = self.scroll_area()
|
||||||
|
if scroll is not None:
|
||||||
|
bar = scroll.verticalScrollBar()
|
||||||
|
bar.setValue(round(bar.value() + fraction * bar.pageStep()))
|
||||||
|
|
||||||
def _navigation_edge(self, end):
|
def _navigation_edge(self, end):
|
||||||
scroll = self.scroll_area()
|
scroll = self.scroll_area()
|
||||||
if scroll is not None:
|
if scroll is not None:
|
||||||
@@ -113,4 +122,3 @@ class ScrollPlotNavigation(PlotNavigation):
|
|||||||
self.move_pan(event)
|
self.move_pan(event)
|
||||||
self._pan_position = None
|
self._pan_position = None
|
||||||
self.unsetCursor()
|
self.unsetCursor()
|
||||||
|
|
||||||
|
|||||||
@@ -336,11 +336,13 @@ class PlotProcessingAttachment(QObject):
|
|||||||
self._dirty = True
|
self._dirty = True
|
||||||
self._refresh_timer.start(0)
|
self._refresh_timer.start(0)
|
||||||
|
|
||||||
def formula_entries(self, x, origin=0., unit=''):
|
def formula_entries(self, x, origin=0., unit='', window=None):
|
||||||
|
from set_devices.plot_formula import window_piece
|
||||||
entries = []
|
entries = []
|
||||||
offset = self._external_offset if self._external_curve is not None else 0.
|
offset = self._external_offset if self._external_curve is not None else 0.
|
||||||
for curve in self.curves:
|
for curve in self.curves:
|
||||||
piece = curve.piece_at(x + offset)
|
piece = (window_piece(curve, x + offset, window[0] + offset, window[1] + offset)
|
||||||
|
if window is not None else curve.piece_at(x + offset))
|
||||||
if piece is not None:
|
if piece is not None:
|
||||||
color_index = self.panel._result_ids[id(curve)] if self._external_curve is None else 0
|
color_index = self.panel._result_ids[id(curve)] if self._external_curve is None else 0
|
||||||
text = curve.label + '\n' + piece.text(origin + offset, unit)
|
text = curve.label + '\n' + piece.text(origin + offset, unit)
|
||||||
@@ -369,7 +371,7 @@ class PlotProcessingAttachment(QObject):
|
|||||||
self.dialog.raise_()
|
self.dialog.raise_()
|
||||||
self.dialog.activateWindow()
|
self.dialog.activateWindow()
|
||||||
|
|
||||||
def paint(self, painter, rect, project):
|
def paint(self, painter, rect, project, project_series=None):
|
||||||
curves = self.curves
|
curves = self.curves
|
||||||
if not curves:
|
if not curves:
|
||||||
return
|
return
|
||||||
@@ -378,7 +380,9 @@ class PlotProcessingAttachment(QObject):
|
|||||||
for curve_index, curve in enumerate(curves):
|
for curve_index, curve in enumerate(curves):
|
||||||
path = QPainterPath()
|
path = QPainterPath()
|
||||||
for index, (x, y) in enumerate(curve.points):
|
for index, (x, y) in enumerate(curve.points):
|
||||||
point = project(x - (self._external_offset if self._external_curve is not None else 0), y, rect)
|
stamp = x - (self._external_offset if self._external_curve is not None else 0)
|
||||||
|
point = (project_series(curve.request.series, stamp, y, rect) if project_series
|
||||||
|
else project(stamp, y, rect))
|
||||||
path.moveTo(point) if index == 0 else path.lineTo(point)
|
path.moveTo(point) if index == 0 else path.lineTo(point)
|
||||||
color_index = self.panel._result_ids[id(curve)] if self._external_curve is None else 0
|
color_index = self.panel._result_ids[id(curve)] if self._external_curve is None else 0
|
||||||
color = QColor(RESULT_COLORS[color_index % len(RESULT_COLORS)])
|
color = QColor(RESULT_COLORS[color_index % len(RESULT_COLORS)])
|
||||||
|
|||||||
Reference in New Issue
Block a user