plotlive 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plotlive/__init__.py +18 -0
- plotlive/_jupyter.py +127 -0
- plotlive/_parsers.py +88 -0
- plotlive/animation.py +279 -0
- plotlive/artists.py +207 -0
- plotlive/axes.py +634 -0
- plotlive/colors.py +324 -0
- plotlive/data/DejaVuSans-Bold.ttf +1 -0
- plotlive/data/DejaVuSans.ttf +1 -0
- plotlive/data/FreeSansBold.ttf +0 -0
- plotlive/drawing.py +168 -0
- plotlive/events.py +226 -0
- plotlive/figure.py +94 -0
- plotlive/fonts.py +73 -0
- plotlive/pyplot.py +333 -0
- plotlive/renderer.py +571 -0
- plotlive/ticks.py +112 -0
- plotlive/transform.py +205 -0
- plotlive-0.1.0.dist-info/METADATA +804 -0
- plotlive-0.1.0.dist-info/RECORD +21 -0
- plotlive-0.1.0.dist-info/WHEEL +4 -0
plotlive/transform.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
_EPS = 1e-300 # floor for log scale to avoid log(0)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _log(v):
|
|
8
|
+
return np.log10(np.maximum(np.asarray(v, float), _EPS))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Transform:
|
|
12
|
+
"""
|
|
13
|
+
Bidirectional mapping between data coordinates and screen pixel coordinates.
|
|
14
|
+
Single source of truth for zoom/pan state per Axes.
|
|
15
|
+
Supports 'linear' and 'log' scales independently on each axis.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.xlim: tuple[float, float] = (0.0, 1.0)
|
|
20
|
+
self.ylim: tuple[float, float] = (0.0, 1.0)
|
|
21
|
+
# (left, top, width, height) in screen pixels — set by renderer before each draw
|
|
22
|
+
self.axes_rect: tuple[int, int, int, int] = (0, 0, 100, 100)
|
|
23
|
+
self._home_xlim: tuple[float, float] = (0.0, 1.0)
|
|
24
|
+
self._home_ylim: tuple[float, float] = (0.0, 1.0)
|
|
25
|
+
self.xscale: str = 'linear'
|
|
26
|
+
self.yscale: str = 'linear'
|
|
27
|
+
|
|
28
|
+
# ------------------------------------------------------------------
|
|
29
|
+
# Internal: normalised position [0, 1] along each axis
|
|
30
|
+
# ------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
def _x_norm(self, x) -> np.ndarray:
|
|
33
|
+
x = np.asarray(x, float)
|
|
34
|
+
xmin, xmax = self.xlim
|
|
35
|
+
if self.xscale == 'log':
|
|
36
|
+
lx = _log(x); lmin = _log(xmin); lmax = _log(xmax)
|
|
37
|
+
span = lmax - lmin if lmax != lmin else 1.0
|
|
38
|
+
return (lx - lmin) / span
|
|
39
|
+
span = xmax - xmin if xmax != xmin else 1.0
|
|
40
|
+
return (x - xmin) / span
|
|
41
|
+
|
|
42
|
+
def _y_norm(self, y) -> np.ndarray:
|
|
43
|
+
y = np.asarray(y, float)
|
|
44
|
+
ymin, ymax = self.ylim
|
|
45
|
+
if self.yscale == 'log':
|
|
46
|
+
ly = _log(y); lmin = _log(ymin); lmax = _log(ymax)
|
|
47
|
+
span = lmax - lmin if lmax != lmin else 1.0
|
|
48
|
+
return (ly - lmin) / span
|
|
49
|
+
span = ymax - ymin if ymax != ymin else 1.0
|
|
50
|
+
return (y - ymin) / span
|
|
51
|
+
|
|
52
|
+
# ------------------------------------------------------------------
|
|
53
|
+
# Forward transform: data → screen
|
|
54
|
+
# ------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
def data_to_screen(
|
|
57
|
+
self, x: float | np.ndarray, y: float | np.ndarray
|
|
58
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
59
|
+
"""Convert data (x, y) to screen (sx, sy). Y is flipped."""
|
|
60
|
+
left, top, w, h = self.axes_rect
|
|
61
|
+
sx = left + self._x_norm(x) * w
|
|
62
|
+
sy = top + h - self._y_norm(y) * h # Y flip
|
|
63
|
+
return sx, sy
|
|
64
|
+
|
|
65
|
+
def data_x_to_screen(self, x: float | np.ndarray) -> np.ndarray:
|
|
66
|
+
left, _, w, _ = self.axes_rect
|
|
67
|
+
return left + self._x_norm(x) * w
|
|
68
|
+
|
|
69
|
+
def data_y_to_screen(self, y: float | np.ndarray) -> np.ndarray:
|
|
70
|
+
_, top, _, h = self.axes_rect
|
|
71
|
+
return top + h - self._y_norm(y) * h
|
|
72
|
+
|
|
73
|
+
# ------------------------------------------------------------------
|
|
74
|
+
# Inverse transform: screen → data
|
|
75
|
+
# ------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
def screen_to_data(self, sx: float, sy: float) -> tuple[float, float]:
|
|
78
|
+
"""Convert screen pixel (sx, sy) to data coordinates."""
|
|
79
|
+
left, top, w, h = self.axes_rect
|
|
80
|
+
xmin, xmax = self.xlim
|
|
81
|
+
ymin, ymax = self.ylim
|
|
82
|
+
|
|
83
|
+
tx = (sx - left) / w if w else 0.0
|
|
84
|
+
ty = (top + h - sy) / h if h else 0.0 # Y flip
|
|
85
|
+
|
|
86
|
+
if self.xscale == 'log':
|
|
87
|
+
lmin = _log(xmin); lmax = _log(xmax)
|
|
88
|
+
x = float(10 ** (lmin + tx * (lmax - lmin)))
|
|
89
|
+
else:
|
|
90
|
+
x = float(xmin + tx * (xmax - xmin))
|
|
91
|
+
|
|
92
|
+
if self.yscale == 'log':
|
|
93
|
+
lmin = _log(ymin); lmax = _log(ymax)
|
|
94
|
+
y = float(10 ** (lmin + ty * (lmax - lmin)))
|
|
95
|
+
else:
|
|
96
|
+
y = float(ymin + ty * (ymax - ymin))
|
|
97
|
+
|
|
98
|
+
return x, y
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
# Mutators
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def zoom(self, sx: float, sy: float, factor: float) -> None:
|
|
105
|
+
"""Zoom by factor centered on screen point (sx, sy). factor<1 = zoom in."""
|
|
106
|
+
cx, cy = self.screen_to_data(sx, sy)
|
|
107
|
+
xmin, xmax = self.xlim
|
|
108
|
+
ymin, ymax = self.ylim
|
|
109
|
+
|
|
110
|
+
if self.xscale == 'log':
|
|
111
|
+
lcx = _log(cx)
|
|
112
|
+
lmin, lmax = float(_log(xmin)), float(_log(xmax))
|
|
113
|
+
self.xlim = (10 ** (lcx - (lcx - lmin) * factor),
|
|
114
|
+
10 ** (lcx + (lmax - lcx) * factor))
|
|
115
|
+
else:
|
|
116
|
+
self.xlim = (cx - (cx - xmin) * factor, cx + (xmax - cx) * factor)
|
|
117
|
+
|
|
118
|
+
if self.yscale == 'log':
|
|
119
|
+
lcy = float(_log(cy))
|
|
120
|
+
lmin, lmax = float(_log(ymin)), float(_log(ymax))
|
|
121
|
+
self.ylim = (10 ** (lcy - (lcy - lmin) * factor),
|
|
122
|
+
10 ** (lcy + (lmax - lcy) * factor))
|
|
123
|
+
else:
|
|
124
|
+
self.ylim = (cy - (cy - ymin) * factor, cy + (ymax - cy) * factor)
|
|
125
|
+
|
|
126
|
+
def pan(self, dpx: float, dpy: float) -> None:
|
|
127
|
+
"""Pan by (dpx, dpy) screen pixels."""
|
|
128
|
+
_, _, w, h = self.axes_rect
|
|
129
|
+
xmin, xmax = self.xlim
|
|
130
|
+
ymin, ymax = self.ylim
|
|
131
|
+
|
|
132
|
+
if self.xscale == 'log':
|
|
133
|
+
lmin, lmax = float(_log(xmin)), float(_log(xmax))
|
|
134
|
+
dlx = dpx / w * (lmax - lmin) if w else 0.0
|
|
135
|
+
self.xlim = (10 ** (lmin + dlx), 10 ** (lmax + dlx))
|
|
136
|
+
else:
|
|
137
|
+
x_range = xmax - xmin if xmax != xmin else 1.0
|
|
138
|
+
dx = dpx / w * x_range if w else 0.0
|
|
139
|
+
self.xlim = (xmin + dx, xmax + dx)
|
|
140
|
+
|
|
141
|
+
if self.yscale == 'log':
|
|
142
|
+
lmin, lmax = float(_log(ymin)), float(_log(ymax))
|
|
143
|
+
dly = -dpy / h * (lmax - lmin) if h else 0.0 # screen Y down = data Y up
|
|
144
|
+
self.ylim = (10 ** (lmin + dly), 10 ** (lmax + dly))
|
|
145
|
+
else:
|
|
146
|
+
y_range = ymax - ymin if ymax != ymin else 1.0
|
|
147
|
+
dy = -dpy / h * y_range if h else 0.0
|
|
148
|
+
self.ylim = (ymin + dy, ymax + dy)
|
|
149
|
+
|
|
150
|
+
def set_home(self) -> None:
|
|
151
|
+
self._home_xlim = self.xlim
|
|
152
|
+
self._home_ylim = self.ylim
|
|
153
|
+
|
|
154
|
+
def reset_to_home(self) -> None:
|
|
155
|
+
self.xlim = self._home_xlim
|
|
156
|
+
self.ylim = self._home_ylim
|
|
157
|
+
|
|
158
|
+
def contains_screen_point(self, sx: float, sy: float) -> bool:
|
|
159
|
+
left, top, w, h = self.axes_rect
|
|
160
|
+
return left <= sx <= left + w and top <= sy <= top + h
|
|
161
|
+
|
|
162
|
+
def auto_scale(
|
|
163
|
+
self,
|
|
164
|
+
all_x: list,
|
|
165
|
+
all_y: list,
|
|
166
|
+
margin: float = 0.05,
|
|
167
|
+
xlim_auto: bool = True,
|
|
168
|
+
ylim_auto: bool = True,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Fit xlim/ylim to data with a margin fraction."""
|
|
171
|
+
if all_x and xlim_auto:
|
|
172
|
+
x_arr = np.concatenate([np.atleast_1d(np.asarray(a, float))
|
|
173
|
+
for a in all_x if np.asarray(a).size > 0])
|
|
174
|
+
x_arr = x_arr[np.isfinite(x_arr)]
|
|
175
|
+
if self.xscale == 'log':
|
|
176
|
+
x_arr = x_arr[x_arr > 0]
|
|
177
|
+
if x_arr.size > 0:
|
|
178
|
+
if self.xscale == 'log':
|
|
179
|
+
lmin, lmax = float(np.log10(x_arr.min())), float(np.log10(x_arr.max()))
|
|
180
|
+
span = lmax - lmin if lmax != lmin else 1.0
|
|
181
|
+
self.xlim = (10 ** (lmin - span * margin),
|
|
182
|
+
10 ** (lmax + span * margin))
|
|
183
|
+
else:
|
|
184
|
+
xmin, xmax = float(x_arr.min()), float(x_arr.max())
|
|
185
|
+
span = xmax - xmin if xmax != xmin else 1.0
|
|
186
|
+
self.xlim = (xmin - span * margin, xmax + span * margin)
|
|
187
|
+
|
|
188
|
+
if all_y and ylim_auto:
|
|
189
|
+
y_arr = np.concatenate([np.atleast_1d(np.asarray(a, float))
|
|
190
|
+
for a in all_y if np.asarray(a).size > 0])
|
|
191
|
+
y_arr = y_arr[np.isfinite(y_arr)]
|
|
192
|
+
if self.yscale == 'log':
|
|
193
|
+
y_arr = y_arr[y_arr > 0]
|
|
194
|
+
if y_arr.size > 0:
|
|
195
|
+
if self.yscale == 'log':
|
|
196
|
+
lmin, lmax = float(np.log10(y_arr.min())), float(np.log10(y_arr.max()))
|
|
197
|
+
span = lmax - lmin if lmax != lmin else 1.0
|
|
198
|
+
self.ylim = (10 ** (lmin - span * margin),
|
|
199
|
+
10 ** (lmax + span * margin))
|
|
200
|
+
else:
|
|
201
|
+
ymin, ymax = float(y_arr.min()), float(y_arr.max())
|
|
202
|
+
span = ymax - ymin if ymax != ymin else 1.0
|
|
203
|
+
self.ylim = (ymin - span * margin, ymax + span * margin)
|
|
204
|
+
|
|
205
|
+
self.set_home()
|