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

@@ -0,0 +1,223 @@
1
+ """
2
+ Spectrogram visualization component
3
+ """
4
+
5
+ import math
6
+ from typing import Optional
7
+
8
+ import numpy as np
9
+ import zarr
10
+
11
+ from ..core.figpack_view import FigpackView
12
+
13
+
14
+ class Spectrogram(FigpackView):
15
+ """
16
+ A spectrogram visualization component for time-frequency data
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ *,
22
+ start_time_sec: float,
23
+ sampling_frequency_hz: float,
24
+ frequency_min_hz: float,
25
+ frequency_delta_hz: float,
26
+ data: np.ndarray,
27
+ ):
28
+ """
29
+ Initialize a Spectrogram view
30
+
31
+ Args:
32
+ start_time_sec: Starting time in seconds
33
+ sampling_frequency_hz: Sampling rate in Hz
34
+ frequency_min_hz: Minimum frequency in Hz
35
+ frequency_delta_hz: Frequency bin spacing in Hz
36
+ data: N×M numpy array where N is timepoints and M is frequency bins
37
+ """
38
+ assert data.ndim == 2, "Data must be a 2D array (timepoints × frequencies)"
39
+ assert sampling_frequency_hz > 0, "Sampling frequency must be positive"
40
+ assert frequency_delta_hz > 0, "Frequency delta must be positive"
41
+
42
+ self.start_time_sec = start_time_sec
43
+ self.sampling_frequency_hz = sampling_frequency_hz
44
+ self.frequency_min_hz = frequency_min_hz
45
+ self.frequency_delta_hz = frequency_delta_hz
46
+ self.data = data.astype(np.float32) # Ensure float32 for efficiency
47
+
48
+ n_timepoints, n_frequencies = data.shape
49
+ self.n_timepoints = n_timepoints
50
+ self.n_frequencies = n_frequencies
51
+
52
+ # Calculate frequency bins
53
+ self.frequency_bins = (
54
+ frequency_min_hz + np.arange(n_frequencies) * frequency_delta_hz
55
+ )
56
+
57
+ # Calculate data range for color scaling
58
+ self.data_min = float(np.nanmin(data))
59
+ self.data_max = float(np.nanmax(data))
60
+
61
+ # Prepare downsampled arrays for efficient rendering
62
+ self.downsampled_data = self._compute_downsampled_data()
63
+
64
+ def _compute_downsampled_data(self) -> dict:
65
+ """
66
+ Compute downsampled arrays at power-of-4 factors using max values only.
67
+
68
+ Returns:
69
+ dict: {factor: (ceil(N/factor), M) float32 array}, where each bin
70
+ contains the maximum value across the time dimension.
71
+ """
72
+ data = self.data # (N, M), float32
73
+ n_timepoints, n_frequencies = data.shape
74
+ downsampled = {}
75
+
76
+ if n_timepoints < 4:
77
+ # No level with factor >= 4 fits the stop condition (factor < N)
78
+ return downsampled
79
+
80
+ def _first_level_from_raw(x: np.ndarray) -> np.ndarray:
81
+ """Build the factor=4 level directly from the raw data."""
82
+ N, M = x.shape
83
+ n_bins = math.ceil(N / 4)
84
+ pad = n_bins * 4 - N
85
+ # Pad time axis with NaNs so max ignores the padded tail
86
+ x_pad = np.pad(
87
+ x, ((0, pad), (0, 0)), mode="constant", constant_values=np.nan
88
+ )
89
+ blk = x_pad.reshape(n_bins, 4, M) # (B, 4, M)
90
+ maxs = np.nanmax(blk, axis=1) # (B, M)
91
+ return maxs.astype(np.float32)
92
+
93
+ def _downsample4_bins(level_max: np.ndarray) -> np.ndarray:
94
+ """
95
+ Build the next pyramid level from the previous one by grouping every 4
96
+ bins. Input is (B, M) -> Output is (ceil(B/4), M).
97
+ """
98
+ B, M = level_max.shape
99
+ n_bins_next = math.ceil(B / 4)
100
+ pad = n_bins_next * 4 - B
101
+ lvl_pad = np.pad(
102
+ level_max,
103
+ ((0, pad), (0, 0)),
104
+ mode="constant",
105
+ constant_values=np.nan,
106
+ )
107
+ blk = lvl_pad.reshape(n_bins_next, 4, M) # (B', 4, M)
108
+
109
+ # Next maxs from maxs
110
+ maxs = np.nanmax(blk, axis=1) # (B', M)
111
+ return maxs.astype(np.float32)
112
+
113
+ # Level 1: factor = 4 from raw data
114
+ factor = 4
115
+ level = _first_level_from_raw(data)
116
+ downsampled[factor] = level
117
+
118
+ # Higher levels: factor *= 4 each time, built from previous level
119
+ factor *= 4 # -> 16
120
+ while factor < n_timepoints / 1000:
121
+ level = _downsample4_bins(level)
122
+ downsampled[factor] = level
123
+ factor *= 4
124
+
125
+ return downsampled
126
+
127
+ def _calculate_optimal_chunk_size(
128
+ self, shape: tuple, target_size_mb: float = 5.0
129
+ ) -> tuple:
130
+ """
131
+ Calculate optimal chunk size for Zarr storage targeting ~5MB per chunk
132
+
133
+ Args:
134
+ shape: Array shape (n_timepoints, n_frequencies)
135
+ target_size_mb: Target chunk size in MB
136
+
137
+ Returns:
138
+ Tuple of chunk dimensions
139
+ """
140
+ # Calculate bytes per element (float32 = 4 bytes)
141
+ bytes_per_element = 4
142
+ target_size_bytes = target_size_mb * 1024 * 1024
143
+
144
+ n_timepoints, n_frequencies = shape
145
+ elements_per_timepoint = n_frequencies
146
+
147
+ # Calculate chunk size in timepoints
148
+ max_timepoints_per_chunk = target_size_bytes // (
149
+ elements_per_timepoint * bytes_per_element
150
+ )
151
+
152
+ # Find next lower power of 2
153
+ chunk_timepoints = 2 ** math.floor(math.log2(max_timepoints_per_chunk))
154
+ chunk_timepoints = max(chunk_timepoints, 1) # At least 1
155
+ chunk_timepoints = min(chunk_timepoints, n_timepoints) # At most n_timepoints
156
+
157
+ # If n_timepoints is less than our calculated size, round down to next power of 2
158
+ if chunk_timepoints > n_timepoints:
159
+ chunk_timepoints = 2 ** math.floor(math.log2(n_timepoints))
160
+
161
+ return (chunk_timepoints, n_frequencies)
162
+
163
+ def _write_to_zarr_group(self, group: zarr.Group) -> None:
164
+ """
165
+ Write the spectrogram data to a Zarr group
166
+
167
+ Args:
168
+ group: Zarr group to write data into
169
+ """
170
+ group.attrs["view_type"] = "Spectrogram"
171
+
172
+ # Store metadata
173
+ group.attrs["start_time_sec"] = self.start_time_sec
174
+ group.attrs["sampling_frequency_hz"] = self.sampling_frequency_hz
175
+ group.attrs["frequency_min_hz"] = self.frequency_min_hz
176
+ group.attrs["frequency_delta_hz"] = self.frequency_delta_hz
177
+ group.attrs["n_timepoints"] = self.n_timepoints
178
+ group.attrs["n_frequencies"] = self.n_frequencies
179
+ group.attrs["data_min"] = self.data_min
180
+ group.attrs["data_max"] = self.data_max
181
+
182
+ # Store frequency bins
183
+ group.create_dataset(
184
+ "frequency_bins",
185
+ data=self.frequency_bins.astype(np.float32),
186
+ compression="blosc",
187
+ compression_opts={"cname": "lz4", "clevel": 5, "shuffle": 1},
188
+ )
189
+
190
+ # Store original data with optimal chunking
191
+ original_chunks = self._calculate_optimal_chunk_size(self.data.shape)
192
+ group.create_dataset(
193
+ "data",
194
+ data=self.data,
195
+ chunks=original_chunks,
196
+ compression="blosc",
197
+ compression_opts={"cname": "lz4", "clevel": 5, "shuffle": 1},
198
+ )
199
+
200
+ # Store downsampled data arrays
201
+ downsample_factors = list(self.downsampled_data.keys())
202
+ group.attrs["downsample_factors"] = downsample_factors
203
+
204
+ for factor, downsampled_array in self.downsampled_data.items():
205
+ dataset_name = f"data_ds_{factor}"
206
+
207
+ # Calculate optimal chunks for this downsampled array
208
+ ds_chunks = self._calculate_optimal_chunk_size(downsampled_array.shape)
209
+
210
+ group.create_dataset(
211
+ dataset_name,
212
+ data=downsampled_array,
213
+ chunks=ds_chunks,
214
+ compression="blosc",
215
+ compression_opts={"cname": "lz4", "clevel": 5, "shuffle": 1},
216
+ )
217
+
218
+ print(f"Stored Spectrogram with {len(downsample_factors)} downsampled levels:")
219
+ print(f" Original: {self.data.shape} (chunks: {original_chunks})")
220
+ for factor in downsample_factors:
221
+ ds_shape = self.downsampled_data[factor].shape
222
+ ds_chunks = self._calculate_optimal_chunk_size(ds_shape)
223
+ print(f" Factor {factor}: {ds_shape} (chunks: {ds_chunks})")
figpack/views/__init__.py CHANGED
@@ -1,4 +1,5 @@
1
1
  from .Box import Box
2
+ from .DataFrame import DataFrame
2
3
  from .Gallery import Gallery
3
4
  from .GalleryItem import GalleryItem
4
5
  from .Image import Image
@@ -7,6 +8,7 @@ from .Markdown import Markdown
7
8
  from .MatplotlibFigure import MatplotlibFigure
8
9
  from .MultiChannelTimeseries import MultiChannelTimeseries
9
10
  from .PlotlyFigure import PlotlyFigure
11
+ from .Spectrogram import Spectrogram
10
12
  from .Splitter import Splitter
11
13
  from .TabLayout import TabLayout
12
14
  from .TabLayoutItem import TabLayoutItem
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: figpack
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -36,6 +36,7 @@ Requires-Dist: spikeinterface; extra == "test"
36
36
  Requires-Dist: matplotlib; extra == "test"
37
37
  Requires-Dist: plotly; extra == "test"
38
38
  Requires-Dist: Pillow; extra == "test"
39
+ Requires-Dist: pandas; extra == "test"
39
40
  Provides-Extra: dev
40
41
  Requires-Dist: pytest>=7.0; extra == "dev"
41
42
  Requires-Dist: pytest-cov>=4.0; extra == "dev"
@@ -1,4 +1,4 @@
1
- figpack/__init__.py,sha256=0qLJ7E5m_1HlQz2yVbgdZj__rO56AZjWGC3NDzswq7U,181
1
+ figpack/__init__.py,sha256=HxAr8mGrHvxeBsPiVIzWFXNs2g74PFwMyFB8yfQK1u4,181
2
2
  figpack/cli.py,sha256=xWF7J2BxUqOLvPu-Kje7Q6oGukTroXsLq8WN8vJgyw0,8321
3
3
  figpack/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  figpack/core/_bundle_utils.py,sha256=JBZh2LJyu0oYHQBBVw5fF3uUNQJY_2bxVf6V7CN10FM,1884
@@ -9,9 +9,9 @@ figpack/core/_upload_bundle.py,sha256=54hdWayJJdRZdx7N9V2aH_X33KkR6hImMjN6tkBTLi
9
9
  figpack/core/_view_figure.py,sha256=o1x1I2VKFrp2W_TStUS3fQblRW8AvGbu7Uy7MgVjofA,4186
10
10
  figpack/core/config.py,sha256=oOR7SlP192vuFhYlS-h14HnG-kd_3gaz0vshXch2RNc,173
11
11
  figpack/core/figpack_view.py,sha256=peJFkoP6HIqyNATzyucxAIq9HuCnK7SRO_-gE_rbEvg,6130
12
- figpack/figpack-figure-dist/index.html,sha256=yRCsTBF07s2OLoSIlkMFUSA5Vp94yR7oh_SX6uXT8d8,486
12
+ figpack/figpack-figure-dist/index.html,sha256=koS5dlEb8lylfHmHnG5pBmXHlMIyK2ICrJ31aw5COmE,486
13
+ figpack/figpack-figure-dist/assets/index-CjiTpC6i.js,sha256=5JLWD3VReahxiEbAB0h61FJCRe4CRZVEzBlX9qUS5m0,1618290
13
14
  figpack/figpack-figure-dist/assets/index-Cmae55E4.css,sha256=Yg0apcYehJwQvSQIUH13S7tsfqWQDevpJsAho0dDf0g,5499
14
- figpack/figpack-figure-dist/assets/index-HXdk2TtM.js,sha256=GhqJkoATKYiprLAbc3V5CYUt-MtKteWLx7KUh1fjA_0,1598198
15
15
  figpack/figpack-figure-dist/assets/neurosift-logo-CLsuwLMO.png,sha256=g5m-TwrGh5f6-9rXtWV-znH4B0nHgc__0GWclRDLUHs,9307
16
16
  figpack/franklab/__init__.py,sha256=HkehqGImJE_sE2vbPDo-HbgtEYaMICb9-230xTYvRTU,56
17
17
  figpack/franklab/views/TrackAnimation.py,sha256=3Jv1Ri4FIwTyqNahinqhHsBH1Bv_iZrEGx12w6diJ2M,5636
@@ -22,9 +22,9 @@ figpack/spike_sorting/views/Autocorrelograms.py,sha256=6MXOYJKUNEhJokiyOs8SGJVF9
22
22
  figpack/spike_sorting/views/AverageWaveforms.py,sha256=mvMkS3wf6MpI95tlGqKxBjxZlHqJr4aqG7SZvHigIsI,5193
23
23
  figpack/spike_sorting/views/CrossCorrelogramItem.py,sha256=uSd0i2hupteuILi_aKp7bYPYpL_PdC3CUDRMOEKUEM0,880
24
24
  figpack/spike_sorting/views/CrossCorrelograms.py,sha256=OGQq_5t_T1zq4eFCpuTAvUIhZQu7ef7LVexKNdByMvo,4415
25
- figpack/spike_sorting/views/RasterPlot.py,sha256=gFdfH9SEm-tf8Ptqw4M1Q7IHRytUOULpBZfM2TdMfZQ,2215
25
+ figpack/spike_sorting/views/RasterPlot.py,sha256=m8Cg-KioAM3CdIJOecpyTqp71HwhQVDzAk3J4Xb4V-c,9940
26
26
  figpack/spike_sorting/views/RasterPlotItem.py,sha256=iW7fuDEjSfvf5YMIwrF_6cmKvD76oCigOUMHtGgBsPI,638
27
- figpack/spike_sorting/views/SpikeAmplitudes.py,sha256=vQYWdJM-3qu568bcfGDC9k9LW_PgvU8j4tN9QYc08Mo,2665
27
+ figpack/spike_sorting/views/SpikeAmplitudes.py,sha256=UOGxReKs5WwH7iRdR-iVN3Pr6wF3AnIV15veoZZzOco,13433
28
28
  figpack/spike_sorting/views/SpikeAmplitudesItem.py,sha256=j5Na-diY-vRUAPu0t0VkyFCSKFnQ_f5HT077mB3Cy8c,1134
29
29
  figpack/spike_sorting/views/UnitSimilarityScore.py,sha256=cJA9MkETos9qHhV1tqgA7SfNEaPo-duXYCE76hSFGnA,948
30
30
  figpack/spike_sorting/views/UnitsTable.py,sha256=M3y1IDJzSnvOaM1-QOyJOVcUcdTkVvxYhEMGd1kmUzs,2969
@@ -32,6 +32,7 @@ figpack/spike_sorting/views/UnitsTableColumn.py,sha256=zBnuoeILTuiVLDvtcOxqa37E5
32
32
  figpack/spike_sorting/views/UnitsTableRow.py,sha256=rEb2hMTA_pl2fTW1nOvnGir0ysfNx4uww3aekZzfWjk,720
33
33
  figpack/spike_sorting/views/__init__.py,sha256=iRq7xPmyhnQ3GffnPC0GxKGEWnlqXY_8IOxsMqYZ1IM,967
34
34
  figpack/views/Box.py,sha256=TfhPFNtVEq71LCucmWk3XX2WxQLdaeRiWGm5BM0k2l4,2236
35
+ figpack/views/DataFrame.py,sha256=VFP-EM_Wnc1G3uimVVMJe08KKWCAZe7DvmYf5e07uTk,3653
35
36
  figpack/views/Gallery.py,sha256=sHlZbaqxcktasmNsJnuxe8WmgUQ6iurG50JiChKSMbQ,3314
36
37
  figpack/views/GalleryItem.py,sha256=b_upJno5P3ANSulbG-h3t6Xj56tPGJ7iVxqyiZu3zaQ,1244
37
38
  figpack/views/Image.py,sha256=hmyAHlRwj0l6fC7aNmHYJFaj-qCqyH67soERm78V5dk,3953
@@ -40,14 +41,15 @@ figpack/views/Markdown.py,sha256=Dl1UX5s0aC-mEx5m6jIJ839YKMNeeVzIDG-EANVrbys,114
40
41
  figpack/views/MatplotlibFigure.py,sha256=YoNtZTItEjatbtNJqATm2H-Oircp5Ca6FZLjHk_B0zg,2778
41
42
  figpack/views/MultiChannelTimeseries.py,sha256=sWr2nW1eoYR7V44wF7fac7IoQ6BOnus1nc4STkgIkYw,8501
42
43
  figpack/views/PlotlyFigure.py,sha256=E33PEkWSj907Ue73bYfZQlF-JFDdWA8jNrG-buFQLgs,2174
44
+ figpack/views/Spectrogram.py,sha256=czjA1I_T86oNsWwXX_GHFM8WZ5UkP9nT8e8hrgav4iY,8110
43
45
  figpack/views/Splitter.py,sha256=x9jLCTlIvDy5p9ymVd0X48KDccyD6bJANhXyFgKEmtE,2007
44
46
  figpack/views/TabLayout.py,sha256=5g3nmL95PfqgI0naqZXHMwLVo2ebDlGX01Hy9044bUw,1898
45
47
  figpack/views/TabLayoutItem.py,sha256=xmHA0JsW_6naJze4_mQuP_Fy0Nm17p2N7w_AsmVRp8k,880
46
48
  figpack/views/TimeseriesGraph.py,sha256=OAaCjO8fo86u_gO_frNfRGxng3tczxGDGKcJEvZo3rE,7469
47
- figpack/views/__init__.py,sha256=npSsSmbxN7_tZcjCK76L2l9dRVqubVXIgBnxj1uhjf4,473
48
- figpack-0.2.6.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
49
- figpack-0.2.6.dist-info/METADATA,sha256=VoUFPcVVhIVIxWp7sExL2kXVONvLYmRHMmXNVIRPqCo,3577
50
- figpack-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
- figpack-0.2.6.dist-info/entry_points.txt,sha256=l6d3siH2LxXa8qJGbjAqpIZtI5AkMSyDeoRDCzdrUto,45
52
- figpack-0.2.6.dist-info/top_level.txt,sha256=lMKGaC5xWmAYBx9Ac1iMokm42KFnJFjmkP2ldyvOo-c,8
53
- figpack-0.2.6.dist-info/RECORD,,
49
+ figpack/views/__init__.py,sha256=h9B6RWTOfgizOMqtEu8xGJxtXAhK-XXroN6ebL-Vcr0,543
50
+ figpack-0.2.8.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
51
+ figpack-0.2.8.dist-info/METADATA,sha256=ih-mdvrks4Dxrtx1ieS82NJCAQp9QcqliCeFqWmUhkk,3616
52
+ figpack-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ figpack-0.2.8.dist-info/entry_points.txt,sha256=l6d3siH2LxXa8qJGbjAqpIZtI5AkMSyDeoRDCzdrUto,45
54
+ figpack-0.2.8.dist-info/top_level.txt,sha256=lMKGaC5xWmAYBx9Ac1iMokm42KFnJFjmkP2ldyvOo-c,8
55
+ figpack-0.2.8.dist-info/RECORD,,