Улучшить две шкалы, навигацию и формулы графиков
This commit is contained in:
@@ -10,17 +10,20 @@ except ImportError:
|
||||
from protocan.plot import Bounds, Viewport
|
||||
|
||||
|
||||
def split_amplitudes(series, minimum_ratio=10):
|
||||
"""Split at the largest decade gap; zero-valued channels join small ones."""
|
||||
magnitudes = sorted((max((abs(v) for v in s.values if math.isfinite(v)), default=0), s.key)
|
||||
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])
|
||||
for i in range(len(positive) - 1)]
|
||||
if not gaps or max(gaps)[0] < minimum_ratio:
|
||||
return set()
|
||||
threshold = max(gaps)[1]
|
||||
return {key for magnitude, key in magnitudes if magnitude <= threshold}
|
||||
def small_amplitude(values, threshold=30):
|
||||
magnitude = max((abs(v) for v in values if math.isfinite(v)), default=None)
|
||||
return magnitude is not None and magnitude <= threshold
|
||||
|
||||
|
||||
def split_amplitudes(series, threshold=30):
|
||||
"""Channels with finite peak magnitude <= 30 use the right amplitude axis."""
|
||||
result = set()
|
||||
for channel in series:
|
||||
if not channel.visible or channel.discrete:
|
||||
continue
|
||||
if small_amplitude(channel.values, threshold):
|
||||
result.add(channel.key)
|
||||
return result
|
||||
|
||||
|
||||
class DualAxisPlot:
|
||||
@@ -33,6 +36,7 @@ class DualAxisPlot:
|
||||
self.right_axis_keys = set()
|
||||
self.right_data_bounds = Bounds(0, 1, 0, 1)
|
||||
self.right_viewport = Viewport()
|
||||
self.right_axis_limits = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _sync_bounds(self):
|
||||
@@ -43,7 +47,7 @@ class DualAxisPlot:
|
||||
super()._sync_bounds()
|
||||
|
||||
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 = [[], []]
|
||||
for series in self._series:
|
||||
if series.visible and not series.discrete:
|
||||
@@ -57,13 +61,50 @@ class DualAxisPlot:
|
||||
bounds, self.right_data_bounds = ranges
|
||||
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
|
||||
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)
|
||||
if self.core else self.right_data_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)
|
||||
|
||||
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):
|
||||
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()
|
||||
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):
|
||||
self.right_viewport = Viewport()
|
||||
super().reset_interaction()
|
||||
@@ -98,7 +148,7 @@ class DualAxisPlot:
|
||||
if ticks and self.core and not self.lock_y:
|
||||
factor = 1.25 ** max(-8, min(8, ticks / 120))
|
||||
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.core, 1, factor, focus_y=(pos.y() - rect.top()) / rect.height())
|
||||
self.update()
|
||||
@@ -111,14 +161,34 @@ class DualAxisPlot:
|
||||
super().paint_axes_and_markers(painter)
|
||||
if not self.right_axis_keys or not self.core:
|
||||
return
|
||||
rect, bounds = self.value_rect(), self.right_visible_bounds
|
||||
painter.save()
|
||||
painter.setPen(QColor('#a4b8cd'))
|
||||
step = self.core.tick_step(bounds.top - bounds.bottom, rect.height())
|
||||
first = math.ceil(bounds.bottom / step)
|
||||
count = min(100, max(0, math.floor(bounds.top / step) - first + 1))
|
||||
for index in range(count):
|
||||
value = (first + index) * step
|
||||
y = rect.top() + bounds.fraction(self.core, value, True) * rect.height()
|
||||
painter.drawText(QPointF(rect.right() + 7, y + 4), f'{value:.4g}')
|
||||
painter.restore()
|
||||
paint_right_axis(painter, self.core, self.value_rect(), self.right_visible_bounds)
|
||||
|
||||
|
||||
def paint_right_axis(painter, core, rect, bounds):
|
||||
painter.save()
|
||||
painter.setPen(QColor('#a4b8cd'))
|
||||
step = core.tick_step(bounds.top - bounds.bottom, rect.height() * 2)
|
||||
first = math.ceil(bounds.bottom / step)
|
||||
count = min(100, max(0, math.floor(bounds.top / step) - first + 1))
|
||||
for index in range(count):
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user