cloudglancer 0.1.6__tar.gz → 0.1.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudglancer
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Simple interactive visualization of 3D point clouds
5
5
  Author-email: Your Name <your.email@example.com>
6
6
  License: MIT
@@ -45,9 +45,8 @@ Simple interactive visualization of 3D point clouds using Plotly.
45
45
  - Interactive 3D scatter plots with pan, zoom, and rotation
46
46
  - Support for categorical and continuous color mapping
47
47
  - Combine multiple plots into subplot grids
48
- - Easy-to-use API with sensible defaults
49
- - Type hints for better IDE support
50
48
  - Plot batched point clouds (B, N, 3)
49
+ - Render a (B, N, 3) batch as a grid of subplots (one cloud per cell)
51
50
  - Export a rotating turntable GIF of any figure
52
51
 
53
52
  ## Installation
@@ -76,6 +75,31 @@ fig = cg.plot(points, size=2.0)
76
75
  cg.animate(fig, "rotation.gif", axis="z", n_frames=60)
77
76
  ```
78
77
 
78
+ Render a batch of point clouds as a grid of subplots (one cloud per cell):
79
+
80
+ ```python
81
+ batch = np.random.randn(6, 500, 3) # (B, N, 3)
82
+
83
+ # Auto near-square grid, single color for every cloud
84
+ fig = cg.plot_grid(batch, colors="#1f77b4", size=1.5)
85
+
86
+ # Or explicit grid + per-cell colors
87
+ fig = cg.plot_grid(batch, rows=2, cols=3,
88
+ colors=["red", "green", "blue", "orange", "purple", "teal"])
89
+
90
+ # Combine with animate() for a rotating GIF of the whole grid
91
+ cg.animate(fig, "grid.gif", n_frames=60, width=1600, height=1200)
92
+ ```
93
+
94
+ Apply a clean, GIF-friendly style (white backgrounds, hidden tick labels,
95
+ light gray axis grid) to every 3D scene in a figure — works for both
96
+ single plots and grids:
97
+
98
+ ```python
99
+ fig = cg.beautify(cg.plot_grid(batch, colors="#1f77b4"))
100
+ cg.animate(fig, "grid.gif", n_frames=60)
101
+ ```
102
+
79
103
  More examples are in the `examples` folder.
80
104
 
81
105
  ### Development Installation
@@ -10,9 +10,8 @@ Simple interactive visualization of 3D point clouds using Plotly.
10
10
  - Interactive 3D scatter plots with pan, zoom, and rotation
11
11
  - Support for categorical and continuous color mapping
12
12
  - Combine multiple plots into subplot grids
13
- - Easy-to-use API with sensible defaults
14
- - Type hints for better IDE support
15
13
  - Plot batched point clouds (B, N, 3)
14
+ - Render a (B, N, 3) batch as a grid of subplots (one cloud per cell)
16
15
  - Export a rotating turntable GIF of any figure
17
16
 
18
17
  ## Installation
@@ -41,6 +40,31 @@ fig = cg.plot(points, size=2.0)
41
40
  cg.animate(fig, "rotation.gif", axis="z", n_frames=60)
42
41
  ```
43
42
 
43
+ Render a batch of point clouds as a grid of subplots (one cloud per cell):
44
+
45
+ ```python
46
+ batch = np.random.randn(6, 500, 3) # (B, N, 3)
47
+
48
+ # Auto near-square grid, single color for every cloud
49
+ fig = cg.plot_grid(batch, colors="#1f77b4", size=1.5)
50
+
51
+ # Or explicit grid + per-cell colors
52
+ fig = cg.plot_grid(batch, rows=2, cols=3,
53
+ colors=["red", "green", "blue", "orange", "purple", "teal"])
54
+
55
+ # Combine with animate() for a rotating GIF of the whole grid
56
+ cg.animate(fig, "grid.gif", n_frames=60, width=1600, height=1200)
57
+ ```
58
+
59
+ Apply a clean, GIF-friendly style (white backgrounds, hidden tick labels,
60
+ light gray axis grid) to every 3D scene in a figure — works for both
61
+ single plots and grids:
62
+
63
+ ```python
64
+ fig = cg.beautify(cg.plot_grid(batch, colors="#1f77b4"))
65
+ cg.animate(fig, "grid.gif", n_frames=60)
66
+ ```
67
+
44
68
  More examples are in the `examples` folder.
