gri-plot 0.2.3__tar.gz → 0.2.5__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.
Files changed (85) hide show
  1. {gri_plot-0.2.3 → gri_plot-0.2.5}/CONTRIBUTING.md +8 -4
  2. {gri_plot-0.2.3 → gri_plot-0.2.5}/PKG-INFO +7 -17
  3. {gri_plot-0.2.3 → gri_plot-0.2.5}/README.md +5 -15
  4. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/__init__.py +10 -26
  5. gri_plot-0.2.5/gri_plot/_heatmap_image.py +132 -0
  6. gri_plot-0.2.5/gri_plot/basemap.py +307 -0
  7. gri_plot-0.2.5/gri_plot/data/natural_earth_boundaries.npz +0 -0
  8. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/figure3d.py +5 -85
  9. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/figure_map.py +188 -17
  10. gri_plot-0.2.5/gri_plot/observables/__init__.py +15 -0
  11. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/observables/los.py +1 -1
  12. gri_plot-0.2.5/gri_plot/polylines.py +60 -0
  13. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/scatter_map.py +44 -22
  14. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/surfaces/__init__.py +3 -12
  15. gri_plot-0.2.5/gri_plot/surfaces/mesh.py +133 -0
  16. {gri_plot-0.2.3 → gri_plot-0.2.5}/pyproject.toml +35 -7
  17. gri_plot-0.2.5/scripts/build_basemap_data.py +241 -0
  18. gri_plot-0.2.5/test/test_basemap.py +169 -0
  19. gri_plot-0.2.5/test/test_figure3d.py +79 -0
  20. gri_plot-0.2.5/test/test_frames.py +136 -0
  21. gri_plot-0.2.5/test/test_heatmap_image.py +137 -0
  22. gri_plot-0.2.5/test/test_polylines.py +52 -0
  23. gri_plot-0.2.5/test/test_surfaces/test_mesh.py +51 -0
  24. gri_plot-0.2.3/.coveragerc +0 -8
  25. gri_plot-0.2.3/.docs_other_projects.md +0 -85
  26. gri_plot-0.2.3/.gitlab-ci-deps.yml +0 -7
  27. gri_plot-0.2.3/.gitlab-ci.yml +0 -95
  28. gri_plot-0.2.3/.init_venv.sh +0 -87
  29. gri_plot-0.2.3/.pre-commit-config.yaml +0 -40
  30. gri_plot-0.2.3/.pre-commit-unskipped.py +0 -38
  31. gri_plot-0.2.3/.python-version +0 -1
  32. gri_plot-0.2.3/.ruff.toml +0 -60
  33. gri_plot-0.2.3/.vscode/settings.json +0 -15
  34. gri_plot-0.2.3/gri_plot/observables/__init__.py +0 -21
  35. gri_plot-0.2.3/gri_plot/observables/fdoa.py +0 -256
  36. gri_plot-0.2.3/gri_plot/observables/tdoa.py +0 -203
  37. gri_plot-0.2.3/gri_plot/observables/terrain.py +0 -295
  38. gri_plot-0.2.3/gri_plot/surfaces/gradients/__init__.py +0 -39
  39. gri_plot-0.2.3/gri_plot/surfaces/gradients/axis.py +0 -76
  40. gri_plot-0.2.3/gri_plot/surfaces/gradients/line.py +0 -88
  41. gri_plot-0.2.3/gri_plot/surfaces/gradients/plane.py +0 -55
  42. gri_plot-0.2.3/gri_plot/surfaces/gradients/point.py +0 -42
  43. gri_plot-0.2.3/gri_plot/surfaces/intersection.py +0 -219
  44. gri_plot-0.2.3/gri_plot/surfaces/mesh.py +0 -605
  45. gri_plot-0.2.3/notes.txt +0 -3
  46. gri_plot-0.2.3/report.xml +0 -1
  47. gri_plot-0.2.3/test/test_figure3d.py +0 -287
  48. gri_plot-0.2.3/test/test_frames.py +0 -44
  49. gri_plot-0.2.3/test/test_observables/test_fdoa.py +0 -59
  50. gri_plot-0.2.3/test/test_observables/test_tdoa.py +0 -72
  51. gri_plot-0.2.3/test/test_observables/test_terrain.py +0 -45
  52. gri_plot-0.2.3/test/test_surfaces/test_intersection.py +0 -203
  53. gri_plot-0.2.3/test/test_surfaces/test_mesh.py +0 -239
  54. gri_plot-0.2.3/uv.lock +0 -765
  55. {gri_plot-0.2.3 → gri_plot-0.2.5}/.gitignore +0 -0
  56. {gri_plot-0.2.3 → gri_plot-0.2.5}/LICENSE +0 -0
  57. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/frames.py +0 -0
  58. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/observables/aoa.py +0 -0
  59. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/observables/range_sphere.py +0 -0
  60. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/plot_ellipse.py +0 -0
  61. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/py.typed +0 -0
  62. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/scatter.py +0 -0
  63. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/__init__.py +0 -0
  64. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/cone.py +0 -0
  65. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/cylinder.py +0 -0
  66. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/ellipse.py +0 -0
  67. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/ellipsoid.py +0 -0
  68. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/meshgen.py +0 -0
  69. {gri_plot-0.2.3 → gri_plot-0.2.5}/gri_plot/shapes/sphere.py +0 -0
  70. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/__init__.py +0 -0
  71. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_figure_map.py +0 -0
  72. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_observables/__init__.py +0 -0
  73. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_observables/test_aoa.py +0 -0
  74. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_observables/test_los.py +0 -0
  75. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_observables/test_range_sphere.py +0 -0
  76. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_plot_ellipse.py +0 -0
  77. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_plot_scattermap.py +0 -0
  78. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_scatter.py +0 -0
  79. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/__init__.py +0 -0
  80. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/test_cone.py +0 -0
  81. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/test_cylinder.py +0 -0
  82. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/test_ellipse.py +0 -0
  83. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/test_ellipsoid.py +0 -0
  84. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_shapes/test_sphere.py +0 -0
  85. {gri_plot-0.2.3 → gri_plot-0.2.5}/test/test_surfaces/__init__.py +0 -0
