feat(plot): configure one or two marker pairs per axis

This commit is contained in:
2026-09-04 12:56:16 +03:00
parent 5f8fc07f2a
commit f6787163dc
4 changed files with 31 additions and 2 deletions

View File

@@ -10,6 +10,8 @@ enum class TrendMarker(val title: String, val horizontal: Boolean) {
data class TrendMarkers( data class TrendMarkers(
val xEnabled: Boolean = true, val xEnabled: Boolean = true,
val yEnabled: Boolean = false, val yEnabled: Boolean = false,
val xPairs: Int = 1,
val yPairs: Int = 1,
val selected: TrendMarker = TrendMarker.A, val selected: TrendMarker = TrendMarker.A,
val a: Double? = null, val a: Double? = null,
val b: Double? = null, val b: Double? = null,
@@ -20,6 +22,9 @@ data class TrendMarkers(
val g: Double? = null, val g: Double? = null,
val h: Double? = null, val h: Double? = null,
) { ) {
init {
require(xPairs in 1..2 && yPairs in 1..2) { "Количество пар маркеров должно быть от 1 до 2" }
}
fun value(marker: TrendMarker): Double? = when (marker) { fun value(marker: TrendMarker): Double? = when (marker) {
TrendMarker.A -> a; TrendMarker.B -> b; TrendMarker.C -> c; TrendMarker.D -> d TrendMarker.A -> a; TrendMarker.B -> b; TrendMarker.C -> c; TrendMarker.D -> d
TrendMarker.E -> e; TrendMarker.F -> f; TrendMarker.G -> g; TrendMarker.H -> h TrendMarker.E -> e; TrendMarker.F -> f; TrendMarker.G -> g; TrendMarker.H -> h
@@ -49,5 +54,10 @@ data class TrendMarkers(
}.sortedWith(compareBy<Pair<TrendMarker, Double>> { it.second } }.sortedWith(compareBy<Pair<TrendMarker, Double>> { it.second }
.thenBy { if (it.first == selected) 0 else 1 }).firstOrNull()?.first .thenBy { if (it.first == selected) 0 else 1 }).firstOrNull()?.first
} }
fun enabled(marker: TrendMarker): Boolean = if (marker.horizontal) yEnabled else xEnabled fun enabled(marker: TrendMarker): Boolean = when (marker) {
TrendMarker.A, TrendMarker.B -> xEnabled
TrendMarker.C, TrendMarker.D -> xEnabled && xPairs >= 2
TrendMarker.E, TrendMarker.F -> yEnabled
TrendMarker.G, TrendMarker.H -> yEnabled && yPairs >= 2
}
} }

View File

@@ -25,6 +25,9 @@ class PlotContractTest {
@Test fun markerCoordinatesSurviveZoomAndDragUsesGestureStart() { @Test fun markerCoordinatesSurviveZoomAndDragUsesGestureStart() {
val full = PlotBounds(1000.0, 2000.0, -10.0, 10.0) val full = PlotBounds(1000.0, 2000.0, -10.0, 10.0)
val markers = TrendMarkers().positioned(full) val markers = TrendMarkers().positioned(full)
assertTrue(markers.enabled(TrendMarker.A))
assertFalse(markers.enabled(TrendMarker.C))
assertTrue(markers.copy(xPairs = 2).enabled(TrendMarker.C))
val zoomed = PlotBounds(1250.0, 1750.0, -5.0, 5.0) val zoomed = PlotBounds(1250.0, 1750.0, -5.0, 5.0)
assertEquals(markers, markers.positioned(zoomed)) assertEquals(markers, markers.positioned(zoomed))
val dragged = markers.drag(TrendMarker.A, 50.0, 500.0, zoomed) val dragged = markers.drag(TrendMarker.A, 50.0, 500.0, zoomed)

View File

@@ -102,6 +102,8 @@ class Bounds:
class Markers: class Markers:
x_enabled: bool = True x_enabled: bool = True
y_enabled: bool = False y_enabled: bool = False
x_pairs: int = 1
y_pairs: int = 1
selected: Marker = Marker.A selected: Marker = Marker.A
a: Optional[float] = None a: Optional[float] = None
b: Optional[float] = None b: Optional[float] = None
@@ -112,11 +114,21 @@ class Markers:
g: Optional[float] = None g: Optional[float] = None
h: Optional[float] = None h: Optional[float] = None
def __post_init__(self) -> None:
if self.x_pairs not in (1, 2) or self.y_pairs not in (1, 2):
raise ValueError("Marker pair count must be 1 or 2")
def value(self, marker: Marker) -> Optional[float]: def value(self, marker: Marker) -> Optional[float]:
return getattr(self, marker.name.lower()) return getattr(self, marker.name.lower())
def enabled(self, marker: Marker) -> bool: def enabled(self, marker: Marker) -> bool:
return self.y_enabled if marker.horizontal else self.x_enabled if marker in (Marker.A, Marker.B):
return self.x_enabled
if marker in (Marker.C, Marker.D):
return self.x_enabled and self.x_pairs >= 2
if marker in (Marker.E, Marker.F):
return self.y_enabled
return self.y_enabled and self.y_pairs >= 2
def move(self, marker: Marker, value: float) -> "Markers": def move(self, marker: Marker, value: float) -> "Markers":
return replace(self, **{marker.name.lower(): value}) return replace(self, **{marker.name.lower(): value})

View File

@@ -5,6 +5,7 @@ import math
import os import os
from pathlib import Path from pathlib import Path
import unittest import unittest
from dataclasses import replace
from protocan.plot import Bounds, Marker, Markers, PlotMath, Viewport from protocan.plot import Bounds, Marker, Markers, PlotMath, Viewport
@@ -31,6 +32,9 @@ class PlotTests(unittest.TestCase):
def test_markers_stay_in_data_coordinates_and_cross(self): def test_markers_stay_in_data_coordinates_and_cross(self):
bounds = Bounds(1000, 2000, -10, 10) bounds = Bounds(1000, 2000, -10, 10)
markers = Markers().positioned(self.core, bounds) markers = Markers().positioned(self.core, bounds)
self.assertTrue(markers.enabled(Marker.A))
self.assertFalse(markers.enabled(Marker.C))
self.assertTrue(replace(markers, x_pairs=2).enabled(Marker.C))
zoomed = bounds.visible(self.core, Viewport(.25, .25, .5, .5)) zoomed = bounds.visible(self.core, Viewport(.25, .25, .5, .5))
self.assertEqual(markers, markers.positioned(self.core, zoomed)) self.assertEqual(markers, markers.positioned(self.core, zoomed))
moved = markers.drag(self.core, Marker.A, 50, 500, zoomed) moved = markers.drag(self.core, Marker.A, 50, 500, zoomed)