MNutils 1.2.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.
Files changed (41) hide show
  1. mnutils/GEExam.py +495 -0
  2. mnutils/GESeries.py +1578 -0
  3. mnutils/__init__.py +54 -0
  4. mnutils/_scripts.py +177 -0
  5. mnutils/fitting/AMARES.py +831 -0
  6. mnutils/fitting/__init__.py +5 -0
  7. mnutils/matlab/__init__.py +13 -0
  8. mnutils/matlab/setup_matlab.py +116 -0
  9. mnutils/plotting/__init__.py +38 -0
  10. mnutils/plotting/_widgets/__init__.py +5 -0
  11. mnutils/plotting/_widgets/_html.py +34 -0
  12. mnutils/plotting/_widgets/_shared/__init__.py +50 -0
  13. mnutils/plotting/_widgets/_shared/dom.js +124 -0
  14. mnutils/plotting/_widgets/_shared/theme.css +140 -0
  15. mnutils/plotting/_widgets/image_grid.css +202 -0
  16. mnutils/plotting/_widgets/image_grid.js +212 -0
  17. mnutils/plotting/_widgets/image_grid.py +86 -0
  18. mnutils/plotting/_widgets/mrsi_inspector.css +376 -0
  19. mnutils/plotting/_widgets/mrsi_inspector.js +681 -0
  20. mnutils/plotting/_widgets/mrsi_inspector.py +70 -0
  21. mnutils/plotting/_widgets/slice_viewer.css +38 -0
  22. mnutils/plotting/_widgets/slice_viewer.js +41 -0
  23. mnutils/plotting/_widgets/slice_viewer.py +35 -0
  24. mnutils/plotting/images.py +1399 -0
  25. mnutils/plotting/saving.py +138 -0
  26. mnutils/plotting/spectra.py +357 -0
  27. mnutils/py.typed +0 -0
  28. mnutils/testing/__init__.py +3 -0
  29. mnutils/testing/_spectra.py +338 -0
  30. mnutils/testing/synthetic.py +373 -0
  31. mnutils/utils/__init__.py +25 -0
  32. mnutils/utils/data_loaders.py +218 -0
  33. mnutils/utils/file_helpers.py +708 -0
  34. mnutils/utils/images.py +202 -0
  35. mnutils/utils/nifti.py +379 -0
  36. mnutils/utils/spectra.py +149 -0
  37. mnutils-1.2.0.dist-info/METADATA +161 -0
  38. mnutils-1.2.0.dist-info/RECORD +41 -0
  39. mnutils-1.2.0.dist-info/WHEEL +4 -0
  40. mnutils-1.2.0.dist-info/entry_points.txt +7 -0
  41. mnutils-1.2.0.dist-info/licenses/LICENSE +28 -0
