fullplot 0.1.0__py3-none-any.whl → 2.0.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.
- fullplot/__init__.py +5 -11
- fullplot/fullplot.py +216 -70
- fullplot/linearization.py +335 -0
- fullplot/maps.py +15 -0
- {fullplot-0.1.0.dist-info → fullplot-2.0.0.dist-info}/METADATA +478 -96
- fullplot-2.0.0.dist-info/RECORD +12 -0
- {fullplot-0.1.0.dist-info → fullplot-2.0.0.dist-info}/WHEEL +1 -1
- fullplot-0.1.0.dist-info/RECORD +0 -11
- {fullplot-0.1.0.dist-info → fullplot-2.0.0.dist-info}/entry_points.txt +0 -0
- {fullplot-0.1.0.dist-info → fullplot-2.0.0.dist-info}/licenses/LICENSE +0 -0
fullplot/__init__.py
CHANGED
|
@@ -1,25 +1,16 @@
|
|
|
1
|
-
"""FullPlot public package interface.
|
|
2
|
-
|
|
3
|
-
FullPlot is a small engineering-data plotting package centered on HDF5 files,
|
|
4
|
-
reusable one-dimensional ``Trace`` objects, shared ``TimeAxis`` objects, and
|
|
5
|
-
rectangular-grid map generation. Most users import the package as ``fullplot``
|
|
6
|
-
or ``fplt`` and use the names re-exported here instead of importing from the
|
|
7
|
-
internal modules directly.
|
|
8
|
-
"""
|
|
1
|
+
"""FullPlot public package interface."""
|
|
9
2
|
|
|
10
3
|
from fullplot.cli import main
|
|
11
4
|
from fullplot.fullplot import (
|
|
12
5
|
AmbiguousDatasetError,
|
|
13
6
|
DatasetNotFoundError,
|
|
14
7
|
FullPlotError,
|
|
15
|
-
H5File,
|
|
16
8
|
PlotDataError,
|
|
17
9
|
TimeAxis,
|
|
18
10
|
Trace,
|
|
19
11
|
VALID_TRACE_ROLES,
|
|
20
12
|
list,
|
|
21
13
|
map,
|
|
22
|
-
open,
|
|
23
14
|
plot,
|
|
24
15
|
read,
|
|
25
16
|
show,
|
|
@@ -29,6 +20,7 @@ from fullplot.fullplot import (
|
|
|
29
20
|
values,
|
|
30
21
|
write_traces,
|
|
31
22
|
)
|
|
23
|
+
from fullplot.linearization import H5File, Linearization, LinearizationError, open
|
|
32
24
|
from fullplot.maps import (
|
|
33
25
|
Axis,
|
|
34
26
|
FullPlotMapError,
|
|
@@ -37,7 +29,7 @@ from fullplot.maps import (
|
|
|
37
29
|
generate_map,
|
|
38
30
|
)
|
|
39
31
|
|
|
40
|
-
__version__ = "0.
|
|
32
|
+
__version__ = "2.0.0"
|
|
41
33
|
|
|
42
34
|
__all__ = [
|
|
43
35
|
"__version__",
|
|
@@ -45,6 +37,8 @@ __all__ = [
|
|
|
45
37
|
"DatasetNotFoundError",
|
|
46
38
|
"AmbiguousDatasetError",
|
|
47
39
|
"PlotDataError",
|
|
40
|
+
"LinearizationError",
|
|
41
|
+
"Linearization",
|
|
48
42
|
"TimeAxis",
|
|
49
43
|
"Trace",
|
|
50
44
|
"VALID_TRACE_ROLES",
|
fullplot/fullplot.py
CHANGED
|
@@ -199,28 +199,11 @@ def _line_style_for_role(role: str, theme: str, color, linewidth: float) -> dict
|
|
|
199
199
|
|
|
200
200
|
@dataclass
|
|
201
201
|
class TimeAxis:
|
|
202
|
-
"""Shared, shiftable time basis for related traces.
|
|
202
|
+
"""Shared, shiftable and windowable time basis for related traces.
|
|
203
203
|
|
|
204
|
-
``
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
convenient to align test data and simulation data without copying every
|
|
208
|
-
trace.
|
|
209
|
-
|
|
210
|
-
Parameters
|
|
211
|
-
----------
|
|
212
|
-
raw:
|
|
213
|
-
One-dimensional, strictly increasing time values in the source data
|
|
214
|
-
units. FullPlot normally uses seconds, but no unit conversion is
|
|
215
|
-
performed.
|
|
216
|
-
name:
|
|
217
|
-
Human-readable axis name, usually the HDF5 dataset basename such as
|
|
218
|
-
``"time"``.
|
|
219
|
-
zero:
|
|
220
|
-
Raw data value that should be displayed as zero. The shifted values are
|
|
221
|
-
always ``raw - zero``.
|
|
222
|
-
attrs:
|
|
223
|
-
Optional metadata copied from the HDF5 dataset or supplied by the user.
|
|
204
|
+
Several ``Trace`` objects can reference one ``TimeAxis``. Shifting or
|
|
205
|
+
windowing the axis updates all of those traces together without changing the
|
|
206
|
+
original source arrays.
|
|
224
207
|
"""
|
|
225
208
|
|
|
226
209
|
raw: np.ndarray
|
|
@@ -240,35 +223,83 @@ class TimeAxis:
|
|
|
240
223
|
self.name = str(self.name)
|
|
241
224
|
self.zero = float(self.zero)
|
|
242
225
|
self.attrs = {} if self.attrs is None else dict(self.attrs)
|
|
226
|
+
self._window_start: float | None = None
|
|
227
|
+
self._window_stop: float | None = None
|
|
243
228
|
|
|
244
229
|
@property
|
|
245
|
-
def
|
|
246
|
-
"""Return
|
|
230
|
+
def full_values(self) -> np.ndarray:
|
|
231
|
+
"""Return all shifted time values before any active window is applied."""
|
|
247
232
|
return self.raw - self.zero
|
|
248
233
|
|
|
234
|
+
def _selection_for(self, start: float | None, stop: float | None) -> slice:
|
|
235
|
+
if start is not None:
|
|
236
|
+
start = float(start)
|
|
237
|
+
if stop is not None:
|
|
238
|
+
stop = float(stop)
|
|
239
|
+
if start is not None and stop is not None and start > stop:
|
|
240
|
+
raise ValueError("TimeAxis window start cannot be greater than stop.")
|
|
241
|
+
|
|
242
|
+
values = self.full_values
|
|
243
|
+
tol = 1e-12
|
|
244
|
+
if self.raw.size > 1:
|
|
245
|
+
tol = max(tol, abs(float(np.median(np.diff(self.raw)))) * 1e-9)
|
|
246
|
+
|
|
247
|
+
left = 0 if start is None else int(np.searchsorted(values, start - tol, side="left"))
|
|
248
|
+
right = len(values) if stop is None else int(np.searchsorted(values, stop + tol, side="right"))
|
|
249
|
+
|
|
250
|
+
if left >= right:
|
|
251
|
+
raise PlotDataError("TimeAxis window contains no samples.")
|
|
252
|
+
|
|
253
|
+
return slice(left, right)
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def selection(self) -> slice:
|
|
257
|
+
"""Slice selecting the active samples from the full time axis."""
|
|
258
|
+
return self._selection_for(self._window_start, self._window_stop)
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def values(self) -> np.ndarray:
|
|
262
|
+
"""Return shifted time values inside the active window."""
|
|
263
|
+
return self.full_values[self.selection]
|
|
264
|
+
|
|
249
265
|
@property
|
|
250
266
|
def value(self) -> np.ndarray:
|
|
251
|
-
"""Alias for :attr:`values
|
|
267
|
+
"""Alias for :attr:`values`."""
|
|
252
268
|
return self.values
|
|
253
269
|
|
|
254
270
|
@property
|
|
255
271
|
def time(self) -> np.ndarray:
|
|
256
|
-
"""Alias for :attr:`values
|
|
272
|
+
"""Alias for :attr:`values`."""
|
|
257
273
|
return self.values
|
|
258
274
|
|
|
275
|
+
@property
|
|
276
|
+
def has_window(self) -> bool:
|
|
277
|
+
"""Whether an active time window is limiting the axis."""
|
|
278
|
+
return self._window_start is not None or self._window_stop is not None
|
|
279
|
+
|
|
280
|
+
@property
|
|
281
|
+
def window_range(self) -> tuple[float | None, float | None]:
|
|
282
|
+
"""Current ``(start, stop)`` window in shifted time coordinates."""
|
|
283
|
+
return self._window_start, self._window_stop
|
|
284
|
+
|
|
259
285
|
@property
|
|
260
286
|
def count(self) -> int:
|
|
261
|
-
"""Number of
|
|
287
|
+
"""Number of samples in the active time window."""
|
|
288
|
+
return int(self.values.size)
|
|
289
|
+
|
|
290
|
+
@property
|
|
291
|
+
def raw_count(self) -> int:
|
|
292
|
+
"""Number of samples in the original time axis."""
|
|
262
293
|
return int(self.raw.size)
|
|
263
294
|
|
|
264
295
|
@property
|
|
265
296
|
def tmin(self) -> float:
|
|
266
|
-
"""First shifted time value."""
|
|
297
|
+
"""First shifted time value in the active window."""
|
|
267
298
|
return float(self.values[0])
|
|
268
299
|
|
|
269
300
|
@property
|
|
270
301
|
def tmax(self) -> float:
|
|
271
|
-
"""Last shifted time value."""
|
|
302
|
+
"""Last shifted time value in the active window."""
|
|
272
303
|
return float(self.values[-1])
|
|
273
304
|
|
|
274
305
|
@property
|
|
@@ -283,53 +314,93 @@ class TimeAxis:
|
|
|
283
314
|
|
|
284
315
|
@property
|
|
285
316
|
def duration(self) -> float:
|
|
286
|
-
"""
|
|
317
|
+
"""Time span covered by the active window."""
|
|
318
|
+
if self.count < 2:
|
|
319
|
+
return 0.0
|
|
320
|
+
return float(self.values[-1] - self.values[0])
|
|
321
|
+
|
|
322
|
+
@property
|
|
323
|
+
def raw_duration(self) -> float:
|
|
324
|
+
"""Time span covered by the original source axis."""
|
|
287
325
|
if self.raw.size < 2:
|
|
288
326
|
return 0.0
|
|
289
327
|
return float(self.raw[-1] - self.raw[0])
|
|
290
328
|
|
|
291
329
|
@property
|
|
292
330
|
def dt_array(self) -> np.ndarray:
|
|
293
|
-
"""
|
|
331
|
+
"""Adjacent spacings inside the active window."""
|
|
332
|
+
return np.diff(self.values)
|
|
333
|
+
|
|
334
|
+
@property
|
|
335
|
+
def raw_dt_array(self) -> np.ndarray:
|
|
336
|
+
"""Adjacent spacings in the original source axis."""
|
|
294
337
|
return np.diff(self.raw)
|
|
295
338
|
|
|
296
339
|
@property
|
|
297
340
|
def dt(self) -> float:
|
|
298
|
-
"""Nominal timestep
|
|
299
|
-
if self.
|
|
341
|
+
"""Nominal timestep in the active window."""
|
|
342
|
+
if self.count < 2:
|
|
300
343
|
return float("nan")
|
|
301
344
|
return float(np.median(self.dt_array))
|
|
302
345
|
|
|
303
346
|
@property
|
|
304
347
|
def is_uniform(self) -> bool:
|
|
305
|
-
"""Whether
|
|
306
|
-
if self.
|
|
348
|
+
"""Whether active samples are approximately uniformly spaced."""
|
|
349
|
+
if self.count < 3:
|
|
307
350
|
return True
|
|
308
351
|
dt = self.dt_array
|
|
309
352
|
return bool(np.allclose(dt, np.median(dt), rtol=1e-6, atol=1e-12))
|
|
310
353
|
|
|
311
354
|
def zero_at(self, time: float) -> "TimeAxis":
|
|
312
355
|
"""Shift the time basis so ``time`` in raw data appears as ``t = 0``."""
|
|
313
|
-
|
|
356
|
+
zero = float(time)
|
|
357
|
+
if zero != self.zero:
|
|
358
|
+
old_zero = self.zero
|
|
359
|
+
self.zero = zero
|
|
360
|
+
try:
|
|
361
|
+
self._selection_for(self._window_start, self._window_stop)
|
|
362
|
+
except (PlotDataError, ValueError):
|
|
363
|
+
self.zero = old_zero
|
|
364
|
+
raise
|
|
314
365
|
return self
|
|
315
366
|
|
|
316
367
|
def align(self, data_time: float, model_time: float = 0.0) -> "TimeAxis":
|
|
317
|
-
"""Shift the axis so one raw data time aligns with one model time.
|
|
368
|
+
"""Shift the axis so one raw data time aligns with one model time."""
|
|
369
|
+
return self.zero_at(float(data_time) - float(model_time))
|
|
370
|
+
|
|
371
|
+
def window(self, start: float | None = None, stop: float | None = None) -> "TimeAxis":
|
|
372
|
+
"""Limit this shared axis, and all attached traces, to a time interval."""
|
|
373
|
+
start = None if start is None else float(start)
|
|
374
|
+
stop = None if stop is None else float(stop)
|
|
375
|
+
self._selection_for(start, stop)
|
|
376
|
+
|
|
377
|
+
if (start, stop) != self.window_range:
|
|
378
|
+
self._window_start = start
|
|
379
|
+
self._window_stop = stop
|
|
380
|
+
return self
|
|
318
381
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
382
|
+
def clear_window(self) -> "TimeAxis":
|
|
383
|
+
"""Restore the full time range."""
|
|
384
|
+
if self.has_window:
|
|
385
|
+
self._window_start = None
|
|
386
|
+
self._window_stop = None
|
|
323
387
|
return self
|
|
324
388
|
|
|
389
|
+
def windowed(self, start: float | None = None, stop: float | None = None) -> "TimeAxis":
|
|
390
|
+
"""Return an independent copy with a time window applied."""
|
|
391
|
+
return self.copy().window(start=start, stop=stop)
|
|
392
|
+
|
|
325
393
|
def copy(self, name: str | None = None) -> "TimeAxis":
|
|
326
|
-
"""Return an independent copy preserving
|
|
327
|
-
|
|
394
|
+
"""Return an independent copy preserving alignment and window state."""
|
|
395
|
+
copied = TimeAxis(
|
|
328
396
|
raw=self.raw.copy(),
|
|
329
397
|
name=self.name if name is None else name,
|
|
330
398
|
zero=self.zero,
|
|
331
399
|
attrs=self.attrs,
|
|
332
400
|
)
|
|
401
|
+
copied._window_start = self._window_start
|
|
402
|
+
copied._window_stop = self._window_stop
|
|
403
|
+
return copied
|
|
333
404
|
|
|
334
405
|
def __len__(self) -> int:
|
|
335
406
|
return self.count
|
|
@@ -343,6 +414,20 @@ class TimeAxis:
|
|
|
343
414
|
def __getitem__(self, item):
|
|
344
415
|
return self.values[item]
|
|
345
416
|
|
|
417
|
+
def __str__(self) -> str:
|
|
418
|
+
"""Return a compact summary of the active time range."""
|
|
419
|
+
active_label = "sample" if self.count == 1 else "samples"
|
|
420
|
+
window = ""
|
|
421
|
+
if self.has_window:
|
|
422
|
+
start, stop = self.window_range
|
|
423
|
+
start_text = "start" if start is None else f"{start:g}"
|
|
424
|
+
stop_text = "end" if stop is None else f"{stop:g}"
|
|
425
|
+
window = f", window={start_text} to {stop_text}"
|
|
426
|
+
return (
|
|
427
|
+
f"TimeAxis({self.name!r}, {self.count}/{self.raw_count} {active_label}, "
|
|
428
|
+
f"range={self.tmin:g} to {self.tmax:g}, zero={self.zero:g}{window})"
|
|
429
|
+
)
|
|
430
|
+
|
|
346
431
|
class Trace:
|
|
347
432
|
"""Prepared one-dimensional engineering data series.
|
|
348
433
|
|
|
@@ -357,8 +442,8 @@ class Trace:
|
|
|
357
442
|
----------
|
|
358
443
|
x:
|
|
359
444
|
One-dimensional x values or a :class:`TimeAxis`. When a ``TimeAxis`` is
|
|
360
|
-
supplied, the trace keeps a reference to it so
|
|
361
|
-
|
|
445
|
+
supplied, the trace keeps a reference to it so alignment and windowing
|
|
446
|
+
changes apply to every trace sharing that axis.
|
|
362
447
|
y:
|
|
363
448
|
One-dimensional y values. Non-finite y-values are preserved so missing
|
|
364
449
|
test-data samples appear as plot gaps instead of being silently removed.
|
|
@@ -387,35 +472,68 @@ class Trace:
|
|
|
387
472
|
else:
|
|
388
473
|
self._x = None
|
|
389
474
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
if self.x.shape != self.y.shape:
|
|
393
|
-
raise PlotDataError(
|
|
394
|
-
f"Trace x and y must have the same length. "
|
|
395
|
-
f"Received {len(self.x)} and {len(self.y)}."
|
|
396
|
-
)
|
|
475
|
+
y_array = _as_1d_numeric_array(y, "Trace y")
|
|
397
476
|
|
|
398
477
|
if self.time_axis is None:
|
|
399
|
-
|
|
478
|
+
if self._x.shape != y_array.shape:
|
|
479
|
+
raise PlotDataError(
|
|
480
|
+
f"Trace x and y must have the same length. "
|
|
481
|
+
f"Received {len(self._x)} and {len(y_array)}."
|
|
482
|
+
)
|
|
483
|
+
self._raw_y = y_array
|
|
400
484
|
|
|
485
|
+
mask = np.isfinite(self._x)
|
|
401
486
|
if not np.any(mask):
|
|
402
487
|
raise PlotDataError("Trace has no finite x values.")
|
|
403
|
-
|
|
404
488
|
if not np.all(mask):
|
|
405
489
|
self._x = self._x[mask]
|
|
406
|
-
self.
|
|
490
|
+
self._raw_y = self._raw_y[mask]
|
|
491
|
+
else:
|
|
492
|
+
if len(y_array) == self.time_axis.raw_count:
|
|
493
|
+
self._raw_y = y_array
|
|
494
|
+
elif len(y_array) == self.time_axis.count:
|
|
495
|
+
self._raw_y = np.full(self.time_axis.raw_count, np.nan, dtype=float)
|
|
496
|
+
self._raw_y[self.time_axis.selection] = y_array
|
|
497
|
+
else:
|
|
498
|
+
raise PlotDataError(
|
|
499
|
+
f"Trace y must match the TimeAxis raw length ({self.time_axis.raw_count}) "
|
|
500
|
+
f"or active length ({self.time_axis.count}). Received {len(y_array)}."
|
|
501
|
+
)
|
|
407
502
|
|
|
408
503
|
self.name = str(name)
|
|
409
504
|
self.role = _normalize_trace_role(role)
|
|
410
505
|
self.attrs = {} if attrs is None else dict(attrs)
|
|
411
506
|
|
|
507
|
+
def __str__(self) -> str:
|
|
508
|
+
"""Return a compact summary without printing the underlying arrays."""
|
|
509
|
+
finite_count = int(np.count_nonzero(self.finite))
|
|
510
|
+
count = len(self.x)
|
|
511
|
+
sample_label = "sample" if count == 1 else "samples"
|
|
512
|
+
return (
|
|
513
|
+
f"Trace({self.name!r}, role={self.role!r}, {count} {sample_label}, "
|
|
514
|
+
f"x={self.tmin:g} to {self.tmax:g}, "
|
|
515
|
+
f"finite={finite_count}/{count})"
|
|
516
|
+
)
|
|
517
|
+
|
|
412
518
|
@property
|
|
413
519
|
def x(self) -> np.ndarray:
|
|
414
|
-
"""Current x-values
|
|
520
|
+
"""Current x-values, including shared alignment and windowing."""
|
|
415
521
|
if self.time_axis is not None:
|
|
416
522
|
return self.time_axis.values
|
|
417
523
|
return self._x
|
|
418
524
|
|
|
525
|
+
@property
|
|
526
|
+
def y(self) -> np.ndarray:
|
|
527
|
+
"""Current y-values, following any shared ``TimeAxis`` window."""
|
|
528
|
+
if self.time_axis is not None:
|
|
529
|
+
return self._raw_y[self.time_axis.selection]
|
|
530
|
+
return self._raw_y
|
|
531
|
+
|
|
532
|
+
@property
|
|
533
|
+
def raw_y(self) -> np.ndarray:
|
|
534
|
+
"""Original y-array before shared ``TimeAxis`` windowing."""
|
|
535
|
+
return self._raw_y
|
|
536
|
+
|
|
419
537
|
@property
|
|
420
538
|
def time(self) -> np.ndarray:
|
|
421
539
|
"""Alias for :attr:`x` when the trace represents time-series data."""
|
|
@@ -466,9 +584,15 @@ class Trace:
|
|
|
466
584
|
FullPlot operations should not invent new display names. If the user
|
|
467
585
|
wants a new legend name, the user can pass name=... or call renamed(...).
|
|
468
586
|
"""
|
|
587
|
+
x_values = self._x_for_new_trace() if x is None else x
|
|
588
|
+
if y is None:
|
|
589
|
+
y_values = self.raw_y.copy() if isinstance(x_values, TimeAxis) else self.y.copy()
|
|
590
|
+
else:
|
|
591
|
+
y_values = np.asarray(y, dtype=float)
|
|
592
|
+
|
|
469
593
|
return Trace(
|
|
470
|
-
x=
|
|
471
|
-
y=
|
|
594
|
+
x=x_values,
|
|
595
|
+
y=y_values,
|
|
472
596
|
name=self.name if name is None else name,
|
|
473
597
|
role=self.role if role is None else role,
|
|
474
598
|
attrs=dict(self.attrs) if attrs is None else dict(attrs),
|
|
@@ -500,7 +624,7 @@ class Trace:
|
|
|
500
624
|
"""
|
|
501
625
|
if isinstance(x, TimeAxis):
|
|
502
626
|
x_values = x
|
|
503
|
-
y_array = np.full(
|
|
627
|
+
y_array = np.full(x.raw_count, float(y), dtype=float)
|
|
504
628
|
else:
|
|
505
629
|
x_values = _as_1d_numeric_array(x, "Trace x", omit_nonfinite=True)
|
|
506
630
|
y_array = np.full_like(x_values, float(y), dtype=float)
|
|
@@ -537,7 +661,7 @@ class Trace:
|
|
|
537
661
|
x_array = xp
|
|
538
662
|
elif isinstance(x, TimeAxis):
|
|
539
663
|
x_values = x
|
|
540
|
-
x_array = x.
|
|
664
|
+
x_array = x.full_values
|
|
541
665
|
else:
|
|
542
666
|
x_values = _as_1d_numeric_array(x, "Trace x", omit_nonfinite=True)
|
|
543
667
|
x_array = x_values
|
|
@@ -565,7 +689,7 @@ class Trace:
|
|
|
565
689
|
"""
|
|
566
690
|
if isinstance(x, TimeAxis):
|
|
567
691
|
x_values = x
|
|
568
|
-
x_array = x.
|
|
692
|
+
x_array = x.full_values
|
|
569
693
|
else:
|
|
570
694
|
x_values = _as_1d_numeric_array(x, "Trace x", omit_nonfinite=True)
|
|
571
695
|
x_array = x_values
|
|
@@ -709,7 +833,7 @@ class Trace:
|
|
|
709
833
|
|
|
710
834
|
return Trace(
|
|
711
835
|
x=x_values,
|
|
712
|
-
y=self.y.copy(),
|
|
836
|
+
y=self.raw_y.copy() if isinstance(x_values, TimeAxis) else self.y.copy(),
|
|
713
837
|
name=self.name if name is None else name,
|
|
714
838
|
role=self.role if role is None else role,
|
|
715
839
|
attrs=merged_attrs,
|
|
@@ -871,14 +995,14 @@ class Trace:
|
|
|
871
995
|
"""
|
|
872
996
|
if isinstance(x, TimeAxis):
|
|
873
997
|
x_values = x
|
|
874
|
-
x_array = x.
|
|
998
|
+
x_array = x.full_values
|
|
875
999
|
else:
|
|
876
1000
|
x_values = _as_1d_numeric_array(x, "resample x", omit_nonfinite=True)
|
|
877
1001
|
x_array = x_values
|
|
878
1002
|
|
|
879
1003
|
return Trace(
|
|
880
1004
|
x=x_values,
|
|
881
|
-
y=
|
|
1005
|
+
y=self.value_at(x_array, method="linear", bounds="nan"),
|
|
882
1006
|
name=self.name if name is None else name,
|
|
883
1007
|
role=self.role,
|
|
884
1008
|
attrs=self.attrs,
|
|
@@ -1215,6 +1339,10 @@ class H5File:
|
|
|
1215
1339
|
self.filename = str(self.filename)
|
|
1216
1340
|
self.root = _clean_root(self.root)
|
|
1217
1341
|
|
|
1342
|
+
def __str__(self) -> str:
|
|
1343
|
+
"""Return the selected file and HDF5 root."""
|
|
1344
|
+
return f"H5File({self.filename!r}, root={self.root!r})"
|
|
1345
|
+
|
|
1218
1346
|
def at(self, group: str) -> "H5File":
|
|
1219
1347
|
"""
|
|
1220
1348
|
Return a new H5File scoped to a specific HDF5 group.
|
|
@@ -1395,7 +1523,7 @@ class H5File:
|
|
|
1395
1523
|
"""Read a one-dimensional HDF5 dataset as a shared ``TimeAxis``.
|
|
1396
1524
|
|
|
1397
1525
|
The returned axis can be passed to ``trace(..., x=time_axis)`` and later
|
|
1398
|
-
shifted
|
|
1526
|
+
shifted, aligned, or windowed once for all attached traces.
|
|
1399
1527
|
"""
|
|
1400
1528
|
|
|
1401
1529
|
with h5py.File(self.filename, "r") as h5:
|
|
@@ -1455,7 +1583,7 @@ class H5File:
|
|
|
1455
1583
|
if x is not None:
|
|
1456
1584
|
if isinstance(x, TimeAxis):
|
|
1457
1585
|
x_values = x
|
|
1458
|
-
x_array = x.
|
|
1586
|
+
x_array = x.full_values
|
|
1459
1587
|
elif isinstance(x, Trace):
|
|
1460
1588
|
x_values = x.y
|
|
1461
1589
|
x_array = x.y
|
|
@@ -1541,6 +1669,9 @@ class H5File:
|
|
|
1541
1669
|
xscale: str = "linear",
|
|
1542
1670
|
yscale: str = "linear",
|
|
1543
1671
|
y2scale: str = "linear",
|
|
1672
|
+
xlim=None,
|
|
1673
|
+
ylim=None,
|
|
1674
|
+
y2lim=None,
|
|
1544
1675
|
legend: bool = True,
|
|
1545
1676
|
legend_location: str = "best",
|
|
1546
1677
|
grid: bool = True,
|
|
@@ -1554,11 +1685,9 @@ class H5File:
|
|
|
1554
1685
|
"""Plot HDF5 datasets and/or ``Trace`` objects.
|
|
1555
1686
|
|
|
1556
1687
|
``y`` selects one or more left-axis traces, while ``y2`` selects optional
|
|
1557
|
-
right-axis traces.
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
function returns the Matplotlib figure and axes so callers can add custom
|
|
1561
|
-
annotations before showing or saving.
|
|
1688
|
+
right-axis traces. ``xlim``, ``ylim``, and ``y2lim`` change only the
|
|
1689
|
+
displayed figure range; they do not modify trace data. The function
|
|
1690
|
+
returns the Matplotlib figure and axes for further customization.
|
|
1562
1691
|
"""
|
|
1563
1692
|
|
|
1564
1693
|
theme = check_theme(theme)
|
|
@@ -1569,12 +1698,14 @@ class H5File:
|
|
|
1569
1698
|
with h5py.File(self.filename, "r") if self.filename else nullcontext(None) as h5:
|
|
1570
1699
|
x_array = None
|
|
1571
1700
|
x_name = None
|
|
1701
|
+
x_time_axis = None
|
|
1572
1702
|
|
|
1573
1703
|
if x is not None:
|
|
1574
1704
|
if isinstance(x, Trace):
|
|
1575
1705
|
x_array = x.y
|
|
1576
1706
|
x_name = x.name
|
|
1577
1707
|
elif isinstance(x, TimeAxis):
|
|
1708
|
+
x_time_axis = x
|
|
1578
1709
|
x_array = x.values
|
|
1579
1710
|
x_name = x.name
|
|
1580
1711
|
else:
|
|
@@ -1619,9 +1750,17 @@ class H5File:
|
|
|
1619
1750
|
|
|
1620
1751
|
if not all_series:
|
|
1621
1752
|
raise PlotDataError("No y data was selected.")
|
|
1753
|
+
if y2lim is not None and not right_series:
|
|
1754
|
+
raise PlotDataError("y2lim requires y2 data.")
|
|
1622
1755
|
|
|
1623
1756
|
dataset_series = [series for series in all_series if series.x is None]
|
|
1624
1757
|
|
|
1758
|
+
if x_time_axis is not None:
|
|
1759
|
+
for series in dataset_series:
|
|
1760
|
+
if series.length == x_time_axis.raw_count:
|
|
1761
|
+
series.data = series.data[x_time_axis.selection]
|
|
1762
|
+
series.length = len(series.data)
|
|
1763
|
+
|
|
1625
1764
|
if x_array is None and dataset_series:
|
|
1626
1765
|
x_length = dataset_series[0].length
|
|
1627
1766
|
x_array = np.arange(x_length)
|
|
@@ -1711,6 +1850,13 @@ class H5File:
|
|
|
1711
1850
|
y2scale=y2scale,
|
|
1712
1851
|
)
|
|
1713
1852
|
|
|
1853
|
+
if xlim is not None:
|
|
1854
|
+
ax.set_xlim(xlim)
|
|
1855
|
+
if ylim is not None:
|
|
1856
|
+
ax.set_ylim(ylim)
|
|
1857
|
+
if y2lim is not None:
|
|
1858
|
+
ax2.set_ylim(y2lim)
|
|
1859
|
+
|
|
1714
1860
|
if grid:
|
|
1715
1861
|
ax.grid(True, **grid_kwargs(theme))
|
|
1716
1862
|
else:
|