figpack 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- {figpack-0.4.0/figpack.egg-info → figpack-0.4.2}/PKG-INFO +1 -1
- {figpack-0.4.0 → figpack-0.4.2}/figpack/__init__.py +1 -1
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_bundle_utils.py +13 -1
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_figure_spec.py +13 -1
- figpack-0.4.2/figpack/core/_remix_patch.py +228 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_save_figure.py +11 -1
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_show_view.py +14 -2
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/figpack_view.py +24 -3
- figpack-0.4.0/figpack/figpack-figure-dist/assets/index-BKMp8GZO.js → figpack-0.4.2/figpack/figpack-figure-dist/assets/index-DCvZuZLF.js +84 -80
- figpack-0.4.0/figpack/figpack-figure-dist/assets/index-GPjx4QpG.css → figpack-0.4.2/figpack/figpack-figure-dist/assets/index-oS-16pKZ.css +1 -1
- {figpack-0.4.0 → figpack-0.4.2}/figpack/figpack-figure-dist/index.html +2 -2
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/TimeseriesGraph.py +9 -0
- {figpack-0.4.0 → figpack-0.4.2/figpack.egg-info}/PKG-INFO +1 -1
- {figpack-0.4.0 → figpack-0.4.2}/figpack.egg-info/SOURCES.txt +4 -2
- {figpack-0.4.0 → figpack-0.4.2}/pyproject.toml +1 -1
- figpack-0.4.2/tests/test_remix_patch.py +128 -0
- {figpack-0.4.0 → figpack-0.4.2}/LICENSE +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/MANIFEST.in +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/README.md +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/cli.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/__init__.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_figure_utils.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_file_handler.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_patch_figure.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_remix_figure.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_revert_patch_figure.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_server_manager.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_upload_bundle.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_view_figure.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/_zarr_consolidate.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/config.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/extension_view.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/figpack_extension.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/core/zarr.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/extensions.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/figpack-figure-dist/assets/neurosift-logo-CLsuwLMO.png +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/utils/__init__.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/Box.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/CaptionedView.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/DataFrame.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/Gallery.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/GalleryItem.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/Iframe.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/Image.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/LayoutItem.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/Markdown.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/MatplotlibFigure.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/MountainLayout.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/MountainLayoutItem.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/MultiChannelTimeseries.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/PlotlyExtension/PlotlyExtension.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/PlotlyExtension/__init__.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/PlotlyExtension/_plotly_extension.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/PlotlyExtension/plotly_view.js +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/Spectrogram.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/Splitter.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/TabLayout.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/TabLayoutItem.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/VerticalLayout.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/VerticalLayoutItem.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack/views/__init__.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack.egg-info/dependency_links.txt +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack.egg-info/entry_points.txt +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack.egg-info/requires.txt +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/figpack.egg-info/top_level.txt +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/setup.cfg +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_box.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_cli.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_core.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_dataframe.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_extension_system.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_figpack_view.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_figure_spec.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_file_handler.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_gallery.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_image.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_markdown.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_matplotlib_figure.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_multichannel_timeseries.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_patch_pages.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_plotly_figure.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_server_manager.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_spectrogram.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_splitter.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_tablayout.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_timeseries_graph.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_upload_bundle.py +0 -0
- {figpack-0.4.0 → figpack-0.4.2}/tests/test_view_figure.py +0 -0
|
@@ -3,6 +3,8 @@ import pathlib
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Optional, List
|
|
5
5
|
|
|
6
|
+
from ._remix_patch import RemixArg, apply_remix_patch
|
|
7
|
+
|
|
6
8
|
import zarr
|
|
7
9
|
|
|
8
10
|
from .figpack_view import FigpackView
|
|
@@ -26,6 +28,7 @@ def prepare_figure_bundle(
|
|
|
26
28
|
title: str,
|
|
27
29
|
description: Optional[str] = None,
|
|
28
30
|
script: Optional[str] = None,
|
|
31
|
+
remix: Optional[RemixArg] = None,
|
|
29
32
|
) -> None:
|
|
30
33
|
"""
|
|
31
34
|
Prepare a figure bundle in the specified temporary directory.
|
|
@@ -33,7 +36,8 @@ def prepare_figure_bundle(
|
|
|
33
36
|
This function:
|
|
34
37
|
1. Copies all files from the figpack-figure-dist directory to tmpdir
|
|
35
38
|
2. Writes the view data to a zarr group
|
|
36
|
-
3. Writes the figure spec (layout and settings) into index.html
|
|
39
|
+
3. Writes the figure spec (layout and settings) into index.html, with a
|
|
40
|
+
remix patch merged in if given
|
|
37
41
|
4. Discovers and writes extension JavaScript files
|
|
38
42
|
5. Consolidates zarr metadata
|
|
39
43
|
|
|
@@ -43,6 +47,8 @@ def prepare_figure_bundle(
|
|
|
43
47
|
title: Title for the figure (required)
|
|
44
48
|
description: Optional description for the figure (markdown supported)
|
|
45
49
|
script: Optional script text used to generate the figure
|
|
50
|
+
remix: Optional remix patch (a file path or a loaded dict) to merge
|
|
51
|
+
into the figure spec
|
|
46
52
|
"""
|
|
47
53
|
html_dir = thisdir / ".." / "figpack-figure-dist"
|
|
48
54
|
if not os.path.exists(html_dir):
|
|
@@ -84,11 +90,17 @@ def prepare_figure_bundle(
|
|
|
84
90
|
index_html_path = pathlib.Path(tmpdir) / "index.html"
|
|
85
91
|
figure_spec = build_figure_spec(zarr_group._zarr_group)
|
|
86
92
|
figure_spec_docs = build_figure_spec_docs(zarr_group._zarr_group)
|
|
93
|
+
remix_record = None
|
|
94
|
+
if remix is not None:
|
|
95
|
+
figure_spec, remix_record = apply_remix_patch(
|
|
96
|
+
figure_spec, remix, zarr_group._zarr_group
|
|
97
|
+
)
|
|
87
98
|
index_html_path.write_text(
|
|
88
99
|
insert_figure_spec_into_html(
|
|
89
100
|
index_html_path.read_text(encoding="utf-8"),
|
|
90
101
|
figure_spec,
|
|
91
102
|
figure_spec_docs,
|
|
103
|
+
remix_record,
|
|
92
104
|
),
|
|
93
105
|
encoding="utf-8",
|
|
94
106
|
)
|
|
@@ -243,7 +243,10 @@ def _escape_for_script(text: str) -> str:
|
|
|
243
243
|
|
|
244
244
|
|
|
245
245
|
def insert_figure_spec_into_html(
|
|
246
|
-
html: str,
|
|
246
|
+
html: str,
|
|
247
|
+
spec: Dict[str, Any],
|
|
248
|
+
docs: Optional[str] = None,
|
|
249
|
+
remix_record: Optional[Dict[str, Any]] = None,
|
|
247
250
|
) -> str:
|
|
248
251
|
"""
|
|
249
252
|
Insert the spec reference and a figpack.render(spec) script at the end of
|
|
@@ -253,6 +256,9 @@ def insert_figure_spec_into_html(
|
|
|
253
256
|
html: The contents of index.html
|
|
254
257
|
spec: The figure spec
|
|
255
258
|
docs: Optional markdown reference for the spec
|
|
259
|
+
remix_record: Optional record of a remix patch applied to the spec,
|
|
260
|
+
written as a <script type="application/json"
|
|
261
|
+
data-figpack-remix-patch> element
|
|
256
262
|
|
|
257
263
|
Returns:
|
|
258
264
|
The updated html
|
|
@@ -270,6 +276,12 @@ def insert_figure_spec_into_html(
|
|
|
270
276
|
+ _escape_for_script(docs)
|
|
271
277
|
+ " </script>\n"
|
|
272
278
|
)
|
|
279
|
+
if remix_record is not None:
|
|
280
|
+
script += (
|
|
281
|
+
' <script type="application/json" data-figpack-remix-patch>\n '
|
|
282
|
+
+ _escape_for_script(format_spec_json(remix_record, indent=4))
|
|
283
|
+
+ "\n </script>\n"
|
|
284
|
+
)
|
|
273
285
|
script += (
|
|
274
286
|
' <script type="module" data-figpack-spec>\n'
|
|
275
287
|
+ f" figpack.render({spec_json}, {{ specVersion: {FIGURE_SPEC_VERSION} }});\n"
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Remix patches: applying a remix's changes when a figure is made
|
|
3
|
+
|
|
4
|
+
A remix patch carries the changes a remix makes to a figure's spec, keyed by
|
|
5
|
+
the figure's structure (child names and props) rather than by the code that
|
|
6
|
+
built the figure. It is exported from the remix page (Export patch) and
|
|
7
|
+
applied with view.show(..., remix=...) or view.save(..., remix=...). Format
|
|
8
|
+
(version 1):
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
"figpack_remix_patch": 1,
|
|
12
|
+
"source": {"figureUrl": "...", "createdAt": "2026-10-23T12:00:00.000Z"},
|
|
13
|
+
"patch": <spec diff>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
The spec diff has the shape of a spec node, {data?, view_type?, props?,
|
|
17
|
+
children?}, with only what the remix changed. It is merged into the figure's
|
|
18
|
+
spec with the same rules as the spec itself:
|
|
19
|
+
|
|
20
|
+
- props replace per key (whole values; lists and objects are not merged)
|
|
21
|
+
- children merge by name
|
|
22
|
+
- data and view_type replace; a data path must exist in data.zarr
|
|
23
|
+
|
|
24
|
+
Parts that do not match the figure are reported as warnings and skipped: a
|
|
25
|
+
child that is neither in the spec nor a subgroup in data.zarr (unless the
|
|
26
|
+
patch gives it its own data), a data path that does not exist, or a value of
|
|
27
|
+
the wrong type. The figpack-manage remix page applies patches with the same
|
|
28
|
+
rules (figpack-manage/src/pages/RemixPage/remixPatch.ts); both are tested
|
|
29
|
+
against the shared cases in tests/remix_patch_cases. See
|
|
30
|
+
docs/remix_patches.md.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import copy
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import pathlib
|
|
37
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
38
|
+
|
|
39
|
+
REMIX_PATCH_VERSION = 1
|
|
40
|
+
|
|
41
|
+
_NODE_KEYS = {"data", "view_type", "props", "children"}
|
|
42
|
+
|
|
43
|
+
RemixArg = Union[str, os.PathLike, Dict[str, Any]]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def load_remix_patch(remix: RemixArg) -> Tuple[Dict[str, Any], Optional[str]]:
|
|
47
|
+
"""
|
|
48
|
+
Load a remix patch from a file path, or check an already-loaded dict
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
remix: Path to a patch file, or the patch as a dict
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
(patch, file name or None)
|
|
55
|
+
"""
|
|
56
|
+
file_name = None
|
|
57
|
+
if isinstance(remix, dict):
|
|
58
|
+
patch = remix
|
|
59
|
+
else:
|
|
60
|
+
path = pathlib.Path(remix)
|
|
61
|
+
file_name = path.name
|
|
62
|
+
patch = json.loads(path.read_text(encoding="utf-8"))
|
|
63
|
+
if not isinstance(patch, dict) or "figpack_remix_patch" not in patch:
|
|
64
|
+
raise ValueError("Not a remix patch (missing figpack_remix_patch)")
|
|
65
|
+
version = patch["figpack_remix_patch"]
|
|
66
|
+
if not isinstance(version, int) or version > REMIX_PATCH_VERSION:
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"Remix patch version {version} is not supported by this figpack "
|
|
69
|
+
f"(supports {REMIX_PATCH_VERSION}); upgrade figpack"
|
|
70
|
+
)
|
|
71
|
+
if not isinstance(patch.get("patch"), dict):
|
|
72
|
+
raise ValueError("Remix patch has no 'patch' object")
|
|
73
|
+
return patch, file_name
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_group_paths(zarr_group) -> Set[str]:
|
|
77
|
+
"""The paths of all groups in a zarr tree, relative to its root ("" for the root)"""
|
|
78
|
+
paths = {""}
|
|
79
|
+
|
|
80
|
+
def visit(group, prefix):
|
|
81
|
+
for name, subgroup in group.groups():
|
|
82
|
+
path = prefix + name
|
|
83
|
+
paths.add(path)
|
|
84
|
+
visit(subgroup, path + "/")
|
|
85
|
+
|
|
86
|
+
visit(zarr_group, "")
|
|
87
|
+
return paths
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def merge_remix_patch(
|
|
91
|
+
spec: Dict[str, Any], patch: Dict[str, Any], group_paths: Set[str]
|
|
92
|
+
) -> Tuple[Dict[str, Any], List[str]]:
|
|
93
|
+
"""
|
|
94
|
+
Merge a spec diff into a figure spec
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
spec: The figure spec (not modified)
|
|
98
|
+
patch: The spec diff (the "patch" field of a remix patch)
|
|
99
|
+
group_paths: Paths of the groups in data.zarr (see get_group_paths)
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
(merged spec, warnings for the parts that were skipped)
|
|
103
|
+
"""
|
|
104
|
+
warnings: List[str] = []
|
|
105
|
+
merged = _merge_node(spec, patch, "", "", group_paths, warnings)
|
|
106
|
+
return merged, warnings
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _normalize_path(path: str) -> str:
|
|
110
|
+
return path.strip("/")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _where(spec_path: str) -> str:
|
|
114
|
+
return spec_path or "root"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _merge_node(
|
|
118
|
+
node: Dict[str, Any],
|
|
119
|
+
patch: Dict[str, Any],
|
|
120
|
+
base: Optional[str],
|
|
121
|
+
spec_path: str,
|
|
122
|
+
group_paths: Set[str],
|
|
123
|
+
warnings: List[str],
|
|
124
|
+
) -> Dict[str, Any]:
|
|
125
|
+
# base: path of the node's counterpart group in data.zarr, if any
|
|
126
|
+
where = _where(spec_path)
|
|
127
|
+
result = copy.deepcopy(node)
|
|
128
|
+
for key in patch:
|
|
129
|
+
if key not in _NODE_KEYS:
|
|
130
|
+
warnings.append(f"remix: unknown key '{key}' at '{where}'; skipped")
|
|
131
|
+
if "data" in patch:
|
|
132
|
+
data = patch["data"]
|
|
133
|
+
if data is None:
|
|
134
|
+
result["data"] = None
|
|
135
|
+
elif isinstance(data, str) and _normalize_path(data) in group_paths:
|
|
136
|
+
result["data"] = data
|
|
137
|
+
elif isinstance(data, str):
|
|
138
|
+
warnings.append(
|
|
139
|
+
f"remix: data path '{data}' at '{where}' is not in data.zarr; skipped"
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
warnings.append(f"remix: data at '{where}' is not a string; skipped")
|
|
143
|
+
if "view_type" in patch:
|
|
144
|
+
if isinstance(patch["view_type"], str):
|
|
145
|
+
result["view_type"] = patch["view_type"]
|
|
146
|
+
else:
|
|
147
|
+
warnings.append(f"remix: view_type at '{where}' is not a string; skipped")
|
|
148
|
+
if "props" in patch:
|
|
149
|
+
if isinstance(patch["props"], dict):
|
|
150
|
+
result["props"] = {**result.get("props", {}), **patch["props"]}
|
|
151
|
+
else:
|
|
152
|
+
warnings.append(f"remix: props at '{where}' is not an object; skipped")
|
|
153
|
+
if "data" in result:
|
|
154
|
+
base = None if result["data"] is None else _normalize_path(result["data"])
|
|
155
|
+
if "children" in patch:
|
|
156
|
+
if isinstance(patch["children"], dict):
|
|
157
|
+
children = result.get("children", {})
|
|
158
|
+
for name, child_patch in patch["children"].items():
|
|
159
|
+
child_path = f"{spec_path}/{name}" if spec_path else name
|
|
160
|
+
if not isinstance(child_patch, dict):
|
|
161
|
+
warnings.append(
|
|
162
|
+
f"remix: child '{_where(child_path)}' is not an object; skipped"
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
counterpart = None
|
|
166
|
+
if base is not None:
|
|
167
|
+
p = f"{base}/{name}" if base else name
|
|
168
|
+
counterpart = p if p in group_paths else None
|
|
169
|
+
if name not in children and counterpart is None:
|
|
170
|
+
if not _has_own_data(child_patch, group_paths):
|
|
171
|
+
warnings.append(
|
|
172
|
+
f"remix: no child '{name}' under '{where}'; skipped"
|
|
173
|
+
)
|
|
174
|
+
continue
|
|
175
|
+
children[name] = _merge_node(
|
|
176
|
+
children.get(name, {}),
|
|
177
|
+
child_patch,
|
|
178
|
+
counterpart,
|
|
179
|
+
child_path,
|
|
180
|
+
group_paths,
|
|
181
|
+
warnings,
|
|
182
|
+
)
|
|
183
|
+
if children:
|
|
184
|
+
result["children"] = children
|
|
185
|
+
else:
|
|
186
|
+
warnings.append(f"remix: children at '{where}' is not an object; skipped")
|
|
187
|
+
return result
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _has_own_data(patch: Dict[str, Any], group_paths: Set[str]) -> bool:
|
|
191
|
+
# Whether a patch node defines its own base: data null (a node defined
|
|
192
|
+
# only by the spec) or an existing data path
|
|
193
|
+
if "data" not in patch:
|
|
194
|
+
return False
|
|
195
|
+
data = patch["data"]
|
|
196
|
+
return data is None or (
|
|
197
|
+
isinstance(data, str) and _normalize_path(data) in group_paths
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def apply_remix_patch(
|
|
202
|
+
spec: Dict[str, Any], remix: RemixArg, zarr_group
|
|
203
|
+
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
204
|
+
"""
|
|
205
|
+
Apply a remix patch to a figure spec, printing a warning for each part
|
|
206
|
+
that was skipped
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
spec: The figure spec built from the zarr tree
|
|
210
|
+
remix: Path to a patch file, or the patch as a dict
|
|
211
|
+
zarr_group: The (raw) root zarr group of the figure
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
(merged spec, record of the applied patch for index.html)
|
|
215
|
+
"""
|
|
216
|
+
patch, file_name = load_remix_patch(remix)
|
|
217
|
+
merged, warnings = merge_remix_patch(
|
|
218
|
+
spec, patch["patch"], get_group_paths(zarr_group)
|
|
219
|
+
)
|
|
220
|
+
for w in warnings:
|
|
221
|
+
print(f"Warning: {w}")
|
|
222
|
+
record = {
|
|
223
|
+
"file": file_name,
|
|
224
|
+
"source": patch.get("source"),
|
|
225
|
+
"patch": patch["patch"],
|
|
226
|
+
"skipped": warnings,
|
|
227
|
+
}
|
|
228
|
+
return merged, record
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import pathlib
|
|
2
2
|
import tempfile
|
|
3
|
+
from typing import Optional
|
|
3
4
|
|
|
4
5
|
from ._bundle_utils import prepare_figure_bundle
|
|
6
|
+
from ._remix_patch import RemixArg
|
|
5
7
|
from .figpack_view import FigpackView
|
|
6
8
|
|
|
7
9
|
|
|
@@ -12,6 +14,7 @@ def _save_figure(
|
|
|
12
14
|
title: str,
|
|
13
15
|
description: str = "",
|
|
14
16
|
script: str = "",
|
|
17
|
+
remix: Optional[RemixArg] = None,
|
|
15
18
|
) -> None:
|
|
16
19
|
"""
|
|
17
20
|
Save the figure to a folder or a .tar.gz file
|
|
@@ -22,6 +25,7 @@ def _save_figure(
|
|
|
22
25
|
title: Title for the figure
|
|
23
26
|
description: Description text with markdown support
|
|
24
27
|
script: Optional script text used to generate the figure
|
|
28
|
+
remix: Optional remix patch to apply (a file path or a loaded dict)
|
|
25
29
|
"""
|
|
26
30
|
output_path_2 = pathlib.Path(output_path)
|
|
27
31
|
if (output_path_2.suffix == ".gz" and output_path_2.suffixes[-2] == ".tar") or (
|
|
@@ -30,7 +34,12 @@ def _save_figure(
|
|
|
30
34
|
# It's a .tar.gz file
|
|
31
35
|
with tempfile.TemporaryDirectory(prefix="figpack_save_") as tmpdir:
|
|
32
36
|
prepare_figure_bundle(
|
|
33
|
-
view,
|
|
37
|
+
view,
|
|
38
|
+
tmpdir,
|
|
39
|
+
title=title,
|
|
40
|
+
description=description,
|
|
41
|
+
script=script,
|
|
42
|
+
remix=remix,
|
|
34
43
|
)
|
|
35
44
|
# Create tar.gz file
|
|
36
45
|
import tarfile
|
|
@@ -46,4 +55,5 @@ def _save_figure(
|
|
|
46
55
|
title=title,
|
|
47
56
|
description=description,
|
|
48
57
|
script=script,
|
|
58
|
+
remix=remix,
|
|
49
59
|
)
|
|
@@ -5,6 +5,7 @@ import webbrowser
|
|
|
5
5
|
from typing import Union
|
|
6
6
|
|
|
7
7
|
from ._bundle_utils import prepare_figure_bundle
|
|
8
|
+
from ._remix_patch import RemixArg
|
|
8
9
|
from ._server_manager import ProcessServerManager
|
|
9
10
|
from ._upload_bundle import _upload_bundle
|
|
10
11
|
from .figpack_view import FigpackView
|
|
@@ -92,6 +93,7 @@ def _show_view(
|
|
|
92
93
|
title: str,
|
|
93
94
|
description: Union[str, None],
|
|
94
95
|
script: Union[str, None],
|
|
96
|
+
remix: Union[RemixArg, None],
|
|
95
97
|
inline: bool,
|
|
96
98
|
inline_height: int,
|
|
97
99
|
wait_for_input: bool,
|
|
@@ -101,7 +103,12 @@ def _show_view(
|
|
|
101
103
|
# Upload behavior: create temporary directory for this upload only
|
|
102
104
|
with tempfile.TemporaryDirectory(prefix="figpack_upload_") as tmpdir:
|
|
103
105
|
prepare_figure_bundle(
|
|
104
|
-
view,
|
|
106
|
+
view,
|
|
107
|
+
tmpdir,
|
|
108
|
+
title=title,
|
|
109
|
+
description=description,
|
|
110
|
+
script=script,
|
|
111
|
+
remix=remix,
|
|
105
112
|
)
|
|
106
113
|
|
|
107
114
|
# Check for API key - required for regular uploads, optional for ephemeral
|
|
@@ -146,7 +153,12 @@ def _show_view(
|
|
|
146
153
|
|
|
147
154
|
# Prepare the figure bundle in the subdirectory
|
|
148
155
|
prepare_figure_bundle(
|
|
149
|
-
view,
|
|
156
|
+
view,
|
|
157
|
+
str(figure_dir),
|
|
158
|
+
title=title,
|
|
159
|
+
description=description,
|
|
160
|
+
script=script,
|
|
161
|
+
remix=remix,
|
|
150
162
|
)
|
|
151
163
|
|
|
152
164
|
# Start or get existing server
|
|
@@ -5,10 +5,13 @@ Base view class for figpack visualization components
|
|
|
5
5
|
import os
|
|
6
6
|
import random
|
|
7
7
|
import string
|
|
8
|
-
from typing import Optional
|
|
8
|
+
from typing import TYPE_CHECKING, Optional
|
|
9
9
|
|
|
10
10
|
from .zarr import Group
|
|
11
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ._remix_patch import RemixArg
|
|
14
|
+
|
|
12
15
|
|
|
13
16
|
class FigpackView:
|
|
14
17
|
"""
|
|
@@ -21,6 +24,7 @@ class FigpackView:
|
|
|
21
24
|
title: str,
|
|
22
25
|
description: Optional[str] = None,
|
|
23
26
|
script: Optional[str] = None,
|
|
27
|
+
remix: Optional["RemixArg"] = None,
|
|
24
28
|
port: Optional[int] = None,
|
|
25
29
|
open_in_browser: Optional[bool] = None,
|
|
26
30
|
upload: Optional[bool] = None,
|
|
@@ -43,6 +47,9 @@ class FigpackView:
|
|
|
43
47
|
title: Title for browser tab and figure (required)
|
|
44
48
|
description: Description text with markdown support (optional)
|
|
45
49
|
script: Optional script text used to generate the figure
|
|
50
|
+
remix: Optional remix patch to apply to the figure spec: a path to a
|
|
51
|
+
patch file exported from the remix page, or the loaded dict (see
|
|
52
|
+
https://flatironinstitute.github.io/figpack/remix_patches.html)
|
|
46
53
|
port: Local server port, random if None
|
|
47
54
|
open_in_browser: Auto-open in browser, auto-detects by environment
|
|
48
55
|
upload: Upload figure to figpack servers, auto-detects by environment
|
|
@@ -164,6 +171,7 @@ class FigpackView:
|
|
|
164
171
|
title=title,
|
|
165
172
|
description=description,
|
|
166
173
|
script=script,
|
|
174
|
+
remix=remix,
|
|
167
175
|
inline=inline,
|
|
168
176
|
inline_height=inline_height,
|
|
169
177
|
wait_for_input=wait_for_input,
|
|
@@ -171,7 +179,13 @@ class FigpackView:
|
|
|
171
179
|
)
|
|
172
180
|
|
|
173
181
|
def save(
|
|
174
|
-
self,
|
|
182
|
+
self,
|
|
183
|
+
output_path: str,
|
|
184
|
+
*,
|
|
185
|
+
title: str,
|
|
186
|
+
description: str = "",
|
|
187
|
+
script: str = "",
|
|
188
|
+
remix: Optional["RemixArg"] = None,
|
|
175
189
|
) -> None:
|
|
176
190
|
"""
|
|
177
191
|
Save as figure either to a folder or to a .tar.gz file
|
|
@@ -180,11 +194,18 @@ class FigpackView:
|
|
|
180
194
|
title: Title for the figure
|
|
181
195
|
description: Description text with markdown support
|
|
182
196
|
script: Optional script text used to generate the figure
|
|
197
|
+
remix: Optional remix patch to apply to the figure spec: a path to a
|
|
198
|
+
patch file exported from the remix page, or the loaded dict
|
|
183
199
|
"""
|
|
184
200
|
from ._save_figure import _save_figure
|
|
185
201
|
|
|
186
202
|
_save_figure(
|
|
187
|
-
self,
|
|
203
|
+
self,
|
|
204
|
+
output_path,
|
|
205
|
+
title=title,
|
|
206
|
+
description=description,
|
|
207
|
+
script=script,
|
|
208
|
+
remix=remix,
|
|
188
209
|
)
|
|
189
210
|
|
|
190
211
|
def write_to_zarr_group(self, group: Group) -> None:
|