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

Files changed (45) hide show
  1. figpack/__init__.py +2 -3
  2. figpack/cli.py +74 -0
  3. figpack/core/__init__.py +2 -2
  4. figpack/core/_bundle_utils.py +85 -18
  5. figpack/core/_file_handler.py +192 -0
  6. figpack/core/_server_manager.py +42 -6
  7. figpack/core/_show_view.py +1 -1
  8. figpack/core/_view_figure.py +43 -12
  9. figpack/core/extension_view.py +7 -25
  10. figpack/core/figpack_extension.py +0 -71
  11. figpack/extensions.py +356 -0
  12. figpack/figpack-figure-dist/assets/{index-D9a3K6eW.css → index-BJUFDPIM.css} +1 -1
  13. figpack/figpack-figure-dist/assets/index-nBpxgXXT.js +91 -0
  14. figpack/figpack-figure-dist/index.html +2 -2
  15. figpack/views/PlotlyExtension/PlotlyExtension.py +4 -50
  16. figpack/views/PlotlyExtension/_plotly_extension.py +46 -0
  17. figpack/views/PlotlyExtension/plotly_view.js +84 -80
  18. figpack/views/__init__.py +1 -0
  19. {figpack-0.2.16.dist-info → figpack-0.2.18.dist-info}/METADATA +1 -1
  20. figpack-0.2.18.dist-info/RECORD +45 -0
  21. figpack/figpack-figure-dist/assets/index-DtOnN02w.js +0 -846
  22. figpack/franklab/__init__.py +0 -5
  23. figpack/franklab/views/TrackAnimation.py +0 -154
  24. figpack/franklab/views/__init__.py +0 -9
  25. figpack/spike_sorting/__init__.py +0 -5
  26. figpack/spike_sorting/views/AutocorrelogramItem.py +0 -32
  27. figpack/spike_sorting/views/Autocorrelograms.py +0 -116
  28. figpack/spike_sorting/views/AverageWaveforms.py +0 -146
  29. figpack/spike_sorting/views/CrossCorrelogramItem.py +0 -35
  30. figpack/spike_sorting/views/CrossCorrelograms.py +0 -131
  31. figpack/spike_sorting/views/RasterPlot.py +0 -284
  32. figpack/spike_sorting/views/RasterPlotItem.py +0 -28
  33. figpack/spike_sorting/views/SpikeAmplitudes.py +0 -364
  34. figpack/spike_sorting/views/SpikeAmplitudesItem.py +0 -38
  35. figpack/spike_sorting/views/UnitMetricsGraph.py +0 -127
  36. figpack/spike_sorting/views/UnitSimilarityScore.py +0 -40
  37. figpack/spike_sorting/views/UnitsTable.py +0 -82
  38. figpack/spike_sorting/views/UnitsTableColumn.py +0 -40
  39. figpack/spike_sorting/views/UnitsTableRow.py +0 -36
  40. figpack/spike_sorting/views/__init__.py +0 -41
  41. figpack-0.2.16.dist-info/RECORD +0 -61
  42. {figpack-0.2.16.dist-info → figpack-0.2.18.dist-info}/WHEEL +0 -0
  43. {figpack-0.2.16.dist-info → figpack-0.2.18.dist-info}/entry_points.txt +0 -0
  44. {figpack-0.2.16.dist-info → figpack-0.2.18.dist-info}/licenses/LICENSE +0 -0
  45. {figpack-0.2.16.dist-info → figpack-0.2.18.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,7 @@ Base class for views that use figpack extensions
3
3
  """
4
4
 
5
5
  from .figpack_view import FigpackView
6
- from .figpack_extension import ExtensionRegistry
6
+ from .figpack_extension import FigpackExtension
7
7
  from ..core.zarr import Group
8
8
 
9
9
 
@@ -12,7 +12,7 @@ class ExtensionView(FigpackView):
12
12
  Base class for views that are rendered by figpack extensions
13
13
  """
14
14
 
15
- def __init__(self, *, extension_name: str):
15
+ def __init__(self, *, extension: FigpackExtension, view_type: str):
16
16
  """
17
17
  Initialize an extension-based view
18
18
 
@@ -20,17 +20,8 @@ class ExtensionView(FigpackView):
20
20
  extension_name: Name of the extension that will render this view
21
21
  """
22
22
  super().__init__()
23
- self.extension_name = extension_name
24
-
25
- # Validate that the extension is registered
26
- registry = ExtensionRegistry.get_instance()
27
- extension = registry.get_extension(extension_name)
28
- if extension is None:
29
- raise ValueError(
30
- f"Extension '{extension_name}' is not registered. "
31
- f"Make sure to register the extension before creating views that use it."
32
- )
33
23
  self.extension = extension
24
+ self.view_type = view_type
34
25
 
35
26
  def _write_to_zarr_group(self, group: Group) -> None:
36
27
  """
@@ -42,18 +33,9 @@ class ExtensionView(FigpackView):
42
33
  group: Zarr group to write data into
43
34
  """
44
35
  # Set the view type to indicate this is an extension view
45
- group.attrs["view_type"] = "ExtensionView"
36
+ group.attrs["view_type"] = self.view_type
46
37
 
47
38
  # Store the extension name so the frontend knows which extension to use
48
- group.attrs["extension_name"] = self.extension_name
49
-
50
- # Store additional script names
51
- group.attrs["additional_script_names"] = list(
52
- self.extension.get_additional_filenames().keys()
53
- )
54
-
55
- # Store extension metadata for debugging/compatibility
56
- registry = ExtensionRegistry.get_instance()
57
- extension = registry.get_extension(self.extension_name)
58
- if extension:
59
- group.attrs["extension_version"] = extension.version
39
+ group.attrs["extension_name"] = self.extension.name
40
+
41
+ group.attrs["extension_version"] = self.extension.version
@@ -64,74 +64,3 @@ class FigpackExtension:
64
64
  original_name: f"extension-{safe_name}-{original_name}"
65
65
  for original_name in self.additional_files.keys()
66
66
  }
