figpack 0.2.12__py3-none-any.whl → 0.2.14__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.

Potentially problematic release.


This version of figpack might be problematic. Click here for more details.

@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/png" href="./assets/neurosift-logo-CLsuwLMO.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>figpack figure</title>
8
- <script type="module" crossorigin src="./assets/index-DvunBzII.js"></script>
8
+ <script type="module" crossorigin src="./assets/index-F6uGre7p.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="./assets/index-D9a3K6eW.css">
10
10
  </head>
11
11
  <body>
figpack/views/Box.py CHANGED
@@ -21,6 +21,7 @@ class Box(FigpackView):
21
21
  direction: Literal["horizontal", "vertical"] = "vertical",
22
22
  show_titles: bool = True,
23
23
  items: List[LayoutItem],
24
+ title: Optional[str] = None,
24
25
  ):
25
26
  """
26
27
  Initialize a Box layout view
@@ -29,6 +30,7 @@ class Box(FigpackView):
29
30
  direction: Layout direction - "horizontal" or "vertical"
30
31
  show_titles: Whether to show titles for layout items
31
32
  items: List of LayoutItem objects containing the child views
33
+ title: Optional title to display at the top of the box
32
34
 
33
35
  Raises:
34
36
  ValueError: If direction is not "horizontal" or "vertical"
@@ -39,6 +41,7 @@ class Box(FigpackView):
39
41
  self.direction = direction
40
42
  self.show_titles = show_titles
41
43
  self.items = items
44
+ self.title = title
42
45
 
43
46
  def _write_to_zarr_group(self, group: zarr.Group) -> None:
44
47
  """
@@ -53,6 +56,7 @@ class Box(FigpackView):
53
56
  # Set layout properties
54
57
  group.attrs["direction"] = self.direction
55
58
  group.attrs["show_titles"] = self.show_titles
59
+ group.attrs["title"] = self.title
56
60
 
57
61
  # Create a list to store item metadata
58
62
  items_metadata = []
@@ -2,7 +2,8 @@
2
2
  Views module for figpack - contains visualization components
3
3
  """
4
4
 
5
- from typing import Any, Dict, List, Optional
5
+ import math
6
+ from typing import Any, Dict, List, Optional, Union
6
7
 
7
8
  import numpy as np
8
9
  import zarr
@@ -22,6 +23,8 @@ class TimeseriesGraph(FigpackView):
22
23
  y_range: Optional[List[float]] = None,
23
24
  hide_x_gridlines: bool = False,
24
25
  hide_y_gridlines: bool = False,
26
+ hide_nav_toolbar: bool = False,
27
+ hide_time_axis_labels: bool = False,
25
28
  y_label: str = "",
26
29
  ):
27
30
  """
@@ -32,12 +35,16 @@ class TimeseriesGraph(FigpackView):
32
35
  y_range: Y-axis range as [min, max]
33
36
  hide_x_gridlines: Whether to hide x-axis gridlines
34
37
  hide_y_gridlines: Whether to hide y-axis gridlines
38
+ hide_nav_toolbar: Whether to hide the navigation toolbar
39
+ hide_time_axis_labels: Whether to hide time axis labels
35
40
  y_label: Label for the y-axis
