Repository: pyvista/pyvista

Issue:
`MultiBlock` repr does not format bounds using scientific notation
### Describe the bug, what's wrong, and what you expected.

The `repr` for a dataset like `PolyData`, `UnstructuredGrid`, etc  uses scientific notation to format the bounds. But `MultiBlock` does not. This is made abundantly clear in the example below where 300+ digits are shown for each multiblock bound. The bounds formatting in the `MultiBlock` bounds `repr` is expected to match the non-multiblock formatting.

### Steps to reproduce the bug.

``` python
import pyvista as pv 
empty_mesh = pv.PolyData().extract_cells(0)
print(empty_mesh)
# UnstructuredGrid (0x12a3161c0)
#   N Cells:    0
#   N Points:   0
#   X Bounds:   1.000e+299, -1.000e+299
#   Y Bounds:   1.000e+299, -1.000e+299
#   Z Bounds:   1.000e+299, -1.000e+299
#   N Arrays:   0

blocks = pv.MultiBlock([empty_mesh])
print(blocks)
# MultiBlock (0x12a0f52e0)
#   N Blocks    1
#   X Bounds    100000000000000005250476025520442024870446858110815915491585411551180245798890819578637137508044786404370444383288387817694252323536043057564479218478670698284838720092657580373783023379478809005936895323497079994508111903896764088007465274278014249457925878882005684283811566947219638686545940054016.000, -100000000000000005250476025520442024870446858110815915491585411551180245798890819578637137508044786404370444383288387817694252323536043057564479218478670698284838720092657580373783023379478809005936895323497079994508111903896764088007465274278014249457925878882005684283811566947219638686545940054016.000
#   Y Bounds    100000000000000005250476025520442024870446858110815915491585411551180245798890819578637137508044786404370444383288387817694252323536043057564479218478670698284838720092657580373783023379478809005936895323497079994508111903896764088007465274278014249457925878882005684283811566947219638686545940054016.000, -100000000000000005250476025520442024870446858110815915491585411551180245798890819578637137508044786404370444383288387817694252323536043057564479218478670698284838720092657580373783023379478809005936895323497079994508111903896764088007465274278014249457925878882005684283811566947219638686545940054016.000
#   Z Bounds    100000000000000005250476025520442024870446858110815915491585411551180245798890819578637137508044786404370444383288387817694252323536043057564479218478670698284838720092657580373783023379478809005936895323497079994508111903896764088007465274278014249457925878882005684283811566947219638686545940054016.000, -100000000000000005250476025520442024870446858110815915491585411551180245798890819578637137508044786404370444383288387817694252323536043057564479218478670698284838720092657580373783023379478809005936895323497079994508111903896764088007465274278014249457925878882005684283811566947219638686545940054016.000
```
EDIT: This bug is not specific to empty meshes. It applies generally to any multiblock bounds.


### System Information

```shell
OS : Darwin (macOS 14.4.1)
            CPU(s) : 8
           Machine : arm64
      Architecture : 64bit
       Environment : Python
        GPU Vendor : Apple
      GPU Renderer : Apple M2
       GPU Version : 4.1 Metal - 88
  MathText Support : True

  Python 3.9.13 (v3.9.13:6de2ca5339, May 17 2022, 11:37:23)  [Clang 13.0.0
  (clang-1300.0.29.30)]

           pyvista : 0.45.dev0
               vtk : 9.3.1
             numpy : 2.0.0
        matplotlib : 3.9.0
            scooby : 0.10.0
             pooch : 1.8.2
            pillow : 10.4.0
           IPython : 8.18.1
          colorcet : 3.1.0
             scipy : 1.13.1
    pytest_pyvista : 0.1.9
```


### Screenshots

_No response_

Discussion:
pyvista-bot: <!-- NETLIFY DEPLOY COMMENT GENERATED BY ACTIONS_NETLIFY - APP ID SHA256: d3574f413fe51079937885070831e027ca5b10f7facb9e609494e0a481a31f79 -->
🚀 Deployed on https://672d34662dca6b672a4d2739--pyvista-dev.netlify.app