67
-
68
-
69
- class ExtensionRegistry:
70
- """
71
- Singleton registry for managing figpack extensions
72
- """
73
-
74
- _instance: Optional["ExtensionRegistry"] = None
75
-
76
- def __init__(self):
77
- self._extensions: Dict[str, FigpackExtension] = {}
78
-
79
- @classmethod
80
- def get_instance(cls) -> "ExtensionRegistry":
81
- """Get the singleton instance of the extension registry"""
82
- if cls._instance is None:
83
- cls._instance = cls()
84
- return cls._instance
85
-
86
- @classmethod
87
- def register(cls, extension: FigpackExtension) -> None:
88
- """
89
- Register an extension with the global registry
90
-
91
- Args:
92
- extension: The extension to register
93
- """
94
- registry = cls.get_instance()
95
- registry._register_extension(extension)
96
-
97
- def _register_extension(self, extension: FigpackExtension) -> None:
98
- """
99
- Internal method to register an extension
100
-
101
- Args:
102
- extension: The extension to register
103
- """
104
- if extension.name in self._extensions:
105
- existing = self._extensions[extension.name]
106
- if existing.version != extension.version:
107
- print(
108
- f"Warning: Replacing extension '{extension.name}' "
109
- f"version {existing.version} with version {extension.version}"
110
- )
111
-
112
- self._extensions[extension.name] = extension
113
-
114
- def get_extension(self, name: str) -> Optional[FigpackExtension]:
115
- """
116
- Get an extension by name
117
-
118
- Args:
119
- name: Name of the extension to retrieve
120
-
121
- Returns:
122
- The extension if found, None otherwise
123
- """
124
- return self._extensions.get(name)
125
-
126
- def get_all_extensions(self) -> Dict[str, FigpackExtension]:
127
- """
128
- Get all registered extensions
129
-
130
- Returns:
131
- Dictionary mapping extension names to extension objects
132
- """
133
- return self._extensions.copy()
134
-
135
- def clear(self) -> None:
136
- """Clear all registered extensions (mainly for testing)"""
137
- self._extensions.clear()
figpack/extensions.py ADDED
@@ -0,0 +1,356 @@
1
+ """
2
+ Extension management functionality for figpack
3
+ """
4
+
5
+ import json
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ from typing import Dict, List, Optional, Tuple
10
+ from urllib.parse import urljoin
11
+
12
+ import requests
13
+
14
+
15
+ # Default wheel repository URL
16
+ DEFAULT_WHEEL_REPO_URL = "https://flatironinstitute.github.io/figpack/wheels/"
17
+
18
+
19
+ def parse_wheel_filename(filename: str) -> Optional[Dict[str, str]]:
20
+ """
21
+ Parse a wheel filename to extract package information.
22
+
23
+ Wheel filename format: {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl
24
+ """
25
+ if not filename.endswith(".whl"):
26
+ return None
27
+
28
+ # Remove .whl extension
29
+ basename = filename[:-4]
30
+
31
+ # Split on hyphens, but be careful about package names with hyphens
32
+ parts = basename.split("-")
33
+
34
+ if len(parts) < 5:
35
+ return None
36
+
37
+ # The last 3 parts are always python_tag, abi_tag, platform_tag
38
+ python_tag = parts[-3]
39
+ abi_tag = parts[-2]
40
+ platform_tag = parts[-1]
41
+
42
+ # Everything before the last 3 parts, split between name and version
43
+ name_version_parts = parts[:-3]
44
+
45
+ # Find where version starts (first part that looks like a version number)
46
+ version_start_idx = 1 # Default to second part
47
+ for i, part in enumerate(name_version_parts[1:], 1):
48
+ if re.match(r"^\d+", part): # Starts with a digit
49
+ version_start_idx = i
50
+ break
51
+
52
+ name = "-".join(name_version_parts[:version_start_idx])
53
+ version = "-".join(name_version_parts[version_start_idx:])
54
+
55
+ return {
56
+ "name": name,
57
+ "version": version,
58
+ "python_tag": python_tag,
59
+ "abi_tag": abi_tag,
60
+ "platform_tag": platform_tag,
61
+ }
62
+
63
+
64
+ class ExtensionManager:
65
+ """Manages figpack extension packages"""
66
+
67
+ def __init__(self, wheel_repo_url: str = DEFAULT_WHEEL_REPO_URL):
68
+ self.wheel_repo_url = wheel_repo_url.rstrip("/") + "/"
69
+ self._available_cache = None
70
+
71
+ def get_installed_packages(self) -> Dict[str, str]:
72
+ """Get currently installed figpack extension packages and their versions"""
73
+ try:
74
+ result = subprocess.run(
75
+ [sys.executable, "-m", "pip", "list", "--format=json"],
76
+ capture_output=True,
77
+ text=True,
78
+ check=True,
79
+ )
80
+ installed_packages = json.loads(result.stdout)
81
+
82
+ # Get available extensions to filter against
83
+ available_extensions = self.get_available_packages()
84
+
85
+ # Filter for figpack extensions
86
+ figpack_extensions = {}
87
+ for pkg in installed_packages:
88
+ pkg_name = pkg["name"]
89
+ if pkg_name in available_extensions:
90
+ figpack_extensions[pkg_name] = pkg["version"]
91
+
92
+ return figpack_extensions
93
+ except (subprocess.CalledProcessError, json.JSONDecodeError) as e:
94
+ print(f"Warning: Could not get installed packages: {e}")
95
+ return {}
96
+
97
+ def get_available_packages(self) -> Dict[str, Dict[str, str]]:
98
+ """
99
+ Get available packages from the wheel repository.
100
+
101
+ Returns:
102
+ Dict mapping package names to their info (version, description, etc.)
103
+ """
104
+ if self._available_cache is not None:
105
+ return self._available_cache
106
+
107
+ try:
108
+ # Try the simple index first as it's easier to parse
109
+ packages = self._get_packages_from_simple_index()
110
+
111
+ # If that fails, try the main index.html
112
+ if not packages:
113
+ packages = self._get_packages_from_main_index()
114
+
115
+ self._available_cache = packages
116
+ return packages
117
+
118
+ except requests.RequestException as e:
119
+ print(f"Warning: Could not fetch available packages: {e}")
120
+ return {}
121
+
122
+ def _get_packages_from_simple_index(self) -> Dict[str, Dict[str, str]]:
123
+ """Get packages from simple.html using regex parsing"""
124
+ try:
125
+ simple_url = urljoin(self.wheel_repo_url, "simple.html")
126
+ response = requests.get(simple_url, timeout=10)
127
+ response.raise_for_status()
128
+
129
+ packages = {}
130
+
131
+ # Parse wheel filenames from simple index using regex
132
+ # Look for href attributes containing .whl files
133
+ wheel_pattern = re.compile(r'href="([^"]*\.whl)"')
134
+ matches = wheel_pattern.findall(response.text)
135
+
136
+ for wheel_filename in matches:
137
+ wheel_info = parse_wheel_filename(wheel_filename)
138
+ if wheel_info and wheel_info["name"].startswith("figpack_"):
139
+ pkg_name = wheel_info["name"]
140
+
141
+ if pkg_name not in packages:
142
+ packages[pkg_name] = {
143
+ "version": wheel_info["version"],
144
+ "description": self._get_package_description(pkg_name),
145
+ }
146
+ else:
147
+ current_version = packages[pkg_name]["version"]
148
+ new_version = wheel_info["version"]
149
+ if self._is_newer_version(new_version, current_version):
150
+ packages[pkg_name]["version"] = new_version
151
+
152
+ return packages
153
+
154
+ except requests.RequestException:
155
+ return {}
156
+
157
+ def _get_packages_from_main_index(self) -> Dict[str, Dict[str, str]]:
158
+ """Get packages from main index.html using regex parsing"""
159
+ try:
160
+ index_url = urljoin(self.wheel_repo_url, "index.html")
161
+ response = requests.get(index_url, timeout=10)
162
+ response.raise_for_status()
163
+
164
+ packages = {}
165
+
166
+ # Parse wheel filenames from main index using regex
167
+ # Look for links to .whl files
168
+ wheel_pattern = re.compile(r'<a[^>]+href="([^"]*\.whl)"[^>]*>')
169
+ matches = wheel_pattern.findall(response.text)
170
+
171
+ for wheel_filename in matches:
172
+ wheel_info = parse_wheel_filename(wheel_filename)
173
+ if wheel_info and wheel_info["name"].startswith("figpack_"):
174
+ pkg_name = wheel_info["name"]
175
+
176
+ if pkg_name not in packages:
177
+ packages[pkg_name] = {
178
+ "version": wheel_info["version"],
179
+ "description": self._get_package_description(pkg_name),
180
+ }
181
+ else:
182
+ current_version = packages[pkg_name]["version"]
183
+ new_version = wheel_info["version"]
184
+ if self._is_newer_version(new_version, current_version):
185
+ packages[pkg_name]["version"] = new_version
186
+
187
+ return packages
188
+
189
+ except requests.RequestException:
190
+ return {}
191
+
192
+ def _get_package_description(self, package_name: str) -> str:
193
+ """Get a description for a package based on its name"""
194
+ descriptions = {
195
+ "figpack_3d": "3D visualization extension using Three.js",
196
+ "figpack_force_graph": "Force-directed graph visualization extension",
197
+ "figpack_franklab": "Frank Lab specific neuroscience visualization tools",
198
+ "figpack_spike_sorting": "Spike sorting specific visualization tools",
199
+ }
200
+ return descriptions.get(package_name, "Figpack extension package")
201
+
202
+ def _is_newer_version(self, version1: str, version2: str) -> bool:
203
+ """Simple version comparison - returns True if version1 > version2"""
204
+ try:
205
+ v1_parts = [int(x) for x in version1.split(".")]
206
+ v2_parts = [int(x) for x in version2.split(".")]
207
+
208
+ # Pad shorter version with zeros
209
+ max_len = max(len(v1_parts), len(v2_parts))
210
+ v1_parts.extend([0] * (max_len - len(v1_parts)))
211
+ v2_parts.extend([0] * (max_len - len(v2_parts)))
212
+
213
+ return v1_parts > v2_parts
214
+ except ValueError:
215
+ # If we can't parse versions, just do string comparison
216
+ return version1 > version2
217
+
218
+ def list_extensions(self) -> None:
219
+ """List all available extensions with their status"""
220
+ print("Available figpack extensions:")
221
+ print()
222
+
223
+ installed = self.get_installed_packages()
224
+ available = self.get_available_packages()
225
+
226
+ if not available:
227
+ print("No extensions found in the wheel repository.")
228
+ return
229
+
230
+ for ext_name, ext_info in available.items():
231
+ if ext_name in installed:
232
+ status = f"✓ {ext_name} (installed: {installed[ext_name]}, latest: {ext_info['version']})"
233
+ else:
234
+ status = f"✗ {ext_name} (not installed, latest: {ext_info['version']})"
235
+
236
+ print(f"{status} - {ext_info['description']}")
237
+
238
+ def install_extensions(
239
+ self, extensions: List[str], upgrade: bool = False, install_all: bool = False
240
+ ) -> bool:
241
+ """Install or upgrade extensions"""
242
+ available = self.get_available_packages()
243
+
244
+ if install_all:
245
+ extensions = list(available.keys())
246
+
247
+ if not extensions:
248
+ print("No extensions specified")
249
+ return False
250
+
251
+ # Validate extension names
252
+ invalid_extensions = [ext for ext in extensions if ext not in available]
253
+ if invalid_extensions:
254
+ print(f"Error: Unknown extensions: {', '.join(invalid_extensions)}")
255
+ print(f"Available extensions: {', '.join(available.keys())}")
256
+ return False
257
+
258
+ success = True
259
+ for extension in extensions:
260
+ if not self._install_single_extension(extension, upgrade):
261
+ success = False
262
+
263
+ return success
264
+
265
+ def _install_single_extension(self, extension: str, upgrade: bool = False) -> bool:
266
+ """Install a single extension"""
267
+ cmd = [
268
+ sys.executable,
269
+ "-m",
270
+ "pip",
271
+ "install",
272
+ "--find-links",
273
+ self.wheel_repo_url,
274
+ extension,
275
+ ]
276
+
277
+ if upgrade:
278
+ cmd.append("--upgrade")
279
+
280
+ try:
281
+ print(f"Installing {extension}...")
282
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
283
+
284
+ # Check if it was actually installed or already satisfied
285
+ if "successfully installed" in result.stdout.lower():
286
+ print(f"✓ Successfully installed {extension}")
287
+ elif "already satisfied" in result.stdout.lower():
288
+ print(f"✓ {extension} is already installed")
289
+ else:
290
+ print(f"✓ {extension} installation completed")
291
+
292
+ return True
293
+
294
+ except subprocess.CalledProcessError as e:
295
+ print(f"✗ Failed to install {extension}")
296
+ if e.stderr:
297
+ print(f"Error: {e.stderr.strip()}")
298
+ return False
299
+
300
+ def uninstall_extensions(self, extensions: List[str]) -> bool:
301
+ """Uninstall extensions"""
302
+ if not extensions:
303
+ print("No extensions specified")
304
+ return False
305
+
306
+ available = self.get_available_packages()
307
+
308
+ # Validate extension names
309
+ invalid_extensions = [ext for ext in extensions if ext not in available]
310
+ if invalid_extensions:
311
+ print(f"Error: Unknown extensions: {', '.join(invalid_extensions)}")
312
+ return False
313
+
314
+ # Check which extensions are actually installed
315
+ installed = self.get_installed_packages()
316
+ not_installed = [ext for ext in extensions if ext not in installed]
317
+
318
+ if not_installed:
319
+ print(
320
+ f"Warning: These extensions are not installed: {', '.join(not_installed)}"
321
+ )
322
+
323
+ to_uninstall = [ext for ext in extensions if ext in installed]
324
+ if not to_uninstall:
325
+ print("No installed extensions to uninstall")
326
+ return True
327
+
328
+ # Confirm uninstallation
329
+ print(f"This will uninstall: {', '.join(to_uninstall)}")
330
+ response = input("Continue? [y/N]: ").strip().lower()
331
+ if response not in ["y", "yes"]:
332
+ print("Uninstallation cancelled")
333
+ return False
334
+
335
+ success = True
336
+ for extension in to_uninstall:
337
+ if not self._uninstall_single_extension(extension):
338
+ success = False
339
+
340
+ return success
341
+
342
+ def _uninstall_single_extension(self, extension: str) -> bool:
343
+ """Uninstall a single extension"""
344
+ cmd = [sys.executable, "-m", "pip", "uninstall", "-y", extension]
345
+
346
+ try:
347
+ print(f"Uninstalling {extension}...")
348
+ subprocess.run(cmd, capture_output=True, text=True, check=True)
349
+ print(f"✓ Successfully uninstalled {extension}")
350
+ return True
351
+
352
+ except subprocess.CalledProcessError as e:
353
+ print(f"✗ Failed to uninstall {extension}")
354
+ if e.stderr:
355
+ print(f"Error: {e.stderr.strip()}")
356
+ return False
@@ -1 +1 @@
1
- .unitEntryBase{padding-right:13px;padding-top:10px}.unselectedUnitEntry{border:1px transparent solid;font-weight:400;color:#000;background-color:#fff}.selectedUnitEntry{border:1px solid blue;font-weight:700;color:#000;background-color:#b5d1ff}.unitIdsColumnLabelStyle{min-width:200px;font-weight:700;padding:7px 5px}.unitLabelsStyle{padding-right:3px;color:#333}.plotUnitLabel{text-align:left;margin-left:20px;margin-top:5px;font-weight:700}.plotWrapperButtonParent{display:inline-block}.plotWrapperStyleButton{padding-top:50px}.plotUnselectedStyle{border:3px solid rgb(202,224,231)}.plotSelectedStyle{border:3px solid #9999ff;background-color:#fff}.plotUnselectableStyle{border:3px solid transparent}.plotCurrentStyle{border:3px solid blue;background-color:#fff}.plotLabelStyle{font-weight:700;text-align:center;overflow:hidden;white-space:nowrap}:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;overflow:hidden}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}.status-bar{position:fixed;bottom:0;left:0;right:0;height:30px;background-color:#2a2a2a;color:#fff;display:flex;align-items:center;padding:0 12px;font-size:12px;border-top:1px solid #444;z-index:1000}.status-bar.warning{background-color:#ff9800;color:#000}.status-bar.error{background-color:#f44336;color:#fff}.status-bar.expired{background-color:#d32f2f;color:#fff}.manage-button{background-color:#444;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;height:22px;margin-left:12px;transition:background-color .2s}.manage-button:hover{background-color:#666}.about-button{background-color:#444;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;height:22px;margin-left:12px;transition:background-color .2s}.about-button:hover{background-color:#666}.about-dialog-overlay{position:fixed;inset:0;background-color:#00000080;display:flex;justify-content:center;align-items:center;z-index:2000}.about-dialog{background-color:#2a2a2a;color:#fff;border-radius:8px;max-width:600px;max-height:80vh;width:90%;box-shadow:0 4px 20px #0000004d;overflow:hidden}.about-dialog-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #444}.about-dialog-header h2{margin:0;font-size:18px;font-weight:600}.about-dialog-close{background:none;border:none;color:#fff;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s}.about-dialog-close:hover{background-color:#444}.about-dialog-content{padding:20px;overflow-y:auto;max-height:calc(80vh - 80px)}.about-dialog-description{line-height:1.6}.about-dialog-description h1,.about-dialog-description h2,.about-dialog-description h3{margin-top:0;margin-bottom:12px}.about-dialog-description p{margin-bottom:12px}.about-dialog-description code{background-color:#444;padding:2px 4px;border-radius:3px;font-family:Courier New,monospace;font-size:.9em}.about-dialog-description pre{background-color:#444;padding:12px;border-radius:4px;overflow-x:auto;margin-bottom:12px}.about-dialog-description ul,.about-dialog-description ol{margin-bottom:12px;padding-left:20px}.about-dialog-description blockquote{border-left:4px solid #666;padding-left:16px;margin:12px 0;font-style:italic}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}.status-bar{background-color:#f5f5f5;color:#333;border-top:1px solid #ddd}.status-bar.warning{background-color:#fff3cd;color:#856404;border-top:1px solid #ffeaa7}.status-bar.error,.status-bar.expired{background-color:#f8d7da;color:#721c24;border-top:1px solid #f5c6cb}.manage-button{background-color:#e0e0e0;color:#333;border:1px solid #ccc}.manage-button:hover{background-color:#d0d0d0}.about-button{background-color:#e0e0e0;color:#333;border:1px solid #ccc}.about-button:hover{background-color:#d0d0d0}.about-dialog{background-color:#fff;color:#333;box-shadow:0 4px 20px #00000026}.about-dialog-header{border-bottom:1px solid #ddd}.about-dialog-close{color:#333}.about-dialog-close:hover{background-color:#f0f0f0}.about-dialog-description code,.about-dialog-description pre{background-color:#f5f5f5;color:#333}.about-dialog-description blockquote{border-left:4px solid #ccc}}.VerticalToolbar{overflow:hidden;position:absolute}.HorizontalToolbar{display:flex;justify-content:center;align-items:center;overflow:hidden;margin:auto}.SortableTableWidget .MuiTableCell-root{padding:1px 0;font-size:12px;border:solid 1px #f0f0f0}.SortableTableWidget .MuiTableRow-root.selectedRow{background-color:#b5d1ff}.SortableTableWidget .MuiTableRow-root.currentRow{background-color:#95b1df}.SortCaretSpan{font-size:16px;color:gray;padding-left:3px;padding-right:5px;padding-top:2px}.SortableTableWidget .unitLabel{width:10px;height:10px;position:relative;display:inline-block}.SortableTableWidget .SortableTableCheckbox{padding:1px}.NiceTable .MuiTableCell-root{padding:5px}.NiceTable .MuiIconButton-root{padding:1px}
1
+ .unitEntryBase{padding-right:13px;padding-top:10px}.unselectedUnitEntry{border:1px transparent solid;font-weight:400;color:#000;background-color:#fff}.selectedUnitEntry{border:1px solid blue;font-weight:700;color:#000;background-color:#b5d1ff}.unitIdsColumnLabelStyle{min-width:200px;font-weight:700;padding:7px 5px}.unitLabelsStyle{padding-right:3px;color:#333}.plotUnitLabel{text-align:left;margin-left:20px;margin-top:5px;font-weight:700}.plotWrapperButtonParent{display:inline-block}.plotWrapperStyleButton{padding-top:50px}.plotUnselectedStyle{border:3px solid rgb(202,224,231)}.plotSelectedStyle{border:3px solid #9999ff;background-color:#fff}.plotUnselectableStyle{border:3px solid transparent}.plotCurrentStyle{border:3px solid blue;background-color:#fff}.plotLabelStyle{font-weight:700;text-align:center;overflow:hidden;white-space:nowrap}:root{font-family:system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}a{font-weight:500;color:#646cff;text-decoration:inherit}a:hover{color:#535bf2}body{margin:0;overflow:hidden}h1{font-size:3.2em;line-height:1.1}button{border-radius:8px;border:1px solid transparent;padding:.6em 1.2em;font-size:1em;font-weight:500;font-family:inherit;background-color:#1a1a1a;cursor:pointer;transition:border-color .25s}button:hover{border-color:#646cff}button:focus,button:focus-visible{outline:4px auto -webkit-focus-ring-color}.status-bar{position:fixed;bottom:0;left:0;right:0;height:30px;background-color:#2a2a2a;color:#fff;display:flex;align-items:center;padding:0 12px;font-size:12px;border-top:1px solid #444;z-index:1000}.status-bar.warning{background-color:#ff9800;color:#000}.status-bar.error{background-color:#f44336;color:#fff}.status-bar.expired{background-color:#d32f2f;color:#fff}.manage-button{background-color:#444;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;height:22px;margin-left:12px;transition:background-color .2s}.manage-button:hover{background-color:#666}.about-button{background-color:#444;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;height:22px;margin-left:12px;transition:background-color .2s}.about-button:hover{background-color:#666}.about-dialog-overlay{position:fixed;inset:0;background-color:#00000080;display:flex;justify-content:center;align-items:center;z-index:2000}.about-dialog{background-color:#2a2a2a;color:#fff;border-radius:8px;max-width:600px;max-height:80vh;width:90%;box-shadow:0 4px 20px #0000004d;overflow:hidden}.about-dialog-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #444}.about-dialog-header h2{margin:0;font-size:18px;font-weight:600}.about-dialog-close{background:none;border:none;color:#fff;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s}.about-dialog-close:hover{background-color:#444}.about-dialog-content{padding:20px;overflow-y:auto;max-height:calc(80vh - 80px)}.about-dialog-description{line-height:1.6}.about-dialog-description h1,.about-dialog-description h2,.about-dialog-description h3{margin-top:0;margin-bottom:12px}.about-dialog-description p{margin-bottom:12px}.about-dialog-description code{background-color:#444;padding:2px 4px;border-radius:3px;font-family:Courier New,monospace;font-size:.9em}.about-dialog-description pre{background-color:#444;padding:12px;border-radius:4px;overflow-x:auto;margin-bottom:12px}.about-dialog-description ul,.about-dialog-description ol{margin-bottom:12px;padding-left:20px}.about-dialog-description blockquote{border-left:4px solid #666;padding-left:16px;margin:12px 0;font-style:italic}.manage-edits-view{display:flex;align-items:center;margin-left:12px}.edits-notification{color:#ff9800;font-weight:500;margin-right:8px}.save-changes-button{background-color:#ff9800;color:#000;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:11px;height:22px;transition:background-color .2s}.save-changes-button:hover{background-color:#f57c00}.dialog-overlay{position:fixed;inset:0;background-color:#00000080;display:flex;justify-content:center;align-items:center;z-index:2000}.dialog-content{background-color:#2a2a2a;color:#fff;border-radius:8px;max-width:500px;width:90%;box-shadow:0 4px 20px #0000004d;overflow:hidden}.dialog-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid #444}.dialog-header h3{margin:0;font-size:18px;font-weight:600}.dialog-close{background:none;border:none;color:#fff;font-size:24px;cursor:pointer;padding:0;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:4px;transition:background-color .2s}.dialog-close:hover{background-color:#444}.dialog-body{padding:20px}.dialog-body p{margin:0 0 12px;line-height:1.5}.dialog-footer{padding:16px 20px;border-top:1px solid #444;display:flex;justify-content:flex-end}.dialog-button{background-color:#444;color:#fff;border:none;padding:8px 16px;border-radius:4px;cursor:pointer;font-size:14px;transition:background-color .2s}.dialog-button:hover{background-color:#666}.dialog-fullscreen{position:fixed;inset:0;background-color:#2a2a2a;color:#fff;z-index:2000;display:flex;flex-direction:column}.dialog-content-fullscreen{display:flex;flex-direction:column;height:100%;overflow:hidden}.dialog-content-fullscreen .dialog-header{flex-shrink:0}.dialog-content-fullscreen .dialog-body{flex:1;overflow-y:auto;padding:20px}.dialog-content-fullscreen .dialog-footer{flex-shrink:0}@media (prefers-color-scheme: light){:root{color:#213547;background-color:#fff}a:hover{color:#747bff}button{background-color:#f9f9f9}.status-bar{background-color:#f5f5f5;color:#333;border-top:1px solid #ddd}.status-bar.warning{background-color:#fff3cd;color:#856404;border-top:1px solid #ffeaa7}.status-bar.error,.status-bar.expired{background-color:#f8d7da;color:#721c24;border-top:1px solid #f5c6cb}.manage-button{background-color:#e0e0e0;color:#333;border:1px solid #ccc}.manage-button:hover{background-color:#d0d0d0}.about-button{background-color:#e0e0e0;color:#333;border:1px solid #ccc}.about-button:hover{background-color:#d0d0d0}.about-dialog{background-color:#fff;color:#333;box-shadow:0 4px 20px #00000026}.about-dialog-header{border-bottom:1px solid #ddd}.about-dialog-close{color:#333}.about-dialog-close:hover{background-color:#f0f0f0}.about-dialog-description code,.about-dialog-description pre{background-color:#f5f5f5;color:#333}.about-dialog-description blockquote{border-left:4px solid #ccc}.edits-notification{color:#e65100}.save-changes-button{background-color:#ff9800;color:#fff;border:1px solid #f57c00}.save-changes-button:hover{background-color:#f57c00}.dialog-content{background-color:#fff;color:#333;box-shadow:0 4px 20px #00000026}.dialog-header{border-bottom:1px solid #ddd}.dialog-close{color:#333}.dialog-close:hover{background-color:#f0f0f0}.dialog-footer{border-top:1px solid #ddd}.dialog-button{background-color:#e0e0e0;color:#333;border:1px solid #ccc}.dialog-button:hover{background-color:#d0d0d0}.dialog-fullscreen{background-color:#fff;color:#333}}