36
41
  """
37
42
  self.legend_opts = legend_opts or {}
38
43
  self.y_range = y_range
39
44
  self.hide_x_gridlines = hide_x_gridlines
40
45
  self.hide_y_gridlines = hide_y_gridlines
46
+ self.hide_nav_toolbar = hide_nav_toolbar
47
+ self.hide_time_axis_labels = hide_time_axis_labels
41
48
  self.y_label = y_label
42
49
 
43
50
  # Internal storage for series data
@@ -118,6 +125,41 @@ class TimeseriesGraph(FigpackView):
118
125
  )
119
126
  )
120
127
 
128
+ def add_uniform_series(
129
+ self,
130
+ *,
131
+ name: str,
132
+ start_time_sec: float,
133
+ sampling_frequency_hz: float,
134
+ data: np.ndarray,
135
+ channel_names: Optional[List[str]] = None,
136
+ colors: Optional[List[str]] = None,
137
+ width: float = 1.0,
138
+ ) -> None:
139
+ """
140
+ Add a uniform timeseries to the graph with optional multi-channel support
141
+
142
+ Args:
143
+ name: Base name of the series for legend
144
+ start_time_sec: Starting time in seconds
145
+ sampling_frequency_hz: Sampling rate in Hz
146
+ data: 1D array (single channel) or 2D array (timepoints × channels)
147
+ channel_names: Optional list of channel names
148
+ colors: Optional list of colors for each channel
149
+ width: Line width
150
+ """
151
+ self._series.append(
152
+ TGUniformSeries(
153
+ name=name,
154
+ start_time_sec=start_time_sec,
155
+ sampling_frequency_hz=sampling_frequency_hz,
156
+ data=data,
157
+ channel_names=channel_names,
158
+ colors=colors,
159
+ width=width,
160
+ )
161
+ )
162
+
121
163
  def _write_to_zarr_group(self, group: zarr.Group) -> None:
122
164
  """
123
165
  Write the graph data to a Zarr group
@@ -133,6 +175,8 @@ class TimeseriesGraph(FigpackView):
133
175
  series._write_to_zarr_group(series_group)
134
176
  elif isinstance(series, TGIntervalSeries):
135
177
  series._write_to_zarr_group(series_group)
178
+ elif isinstance(series, TGUniformSeries):
179
+ series._write_to_zarr_group(series_group)
136
180
  else:
137
181
  raise ValueError(f"Unknown series type: {type(series)}")
138
182
 
@@ -142,6 +186,8 @@ class TimeseriesGraph(FigpackView):
142
186
  group.attrs["y_range"] = self.y_range
143
187
  group.attrs["hide_x_gridlines"] = self.hide_x_gridlines
144
188
  group.attrs["hide_y_gridlines"] = self.hide_y_gridlines
189
+ group.attrs["hide_nav_toolbar"] = self.hide_nav_toolbar
190
+ group.attrs["hide_time_axis_labels"] = self.hide_time_axis_labels
145
191
  group.attrs["y_label"] = self.y_label
146
192
 
147
193
  # series names
@@ -253,3 +299,243 @@ class TGIntervalSeries:
253
299
  group.attrs["series_type"] = "interval"
254
300
  group.attrs["color"] = self.color
255
301
  group.attrs["alpha"] = self.alpha