@@ -0,0 +1,202 @@
1
+ /* Every size that decides how big the plot area gets, in one place and in
2
+ rem so a reader's font size scales the whole widget together. Override any
3
+ of them on the widget element to retune a page.
4
+
5
+ Nothing here may exceed the pane the widget lands in: `.mnu-viewer` clips
6
+ its overflow, so a layout that is too wide doesn't scroll, it disappears.
7
+ Widths are therefore percentages of the host, with the rem values acting as
8
+ caps on top of them. */
9
+ .mnutils-image-grid {
10
+ --mnu-grid-max-w: 100%; /* the plot area stops growing here */
11
+ --mnu-grid-max-h: 50vh; /* taller than this, the panel box scrolls */
12
+ --mnu-panel-max-h: 17rem; /* no single panel taller than this */
13
+ --mnu-panel-min-h: 8rem; /* ...nor shorter than this */
14
+ --mnu-panel-min-w: 9rem; /* narrower than this, the row wraps instead */
15
+ --mnu-grid-gap: 0.7rem;
16
+ --mnu-panel-pad: 0.7rem;
17
+
18
+ /* How wide the panels would like to be: their height cap times their aspect
19
+ ratio (--mnu-aspect is set per grid by the JS once the first frame has
20
+ loaded), times the nominal column count, plus gaps and padding. This cap
21
+ is what stops a wide window stretching four 64x64 frames across the whole
22
+ screen -- the columns below would otherwise share out every pixel going. */
23
+ --mnu-panel-w-cap: calc(var(--mnu-panel-max-h) * var(--mnu-aspect, 1));
24
+
25
+ /* The floor is the same trade read from the other end. A panel is as tall as
26
+ its width and aspect ratio make it, so "never shorter than
27
+ --mnu-panel-min-h" means "never narrower than that height times the aspect
28
+ ratio" -- for a 5x20 frame that is four times --mnu-panel-min-h. Columns
29
+ take the wider of that and --mnu-panel-min-w, so a row of wide frames wraps
30
+ sooner rather than flattening into strips. */
31
+ --mnu-panel-min-w-eff: max(
32
+ var(--mnu-panel-min-w),
33
+ calc(var(--mnu-panel-min-h) * var(--mnu-aspect, 1))
34
+ );
35
+ --mnu-grid-w-cap: calc(
36
+ var(--mnu-grid-cols, 1) * var(--mnu-panel-w-cap) +
37
+ (var(--mnu-grid-cols, 1) - 1) * var(--mnu-grid-gap) + 2 *
38
+ var(--mnu-panel-pad)
39
+ );
40
+
41
+ margin: 1em 0 0 1em;
42
+ width: 100%;
43
+ max-width: min(calc(var(--mnu-grid-max-w) - 1em), var(--mnu-grid-w-cap));
44
+ /* The panel grid alone can size the widget narrower than its own control
45
+ bar needs (a single small panel, say) -- floor it at the bar's worst-case
46
+ width (label + a 3-digit readout + a usable slider + the border toggle)
47
+ so the slider stays on the bar's first line instead of wrapping under it. */
48
+ min-width: min(100%, 16rem);
49
+ }
50
+
51
+ .mnutils-image-grid-title {
52
+ padding: 0.6em 0.7em 0.2em;
53
+ font-size: 1.05em;
54
+ font-weight: 700;
55
+ text-align: left;
56
+ color: var(--mnu-fg-strong);
57
+ }
58
+
59
+ /* The bar wraps instead of pushing the slider past the clipped edge: in a
60
+ narrow pane the label and readout take the first line and the slider drops
61
+ to the second, at whatever width is left. */
62
+ .mnutils-image-grid-bar {
63
+ flex-wrap: wrap;
64
+ justify-content: flex-start;
65
+ max-width: 100%;
66
+ border-bottom: 1px solid var(--mnu-border-soft);
67
+ }
68
+
69
+ .mnutils-image-grid-bar .mnu-lbl,
70
+ .mnutils-image-grid-bar .mnu-readout {
71
+ font-size: 1.1em;
72
+ }
73
+
74
+ .mnutils-image-grid-bar .mnu-readout {
75
+ font-family: inherit;
76
+ font-weight: 700;
77
+ line-height: inherit;
78
+ min-width: 0;
79
+ color: var(--mnu-fg-strong);
80
+ }
81
+
82
+ .mnutils-image-grid-bar .mnu-slider {
83
+ flex: 1 1 8rem;
84
+ width: auto;
85
+ min-width: 0;
86
+ max-width: 20rem;
87
+ height: 1.3em;
88
+ }
89
+
90
+ /* --mnu-grid-max-h bounds *this box*, not the panels inside it: past that
91
+ height the grid scrolls rather than shrinking its images. Twelve 32x32
92
+ panels are then twelve full-size panels behind a scrollbar, instead of
93
+ twelve stamps -- past a handful of rows, dividing a fixed budget between
94
+ them makes every panel unreadable at once.
95
+ `scrollbar-gutter: stable` reserves the scrollbar's width whether or not it
96
+ is showing, so the panels don't jump sideways when a slice change is the
97
+ thing that makes the content overflow. */
98
+ .mnutils-image-grid-panel {
99
+ padding: var(--mnu-panel-pad);
100
+ border-bottom: none;
101
+ max-height: var(--mnu-grid-max-h);
102
+ overflow-y: auto;
103
+ overscroll-behavior: auto;
104
+ scrollbar-gutter: stable;
105
+ }
106
+
107
+ /* `auto-fit` + `minmax(--mnu-panel-min-w-eff, 1fr)`: as many columns as fit at
108
+ a readable size, wrapping to a new row when they don't -- where "readable"
109
+ is both floors above, whichever binds. The nominal column count
110
+ (`GRID_MAX_COLS`, five) is enforced from above by the widget's max-width
111
+ rather than by a fixed track list -- a fixed list is what cut the fourth
112
+ panel off in a narrow editor pane, since grid columns never wrap.
113
+ `min(..., 100%)` keeps the minimum itself from overflowing a pane narrower
114
+ than a single panel. */
115
+ .mnutils-image-grid-cells {
116
+ display: grid;
117
+ grid-template-columns: repeat(
118
+ auto-fit,
119
+ minmax(min(var(--mnu-panel-min-w-eff), 100%), 1fr)
120
+ );
121
+ gap: var(--mnu-grid-gap);
122
+ align-items: start;
123
+ justify-items: start;
124
+ }
125
+
126
+ .mnutils-image-grid-cell {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 0.35em;
130
+ margin: 0;
131
+ min-width: 0;
132
+ width: 100%;
133
+ }
134
+
135
+ /* Width-driven, not height-driven: a panel fills its column, and the column is
136
+ what shrinks when the pane narrows or the grid gains images -- so more
137
+ images means smaller images rather than clipped ones. Height follows from
138
+ the aspect ratio, capped by --mnu-panel-max-h so that a single row can't
139
+ render a 512-row volume at full size. How many rows there are no longer
140
+ enters into it: the box above scrolls instead.
141
+
142
+ The floor is enforced by the column width above rather than here, so that
143
+ the panel grows *wider* to reach --mnu-panel-min-h instead of being
144
+ letterboxed into it. `min-height` stays as a backstop for the one case the
145
+ columns can't fix -- a pane too narrow for even a single minimum-width
146
+ column -- and, with `object-fit: contain`, for a frame so tall that the
147
+ height cap bites before the width one. Either way the frame keeps its
148
+ aspect ratio; only the padding around it changes. */
149
+ .mnutils-image-grid-image {
150
+ display: block;
151
+ width: 100%;
152
+ height: auto;
153
+ min-height: var(--mnu-panel-min-h);
154
+ max-height: var(--mnu-panel-max-h);
155
+ object-fit: contain;
156
+ /* Set per image by the JS: `pixelated` whenever the frame is being scaled
157
+ up, so a 20x20 array reads as 400 big square pixels instead of a blur,
158
+ and `auto` when it is being scaled down, where nearest-neighbour would
159
+ just alias. */
160
+ image-rendering: var(--mnu-image-rendering, auto);
161
+ }
162
+
163
+ /* Toggled by the bar's "Border" checkbox. `border-box` keeps the outline
164
+ inside the column the panel already occupies, so ticking it doesn't reflow
165
+ the grid. */
166
+ .mnutils-image-grid-cells.is-bordered .mnutils-image-grid-image {
167
+ box-sizing: border-box;
168
+ border: 1px solid var(--mnu-border);
169
+ }
170
+
171
+ .mnutils-image-grid-caption {
172
+ font-size: 0.85em;
173
+ text-align: center;
174
+ color: var(--mnu-fg);
175
+ }
176
+
177
+ .mnutils-image-grid-cbar {
178
+ display: flex;
179
+ align-items: center;
180
+ gap: 0.4em;
181
+ width: 100%;
182
+ }
183
+
184
+ .mnutils-image-grid-cbar-shared {
185
+ margin: 0.7rem auto 0;
186
+ max-width: min(100%, 26rem);
187
+ }
188
+
189
+ .mnutils-image-grid-cbar-strip {
190
+ flex: 1;
191
+ min-width: 0;
192
+ height: 0.55em;
193
+ border: 1px solid var(--mnu-border);
194
+ border-radius: 2px;
195
+ }
196
+
197
+ .mnutils-image-grid-cbar-tick {
198
+ font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
199
+ font-size: 0.7em;
200
+ color: var(--mnu-muted);
201
+ white-space: nowrap;
202
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Grid of pre-rendered image panels driven by one shared slice slider.
3
+ *
4
+ * The frames are bare rasters straight from PIL; the figure title, panel
5
+ * captions and colorbar are built here instead of being baked into the pixels
6
+ * (see `image_grid.py`). Frames are slice-major -- `frames[slice][panel]` --
7
+ * so moving the slider swaps every panel's `src` in one pass.
8
+ */
9
+
10
+ /** Format a colorbar tick the way a reader expects to see a data value. */
11
+ function formatTick(value) {
12
+ if (!Number.isFinite(value)) return String(value);
13
+ const magnitude = Math.abs(value);
14
+ if (value === 0) return "0";
15
+ if (magnitude >= 1e4 || magnitude < 1e-2) return value.toExponential(1);
16
+ return String(Number(value.toPrecision(3)));
17
+ }
18
+
19
+ /**
20
+ * Build a colorbar: a `linear-gradient` strip flanked by its two bounds.
21
+ *
22
+ * The gradient is the matplotlib colormap sampled to hex stops in Python, so
23
+ * the bar and the pixels it explains come from one colormap and can't drift.
24
+ * Returns the wrapper plus a `setBounds` that rewrites only the tick text --
25
+ * `colorbar_mode="each"` autoscales per slice, so those two nodes change on
26
+ * every slider move while the gradient never does.
27
+ */
28
+ function makeColorbar(stops) {
29
+ const wrap = document.createElement("div");
30
+ wrap.className = "mnutils-image-grid-cbar";
31
+
32
+ const lo = document.createElement("span");
33
+ lo.className = "mnutils-image-grid-cbar-tick";
34
+
35
+ const strip = document.createElement("div");
36
+ strip.className = "mnutils-image-grid-cbar-strip";
37
+ strip.style.background = `linear-gradient(to right, ${stops.join(", ")})`;
38
+
39
+ const hi = document.createElement("span");
40
+ hi.className = "mnutils-image-grid-cbar-tick";
41
+
42
+ wrap.append(lo, strip, hi);
43
+
44
+ return {
45
+ wrap,
46
+ setBounds: ([low, high]) => {
47
+ lo.textContent = formatTick(low);
48
+ hi.textContent = formatTick(high);
49
+ },
50
+ };
51
+ }
52
+
53
+ function renderWidget(data, el) {
54
+ const {
55
+ frames,
56
+ bounds,
57
+ colormap_stops: stops,
58
+ n_slices: nSlices,
59
+ num_panels: numPanels,
60
+ num_cols: numCols,
61
+ titles,
62
+ fig_title: figTitle,
63
+ colorbar_mode: colorbarMode,
64
+ initial_index: initialIndex,
65
+ slice_label: sliceLabel,
66
+ } = data;
67
+
68
+ // WebP with an alpha channel: values the colormap marks "bad" (NaN, and so
69
+ // anything masked out by `zeros_as_nan`) are transparent rather than a
70
+ // colour, so the panel background shows through in both light and dark.
71
+ const urls = frames.map((slice) =>
72
+ slice.map((b64) => URL.createObjectURL(new Blob([b64ToBytes(b64)], { type: "image/webp" }))),
73
+ );
74
+
75
+ const viewer = document.createElement("div");
76
+ viewer.className = "mnu-viewer mnutils-image-grid";
77
+
78
+ if (figTitle) {
79
+ const heading = document.createElement("div");
80
+ heading.className = "mnutils-image-grid-title";
81
+ heading.textContent = figTitle;
82
+ viewer.append(heading);
83
+ }
84
+
85
+ const panel = document.createElement("div");
86
+ panel.className = "mnu-panel mnutils-image-grid-panel";
87
+
88
+ const grid = document.createElement("div");
89
+ grid.className = "mnutils-image-grid-cells";
90
+ // The column count is nominal: the CSS wraps to fewer columns when the pane
91
+ // is too narrow for them, and caps the widget's width so a wide pane doesn't
92
+ // fit more. Rows are not counted -- however many they come to, the panel box
93
+ // scrolls once they outgrow it.
94
+ const cols = Math.min(numCols, numPanels);
95
+ viewer.style.setProperty("--mnu-grid-cols", String(cols));
96
+
97
+ const images = [];
98
+ const colorbars = [];
99
+ for (let p = 0; p < numPanels; p++) {
100
+ const cell = document.createElement("figure");
101
+ cell.className = "mnutils-image-grid-cell";
102
+
103
+ const img = document.createElement("img");
104
+ img.className = "mnutils-image-grid-image";
105
+ img.src = urls[initialIndex][p];
106
+ img.alt = titles[p] ? `Panel: ${titles[p]}` : `Panel ${p + 1}`;
107
+ images.push(img);
108
+ cell.append(img);
109
+
110
+ if (titles[p]) {
111
+ const caption = document.createElement("figcaption");
112
+ caption.className = "mnutils-image-grid-caption";
113
+ caption.textContent = titles[p];
114
+ cell.append(caption);
115
+ }
116
+
117
+ if (colorbarMode === "each") {
118
+ const cbar = makeColorbar(stops);
119
+ colorbars.push(cbar);
120
+ cell.append(cbar.wrap);
121
+ }
122
+
123
+ grid.append(cell);
124
+ }
125
+
126
+ panel.append(grid);
127
+
128
+ if (colorbarMode === "single") {
129
+ const cbar = makeColorbar(stops);
130
+ cbar.wrap.classList.add("mnutils-image-grid-cbar-shared");
131
+ colorbars.push(cbar);
132
+ panel.append(cbar.wrap);
133
+ }
134
+
135
+ // Low-resolution data should look like the grid of values it is. Once a
136
+ // frame is drawn wider than its own pixel count the browser's smooth
137
+ // upscaling turns a 20x20 array into a blur, so those get `pixelated`;
138
+ // frames being scaled *down* keep the smooth default, where nearest
139
+ // -neighbour would only alias. Rechecked on resize, since which side of
140
+ // 1:1 a panel falls on depends on the width it ends up with.
141
+ const syncPixelation = () => {
142
+ for (const img of images) {
143
+ if (!img.naturalWidth) continue;
144
+ img.style.setProperty(
145
+ "--mnu-image-rendering",
146
+ img.getBoundingClientRect().width > img.naturalWidth ? "pixelated" : "auto",
147
+ );
148
+ }
149
+ };
150
+
151
+ // The frames' aspect ratio decides how wide the panels want to be, and so
152
+ // how wide the whole widget is allowed to grow (see --mnu-panel-w-cap).
153
+ // Only measurable once a frame has decoded, hence the load handler.
154
+ const syncAspect = () => {
155
+ const img = images.find((candidate) => candidate.naturalWidth);
156
+ if (img) viewer.style.setProperty("--mnu-aspect", String(img.naturalWidth / img.naturalHeight));
157
+ };
158
+
159
+ for (const img of images) {
160
+ img.addEventListener("load", () => {
161
+ syncAspect();
162
+ syncPixelation();
163
+ });
164
+ }
165
+ if (typeof ResizeObserver === "function") new ResizeObserver(syncPixelation).observe(grid);
166
+
167
+ const showSlice = (idx) => {
168
+ for (let p = 0; p < numPanels; p++) images[p].src = urls[idx][p];
169
+ // "single" has one bar for the whole grid, so panel 0's bounds are the
170
+ // grid's bounds -- with a shared colorbar every panel shares them anyway.
171
+ colorbars.forEach((cbar, i) => cbar.setBounds(bounds[idx][colorbarMode === "each" ? i : 0]));
172
+ };
173
+ showSlice(initialIndex);
174
+
175
+ // A panel's edge is invisible wherever the data is NaN, since those pixels
176
+ // are transparent -- which makes a mostly-masked image look like it has an
177
+ // arbitrary shape. The border draws the frame's real extent back in, off by
178
+ // default because it is clutter on an image that fills its own bounds.
179
+ const { toggle } = makeToggle({
180
+ label: "Border",
181
+ checked: false,
182
+ onChange: (on) => grid.classList.toggle("is-bordered", on),
183
+ });
184
+
185
+ // A single-slice stack has nothing to scrub, so it gets a bar with just the
186
+ // toggle rather than a disabled slider whose only honest readout is "0 of 0".
187
+ const controls = [];
188
+ if (nSlices > 1) {
189
+ const { lbl, slider, readout } = makeSliceSlider({
190
+ min: 0,
191
+ max: nSlices - 1,
192
+ value: initialIndex,
193
+ label: sliceLabel,
194
+ formatReadout: (idx) => String(idx),
195
+ onInput: showSlice,
196
+ });
197
+ // Reserve the readout's full width up front, sized to the largest index
198
+ // it will ever show -- otherwise a 3-digit slice count only grows the bar
199
+ // past its wrap threshold once the slider is dragged there, so the layout
200
+ // shifts mid-scrub instead of on load.
201
+ readout.style.minWidth = `${String(nSlices - 1).length}ch`;
202
+ controls.push(makeGroup(lbl, readout), slider);
203
+ }
204
+ const bar = makeBar(...controls, toggle);
205
+ bar.classList.add("mnutils-image-grid-bar");
206
+ viewer.append(bar);
207
+
208
+ viewer.append(panel);
209
+ el.append(viewer);
210
+ }
211
+
212
+ export default renderWidget;
@@ -0,0 +1,86 @@
1
+ import base64
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from ._html import render_html
6
+ from ._shared import load_css, load_esm
7
+
8
+ _DIR = Path(__file__).parent
9
+ _JS = load_esm(_DIR / "image_grid.js")
10
+ _CSS = load_css(_DIR / "image_grid.css")
11
+
12
+
13
+ class ImageGridWidget:
14
+ """Grid of pre-rendered image panels with one shared slice slider.
15
+
16
+ The panels arrive as bare rasters -- no titles, no colorbar, no axes --
17
+ because they were produced by `plotting.images` straight from PIL rather
18
+ than from a matplotlib `Figure`. Everything that used to be drawn *around*
19
+ the pixels is rebuilt here as DOM: the figure title as a heading, each
20
+ panel's title as a `<figcaption>`, and the colorbar as a CSS gradient
21
+ strip with text ticks. That keeps the text sharp at any zoom and lets a
22
+ slice change rewrite a couple of text nodes instead of a whole raster.
23
+
24
+ Like every widget in this package the payload is baked in at construction,
25
+ so the slider keeps working in a statically built docs page with no kernel
26
+ behind it.
27
+
28
+ Parameters
29
+ ----------
30
+ frames : list[list[bytes]]
31
+ Encoded panel images, slice-major: ``frames[slice_idx][panel_idx]``.
32
+ bounds : list[list[tuple[float, float]]]
33
+ The ``(vmin, vmax)`` the matching frame was normalised with, same
34
+ indexing as `frames`. Only read when a colorbar is shown, but always
35
+ per slice, since ``colorbar_mode="each"`` autoscales every panel on
36
+ every slice.
37
+ colormap_stops : list[str]
38
+ The colormap sampled to CSS hex colors, low to high; interpolated into
39
+ a `linear-gradient` for the colorbar strip.
40
+ num_cols : int
41
+ Panels per row.
42
+ titles : list[str]
43
+ Per-panel captions. May be shorter than the number of panels (a panel
44
+ without a title simply gets none).
45
+ fig_title : str
46
+ Heading above the whole grid; omitted when empty.
47
+ colorbar_mode : str
48
+ ``"single"`` for one strip under the grid, ``"each"`` for a strip per
49
+ panel, ``""`` for none.
50
+ initial_index : int, optional
51
+ Slice shown on first display, by default 0.
52
+ slice_label : str, optional
53
+ Label next to the slider, by default ``"Slice"``.
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ frames: list[list[bytes]],
59
+ bounds: list[list[tuple[float, float]]],
60
+ colormap_stops: list[str],
61
+ num_cols: int,
62
+ titles: list[str] | None = None,
63
+ fig_title: str = "",
64
+ colorbar_mode: str = "",
65
+ initial_index: int = 0,
66
+ slice_label: str = "Slice",
67
+ ) -> None:
68
+ self._data: dict[str, Any] = {
69
+ "frames": [
70
+ [base64.b64encode(panel).decode("ascii") for panel in slice_frames]
71
+ for slice_frames in frames
72
+ ],
73
+ "bounds": [[list(panel) for panel in slice_bounds] for slice_bounds in bounds],
74
+ "colormap_stops": colormap_stops,
75
+ "n_slices": len(frames),
76
+ "num_panels": len(frames[0]) if frames else 0,
77
+ "num_cols": num_cols,
78
+ "titles": titles if titles else [],
79
+ "fig_title": fig_title,
80
+ "colorbar_mode": colorbar_mode,
81
+ "initial_index": initial_index,
82
+ "slice_label": slice_label,
83
+ }
84
+
85
+ def _repr_html_(self) -> str:
86
+ return render_html(_JS, _CSS, self._data)