45
69
 
46
70
  ### Development Installation
@@ -0,0 +1,7 @@
1
+ """cloudglancer: Simple interactive visualization of 3D point clouds"""
2
+
3
+ from cloudglancer.scatter import plot, combine_plots, plot_grid, beautify
4
+ from cloudglancer.animate import animate
5
+
6
+ __version__ = "0.1.0"
7
+ __all__ = ["plot", "combine_plots", "plot_grid", "beautify", "animate"]
@@ -1,6 +1,7 @@
1
1
  """Core scatter plotting functionality for 3D point clouds."""
2
2
 
3
- from typing import Optional, List, Dict, Union
3
+ import math
4
+ from typing import Optional, List, Dict, Tuple, Union
4
5
  import plotly.express as px
5
6
  import pandas as pd
6
7
  import numpy as np
@@ -179,3 +180,164 @@ def combine_plots(figs: List[Figure], rows: int = 1, cols: int = 2, aspectmode:
179
180
  )
180
181
 
181
182
  return combined_fig
183
+
184
+
185
+ _DEFAULT_AXIS_STYLE = dict(
186
+ backgroundcolor="white",
187
+ gridcolor="lightgray",
188
+ showbackground=True,
189
+ showticklabels=False,
190
+ title="",
191
+ zerolinecolor="lightgray",
192
+ )
193
+
194
+
195
+ def beautify(
196
+ fig: Figure,
197
+ paper_bgcolor: str = "white",
198
+ scene_bgcolor: str = "white",
199
+ axis_style: Optional[Dict] = None,
200
+ ) -> Figure:
201
+ """
202
+ Apply a clean, GIF-friendly style to every 3D scene in a figure.
203
+
204
+ Hides axis tick labels and titles, gives axes a light gray grid, and
205
+ paints both the figure paper and each 3D scene's background. Works on
206
+ single-scene figures (from :func:`plot`) and multi-scene figures (from
207
+ :func:`plot_grid` / :func:`combine_plots`), which use layout keys
208
+ ``scene``, ``scene2``, ``scene3``, .... Mutates and returns ``fig``.
209
+
210
+ Args:
211
+ fig (plotly.graph_objects.Figure): Figure to style in place.
212
+ paper_bgcolor (str, optional): Color of the area outside the 3D
213
+ scenes. Default ``"white"``.
214
+ scene_bgcolor (str, optional): Color of the area inside each 3D
215
+ scene's box. Default ``"white"``.
216
+ axis_style (dict, optional): Override the per-axis style applied
217
+ to ``xaxis``/``yaxis``/``zaxis`` of every scene. When ``None``
218
+ a clean default (light gray grid, hidden tick labels and
219
+ titles) is used.
220
+
221
+ Returns:
222
+ plotly.graph_objects.Figure: The same ``fig``, restyled.
223
+
224
+ Examples:
225
+ >>> import numpy as np
226
+ >>> import cloudglancer as cg
227
+ >>> pts = np.random.randn(6, 200, 3)
228
+ >>> fig = cg.beautify(cg.plot_grid(pts, colors="#1f77b4"))
229
+ >>> cg.animate(fig, "grid.gif", n_frames=60)
230
+ """
231
+ style = dict(_DEFAULT_AXIS_STYLE if axis_style is None else axis_style)
232
+
233
+ fig.update_layout(paper_bgcolor=paper_bgcolor)
234
+
235
+ for key in fig.layout:
236
+ if key == "scene" or (key.startswith("scene") and key[len("scene"):].isdigit()):
237
+ fig.layout[key].update(
238
+ bgcolor=scene_bgcolor,
239
+ xaxis=style,
240
+ yaxis=style,
241
+ zaxis=style,
242
+ )
243
+
244
+ return fig
245
+
246
+
247
+ def _resolve_grid_shape(n: int, rows: Optional[int], cols: Optional[int]) -> Tuple[int, int]:
248
+ if rows is None and cols is None:
249
+ cols = int(math.ceil(math.sqrt(n)))
250
+ rows = int(math.ceil(n / cols))
251
+ elif rows is None:
252
+ rows = int(math.ceil(n / cols))
253
+ elif cols is None:
254
+ cols = int(math.ceil(n / rows))
255
+ if rows * cols < n:
256
+ raise ValueError(f"rows*cols={rows * cols} < B={n}")
257
+ return rows, cols
258
+
259
+
260
+ def plot_grid(
261
+ points: np.ndarray,
262
+ rows: Optional[int] = None,
263
+ cols: Optional[int] = None,
264
+ colors: Optional[Union[str, List[str]]] = None,
265
+ size: float = 1.5,
266
+ aspectmode: Optional[str] = 'data',
267
+ showlegend: bool = False,
268
+ ) -> Figure:
269
+ """
270
+ Render a batch of 3D point clouds as a grid of subplots, one cloud per cell.
271
+
272
+ Args:
273
+ points (np.ndarray): Array of shape (B, N, 3). Each batch element is
274
+ rendered into its own subplot.
275
+ rows (int, optional): Number of grid rows. If both ``rows`` and ``cols``
276
+ are ``None``, a near-square grid is chosen automatically. If only
277
+ one is given, the other is derived from ``B``.
278
+ cols (int, optional): Number of grid columns. See ``rows``.
279
+ colors (str or list of str, optional): Color for each subplot. Pass a
280
+ single color string to use it for every cloud, or a list of length
281
+ ``B`` to color each cloud individually. Defaults to Plotly's
282
+ qualitative palette.
283
+ size (float, optional): Marker size. Default is 1.5.
284
+ aspectmode (str, optional): Aspect mode for each 3D scene. Default
285
+ ``'data'``.
286
+ showlegend (bool, optional): Whether to show the figure legend.
287
+ Default ``False`` (legends are noisy in grids).
288
+
289
+ Returns:
290
+ plotly.graph_objects.Figure: A combined figure with the grid of
291
+ subplots, ready for ``.show()`` or for passing to
292
+ :func:`cloudglancer.animate`.
293
+
294
+ Raises:
295
+ ValueError: If ``points`` is not (B, N, 3), if ``colors`` is a list
296
+ whose length does not match ``B``, or if ``rows*cols < B``.
297
+
298
+ Examples:
299
+ >>> import numpy as np
300
+ >>> import cloudglancer as cg
301
+ >>> pts = np.random.randn(6, 200, 3)
302
+ >>> fig = cg.plot_grid(pts, colors="#1f77b4", size=1.5)
303
+ >>> cg.animate(fig, "grid.gif", n_frames=60)
304
+ """
305
+ if points.ndim != 3 or points.shape[-1] != 3:
306
+ raise ValueError(f"points must be of shape (B, N, 3); got {points.shape}")
307
+
308
+ B = points.shape[0]
309
+ rows, cols = _resolve_grid_shape(B, rows, cols)
310
+
311
+ if colors is None:
312
+ palette = px.colors.qualitative.Plotly
313
+ cell_colors = [palette[i % len(palette)] for i in range(B)]
314
+ elif isinstance(colors, str):
315
+ cell_colors = [colors] * B
316
+ else:
317
+ if len(colors) != B:
318
+ raise ValueError(
319
+ f"colors must have length B={B}; got {len(colors)}"
320
+ )
321
+ cell_colors = list(colors)
322
+
323
+ figs = []
324
+ for i in range(rows * cols):
325
+ if i < B:
326
+ sub = plot(
327
+ points[i:i + 1],
328
+ batch_colors=[cell_colors[i]],
329
+ size=size,
330
+ aspectmode=aspectmode,
331
+ )
332
+ else:
333
+ sub = plot(
334
+ np.zeros((1, 1, 3), dtype=points.dtype),
335
+ batch_colors=["rgba(0,0,0,0)"],
336
+ size=size,
337
+ aspectmode=aspectmode,
338
+ )
339
+ figs.append(sub)
340
+
341
+ combined = combine_plots(figs, rows=rows, cols=cols, aspectmode=aspectmode)
342
+ combined.update_layout(showlegend=showlegend)
343
+ return combined
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cloudglancer
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Simple interactive visualization of 3D point clouds
5
5
  Author-email: Your Name <your.email@example.com>
