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/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