@@ -54,12 +54,12 @@ ruff check # lint
54
54
  ruff format # format
55
55
  ```
56
56
 
57
- ### Pyright
57
+ ### ty
58
58
 
59
- [Pyright](https://microsoft.github.io/pyright/#/) for static type checking:
59
+ [ty](https://docs.astral.sh/ty/) for static type checking:
60
60
 
61
61
  ```bash
62
- pyright
62
+ ty check
63
63
  ```
64
64
 
65
65
  ### Pre-commit
@@ -110,4 +110,8 @@ by Astral and add to your `settings.json`:
110
110
  }
111
111
  ```
112
112
 
113
- Pyright is included in VS Code's Pylance extension by default.
113
+ For type checking in VS Code, install the
114
+ [ty extension](https://marketplace.visualstudio.com/items?itemName=astral-sh.ty)
115
+ by Astral. To avoid conflicts with Pylance, either let ty disable the Python
116
+ language server (the default) or keep Pylance and set
117
+ `"ty.disableLanguageServices": true` to use ty for diagnostics only.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gri-plot
3
- Version: 0.2.3
3
+ Version: 0.2.5
4
4
  Summary: 3D geolocation visualization and 2D plotting utilities built on Plotly
5
5
  Project-URL: Homepage, https://geosolresearch.com
6
6
  Project-URL: Repository, https://gitlab.com/geosol-foss/python/gri-plot
@@ -19,8 +19,8 @@ Classifier: Topic :: Scientific/Engineering :: Visualization
19
19
  Requires-Python: >=3.12
20
20
  Requires-Dist: gri-utils>=0.3.3
21
21
  Requires-Dist: numpy>=2.3.3
22
+ Requires-Dist: pillow>=11.0.0
22
23
  Requires-Dist: plotly>=6.3.0
23
- Requires-Dist: scikit-image>=0.25.0
24
24
  Description-Content-Type: text/markdown
25
25
 
26
26
  [![GeoSol Research Logo](https://geosolresearch.com/logos/foss_logo.png "GeoSol Research")](https://geosolresearch.com)
@@ -31,7 +31,7 @@ Description-Content-Type: text/markdown
31
31
 
32
32
  ## Overview
33
33
 
34
- This library renders implicit surfaces (TDOA hyperboloids, FDOA iso-Doppler surfaces, AOA cones, range spheres, terrain) and geometric shapes (ellipsoids, spheres, cones, cylinders) as interactive 3D meshes. It also provides shorthand functions for 2D scatter plots and geographic maps.
34
+ This library renders parametric geometric shapes (ellipsoids, spheres, cones, cylinders) and observable surfaces that inherit from them (AOA cones, line-of-sight rays, range spheres) as interactive 3D meshes. It also provides shorthand functions for 2D scatter plots and geographic maps.
35
35
 
36
36
  ## Installation
37
37
 
@@ -86,14 +86,12 @@ Scene builder for composing 3D visualizations from multiple surfaces and points:
86
86
  - **`add_surface()`**: Add any `ImplicitShape` (shapes or observables)
87
87
  - **`add_points()`**: Add labeled scatter points
88
88
  - **`add_line()`**: Add line segments connecting points
89
- - **`add_intersection()`**: Highlight where multiple surfaces intersect
90
89
  - **`plot_surfaces()`**: Convenience function for quick multi-surface plots
91
90
  - Method chaining, automatic color cycling from D3 palette, `plotly_dark` template
92
91
 
93
92
  ```python
94
93
  from gri_plot import Figure3D
95
94
  from gri_plot.shapes import Sphere
96
- from gri_plot.observables import TdoaSurface
97
95
  import numpy as np
98
96
 
99
97
  fig = Figure3D()
@@ -113,7 +111,7 @@ Coordinate frame handling for 3D display:
113
111
 
114
112
  ## Shapes
115
113
 
116
- Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation (for intersection calculations).
114
+ Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation.
117
115
 
118
116
  - **Ellipsoid**: From center + covariance matrix, or center + semi-axes + rotation matrix
119
117
  - **Sphere**: From center + radius
@@ -123,31 +121,23 @@ Geometric primitives implementing the `ImplicitShape` ABC. Each supports paramet
123
121
 
124
122
  ## Observables
125
123
 
126
- Geolocation-specific surfaces for visualizing measurement iso-surfaces. Some inherit from shapes (reusing parametric rendering), others use marching cubes for mesh extraction.
124
+ Geolocation-specific surfaces that inherit from parametric shapes:
127
125
 
128
- - **TdoaSurface**: TDOA hyperboloid where range_1 - range_2 = c x tdoa, from two collector positions + time difference
129
- - **FdoaSurface**: FDOA iso-Doppler surface from two collector positions/velocities + carrier frequency + frequency difference
130
126
  - **AoaSurface**: AOA cone (extends Cone) from collector position + measured direction + angular error
131
127
  - **LosSurface**: Line of sight ray (extends Cylinder) from start point + direction + length; also provides `to_line_trace()` for simple line rendering
132
128
  - **RangeSphere**: Range/TOA sphere (extends Sphere) from collector position + range measurement
133
- - **TerrainSurface**: Terrain from XYZ vertex grid with interpolated residual evaluation; factory methods `from_xyz_grids()` and `from_elevation_grid()`
134
129
 
135
130
  ## Surfaces
136
131
 
137
- Low-level infrastructure for implicit surface rendering:
132
+ Low-level infrastructure for shape rendering:
138
133
 
139
134
  - **ImplicitShape ABC**: Interface requiring `residual_fn()`, `get_bounds_xyz()`, `to_mesh()`, `to_trace()`, `is_volume`, `label`; also provides `contains()` for point-in-shape testing
140
- - **Mesh utilities**: `field_to_mesh()` (marching cubes with Newton-Raphson surface projection and adaptive refinement), `grid_to_mesh()` (parametric grid triangulation), `vertices_to_mesh3d()` (vertices/faces to Plotly Mesh3d trace)
141
- - **IntersectionField**: Euclidean distance in residual space for highlighting multi-surface intersections; distinguishes volumes (interior contribution) from surfaces (boundary contribution)
142
- - **Gradient functions**: Intensity calculators for mesh coloring -- `distance_from_point`, `distance_from_line`, `distance_from_line_segment`, `distance_from_plane`, `distance_from_axis`, `axis_value`
135
+ - **Mesh utilities**: `grid_to_mesh()` (parametric grid triangulation), `vertices_to_mesh3d()` (vertices/faces to Plotly Mesh3d trace)
143
136
 
144
137
  ## Dependencies
145
138
 
146
139
  - **plotly**: Interactive 2D and 3D visualization
147
- - **matplotlib**: Static plot support
148
140
  - **numpy**: Array operations
149
- - **scipy**: Marching cubes mesh extraction
150
- - **scikit-image**: Additional mesh processing
151
141
  - **gri-utils**: Coordinate conversions and constants
152
142
 
153
143
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## Overview
8
8
 
9
- This library renders implicit surfaces (TDOA hyperboloids, FDOA iso-Doppler surfaces, AOA cones, range spheres, terrain) and geometric shapes (ellipsoids, spheres, cones, cylinders) as interactive 3D meshes. It also provides shorthand functions for 2D scatter plots and geographic maps.
9
+ This library renders parametric geometric shapes (ellipsoids, spheres, cones, cylinders) and observable surfaces that inherit from them (AOA cones, line-of-sight rays, range spheres) as interactive 3D meshes. It also provides shorthand functions for 2D scatter plots and geographic maps.
10
10
 
11
11
  ## Installation
12
12
 
@@ -61,14 +61,12 @@ Scene builder for composing 3D visualizations from multiple surfaces and points:
61
61
  - **`add_surface()`**: Add any `ImplicitShape` (shapes or observables)
62
62
  - **`add_points()`**: Add labeled scatter points
63
63
  - **`add_line()`**: Add line segments connecting points
64
- - **`add_intersection()`**: Highlight where multiple surfaces intersect
65
64
  - **`plot_surfaces()`**: Convenience function for quick multi-surface plots
66
65
  - Method chaining, automatic color cycling from D3 palette, `plotly_dark` template
67
66
 
68
67
  ```python
69
68
  from gri_plot import Figure3D
70
69
  from gri_plot.shapes import Sphere
71
- from gri_plot.observables import TdoaSurface
72
70
  import numpy as np
73
71
 
74
72
  fig = Figure3D()
@@ -88,7 +86,7 @@ Coordinate frame handling for 3D display:
88
86
 
89
87
  ## Shapes
90
88
 
91
- Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation (for intersection calculations).
89
+ Geometric primitives implementing the `ImplicitShape` ABC. Each supports parametric mesh generation (fast, exact geometry) and implicit residual evaluation.
92
90
 
93
91
  - **Ellipsoid**: From center + covariance matrix, or center + semi-axes + rotation matrix
94
92
  - **Sphere**: From center + radius
@@ -98,31 +96,23 @@ Geometric primitives implementing the `ImplicitShape` ABC. Each supports paramet
98
96
 
99
97
  ## Observables
100
98
 
101
- Geolocation-specific surfaces for visualizing measurement iso-surfaces. Some inherit from shapes (reusing parametric rendering), others use marching cubes for mesh extraction.
99
+ Geolocation-specific surfaces that inherit from parametric shapes:
102
100
 
103
- - **TdoaSurface**: TDOA hyperboloid where range_1 - range_2 = c x tdoa, from two collector positions + time difference
104
- - **FdoaSurface**: FDOA iso-Doppler surface from two collector positions/velocities + carrier frequency + frequency difference
105
101
  - **AoaSurface**: AOA cone (extends Cone) from collector position + measured direction + angular error
106
102
  - **LosSurface**: Line of sight ray (extends Cylinder) from start point + direction + length; also provides `to_line_trace()` for simple line rendering
107
103
  - **RangeSphere**: Range/TOA sphere (extends Sphere) from collector position + range measurement
108
- - **TerrainSurface**: Terrain from XYZ vertex grid with interpolated residual evaluation; factory methods `from_xyz_grids()` and `from_elevation_grid()`
109
104
 
110
105
  ## Surfaces
111
106
 
112
- Low-level infrastructure for implicit surface rendering:
107
+ Low-level infrastructure for shape rendering:
113
108
 
114
109
  - **ImplicitShape ABC**: Interface requiring `residual_fn()`, `get_bounds_xyz()`, `to_mesh()`, `to_trace()`, `is_volume`, `label`; also provides `contains()` for point-in-shape testing
115
- - **Mesh utilities**: `field_to_mesh()` (marching cubes with Newton-Raphson surface projection and adaptive refinement), `grid_to_mesh()` (parametric grid triangulation), `vertices_to_mesh3d()` (vertices/faces to Plotly Mesh3d trace)
116
- - **IntersectionField**: Euclidean distance in residual space for highlighting multi-surface intersections; distinguishes volumes (interior contribution) from surfaces (boundary contribution)
117
- - **Gradient functions**: Intensity calculators for mesh coloring -- `distance_from_point`, `distance_from_line`, `distance_from_line_segment`, `distance_from_plane`, `distance_from_axis`, `axis_value`
110
+ - **Mesh utilities**: `grid_to_mesh()` (parametric grid triangulation), `vertices_to_mesh3d()` (vertices/faces to Plotly Mesh3d trace)
118
111
 
119
112
  ## Dependencies
120
113
 
121
114
  - **plotly**: Interactive 2D and 3D visualization
122
- - **matplotlib**: Static plot support
123
115
  - **numpy**: Array operations
124
- - **scipy**: Marching cubes mesh extraction
125
- - **scikit-image**: Additional mesh processing
126
116
  - **gri-utils**: Coordinate conversions and constants
127
117
 
128
118
 
@@ -1,21 +1,22 @@
1
1
  """Plotting utilities for GeoSol Research.
2
2
 
3
3
  This package provides utilities for 2D and 3D visualization using Plotly,
4
- including scatter plots, geographic maps, and 3D surfaces for geolocation
5
- observables.
4
+ including scatter plots, geographic maps, and parametric 3D shapes for
5
+ geolocation observables.
6
6
 
7
7
  Main components:
8
8
  - scatter: 2D scatter plots with Plotly
9
9
  - scatter_map: Geographic scatter maps
10
10
  - Figure3D: 3D figure for combining surfaces
11
11
  - shapes: Geometric shapes (Ellipsoid, Sphere, Cone, Cylinder)
12
- - observables: Geolocation surfaces (TDOA, FDOA, AOA, etc.)
13
- - surfaces: Low-level surface utilities
12
+ - observables: Parametric geolocation surfaces (AOA, LOS, Range)
13
+ - surfaces: ImplicitShape ABC and mesh trace utilities
14
14
  - frames: Coordinate frame handling
15
15
  """
16
16
 
17
17
  # 2D plotting
18
18
  # 3D plotting infrastructure
19
+ from .basemap import BasemapStyle, add_offline_basemap
19
20
  from .figure3d import DEFAULT_COLORS, Figure3D, plot_surfaces
20
21
  from .figure_map import FigureMap, MapStyle
21
22
  from .frames import Bounds, Frame, FrameTransformer
@@ -23,13 +24,11 @@ from .frames import Bounds, Frame, FrameTransformer
23
24
  # Observables
24
25
  from .observables import (
25
26
  AoaSurface,
26
- FdoaSurface,
27
27
  LosSurface,
28
28
  RangeSphere,
29
- TdoaSurface,
30
- TerrainSurface,
31
29
  )
32
30
  from .plot_ellipse import plot_ellipse
31
+ from .polylines import split_at_antimeridian
33
32
  from .scatter import scatter
34
33
  from .scatter_map import scatter_map
35
34
 
@@ -45,25 +44,17 @@ from .surfaces import (
45
44
  expand_bounds,
46
45
  merge_bounds,
47
46
  )
48
- from .surfaces.gradients import (
49
- axis_value,
50
- distance_from_axis,
51
- distance_from_line,
52
- distance_from_line_segment,
53
- distance_from_plane,
54
- distance_from_point,
55
- )
56
- from .surfaces.mesh import field_to_mesh, grid_to_mesh, vertices_to_mesh3d
47
+ from .surfaces.mesh import grid_to_mesh, vertices_to_mesh3d
57
48
 
58
49
  __all__ = [
59
50
  "DEFAULT_COLORS",
60
51
  "AoaSurface",
52
+ "BasemapStyle",
61
53
  "Bounds",
62
54
  "Cone",
63
55
  "Cylinder",
64
56
  "Ellipse",
65
57
  "Ellipsoid",
66
- "FdoaSurface",
67
58
  "Figure3D",
68
59
  "FigureMap",
69
60
  "Frame",
@@ -73,19 +64,11 @@ __all__ = [
73
64
  "MapStyle",
74
65
  "RangeSphere",
75
66
  "Sphere",
76
- "TdoaSurface",
77
- "TerrainSurface",
78
- "axis_value",
67
+ "add_offline_basemap",
79
68
  "cone_mesh",
80
69
  "cylinder_mesh",
81
- "distance_from_axis",
82
- "distance_from_line",
83
- "distance_from_line_segment",
84
- "distance_from_plane",
85
- "distance_from_point",
86
70
  "ellipsoid_mesh",
87
71
  "expand_bounds",
88
- "field_to_mesh",
89
72
  "grid_to_mesh",
90
73
  "merge_bounds",
91
74
  "plot_ellipse",
@@ -93,5 +76,6 @@ __all__ = [
93
76
  "scatter",
94
77
  "scatter_map",
95
78
  "sphere_mesh",
79
+ "split_at_antimeridian",
96
80
  "vertices_to_mesh3d",
97
81
  ]
@@ -0,0 +1,132 @@
1
+ """PNG heatmap image generation for map-figure overlays.
2
+
3
+ Plotly's ``Scattermap`` cannot host a ``go.Heatmap`` trace because heatmaps
4
+ live in a Cartesian plot area, not a map. The workaround this module
5
+ implements is to encode the scalar field as an RGBA PNG (base64 data URI)
6
+ and add it to ``layout.map.layers`` as an ``sourcetype="image"`` entry.
7
+
8
+ The work this module does:
9
+
10
+ * colorscale sampling into a 256-entry lookup table;
11
+ * a Mercator pre-resample of the lat axis so the image aligns with the
12
+ basemap at non-equatorial latitudes (the image-layer is stretched
13
+ linearly in Mercator y; our grid is linear in geodetic latitude);
14
+ * a Gaussian blur on the alpha channel so the footprint mask's hard
15
+ per-cell edges fade smoothly at the boundary.
16
+
17
+ All functions here are private helpers for ``FigureMap.add_heatmap_image``.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import base64
23
+ import io
24
+ import math
25
+ from typing import TYPE_CHECKING
26
+
27
+ import numpy as np
28
+ import plotly.colors as pc
29
+ from PIL import Image, ImageFilter
30
+
31
+ if TYPE_CHECKING:
32
+ from numpy.typing import NDArray
33
+
34
+ LUT_SIZE = 256
35
+
36
+
37
+ def _colorscale_lut(name: str, n: int = LUT_SIZE) -> NDArray[np.uint8]:
38
+ """Sample a named Plotly colorscale into an (n, 3) uint8 RGB lookup table."""
39
+ samples = pc.sample_colorscale(name, np.linspace(0.0, 1.0, n))
40
+ rows = [
41
+ [int(c) for c in s.removeprefix("rgb(").rstrip(")").split(",")] for s in samples
42
+ ]
43
+ return np.array(rows, dtype=np.uint8)
44
+
45
+
46
+ def _mercator_remap(
47
+ values: NDArray[np.floating],
48
+ lat_axis: NDArray[np.floating],
49
+ ) -> NDArray[np.floating]:
50
+ """Resample a (M, N) grid so rows are linearly spaced in Mercator y.
51
+
52
+ The input ``values`` are assumed to be on a grid whose row i has
53
+ latitude ``lat_axis[i]`` in degrees (linear in geodetic latitude).
54
+ Returns an array of the same shape whose row i corresponds to a
55
+ latitude linearly spaced in Mercator y between the endpoints of
56
+ ``lat_axis``. This is what the Plotly image layer expects when its
57
+ ``coordinates`` corners are geodetic lat/lon.
58
+ """
59
+ m = len(lat_axis)
60
+ y_min = math.log(math.tan(math.pi / 4.0 + math.radians(float(lat_axis[0])) / 2.0))
61
+ y_max = math.log(math.tan(math.pi / 4.0 + math.radians(float(lat_axis[-1])) / 2.0))
62
+ target_y = np.linspace(y_min, y_max, m)
63
+ target_lat = np.degrees(2.0 * (np.arctan(np.exp(target_y)) - math.pi / 4.0))
64
+ src_idx = np.interp(target_lat, lat_axis, np.arange(m, dtype=np.float64))
65
+ src_lo = np.floor(src_idx).astype(np.int64)
66
+ src_hi = np.minimum(src_lo + 1, m - 1)
67
+ frac = (src_idx - src_lo)[:, None]
68
+ return (1.0 - frac) * values[src_lo] + frac * values[src_hi]
69
+
70
+
71
+ def values_to_png_data_uri( # noqa: PLR0913
72
+ values: NDArray[np.floating],
73
+ lat_axis: NDArray[np.floating],
74
+ colorscale: str,
75
+ *,
76
+ alpha: float,
77
+ symmetric: bool,
78
+ blur_px: float,
79
+ value_range: tuple[float, float] | None,
80
+ ) -> tuple[str, float, float]:
81
+ """Render a lat/lon-gridded scalar field to a PNG data URI.
82
+
83
+ Args:
84
+ values: Scalar field, shape (M, N). NaN cells are rendered
85
+ transparent.
86
+ lat_axis: Latitude per row in degrees, shape (M,). Used both to
87
+ pick the input-row colormap and to Mercator-resample before
88
+ encoding.
89
+ colorscale: Plotly colorscale name.
90
+ alpha: Opacity (0-1) for finite cells.
91
+ symmetric: When True and the values span zero, expand the color
92
+ range to be symmetric about zero. Useful for diverging
93
+ colorscales on signed fields (TDOA-dot).
94
+ blur_px: Gaussian blur radius (pixels) applied to the alpha
95
+ channel only. Softens the per-cell stair-step at the mask
96
+ edge without blurring the color data. Set to 0 to disable.
97
+ value_range: Explicit (vmin, vmax) override. Takes precedence
98
+ over ``symmetric`` and the implicit min/max.
99
+
100
+ Returns:
101
+ Tuple of (data URI string, vmin, vmax).
102
+ """
103
+ v = np.asarray(values, dtype=np.float64)
104
+ if value_range is not None:
105
+ vmin, vmax = value_range
106
+ else:
107
+ vmin, vmax = float(np.nanmin(v)), float(np.nanmax(v))
108
+ if symmetric and vmin < 0.0 < vmax:
109
+ vabs = max(abs(vmin), abs(vmax))
110
+ vmin, vmax = -vabs, vabs
111
+
112
+ v_remap = _mercator_remap(v, np.asarray(lat_axis, dtype=np.float64))
113
+ finite = np.isfinite(v_remap)
114
+ v_safe = np.where(finite, v_remap, vmin)
115
+ denom = vmax - vmin if vmax > vmin else 1.0
116
+ norm = np.clip((v_safe - vmin) / denom, 0.0, 1.0)
117
+ idx = (norm * (LUT_SIZE - 1)).astype(np.int32)
118
+
119
+ rgb = _colorscale_lut(colorscale)[idx]
120
+ a = np.where(finite, int(alpha * 255), 0).astype(np.uint8)
121
+ rgba = np.dstack([rgb, a])[::-1, :, :] # flip rows so lat increases upward
122
+
123
+ img = Image.fromarray(rgba, mode="RGBA")
124
+ if blur_px > 0:
125
+ r_ch, g_ch, b_ch, a_ch = img.split()
126
+ a_ch = a_ch.filter(ImageFilter.GaussianBlur(radius=blur_px))
127
+ img = Image.merge("RGBA", (r_ch, g_ch, b_ch, a_ch))
128
+
129
+ buf = io.BytesIO()
130
+ img.save(buf, format="PNG")
131
+ uri = "data:image/png;base64," + base64.b64encode(buf.getvalue()).decode("ascii")
132
+ return uri, vmin, vmax