6
6
  License: MIT
@@ -45,9 +45,8 @@ Simple interactive visualization of 3D point clouds using Plotly.
45
45
  - Interactive 3D scatter plots with pan, zoom, and rotation
46
46
  - Support for categorical and continuous color mapping
47
47
  - Combine multiple plots into subplot grids
48
- - Easy-to-use API with sensible defaults
49
- - Type hints for better IDE support
50
48
  - Plot batched point clouds (B, N, 3)
49
+ - Render a (B, N, 3) batch as a grid of subplots (one cloud per cell)
51
50
  - Export a rotating turntable GIF of any figure
52
51
 
53
52
  ## Installation
@@ -76,6 +75,31 @@ fig = cg.plot(points, size=2.0)
76
75
  cg.animate(fig, "rotation.gif", axis="z", n_frames=60)
77
76
  ```
78
77
 
78
+ Render a batch of point clouds as a grid of subplots (one cloud per cell):
79
+
80
+ ```python
81
+ batch = np.random.randn(6, 500, 3) # (B, N, 3)
82
+
83
+ # Auto near-square grid, single color for every cloud
84
+ fig = cg.plot_grid(batch, colors="#1f77b4", size=1.5)
85
+
86
+ # Or explicit grid + per-cell colors
87
+ fig = cg.plot_grid(batch, rows=2, cols=3,
88
+ colors=["red", "green", "blue", "orange", "purple", "teal"])
89
+
90
+ # Combine with animate() for a rotating GIF of the whole grid
91
+ cg.animate(fig, "grid.gif", n_frames=60, width=1600, height=1200)
92
+ ```
93
+
94
+ Apply a clean, GIF-friendly style (white backgrounds, hidden tick labels,
95
+ light gray axis grid) to every 3D scene in a figure — works for both
96
+ single plots and grids:
97
+
98
+ ```python
99
+ fig = cg.beautify(cg.plot_grid(batch, colors="#1f77b4"))
100
+ cg.animate(fig, "grid.gif", n_frames=60)
101
+ ```
102
+
79
103
  More examples are in the `examples` folder.
80
104
 
81
105
  ### Development Installation
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cloudglancer"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  description = "Simple interactive visualization of 3D point clouds"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -2,7 +2,7 @@
2
2
 
3
3
  import numpy as np
4
4
  import pytest
5
- from cloudglancer import plot, combine_plots
5
+ from cloudglancer import plot, combine_plots, plot_grid, beautify
6
6
 
7
7
 
8
8
  def test_plot_basic():
@@ -154,3 +154,105 @@ def test_plot_batched_with_labels_raises():
154
154
  labels = np.zeros(300)
155
155
  with pytest.raises(ValueError, match="labels are not supported"):
156
156
  plot(points, labels=labels)
157
+
158
+
159
+ def _scene_count(fig):
160
+ return sum(
161
+ 1 for k in fig.layout
162
+ if k == "scene" or (k.startswith("scene") and k[5:].isdigit())
163
+ )
164
+
165
+
166
+ def test_plot_grid_auto_shape():
167
+ """Auto grid for B=5 → 3 cols x 2 rows = 6 cells."""
168
+ pts = np.random.randn(5, 30, 3)
169
+ fig = plot_grid(pts)
170
+ assert _scene_count(fig) == 6
171
+ assert fig.layout.showlegend is False
172
+
173
+
174
+ def test_plot_grid_explicit_shape():
175
+ pts = np.random.randn(4, 30, 3)
176
+ fig = plot_grid(pts, rows=2, cols=2)
177
+ assert _scene_count(fig) == 4
178
+ assert len(fig.data) == 4
179
+
180
+
181
+ def test_plot_grid_single_color_string():
182
+ pts = np.random.randn(3, 20, 3)
183
+ fig = plot_grid(pts, colors="#1f77b4")
184
+ cloud_traces = [t for t in fig.data if len(t.x) > 1]
185
+ assert len(cloud_traces) == 3
186
+ for t in cloud_traces:
187
+ assert t.marker.color == "#1f77b4"
188
+
189
+
190
+ def test_plot_grid_color_list():
191
+ pts = np.random.randn(3, 20, 3)
192
+ fig = plot_grid(pts, colors=["red", "green", "blue"])
193
+ cloud_traces = [t for t in fig.data if len(t.x) > 1]
194
+ colors = [t.marker.color for t in cloud_traces]
195
+ assert sorted(colors) == sorted(["red", "green", "blue"])
196
+
197
+
198
+ def test_plot_grid_invalid_shape():
199
+ with pytest.raises(ValueError, match="points must be of shape"):
200
+ plot_grid(np.random.randn(3, 20, 2))
201
+
202
+
203
+ def test_plot_grid_grid_too_small():
204
+ pts = np.random.randn(5, 20, 3)
205
+ with pytest.raises(ValueError, match="rows\\*cols"):
206
+ plot_grid(pts, rows=2, cols=2)
207
+
208
+
209
+ def test_plot_grid_color_list_length_mismatch():
210
+ pts = np.random.randn(3, 20, 3)
211
+ with pytest.raises(ValueError, match="colors must have length"):
212
+ plot_grid(pts, colors=["red", "green"])
213
+
214
+
215
+ def test_plot_grid_pads_when_grid_larger_than_b():
216
+ """B=3 in a 2x2 grid → 4 scenes, with the last cell padded."""
217
+ pts = np.random.randn(3, 20, 3)
218
+ fig = plot_grid(pts, rows=2, cols=2)
219
+ assert _scene_count(fig) == 4
220
+ assert len(fig.data) == 4
221
+
222
+
223
+ def test_beautify_single_scene():
224
+ pts = np.random.randn(50, 3)
225
+ fig = beautify(plot(pts))
226
+ assert fig.layout.paper_bgcolor == "white"
227
+ assert fig.layout.scene.bgcolor == "white"
228
+ assert fig.layout.scene.xaxis.showticklabels is False
229
+ assert fig.layout.scene.yaxis.gridcolor == "lightgray"
230
+
231
+
232
+ def test_beautify_applies_to_every_grid_scene():
233
+ """All scenes (not just `scene`) should get the styling."""
234
+ pts = np.random.randn(4, 30, 3)
235
+ fig = beautify(plot_grid(pts, rows=2, cols=2))
236
+ scene_keys = [
237
+ k for k in fig.layout
238
+ if k == "scene" or (k.startswith("scene") and k[5:].isdigit())
239
+ ]
240
+ assert len(scene_keys) == 4
241
+ for k in scene_keys:
242
+ scene = fig.layout[k]
243
+ assert scene.bgcolor == "white"
244
+ assert scene.xaxis.showticklabels is False
245
+ assert scene.yaxis.showticklabels is False
246
+ assert scene.zaxis.showticklabels is False
247
+
248
+
249
+ def test_beautify_custom_colors():
250
+ fig = beautify(plot(np.random.randn(20, 3)),
251
+ paper_bgcolor="black", scene_bgcolor="black")
252
+ assert fig.layout.paper_bgcolor == "black"
253
+ assert fig.layout.scene.bgcolor == "black"
254
+
255
+
256
+ def test_beautify_returns_same_figure():
257
+ fig = plot(np.random.randn(20, 3))
258
+ assert beautify(fig) is fig
@@ -1,7 +0,0 @@
1
- """cloudglancer: Simple interactive visualization of 3D point clouds"""
2
-
3
- from cloudglancer.scatter import plot, combine_plots
4
- from cloudglancer.animate import animate
5
-
6
- __version__ = "0.1.0"
7
- __all__ = ["plot", "combine_plots", "animate"]
File without changes
File without changes
File without changes