scitex 2.7.3__py3-none-any.whl → 2.8.1__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 (160) hide show
  1. scitex/__version__.py +1 -1
  2. scitex/dev/plt/__init__.py +0 -0
  3. scitex/dev/plt/plot_mpl_axhline.py +0 -0
  4. scitex/dev/plt/plot_mpl_axhspan.py +0 -0
  5. scitex/dev/plt/plot_mpl_axvline.py +0 -0
  6. scitex/dev/plt/plot_mpl_axvspan.py +0 -0
  7. scitex/dev/plt/plot_mpl_bar.py +0 -0
  8. scitex/dev/plt/plot_mpl_barh.py +0 -0
  9. scitex/dev/plt/plot_mpl_boxplot.py +0 -0
  10. scitex/dev/plt/plot_mpl_contour.py +0 -0
  11. scitex/dev/plt/plot_mpl_contourf.py +0 -0
  12. scitex/dev/plt/plot_mpl_errorbar.py +0 -0
  13. scitex/dev/plt/plot_mpl_eventplot.py +0 -0
  14. scitex/dev/plt/plot_mpl_fill.py +0 -0
  15. scitex/dev/plt/plot_mpl_fill_between.py +0 -0
  16. scitex/dev/plt/plot_mpl_hexbin.py +0 -0
  17. scitex/dev/plt/plot_mpl_hist.py +0 -0
  18. scitex/dev/plt/plot_mpl_hist2d.py +0 -0
  19. scitex/dev/plt/plot_mpl_imshow.py +0 -0
  20. scitex/dev/plt/plot_mpl_pcolormesh.py +0 -0
  21. scitex/dev/plt/plot_mpl_pie.py +0 -0
  22. scitex/dev/plt/plot_mpl_plot.py +0 -0
  23. scitex/dev/plt/plot_mpl_quiver.py +0 -0
  24. scitex/dev/plt/plot_mpl_scatter.py +0 -0
  25. scitex/dev/plt/plot_mpl_stackplot.py +0 -0
  26. scitex/dev/plt/plot_mpl_stem.py +0 -0
  27. scitex/dev/plt/plot_mpl_step.py +0 -0
  28. scitex/dev/plt/plot_mpl_violinplot.py +0 -0
  29. scitex/dev/plt/plot_sns_barplot.py +0 -0
  30. scitex/dev/plt/plot_sns_boxplot.py +0 -0
  31. scitex/dev/plt/plot_sns_heatmap.py +0 -0
  32. scitex/dev/plt/plot_sns_histplot.py +0 -0
  33. scitex/dev/plt/plot_sns_kdeplot.py +0 -0
  34. scitex/dev/plt/plot_sns_lineplot.py +0 -0
  35. scitex/dev/plt/plot_sns_scatterplot.py +0 -0
  36. scitex/dev/plt/plot_sns_stripplot.py +0 -0
  37. scitex/dev/plt/plot_sns_swarmplot.py +0 -0
  38. scitex/dev/plt/plot_sns_violinplot.py +0 -0
  39. scitex/dev/plt/plot_stx_bar.py +0 -0
  40. scitex/dev/plt/plot_stx_barh.py +0 -0
  41. scitex/dev/plt/plot_stx_box.py +0 -0
  42. scitex/dev/plt/plot_stx_boxplot.py +0 -0
  43. scitex/dev/plt/plot_stx_conf_mat.py +0 -0
  44. scitex/dev/plt/plot_stx_contour.py +0 -0
  45. scitex/dev/plt/plot_stx_ecdf.py +0 -0
  46. scitex/dev/plt/plot_stx_errorbar.py +0 -0
  47. scitex/dev/plt/plot_stx_fill_between.py +0 -0
  48. scitex/dev/plt/plot_stx_fillv.py +0 -0
  49. scitex/dev/plt/plot_stx_heatmap.py +0 -0
  50. scitex/dev/plt/plot_stx_image.py +0 -0
  51. scitex/dev/plt/plot_stx_imshow.py +0 -0
  52. scitex/dev/plt/plot_stx_joyplot.py +0 -0
  53. scitex/dev/plt/plot_stx_kde.py +0 -0
  54. scitex/dev/plt/plot_stx_line.py +0 -0
  55. scitex/dev/plt/plot_stx_mean_ci.py +0 -0
  56. scitex/dev/plt/plot_stx_mean_std.py +0 -0
  57. scitex/dev/plt/plot_stx_median_iqr.py +0 -0
  58. scitex/dev/plt/plot_stx_raster.py +0 -0
  59. scitex/dev/plt/plot_stx_rectangle.py +0 -0
  60. scitex/dev/plt/plot_stx_scatter.py +0 -0
  61. scitex/dev/plt/plot_stx_shaded_line.py +0 -0
  62. scitex/dev/plt/plot_stx_violin.py +0 -0
  63. scitex/dev/plt/plot_stx_violinplot.py +0 -0
  64. scitex/diagram/README.md +197 -0
  65. scitex/diagram/__init__.py +48 -0
  66. scitex/diagram/_compile.py +312 -0
  67. scitex/diagram/_diagram.py +355 -0
  68. scitex/diagram/_presets.py +173 -0
  69. scitex/diagram/_schema.py +182 -0
  70. scitex/diagram/_split.py +278 -0
  71. scitex/fig/editor/__init__.py +5 -2
  72. scitex/fig/editor/_dearpygui_editor.py +1 -1
  73. scitex/fig/editor/_mpl_editor.py +1 -1
  74. scitex/fig/editor/_qt_editor.py +1 -1
  75. scitex/fig/editor/_tkinter_editor.py +1 -1
  76. scitex/fig/editor/edit/__init__.py +50 -0
  77. scitex/fig/editor/edit/backend_detector.py +109 -0
  78. scitex/fig/editor/edit/bundle_resolver.py +240 -0
  79. scitex/fig/editor/edit/editor_launcher.py +239 -0
  80. scitex/fig/editor/edit/manual_handler.py +53 -0
  81. scitex/fig/editor/edit/panel_loader.py +232 -0
  82. scitex/fig/editor/edit/path_resolver.py +67 -0
  83. scitex/fig/editor/flask_editor/_bbox.py +23 -0
  84. scitex/fig/editor/flask_editor/_core.py +908 -103
  85. scitex/fig/editor/flask_editor/_renderer.py +74 -0
  86. scitex/fig/editor/flask_editor/static/css/base/reset.css +41 -0
  87. scitex/fig/editor/flask_editor/static/css/base/typography.css +16 -0
  88. scitex/fig/editor/flask_editor/static/css/base/variables.css +85 -0
  89. scitex/fig/editor/flask_editor/static/css/components/buttons.css +217 -0
  90. scitex/fig/editor/flask_editor/static/css/components/context-menu.css +93 -0
  91. scitex/fig/editor/flask_editor/static/css/components/dropdown.css +57 -0
  92. scitex/fig/editor/flask_editor/static/css/components/forms.css +112 -0
  93. scitex/fig/editor/flask_editor/static/css/components/modal.css +59 -0
  94. scitex/fig/editor/flask_editor/static/css/components/sections.css +212 -0
  95. scitex/fig/editor/flask_editor/static/css/features/canvas.css +176 -0
  96. scitex/fig/editor/flask_editor/static/css/features/element-inspector.css +190 -0
  97. scitex/fig/editor/flask_editor/static/css/features/loading.css +59 -0
  98. scitex/fig/editor/flask_editor/static/css/features/overlay.css +45 -0
  99. scitex/fig/editor/flask_editor/static/css/features/panel-grid.css +95 -0
  100. scitex/fig/editor/flask_editor/static/css/features/selection.css +101 -0
  101. scitex/fig/editor/flask_editor/static/css/features/statistics.css +138 -0
  102. scitex/fig/editor/flask_editor/static/css/index.css +31 -0
  103. scitex/fig/editor/flask_editor/static/css/layout/container.css +7 -0
  104. scitex/fig/editor/flask_editor/static/css/layout/controls.css +56 -0
  105. scitex/fig/editor/flask_editor/static/css/layout/preview.css +78 -0
  106. scitex/fig/editor/flask_editor/static/js/alignment/axis.js +314 -0
  107. scitex/fig/editor/flask_editor/static/js/alignment/basic.js +107 -0
  108. scitex/fig/editor/flask_editor/static/js/alignment/distribute.js +54 -0
  109. scitex/fig/editor/flask_editor/static/js/canvas/canvas.js +172 -0
  110. scitex/fig/editor/flask_editor/static/js/canvas/dragging.js +258 -0
  111. scitex/fig/editor/flask_editor/static/js/canvas/resize.js +48 -0
  112. scitex/fig/editor/flask_editor/static/js/canvas/selection.js +71 -0
  113. scitex/fig/editor/flask_editor/static/js/core/api.js +288 -0
  114. scitex/fig/editor/flask_editor/static/js/core/state.js +143 -0
  115. scitex/fig/editor/flask_editor/static/js/core/utils.js +245 -0
  116. scitex/fig/editor/flask_editor/static/js/dev/element-inspector.js +992 -0
  117. scitex/fig/editor/flask_editor/static/js/editor/bbox.js +339 -0
  118. scitex/fig/editor/flask_editor/static/js/editor/element-drag.js +286 -0
  119. scitex/fig/editor/flask_editor/static/js/editor/overlay.js +371 -0
  120. scitex/fig/editor/flask_editor/static/js/editor/preview.js +293 -0
  121. scitex/fig/editor/flask_editor/static/js/main.js +426 -0
  122. scitex/fig/editor/flask_editor/static/js/shortcuts/context-menu.js +152 -0
  123. scitex/fig/editor/flask_editor/static/js/shortcuts/keyboard.js +265 -0
  124. scitex/fig/editor/flask_editor/static/js/ui/controls.js +184 -0
  125. scitex/fig/editor/flask_editor/static/js/ui/download.js +57 -0
  126. scitex/fig/editor/flask_editor/static/js/ui/help.js +100 -0
  127. scitex/fig/editor/flask_editor/static/js/ui/theme.js +34 -0
  128. scitex/fig/editor/flask_editor/templates/__init__.py +95 -5
  129. scitex/fig/editor/flask_editor/templates/_html.py +27 -9
  130. scitex/fig/editor/flask_editor/templates/_scripts.py +1928 -131
  131. scitex/fig/editor/flask_editor/templates/_styles.py +363 -51
  132. scitex/fig/io/_bundle.py +97 -12
  133. scitex/io/__init__.py +12 -0
  134. scitex/io/_bundle.py +69 -10
  135. scitex/io/_zip_bundle.py +439 -0
  136. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +0 -0
  137. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +0 -0
  138. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +0 -0
  139. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +0 -0
  140. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +0 -0
  141. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +0 -0
  142. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +0 -0
  143. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +0 -0
  144. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +0 -0
  145. scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +0 -0
  146. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +0 -0
  147. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +0 -0
  148. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +0 -0
  149. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +0 -0
  150. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +0 -0
  151. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +0 -0
  152. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +0 -0
  153. scitex/plt/io/_layered_bundle.py +0 -0
  154. scitex/schema/_plot.py +0 -0
  155. {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/METADATA +1 -1
  156. {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/RECORD +78 -22
  157. scitex/fig/editor/_edit.py +0 -751
  158. {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/WHEEL +0 -0
  159. {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/entry_points.txt +0 -0
  160. {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/licenses/LICENSE +0 -0
scitex/fig/io/_bundle.py CHANGED
@@ -333,31 +333,57 @@ def _copy_nested_pltz_bundles(plots: Dict[str, Any], dir_path: Path) -> None:
333
333
 
334
334
  Args:
335
335
  plots: Dict mapping panel IDs to either:
336
- - source_path: Path to existing .pltz.d directory
336
+ - source_path: Path to existing .pltz.d directory or .pltz zip
337
337
  - bundle_data: Dict with spec/data (will use save_bundle)
338
338
  dir_path: Target figz directory.
339
339
  """
340
340
  for panel_id, plot_source in plots.items():
341
- target_path = dir_path / f"{panel_id}.pltz.d"
342
-
343
341
  if isinstance(plot_source, (str, Path)):
344
342
  # Direct copy from source path
345
343
  source_path = Path(plot_source)
346
- if source_path.exists() and source_path.is_dir():
344
+
345
+ if source_path.is_dir() and str(source_path).endswith('.pltz.d'):
346
+ # Source is .pltz.d directory - copy as directory
347
+ target_path = dir_path / f"{panel_id}.pltz.d"
347
348
  if target_path.exists():
348
349
  shutil.rmtree(target_path)
349
350
  shutil.copytree(source_path, target_path)
351
+
352
+ elif source_path.is_file() and str(source_path).endswith('.pltz'):
353
+ # Source is .pltz zip file - copy as zip file (preserving zip format)
354
+ target_path = dir_path / f"{panel_id}.pltz"
355
+ if target_path.exists():
356
+ target_path.unlink()
357
+ shutil.copy2(source_path, target_path)
358
+
359
+ elif source_path.exists():
360
+ # Unknown format - try to copy as directory
361
+ target_path = dir_path / f"{panel_id}.pltz.d"
362
+ if source_path.is_dir():
363
+ if target_path.exists():
364
+ shutil.rmtree(target_path)
365
+ shutil.copytree(source_path, target_path)
366
+
350
367
  elif isinstance(plot_source, dict):
351
368
  # Check if it has source_path for direct copy
352
369
  if "source_path" in plot_source:
353
370
  source_path = Path(plot_source["source_path"])
354
- if source_path.exists() and source_path.is_dir():
371
+ if source_path.is_file() and str(source_path).endswith('.pltz'):
372
+ # .pltz zip file
373
+ target_path = dir_path / f"{panel_id}.pltz"
374
+ if target_path.exists():
375
+ target_path.unlink()
376
+ shutil.copy2(source_path, target_path)
377
+ elif source_path.exists() and source_path.is_dir():
378
+ # .pltz.d directory
379
+ target_path = dir_path / f"{panel_id}.pltz.d"
355
380
  if target_path.exists():
356
381
  shutil.rmtree(target_path)
357
382
  shutil.copytree(source_path, target_path)
358
383
  else:
359
384
  # Fallback to save_bundle (will lose images)
360
385
  from scitex.io._bundle import save_bundle, BundleType
386
+ target_path = dir_path / f"{panel_id}.pltz.d"
361
387
  save_bundle(plot_source, target_path, bundle_type=BundleType.PLTZ)
362
388
 
363
389
 
@@ -376,9 +402,33 @@ def _generate_figz_overview(dir_path: Path, spec: Dict, data: Dict, basename: st
376
402
  from PIL import Image
377
403
  import numpy as np
378
404
  import warnings
405
+ import tempfile
406
+ import zipfile
407
+
408
+ # Find all panel bundles (both .pltz.d directories and .pltz zip files)
409
+ panel_dirs = []
410
+ temp_dirs_to_cleanup = []
411
+
412
+ for item in dir_path.iterdir():
413
+ if item.is_dir() and str(item).endswith('.pltz.d'):
414
+ panel_dirs.append(item)
415
+ elif item.is_file() and str(item).endswith('.pltz'):
416
+ # Extract .pltz zip to temp directory for overview generation
417
+ temp_dir = tempfile.mkdtemp(prefix=f'scitex_overview_{item.stem}_')
418
+ temp_dirs_to_cleanup.append(temp_dir)
419
+ with zipfile.ZipFile(item, 'r') as zf:
420
+ zf.extractall(temp_dir)
421
+ # Find the extracted .pltz.d directory
422
+ extracted = Path(temp_dir)
423
+ for subitem in extracted.iterdir():
424
+ if subitem.is_dir() and str(subitem).endswith('.pltz.d'):
425
+ panel_dirs.append(subitem)
426
+ break
427
+ else:
428
+ # Use temp dir directly if no .pltz.d subfolder
429
+ panel_dirs.append(extracted)
379
430
 
380
- # Find all panel directories
381
- panel_dirs = sorted(dir_path.glob("*.pltz.d"))
431
+ panel_dirs = sorted(panel_dirs, key=lambda x: x.name)
382
432
  n_panels = len(panel_dirs)
383
433
 
384
434
  if n_panels == 0:
@@ -519,6 +569,13 @@ def _generate_figz_overview(dir_path: Path, spec: Dict, data: Dict, basename: st
519
569
  fig.savefig(overview_path, dpi=150, bbox_inches="tight", facecolor="white")
520
570
  plt.close(fig)
521
571
 
572
+ # Cleanup temp directories
573
+ for temp_dir in temp_dirs_to_cleanup:
574
+ try:
575
+ shutil.rmtree(temp_dir)
576
+ except Exception:
577
+ pass
578
+
522
579
 
523
580
  def _draw_bboxes_from_geometry(ax, geometry_data: Dict) -> None:
524
581
  """Draw bboxes from geometry data on an axes.
@@ -745,6 +802,8 @@ def _generate_figz_geometry_cache(dir_path: Path, spec: Dict, basename: str) ->
745
802
  basename: Base filename for bundle files.
746
803
  """
747
804
  from datetime import datetime
805
+ import tempfile
806
+ import zipfile
748
807
 
749
808
  cache_dir = dir_path / "cache"
750
809
  cache_dir.mkdir(parents=True, exist_ok=True)
@@ -756,11 +815,30 @@ def _generate_figz_geometry_cache(dir_path: Path, spec: Dict, basename: str) ->
756
815
  "generated_at": datetime.now().isoformat(),
757
816
  }
758
817
 
759
- # Find all panel directories
760
- panel_dirs = sorted(dir_path.glob("*.pltz.d"))
818
+ # Find all panel bundles (both .pltz.d directories and .pltz zip files)
819
+ panel_sources = []
820
+ temp_dirs_to_cleanup = []
821
+
822
+ for item in dir_path.iterdir():
823
+ if item.is_dir() and str(item).endswith('.pltz.d'):
824
+ panel_sources.append((item.stem.replace('.pltz', ''), item))
825
+ elif item.is_file() and str(item).endswith('.pltz'):
826
+ # Extract .pltz zip to temp directory
827
+ temp_dir = tempfile.mkdtemp(prefix=f'scitex_geom_{item.stem}_')
828
+ temp_dirs_to_cleanup.append(temp_dir)
829
+ with zipfile.ZipFile(item, 'r') as zf:
830
+ zf.extractall(temp_dir)
831
+ extracted = Path(temp_dir)
832
+ for subitem in extracted.iterdir():
833
+ if subitem.is_dir() and str(subitem).endswith('.pltz.d'):
834
+ panel_sources.append((item.stem, subitem))
835
+ break
836
+ else:
837
+ panel_sources.append((item.stem, extracted))
761
838
 
762
- for panel_dir in panel_dirs:
763
- panel_id = panel_dir.stem.replace(".pltz", "")
839
+ panel_sources = sorted(panel_sources, key=lambda x: x[0])
840
+
841
+ for panel_id, panel_dir in panel_sources:
764
842
 
765
843
  # Load panel geometry
766
844
  panel_geometry_path = panel_dir / "cache" / "geometry_px.json"
@@ -790,7 +868,7 @@ def _generate_figz_geometry_cache(dir_path: Path, spec: Dict, basename: str) ->
790
868
  "figure_id": basename,
791
869
  "generated_at": datetime.now().isoformat(),
792
870
  "size_mm": [size.get("width_mm", 0), size.get("height_mm", 0)],
793
- "panels_count": len(panel_dirs),
871
+ "panels_count": len(panel_sources),
794
872
  "schema": spec.get("schema", {}),
795
873
  }
796
874
 
@@ -798,6 +876,13 @@ def _generate_figz_geometry_cache(dir_path: Path, spec: Dict, basename: str) ->
798
876
  with open(manifest_path, "w") as f:
799
877
  json.dump(manifest, f, indent=2)
800
878
 
879
+ # Cleanup temp directories
880
+ for temp_dir in temp_dirs_to_cleanup:
881
+ try:
882
+ shutil.rmtree(temp_dir)
883
+ except Exception:
884
+ pass
885
+
801
886
 
802
887
  def _embed_metadata_in_export(
803
888
  file_path: Path, spec: Dict[str, Any], fmt: str
scitex/io/__init__.py CHANGED
@@ -85,6 +85,14 @@ except ImportError:
85
85
  embed_metadata = None
86
86
  has_metadata = None
87
87
 
88
+ # Import ZipBundle for in-memory zip access
89
+ try:
90
+ from ._zip_bundle import ZipBundle, open_bundle, create_bundle
91
+ except ImportError:
92
+ ZipBundle = None
93
+ open_bundle = None
94
+ create_bundle = None
95
+
88
96
  __all__ = [
89
97
  # Primary I/O
90
98
  "save",
@@ -96,6 +104,10 @@ __all__ = [
96
104
  "reload",
97
105
  "flush",
98
106
  "cache",
107
+ # Zip bundle access
108
+ "ZipBundle",
109
+ "open_bundle",
110
+ "create_bundle",
99
111
  ]
100
112
 
101
113
  # EOF
scitex/io/_bundle.py CHANGED
@@ -307,11 +307,13 @@ def validate_bundle(
307
307
  return result
308
308
 
309
309
 
310
- def load_bundle(path: Union[str, Path]) -> Dict[str, Any]:
310
+ def load_bundle(path: Union[str, Path], in_memory: bool = True) -> Dict[str, Any]:
311
311
  """Load bundle from directory or ZIP transparently.
312
312
 
313
313
  Args:
314
314
  path: Path to bundle (directory or ZIP).
315
+ in_memory: If True, load ZIP contents in-memory without extracting.
316
+ If False, extract to temp directory (legacy behavior).
315
317
 
316
318
  Returns:
317
319
  Bundle data as dictionary with:
@@ -335,17 +337,42 @@ def load_bundle(path: Union[str, Path]) -> Dict[str, Any]:
335
337
 
336
338
  # Handle ZIP vs directory
337
339
  if p.is_file() and p.suffix in BUNDLE_EXTENSIONS:
338
- # ZIP archive - extract to temp and load
339
340
  result["is_zip"] = True
340
- import tempfile
341
- temp_dir = Path(tempfile.mkdtemp())
342
- with zipfile.ZipFile(p, "r") as zf:
343
- zf.extractall(temp_dir)
344
- bundle_dir = temp_dir
341
+
342
+ if in_memory:
343
+ # In-memory loading using ZipBundle
344
+ from ._zip_bundle import ZipBundle
345
+ with ZipBundle(p, mode="r") as zb:
346
+ result["_zip_bundle"] = zb
347
+ # Load common files in-memory
348
+ try:
349
+ result["spec"] = zb.read_json("spec.json")
350
+ except FileNotFoundError:
351
+ result["spec"] = None
352
+ try:
353
+ result["style"] = zb.read_json("style.json")
354
+ except FileNotFoundError:
355
+ result["style"] = None
356
+ try:
357
+ result["data"] = zb.read_csv("data.csv")
358
+ except FileNotFoundError:
359
+ result["data"] = None
360
+
361
+ # Get file list
362
+ result["files"] = zb.namelist()
363
+
364
+ return result
365
+ else:
366
+ # Legacy: extract to temp and load
367
+ import tempfile
368
+ temp_dir = Path(tempfile.mkdtemp())
369
+ with zipfile.ZipFile(p, "r") as zf:
370
+ zf.extractall(temp_dir)
371
+ bundle_dir = temp_dir
345
372
  else:
346
373
  bundle_dir = p
347
374
 
348
- # Delegate to domain-specific loaders
375
+ # Delegate to domain-specific loaders (for directory bundles or legacy mode)
349
376
  if bundle_type == BundleType.FIGZ:
350
377
  from scitex.fig.io._bundle import load_figz_bundle
351
378
  result.update(load_figz_bundle(bundle_dir))
@@ -364,6 +391,7 @@ def save_bundle(
364
391
  path: Union[str, Path],
365
392
  bundle_type: Optional[str] = None,
366
393
  as_zip: bool = False,
394
+ atomic: bool = True,
367
395
  ) -> Path:
368
396
  """Save data as a bundle.
369
397
 
@@ -372,6 +400,7 @@ def save_bundle(
372
400
  path: Output path (with or without .d suffix).
373
401
  bundle_type: Bundle type ('figz', 'pltz', 'statsz'). Auto-detected if None.
374
402
  as_zip: If True, save as ZIP archive.
403
+ atomic: If True, use atomic write (temp file + rename) for ZIP.
375
404
 
376
405
  Returns:
377
406
  Path to saved bundle.
@@ -399,7 +428,37 @@ def save_bundle(
399
428
  else:
400
429
  dir_path = p
401
430
 
402
- # Create directory
431
+ # For direct ZIP saving with atomic writes, use ZipBundle
432
+ # Note: figz bundles need special handling for nested pltz panels,
433
+ # so they go through the directory-based save_figz_bundle path
434
+ if save_as_zip and atomic and bundle_type != BundleType.FIGZ:
435
+ from ._zip_bundle import ZipBundle
436
+
437
+ with ZipBundle(zip_path, mode="w") as zb:
438
+ # Write spec
439
+ if "spec" in data:
440
+ zb.write_json("spec.json", data["spec"])
441
+
442
+ # Write style
443
+ if "style" in data:
444
+ zb.write_json("style.json", data["style"])
445
+
446
+ # Write CSV data
447
+ if "data" in data and data["data"] is not None:
448
+ import pandas as pd
449
+ if isinstance(data["data"], pd.DataFrame):
450
+ zb.write_csv("data.csv", data["data"])
451
+
452
+ # Write exports (PNG, SVG, etc.)
453
+ for key in ["png", "svg", "pdf"]:
454
+ if key in data and data[key] is not None:
455
+ export_data = data[key]
456
+ if isinstance(export_data, bytes):
457
+ zb.write_bytes(f"exports/figure.{key}", export_data)
458
+
459
+ return zip_path
460
+
461
+ # Create directory for non-ZIP or non-atomic saves
403
462
  dir_path.mkdir(parents=True, exist_ok=True)
404
463
 
405
464
  # Delegate to domain-specific savers
@@ -417,7 +476,7 @@ def save_bundle(
417
476
  else:
418
477
  raise ValueError(f"Unknown bundle type: {bundle_type}")
419
478
 
420
- # Pack to ZIP if requested
479
+ # Pack to ZIP if requested (non-atomic path)
421
480
  if save_as_zip:
422
481
  pack_bundle(dir_path, zip_path)
423
482
  shutil.rmtree(dir_path) # Remove temp directory