Patch:
diff --git a/pyvista/core/composite.py b/pyvista/core/composite.py
index fd664956..8bb1fa01 100644
--- a/pyvista/core/composite.py
+++ b/pyvista/core/composite.py
@@ -917,11 +917,11 @@ class MultiBlock(
     def _get_attrs(self):
         """Return the representation methods (internal helper)."""
         attrs = []
-        attrs.append(('N Blocks', self.n_blocks, '{}'))
+        attrs.append(('N Blocks:', self.n_blocks, '{}'))
         bds = self.bounds
-        attrs.append(('X Bounds', (bds[0], bds[1]), '{:.3f}, {:.3f}'))
-        attrs.append(('Y Bounds', (bds[2], bds[3]), '{:.3f}, {:.3f}'))
-        attrs.append(('Z Bounds', (bds[4], bds[5]), '{:.3f}, {:.3f}'))
+        attrs.append(('X Bounds:', (bds[0], bds[1]), '{:.3e}, {:.3e}'))
+        attrs.append(('Y Bounds:', (bds[2], bds[3]), '{:.3e}, {:.3e}'))
+        attrs.append(('Z Bounds:', (bds[4], bds[5]), '{:.3e}, {:.3e}'))
         return attrs
 
     def _repr_html_(self) -> str:
@@ -964,7 +964,7 @@ class MultiBlock(
         # return a string that is Python console friendly
         fmt = f'{type(self).__name__} ({hex(id(self))})\n'
         # now make a call on the object to get its attributes as a list of len 2 tuples
-        max_len = max(len(attr[0]) for attr in self._get_attrs()) + 4
+        max_len = max(len(attr[0]) for attr in self._get_attrs()) + 3
         row = '  {:%ds}{}\n' % max_len
         for attr in self._get_attrs():
             try:
diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py
index 346447d5..1920e567 100644
--- a/pyvista/core/filters/data_set.py
+++ b/pyvista/core/filters/data_set.py
@@ -5814,10 +5814,10 @@ class DataSetFilters:
         ... )
         >>> extracted
         MultiBlock (...)
-          N Blocks    3
-          X Bounds    0.000, 1.000
-          Y Bounds    0.000, 1.000
-          Z Bounds    0.000, 5.000
+          N Blocks:   3
+          X Bounds:   0.000e+00, 1.000e+00
+          Y Bounds:   0.000e+00, 1.000e+00
+          Z Bounds:   0.000e+00, 5.000e+00
         >>> extracted.plot(multi_colors=True)
 
         Extract values from multi-component scalars.
diff --git a/pyvista/core/utilities/geometric_sources.py b/pyvista/core/utilities/geometric_sources.py
index 313c0e16..1dda6a62 100644
--- a/pyvista/core/utilities/geometric_sources.py
+++ b/pyvista/core/utilities/geometric_sources.py
@@ -4028,10 +4028,10 @@ class CubeFacesSource(CubeSource):
 
     >>> output
     MultiBlock (...)
-      N Blocks    6
-      X Bounds    -0.500, 0.500
-      Y Bounds    -0.500, 0.500
-      Z Bounds    -0.500, 0.500
+      N Blocks:   6
+      X Bounds:   -5.000e-01, 5.000e-01
+      Y Bounds:   -5.000e-01, 5.000e-01
+      Z Bounds:   -5.000e-01, 5.000e-01
 
     >>> cube_source = pv.CubeSource()
     >>> cube_source.output
diff --git a/pyvista/plotting/composite_mapper.py b/pyvista/plotting/composite_mapper.py
index bbc2434d..64225a38 100644
--- a/pyvista/plotting/composite_mapper.py
+++ b/pyvista/plotting/composite_mapper.py
@@ -465,10 +465,10 @@ class CompositeAttributes(_vtk.vtkCompositeDataDisplayAttributes):
         >>> actor, mapper = pl.add_composite(dataset)
         >>> mapper.block_attr.get_block(0)
         MultiBlock (...)
-          N Blocks    2
-          X Bounds    -0.500, 0.500
-          Y Bounds    -0.500, 0.500
-          Z Bounds    -0.500, 1.500
+          N Blocks:   2
+          X Bounds:   -5.000e-01, 5.000e-01
+          Y Bounds:   -5.000e-01, 5.000e-01
+          Z Bounds:   -5.000e-01, 1.500e+00
 
         Note this is the same as using ``__getitem__``
 
@@ -582,10 +582,10 @@ class CompositePolyDataMapper(
         >>> actor, mapper = pl.add_composite(dataset)
         >>> mapper.dataset
         MultiBlock (...)
-          N Blocks    2
-          X Bounds    -0.500, 0.500
-          Y Bounds    -0.500, 0.500
-          Z Bounds    -0.500, 1.500
+          N Blocks:   2
+          X Bounds:   -5.000e-01, 5.000e-01
+          Y Bounds:   -5.000e-01, 5.000e-01
+          Z Bounds:   -5.000e-01, 1.500e+00
 
         """
         return self._dataset


Test patch:
diff --git a/tests/core/test_composite.py b/tests/core/test_composite.py
index 63b64e9e..8b16ba45 100644
--- a/tests/core/test_composite.py
+++ b/tests/core/test_composite.py
@@ -3,6 +3,7 @@ from __future__ import annotations
 import itertools
 import pathlib
 import platform
+import re
 import weakref
 
 import numpy as np
@@ -314,8 +315,32 @@ def test_multi_block_clean(rectilinear, uniform, ant):
 def test_multi_block_repr(multiblock_all_with_nested_and_none):
     multi = multiblock_all_with_nested_and_none
     assert multi._repr_html_() is not None
-    assert repr(multi) is not None
-    assert str(multi) is not None
+    pattern = (
+        r'MultiBlock \(0x[0-9a-fA-F]+\)'
+        r'\s+N Blocks:\s{3}\d+'
+        r'\s+X Bounds:\s{3}[+-]?\d*\.\d{3}e[+-]\d+,\s+[-+]?\d\.\d+e[+-]\d+'
+        r'\s+Y Bounds:\s{3}[+-]?\d*\.\d{3}e[+-]\d+,\s+[-+]?\d\.\d+e[+-]\d+'
+        r'\s+Z Bounds:\s{3}[+-]?\d*\.\d{3}e[+-]\d+,\s+[-+]?\d\.\d+e[+-]\d+'
+    )
+    match = re.search(pattern, repr(multi))
+    assert repr(multi) == match.string
+    assert str(multi) == match.string
+
+
+def test_multi_block_repr_bounds():
+    empty_poly = pv.PolyData().extract_cells(0)
+    poly_x_bounds = repr(empty_poly).splitlines()[3]
+    poly_y_bounds = repr(empty_poly).splitlines()[4]
+    poly_z_bounds = repr(empty_poly).splitlines()[5]
+
+    empty_multiblock = pv.MultiBlock([empty_poly])
+    multi_x_bounds = repr(empty_multiblock).splitlines()[2]
+    multi_y_bounds = repr(empty_multiblock).splitlines()[3]
+    multi_z_bounds = repr(empty_multiblock).splitlines()[4]
+
+    assert multi_x_bounds == poly_x_bounds
+    assert multi_y_bounds == poly_y_bounds
+    assert multi_z_bounds == poly_z_bounds
 
 
 def test_multi_block_eq(multiblock_all_with_nested_and_none):