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/pyplot.py
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""
|
|
2
|
+
plotlive.pyplot — drop-in state machine API for matplotlib.pyplot.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
import numpy as np
|
|
6
|
+
from .figure import Figure
|
|
7
|
+
from .animation import FuncAnimation
|
|
8
|
+
from .events import InteractionState, draw_help_overlay
|
|
9
|
+
from .renderer import FigureRenderer, AxesRenderer
|
|
10
|
+
|
|
11
|
+
# ------------------------------------------------------------------
|
|
12
|
+
# Global state (mirrors matplotlib._pylab_helpers)
|
|
13
|
+
# ------------------------------------------------------------------
|
|
14
|
+
_figures: list[Figure] = []
|
|
15
|
+
_current_figure: Figure | None = None
|
|
16
|
+
_current_axes = None # Axes | None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ------------------------------------------------------------------
|
|
20
|
+
# Figure / Axes management
|
|
21
|
+
# ------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
def figure(num=None, figsize=(6.4, 4.8), dpi=100, facecolor='white',
|
|
24
|
+
edgecolor='white', **kwargs) -> Figure:
|
|
25
|
+
global _current_figure, _current_axes
|
|
26
|
+
fig = Figure(figsize=figsize, facecolor=facecolor)
|
|
27
|
+
_figures.append(fig)
|
|
28
|
+
_current_figure = fig
|
|
29
|
+
_current_axes = None
|
|
30
|
+
return fig
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def gcf() -> Figure:
|
|
34
|
+
global _current_figure
|
|
35
|
+
if _current_figure is None:
|
|
36
|
+
_current_figure = figure()
|
|
37
|
+
return _current_figure
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def gca(**kwargs):
|
|
41
|
+
global _current_axes
|
|
42
|
+
if _current_axes is None:
|
|
43
|
+
fig = gcf()
|
|
44
|
+
if fig.axes:
|
|
45
|
+
_current_axes = fig.axes[-1]
|
|
46
|
+
else:
|
|
47
|
+
_current_axes = fig.add_subplot(1, 1, 1)
|
|
48
|
+
return _current_axes
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def sca(ax):
|
|
52
|
+
global _current_axes, _current_figure
|
|
53
|
+
_current_axes = ax
|
|
54
|
+
_current_figure = ax._figure
|
|
55
|
+
return ax
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def subplots(nrows=1, ncols=1, squeeze=True, figsize=(6.4, 4.8),
|
|
59
|
+
sharex=False, sharey=False, **kwargs):
|
|
60
|
+
global _current_figure, _current_axes
|
|
61
|
+
fig = Figure(figsize=figsize)
|
|
62
|
+
_figures.append(fig)
|
|
63
|
+
_current_figure = fig
|
|
64
|
+
_, axs = fig.subplots(nrows, ncols, squeeze=squeeze)
|
|
65
|
+
# Set current axes to the first one
|
|
66
|
+
if isinstance(axs, np.ndarray):
|
|
67
|
+
first = axs.flat[0]
|
|
68
|
+
else:
|
|
69
|
+
first = axs
|
|
70
|
+
_current_axes = first
|
|
71
|
+
return fig, axs
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def subplot(nrows=1, ncols=1, index=1, **kwargs):
|
|
75
|
+
global _current_axes
|
|
76
|
+
fig = gcf()
|
|
77
|
+
ax = fig.add_subplot(nrows, ncols, index, **kwargs)
|
|
78
|
+
_current_axes = ax
|
|
79
|
+
return ax
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def clf() -> None:
|
|
83
|
+
fig = gcf()
|
|
84
|
+
fig.axes.clear()
|
|
85
|
+
fig._suptitle = ''
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cla() -> None:
|
|
89
|
+
gca().cla()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def close(fig=None) -> None:
|
|
93
|
+
global _figures, _current_figure, _current_axes
|
|
94
|
+
if fig is None:
|
|
95
|
+
fig = _current_figure
|
|
96
|
+
if fig in _figures:
|
|
97
|
+
_figures.remove(fig)
|
|
98
|
+
if _current_figure is fig:
|
|
99
|
+
_current_figure = _figures[-1] if _figures else None
|
|
100
|
+
_current_axes = None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ------------------------------------------------------------------
|
|
104
|
+
# Delegation to current axes
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def plot(*args, **kwargs):
|
|
108
|
+
return gca().plot(*args, **kwargs)
|
|
109
|
+
|
|
110
|
+
def scatter(x, y, **kwargs):
|
|
111
|
+
return gca().scatter(x, y, **kwargs)
|
|
112
|
+
|
|
113
|
+
def hist(x, **kwargs):
|
|
114
|
+
return gca().hist(x, **kwargs)
|
|
115
|
+
|
|
116
|
+
def bar(x, height, **kwargs):
|
|
117
|
+
return gca().bar(x, height, **kwargs)
|
|
118
|
+
|
|
119
|
+
def barh(y, width, **kwargs):
|
|
120
|
+
return gca().barh(y, width, **kwargs)
|
|
121
|
+
|
|
122
|
+
def imshow(X, **kwargs):
|
|
123
|
+
return gca().imshow(X, **kwargs)
|
|
124
|
+
|
|
125
|
+
def xlabel(s, **kwargs):
|
|
126
|
+
gca().set_xlabel(s, **kwargs)
|
|
127
|
+
|
|
128
|
+
def ylabel(s, **kwargs):
|
|
129
|
+
gca().set_ylabel(s, **kwargs)
|
|
130
|
+
|
|
131
|
+
def title(s, **kwargs):
|
|
132
|
+
gca().set_title(s, **kwargs)
|
|
133
|
+
|
|
134
|
+
def legend(*args, **kwargs):
|
|
135
|
+
return gca().legend(*args, **kwargs)
|
|
136
|
+
|
|
137
|
+
def grid(visible=True, **kwargs):
|
|
138
|
+
gca().grid(visible, **kwargs)
|
|
139
|
+
|
|
140
|
+
def xlim(*args, **kwargs):
|
|
141
|
+
if args:
|
|
142
|
+
return gca().set_xlim(*args, **kwargs)
|
|
143
|
+
return gca().get_xlim()
|
|
144
|
+
|
|
145
|
+
def ylim(*args, **kwargs):
|
|
146
|
+
if args:
|
|
147
|
+
return gca().set_ylim(*args, **kwargs)
|
|
148
|
+
return gca().get_ylim()
|
|
149
|
+
|
|
150
|
+
def xscale(value, **kwargs):
|
|
151
|
+
gca().set_xscale(value, **kwargs)
|
|
152
|
+
|
|
153
|
+
def yscale(value, **kwargs):
|
|
154
|
+
gca().set_yscale(value, **kwargs)
|
|
155
|
+
|
|
156
|
+
def xticks(ticks=None, labels=None, **kwargs):
|
|
157
|
+
if ticks is None:
|
|
158
|
+
return gca().get_xticks()
|
|
159
|
+
gca().set_xticks(ticks, labels, **kwargs)
|
|
160
|
+
|
|
161
|
+
def yticks(ticks=None, labels=None, **kwargs):
|
|
162
|
+
if ticks is None:
|
|
163
|
+
return gca().get_yticks()
|
|
164
|
+
gca().set_yticks(ticks, labels, **kwargs)
|
|
165
|
+
|
|
166
|
+
def tight_layout(**kwargs):
|
|
167
|
+
gcf().tight_layout(**kwargs)
|
|
168
|
+
|
|
169
|
+
def suptitle(t, **kwargs):
|
|
170
|
+
gcf().suptitle(t, **kwargs)
|
|
171
|
+
|
|
172
|
+
def colorbar(mappable=None, ax=None, **kwargs):
|
|
173
|
+
"""Associate a colorbar with the current (or specified) axes image."""
|
|
174
|
+
target_ax = ax or gca()
|
|
175
|
+
if mappable is not None and hasattr(mappable, 'cmap_name'):
|
|
176
|
+
target_ax._colorbar_image = mappable
|
|
177
|
+
elif target_ax.images:
|
|
178
|
+
target_ax._colorbar_image = target_ax.images[-1]
|
|
179
|
+
|
|
180
|
+
def axhline(y=0, xmin=0, xmax=1, **kwargs):
|
|
181
|
+
ax = gca()
|
|
182
|
+
xlim = ax.get_xlim()
|
|
183
|
+
x0 = xlim[0] + xmin * (xlim[1] - xlim[0])
|
|
184
|
+
x1 = xlim[0] + xmax * (xlim[1] - xlim[0])
|
|
185
|
+
return ax.plot([x0, x1], [y, y], **kwargs)
|
|
186
|
+
|
|
187
|
+
def axvline(x=0, ymin=0, ymax=1, **kwargs):
|
|
188
|
+
ax = gca()
|
|
189
|
+
ylim = ax.get_ylim()
|
|
190
|
+
y0 = ylim[0] + ymin * (ylim[1] - ylim[0])
|
|
191
|
+
y1 = ylim[0] + ymax * (ylim[1] - ylim[0])
|
|
192
|
+
return ax.plot([x, x], [y0, y1], **kwargs)
|
|
193
|
+
|
|
194
|
+
def savefig(fname: str, **kwargs):
|
|
195
|
+
gcf().savefig(fname, **kwargs)
|
|
196
|
+
|
|
197
|
+
def fill_between(x, y1, y2=0, **kwargs):
|
|
198
|
+
return gca().fill_between(x, y1, y2, **kwargs)
|
|
199
|
+
|
|
200
|
+
def errorbar(x, y, **kwargs):
|
|
201
|
+
return gca().errorbar(x, y, **kwargs)
|
|
202
|
+
|
|
203
|
+
def boxplot(data, **kwargs):
|
|
204
|
+
return gca().boxplot(data, **kwargs)
|
|
205
|
+
|
|
206
|
+
def violinplot(data, **kwargs):
|
|
207
|
+
return gca().violinplot(data, **kwargs)
|
|
208
|
+
|
|
209
|
+
def pie(x, **kwargs):
|
|
210
|
+
return gca().pie(x, **kwargs)
|
|
211
|
+
|
|
212
|
+
def stackplot(x, *ys, **kwargs):
|
|
213
|
+
return gca().stackplot(x, *ys, **kwargs)
|
|
214
|
+
|
|
215
|
+
# ------------------------------------------------------------------
|
|
216
|
+
# Animation
|
|
217
|
+
# ------------------------------------------------------------------
|
|
218
|
+
|
|
219
|
+
def save_animation(filename: str, fps: int | None = None,
|
|
220
|
+
fig: Figure | None = None) -> None:
|
|
221
|
+
"""Export the current figure's animation to a GIF or video file."""
|
|
222
|
+
f = fig if fig is not None else gcf()
|
|
223
|
+
if f._animation is None:
|
|
224
|
+
raise RuntimeError(
|
|
225
|
+
'No animation on the current figure. Call plt.animate() first.'
|
|
226
|
+
)
|
|
227
|
+
f._animation.save(filename, fps=fps)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def animate(update_fn, frames=None, interval: int = 200,
|
|
231
|
+
repeat: bool = True, fig: Figure | None = None,
|
|
232
|
+
fargs=None, save_count: int | None = None) -> FuncAnimation:
|
|
233
|
+
"""
|
|
234
|
+
Convenience wrapper — create and register an animation on the current figure.
|
|
235
|
+
|
|
236
|
+
For full matplotlib compatibility, construct FuncAnimation directly::
|
|
237
|
+
|
|
238
|
+
from plotlive.animation import FuncAnimation
|
|
239
|
+
anim = FuncAnimation(fig, update, frames=50, interval=200)
|
|
240
|
+
"""
|
|
241
|
+
if fig is None:
|
|
242
|
+
fig = gcf()
|
|
243
|
+
anim = FuncAnimation(fig, update_fn, frames=frames, interval=interval,
|
|
244
|
+
repeat=repeat, fargs=fargs, save_count=save_count)
|
|
245
|
+
fig._animation = anim
|
|
246
|
+
return anim
|
|
247
|
+
|
|
248
|
+
# ------------------------------------------------------------------
|
|
249
|
+
# Main event loop
|
|
250
|
+
# ------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
def show(block: bool = True) -> None:
|
|
253
|
+
"""Open pygame window (non-Jupyter) or display inline (Jupyter notebook)."""
|
|
254
|
+
global _current_figure, _current_axes
|
|
255
|
+
import pygame
|
|
256
|
+
|
|
257
|
+
if not _figures:
|
|
258
|
+
return
|
|
259
|
+
|
|
260
|
+
# ----- Jupyter / IPython inline display -----
|
|
261
|
+
from . import _jupyter
|
|
262
|
+
if _jupyter.is_jupyter():
|
|
263
|
+
if not pygame.get_init():
|
|
264
|
+
pygame.init()
|
|
265
|
+
|
|
266
|
+
def _set_current(fig):
|
|
267
|
+
global _current_figure, _current_axes
|
|
268
|
+
_current_figure = fig
|
|
269
|
+
_current_axes = fig.axes[0] if fig.axes else None
|
|
270
|
+
|
|
271
|
+
for fig in list(_figures):
|
|
272
|
+
if fig._animation is not None:
|
|
273
|
+
_jupyter.show_animation(fig, _set_current)
|
|
274
|
+
else:
|
|
275
|
+
_jupyter.show_figure(fig)
|
|
276
|
+
_figures.clear()
|
|
277
|
+
_current_figure = None
|
|
278
|
+
_current_axes = None
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
fig = _current_figure or _figures[0]
|
|
282
|
+
|
|
283
|
+
if not pygame.get_init():
|
|
284
|
+
pygame.init()
|
|
285
|
+
|
|
286
|
+
screen = pygame.display.set_mode(fig.pixel_size, pygame.RESIZABLE)
|
|
287
|
+
pygame.display.set_caption('plotlive')
|
|
288
|
+
clock = pygame.time.Clock()
|
|
289
|
+
state = InteractionState(fig)
|
|
290
|
+
|
|
291
|
+
# Animation starts PAUSED — user presses Space to begin
|
|
292
|
+
anim_event_id = fig._animation.event_id if fig._animation else None
|
|
293
|
+
|
|
294
|
+
dirty = True
|
|
295
|
+
running = True
|
|
296
|
+
|
|
297
|
+
while running:
|
|
298
|
+
for event in pygame.event.get():
|
|
299
|
+
if event.type == pygame.QUIT:
|
|
300
|
+
running = False
|
|
301
|
+
elif anim_event_id and event.type == anim_event_id:
|
|
302
|
+
if fig._animation:
|
|
303
|
+
dirty |= fig._animation._on_timer()
|
|
304
|
+
else:
|
|
305
|
+
dirty |= state.handle_event(event, screen=screen)
|
|
306
|
+
|
|
307
|
+
if dirty:
|
|
308
|
+
fig_surface = FigureRenderer(fig).render()
|
|
309
|
+
screen.blit(fig_surface, (0, 0))
|
|
310
|
+
|
|
311
|
+
# Hover tooltip overlay
|
|
312
|
+
hits = state.get_hover_hits()
|
|
313
|
+
if hits:
|
|
314
|
+
r = AxesRenderer(fig.axes[0], 0, 0)
|
|
315
|
+
r._draw_tooltip(screen, hits, pygame.mouse.get_pos())
|
|
316
|
+
|
|
317
|
+
# Help panel overlay
|
|
318
|
+
if state.show_help:
|
|
319
|
+
draw_help_overlay(screen)
|
|
320
|
+
|
|
321
|
+
pygame.display.flip()
|
|
322
|
+
dirty = False
|
|
323
|
+
|
|
324
|
+
clock.tick(60)
|
|
325
|
+
|
|
326
|
+
if fig._animation:
|
|
327
|
+
fig._animation.pause()
|
|
328
|
+
pygame.quit()
|
|
329
|
+
|
|
330
|
+
# Reset state so the library can be used again
|
|
331
|
+
_figures.clear()
|
|
332
|
+
_current_figure = None
|
|
333
|
+
_current_axes = None
|