302
+
303
+
304
+ class TGUniformSeries:
305
+ def __init__(
306
+ self,
307
+ *,
308
+ name: str,
309
+ start_time_sec: float,
310
+ sampling_frequency_hz: float,
311
+ data: np.ndarray,
312
+ channel_names: Optional[List[str]] = None,
313
+ colors: Optional[List[str]] = None,
314
+ width: float = 1.0,
315
+ ):
316
+ assert sampling_frequency_hz > 0, "Sampling frequency must be positive"
317
+
318
+ # Handle both 1D and 2D data
319
+ if data.ndim == 1:
320
+ # Convert 1D to 2D with single channel
321
+ data = data.reshape(-1, 1)
322
+ elif data.ndim == 2:
323
+ # Already 2D, use as-is
324
+ pass
325
+ else:
326
+ raise ValueError("Data must be 1D or 2D array")
327
+
328
+ n_timepoints, n_channels = data.shape
329
+
330
+ self.name = name
331
+ self.start_time_sec = start_time_sec
332
+ self.sampling_frequency_hz = sampling_frequency_hz
333
+ self.data = data.astype(np.float32) # Ensure float32 for efficiency
334
+
335
+ # Set channel names
336
+ if channel_names is None:
337
+ if n_channels == 1:
338
+ self.channel_names = [name]
339
+ else:
340
+ self.channel_names = [f"{name}_ch_{i}" for i in range(n_channels)]
341
+ else:
342
+ assert len(channel_names) == n_channels, (
343
+ f"Number of channel_names ({len(channel_names)}) must match "
344
+ f"number of channels ({n_channels})"
345
+ )
346
+ self.channel_names = [str(ch_name) for ch_name in channel_names]
347
+
348
+ # Set colors
349
+ if colors is None:
350
+ # Default colors for multiple channels
351
+ default_colors = [
352
+ "blue",
353
+ "red",
354
+ "green",
355
+ "orange",
356
+ "purple",
357
+ "brown",
358
+ "pink",
359
+ "gray",
360
+ ]
361
+ self.colors = [
362
+ default_colors[i % len(default_colors)] for i in range(n_channels)
363
+ ]
364
+ else:
365
+ assert len(colors) == n_channels, (
366
+ f"Number of colors ({len(colors)}) must match "
367
+ f"number of channels ({n_channels})"
368
+ )
369
+ self.colors = colors
370
+
371
+ self.width = width
372
+
373
+ # Prepare downsampled arrays for efficient rendering
374
+ self.downsampled_data = self._compute_downsampled_data()
375
+
376
+ def _compute_downsampled_data(self) -> dict:
377
+ """
378
+ Compute downsampled arrays at power-of-4 factors using a vectorized
379
+ min/max pyramid with NaN padding for partial bins.
380
+
381
+ Returns:
382
+ dict: {factor: (ceil(N/factor), 2, M) float32 array}, where the second
383
+ axis stores [min, max] per bin per channel.
384
+ """
385
+ data = self.data # (N, M), float32
386
+ n_timepoints, n_channels = data.shape
387
+ downsampled = {}
388
+
389
+ if n_timepoints < 4:
390
+ # No level with factor >= 4 fits the stop condition (factor < N)
391
+ return downsampled
392
+
393
+ def _first_level_from_raw(x: np.ndarray) -> np.ndarray:
394
+ """Build the factor=4 level directly from the raw data."""
395
+ N, M = x.shape
396
+ n_bins = math.ceil(N / 4)
397
+ pad = n_bins * 4 - N
398
+ # Pad time axis with NaNs so min/max ignore the padded tail
399
+ x_pad = np.pad(
400
+ x, ((0, pad), (0, 0)), mode="constant", constant_values=np.nan
401
+ )
402
+ blk = x_pad.reshape(n_bins, 4, M) # (B, 4, M)
403
+ mins = np.nanmin(blk, axis=1) # (B, M)
404
+ maxs = np.nanmax(blk, axis=1) # (B, M)
405
+ out = np.empty((n_bins, 2, M), dtype=np.float32)
406
+ out[:, 0, :] = mins
407
+ out[:, 1, :] = maxs
408
+ return out
409
+
410
+ def _downsample4_bins(level_minmax: np.ndarray) -> np.ndarray:
411
+ """
412
+ Build the next pyramid level from the previous one by grouping every 4
413
+ bins. Input is (B, 2, M) -> Output is (ceil(B/4), 2, M).
414
+ """
415
+ B, two, M = level_minmax.shape
416
+ assert two == 2
417
+ n_bins_next = math.ceil(B / 4)
418
+ pad = n_bins_next * 4 - B
419
+ lvl_pad = np.pad(
420
+ level_minmax,
421
+ ((0, pad), (0, 0), (0, 0)),
422
+ mode="constant",
423
+ constant_values=np.nan,
424
+ )
425
+ blk = lvl_pad.reshape(n_bins_next, 4, 2, M) # (B', 4, 2, M)
426
+
427
+ # Next mins from mins; next maxs from maxs
428
+ mins = np.nanmin(blk[:, :, 0, :], axis=1) # (B', M)
429
+ maxs = np.nanmax(blk[:, :, 1, :], axis=1) # (B', M)
430
+
431
+ out = np.empty((n_bins_next, 2, M), dtype=np.float32)
432
+ out[:, 0, :] = mins
433
+ out[:, 1, :] = maxs
434
+ return out
435
+
436
+ # Level 1: factor = 4 from raw data
437
+ factor = 4
438
+ level = _first_level_from_raw(data)
439
+ downsampled[factor] = level
440
+
441
+ # Higher levels: factor *= 4 each time, built from previous level
442
+ factor *= 4 # -> 16
443
+ while factor < n_timepoints / 1000:
444
+ level = _downsample4_bins(level)
445
+ downsampled[factor] = level
446
+ factor *= 4
447
+
448
+ return downsampled
449
+
450
+ def _calculate_optimal_chunk_size(
451
+ self, shape: tuple, target_size_mb: float = 5.0
452
+ ) -> tuple:
453
+ """
454
+ Calculate optimal chunk size for Zarr storage targeting ~5MB per chunk
455
+
456
+ Args:
457
+ shape: Array shape (n_timepoints, ..., n_channels)
458
+ target_size_mb: Target chunk size in MB
459
+
460
+ Returns:
461
+ Tuple of chunk dimensions
462
+ """
463
+ # Calculate bytes per element (float32 = 4 bytes)
464
+ bytes_per_element = 4
465
+ target_size_bytes = target_size_mb * 1024 * 1024
466
+
467
+ if len(shape) == 2: # Original data: (n_timepoints, n_channels)
468
+ n_timepoints, n_channels = shape
469
+ elements_per_timepoint = n_channels
470
+ elif len(shape) == 3: # Downsampled data: (n_timepoints, 2, n_channels)
471
+ n_timepoints, _, n_channels = shape
472
+ elements_per_timepoint = 2 * n_channels
473
+ else:
474
+ raise ValueError(f"Unsupported shape: {shape}")
475
+
476
+ # Calculate chunk size in timepoints
477
+ max_timepoints_per_chunk = target_size_bytes // (
478
+ elements_per_timepoint * bytes_per_element
479
+ )
480
+
481
+ # Find next lower power of 2
482
+ chunk_timepoints = 2 ** math.floor(math.log2(max_timepoints_per_chunk))
483
+ chunk_timepoints = max(chunk_timepoints, 1) # At least 1
484
+ chunk_timepoints = min(chunk_timepoints, n_timepoints) # At most n_timepoints
485
+
486
+ # If n_timepoints is less than our calculated size, round down to next power of 2
487
+ if chunk_timepoints > n_timepoints:
488
+ chunk_timepoints = 2 ** math.floor(math.log2(n_timepoints))
489
+
490
+ if len(shape) == 2:
491
+ return (chunk_timepoints, n_channels)
492
+ else: # len(shape) == 3
493
+ return (chunk_timepoints, 2, n_channels)
494
+
495
+ def _write_to_zarr_group(self, group: zarr.Group) -> None:
496
+ """
497
+ Write the uniform series data to a Zarr group
498
+
499
+ Args:
500
+ group: Zarr group to write data into
501
+ """
502
+ group.attrs["series_type"] = "uniform"
503
+
504
+ # Store metadata
505
+ group.attrs["start_time_sec"] = self.start_time_sec
506
+ group.attrs["sampling_frequency_hz"] = self.sampling_frequency_hz
507
+ group.attrs["channel_names"] = self.channel_names
508
+ group.attrs["colors"] = self.colors
509
+ group.attrs["width"] = self.width
510
+
511
+ n_timepoints, n_channels = self.data.shape
512
+ group.attrs["n_timepoints"] = n_timepoints
513
+ group.attrs["n_channels"] = n_channels
514
+
515
+ # Store original data with optimal chunking
516
+ original_chunks = self._calculate_optimal_chunk_size(self.data.shape)
517
+ group.create_dataset(
518
+ "data",
519
+ data=self.data,
520
+ chunks=original_chunks,
521
+ compression="blosc",
522
+ compression_opts={"cname": "lz4", "clevel": 5, "shuffle": 1},
523
+ )
524
+
525
+ # Store downsampled data arrays
526
+ downsample_factors = list(self.downsampled_data.keys())
527
+ group.attrs["downsample_factors"] = downsample_factors
528
+
529
+ for factor, downsampled_array in self.downsampled_data.items():
530
+ dataset_name = f"data_ds_{factor}"
531
+
532
+ # Calculate optimal chunks for this downsampled array
533
+ ds_chunks = self._calculate_optimal_chunk_size(downsampled_array.shape)
534
+
535
+ group.create_dataset(
536
+ dataset_name,
537
+ data=downsampled_array,
538
+ chunks=ds_chunks,
539
+ compression="blosc",
540
+ compression_opts={"cname": "lz4", "clevel": 5, "shuffle": 1},
541
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: figpack
3
- Version: 0.2.12
3
+ Version: 0.2.14
4
4
  Summary: A Python package for creating shareable, interactive visualizations in the browser
5
5
  Author-email: Jeremy Magland <jmagland@flatironinstitute.org>
6
6
  License: Apache-2.0
@@ -1,4 +1,4 @@
1
- figpack/__init__.py,sha256=Cb1XSVRMpZegRqP62Tcld-PGzOpS2AO413SnbaziETg,360
1
+ figpack/__init__.py,sha256=KdJ9_xbzXf9Bpdnm_0yFr6fWOzJY9CHQJRQOkZOZ7jA,360
2
2
  figpack/cli.py,sha256=xWF7J2BxUqOLvPu-Kje7Q6oGukTroXsLq8WN8vJgyw0,8321
3
3
  figpack/core/__init__.py,sha256=V4wVdyBJ80mi9Rz8HjDSQNkqhqYB6sq4vWH3xQ10kaE,232
4
4
  figpack/core/_bundle_utils.py,sha256=16hgTExPLkJCtGjVUCLlnbs_qgns6v01egVr3CEnUXE,6082
@@ -11,9 +11,9 @@ figpack/core/config.py,sha256=oOR7SlP192vuFhYlS-h14HnG-kd_3gaz0vshXch2RNc,173
11
11
  figpack/core/extension_view.py,sha256=flFj8X4XuDzzOyeOe3SBddbr_t4wbg1V3vFzJoEVAB4,1995
12
12
  figpack/core/figpack_extension.py,sha256=EJHZpe7GsQMUnSvxcYf8374-f6n85F_k1IEelFMRFP8,4332
13
13
  figpack/core/figpack_view.py,sha256=X_EtpWTloENfRnDEJcBxXBPditaObv5BWMzO-_stAho,6297
14
- figpack/figpack-figure-dist/index.html,sha256=KfLOqUjEskQz4IIEBQzZkvKk98OPHw0S0XMewpC5dsE,486
14
+ figpack/figpack-figure-dist/index.html,sha256=XPYsJikUR1-63RUHsg32ruDH-agK82a6YZdCu4J3WDo,486
15
15
  figpack/figpack-figure-dist/assets/index-D9a3K6eW.css,sha256=ki61XkOz_TBJnU9Qyk5EgBzh2-_ilZQui2i8DHSarEo,5584
16
- figpack/figpack-figure-dist/assets/index-DvunBzII.js,sha256=arbzJNLZqctCBHw9XY0xmmOf9Pv7hOvroENh3NMYY4c,1620482
16
+ figpack/figpack-figure-dist/assets/index-F6uGre7p.js,sha256=W3_Ue6DWBr7r7n2--jwQ6ZdOTPr5gsGChqtRI9rXXSA,1625513
17
17
  figpack/figpack-figure-dist/assets/neurosift-logo-CLsuwLMO.png,sha256=g5m-TwrGh5f6-9rXtWV-znH4B0nHgc__0GWclRDLUHs,9307
18
18
  figpack/franklab/__init__.py,sha256=HkehqGImJE_sE2vbPDo-HbgtEYaMICb9-230xTYvRTU,56
19
19
  figpack/franklab/views/TrackAnimation.py,sha256=3Jv1Ri4FIwTyqNahinqhHsBH1Bv_iZrEGx12w6diJ2M,5636
@@ -34,7 +34,7 @@ figpack/spike_sorting/views/UnitsTable.py,sha256=M3y1IDJzSnvOaM1-QOyJOVcUcdTkVvx
34
34
  figpack/spike_sorting/views/UnitsTableColumn.py,sha256=zBnuoeILTuiVLDvtcOxqa37E5WlbR12rlwNJUeWXxY4,847
35
35
  figpack/spike_sorting/views/UnitsTableRow.py,sha256=rEb2hMTA_pl2fTW1nOvnGir0ysfNx4uww3aekZzfWjk,720
36
36
  figpack/spike_sorting/views/__init__.py,sha256=2caaV9yxi97aHoYfUWUYyyIXQSZJRtRVqmPOW9ZeG1I,1159
37
- figpack/views/Box.py,sha256=TfhPFNtVEq71LCucmWk3XX2WxQLdaeRiWGm5BM0k2l4,2236
37
+ figpack/views/Box.py,sha256=KEItT39xpw0Nl_4NU3WpPmi67wQKVSgiMyB4zAFGf4w,2409
38
38
  figpack/views/DataFrame.py,sha256=VFP-EM_Wnc1G3uimVVMJe08KKWCAZe7DvmYf5e07uTk,3653
39
39
  figpack/views/Gallery.py,sha256=sHlZbaqxcktasmNsJnuxe8WmgUQ6iurG50JiChKSMbQ,3314
40
40
  figpack/views/GalleryItem.py,sha256=b_upJno5P3ANSulbG-h3t6Xj56tPGJ7iVxqyiZu3zaQ,1244
@@ -47,14 +47,14 @@ figpack/views/Spectrogram.py,sha256=GfTNinkuMFu4dxn35MvSB4Perz84sx4LMcjUrOviz6c,
47
47
  figpack/views/Splitter.py,sha256=x9jLCTlIvDy5p9ymVd0X48KDccyD6bJANhXyFgKEmtE,2007
48
48
  figpack/views/TabLayout.py,sha256=5g3nmL95PfqgI0naqZXHMwLVo2ebDlGX01Hy9044bUw,1898
49
49
  figpack/views/TabLayoutItem.py,sha256=xmHA0JsW_6naJze4_mQuP_Fy0Nm17p2N7w_AsmVRp8k,880
50
- figpack/views/TimeseriesGraph.py,sha256=OAaCjO8fo86u_gO_frNfRGxng3tczxGDGKcJEvZo3rE,7469
50
+ figpack/views/TimeseriesGraph.py,sha256=nfmlh4LYB4Ut7htMZTtArqxUAsh66TW8dSx7jE3vdRI,17929
51
51
  figpack/views/__init__.py,sha256=nyd3Ot2x702W4j9oBN0lK6i0DZzg9Ai41XoYCm5DeQ8,546
52
52
  figpack/views/PlotlyExtension/PlotlyExtension.py,sha256=_-GiqzHFb9uF1O6RNKEkd85SX8z9z5kFOh8tO_9aAbk,3778
53
53
  figpack/views/PlotlyExtension/__init__.py,sha256=80Wy1mDMWyagjuR99ECxJePIYpRQ6TSyHkB0uZoBZ_0,70
54
54
  figpack/views/PlotlyExtension/plotly_view.js,sha256=gCsZS0IaYGTN5a3DC2c4NmzoOxZi1xGfCAYI6WSoFpM,3596
55
- figpack-0.2.12.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
56
- figpack-0.2.12.dist-info/METADATA,sha256=IyjYIuqCQXp3_TFeUyfEI7XV6Ac6oJum5wq5Qo_lqN8,3617
57
- figpack-0.2.12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
- figpack-0.2.12.dist-info/entry_points.txt,sha256=l6d3siH2LxXa8qJGbjAqpIZtI5AkMSyDeoRDCzdrUto,45
59
- figpack-0.2.12.dist-info/top_level.txt,sha256=lMKGaC5xWmAYBx9Ac1iMokm42KFnJFjmkP2ldyvOo-c,8
60
- figpack-0.2.12.dist-info/RECORD,,
55
+ figpack-0.2.14.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
56
+ figpack-0.2.14.dist-info/METADATA,sha256=oJeyH6JXAlLFa7evpVpDEHuFzDnNrpeKd4jv0llhxuM,3617
57
+ figpack-0.2.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
58
+ figpack-0.2.14.dist-info/entry_points.txt,sha256=l6d3siH2LxXa8qJGbjAqpIZtI5AkMSyDeoRDCzdrUto,45
59
+ figpack-0.2.14.dist-info/top_level.txt,sha256=lMKGaC5xWmAYBx9Ac1iMokm42KFnJFjmkP2ldyvOo-c,8
60
+ figpack-0.2.14.dist-info/RECORD,,