core-lens 0.1.dev155__tar.gz → 0.1.dev160__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 (91) hide show
  1. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/PKG-INFO +1 -1
  2. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/_version.py +2 -2
  3. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/base/namespaces/plot.py +131 -8
  4. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/base/namespaces/stats.py +12 -1
  5. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_plot.py +2 -2
  6. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.github/ISSUE_TEMPLATE/blank-proposal.yaml +0 -0
  7. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.github/ISSUE_TEMPLATE/bug-report.yaml +0 -0
  8. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.github/ISSUE_TEMPLATE/feature-request.yaml +0 -0
  9. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.github/pull_request_template.md +0 -0
  10. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.github/workflows/ci.yml +0 -0
  11. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.github/workflows/gh-pages.yml +0 -0
  12. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.github/workflows/pre-release.yml +0 -0
  13. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.github/workflows/release.yml +0 -0
  14. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.gitignore +0 -0
  15. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.gitmessage +0 -0
  16. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.pre-commit-config.yaml +0 -0
  17. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/.python-version +0 -0
  18. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/CONTRIBUTING.md +0 -0
  19. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/LICENSE +0 -0
  20. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/README.md +0 -0
  21. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/SKILLS.md +0 -0
  22. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/benchmarks/README.md +0 -0
  23. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/benchmarks/bench_aoi.py +0 -0
  24. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/benchmarks/bench_export.py +0 -0
  25. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/benchmarks/bench_polars_utils.py +0 -0
  26. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/benchmarks/bench_result.py +0 -0
  27. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/benchmarks/bench_schema.py +0 -0
  28. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/benchmarks/bench_season.py +0 -0
  29. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/benchmarks/bench_spatial.py +0 -0
  30. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/benchmarks/bench_view.py +0 -0
  31. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/benchmarks/run_all.sh +0 -0
  32. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/Makefile +0 -0
  33. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/make.bat +0 -0
  34. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/concepts.md +0 -0
  35. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/conf.py +0 -0
  36. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/export.md +0 -0
  37. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/index.rst +0 -0
  38. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/intro.md +0 -0
  39. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/logging.md +0 -0
  40. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/plots.md +0 -0
  41. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/plugins.md +0 -0
  42. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/queries.md +0 -0
  43. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/quickstart.md +0 -0
  44. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/docs/source/stats.md +0 -0
  45. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/examples/demo_mws.py +0 -0
  46. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/examples/demo_tehsil.py +0 -0
  47. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/hooks/mypy.sh +0 -0
  48. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/hooks/no-parquet-outside-fixtures.sh +0 -0
  49. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/hooks/pytest.sh +0 -0
  50. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/pyproject.toml +0 -0
  51. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/__init__.py +0 -0
  52. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/__main__.py +0 -0
  53. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/aoi.py +0 -0
  54. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/base/__init__.py +0 -0
  55. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/base/entity.py +0 -0
  56. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/base/namespaces/__init__.py +0 -0
  57. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/base/result.py +0 -0
  58. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/base/view.py +0 -0
  59. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/entities/__init__.py +0 -0
  60. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/entities/mws.py +0 -0
  61. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/entities/tehsil.py +0 -0
  62. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/entities/waterbody.py +0 -0
  63. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/export/__init__.py +0 -0
  64. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/export/formats.py +0 -0
  65. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/py.typed +0 -0
  66. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/schema/__init__.py +0 -0
  67. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/schema/detection.py +0 -0
  68. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/schema/profile.py +0 -0
  69. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/utils/__init__.py +0 -0
  70. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/utils/paths.py +0 -0
  71. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/utils/polars_utils.py +0 -0
  72. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/utils/season.py +0 -0
  73. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/src/core_lens/utils/spatial.py +0 -0
  74. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/fixtures/generate_fixtures.py +0 -0
  75. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/conftest.py +0 -0
  76. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_aoi.py +0 -0
  77. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_entities.py +0 -0
  78. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_entity.py +0 -0
  79. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_export.py +0 -0
  80. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_main.py +0 -0
  81. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_polars_utils.py +0 -0
  82. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_profile.py +0 -0
  83. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_result.py +0 -0
  84. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_schema_detection.py +0 -0
  85. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_schema_profile.py +0 -0
  86. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_season.py +0 -0
  87. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_season_config.py +0 -0
  88. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_spatial.py +0 -0
  89. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_stats.py +0 -0
  90. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/tests/unit/test_view.py +0 -0
  91. {core_lens-0.1.dev155 → core_lens-0.1.dev160}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: core-lens
3
- Version: 0.1.dev155
3
+ Version: 0.1.dev160
4
4
  Summary: Query, analyse, and visualise CoreStack's microwatershed and Earth science data through a clean, composable Python API.
5
5
  Project-URL: Homepage, https://github.com/ApoorvaKashyap/core-lens
6
6
  Project-URL: Issues, https://github.com/ApoorvaKashyap/core-lens/issues
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.1.dev155'
22
- __version_tuple__ = version_tuple = (0, 1, 'dev155')
21
+ __version__ = version = '0.1.dev160'
22
+ __version_tuple__ = version_tuple = (0, 1, 'dev160')
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -27,7 +27,6 @@ class SubplotOn(Enum):
27
27
 
28
28
 
29
29
  if TYPE_CHECKING:
30
- import lonboard
31
30
  from bokeh.plotting import figure as BokehFigure
32
31
  from core_lens.base.result import Result
33
32
 
@@ -224,6 +223,113 @@ def _apply_theme(fig: Any, result: "Result", title: str) -> None:
224
223
  fig.add_layout(subtitle)
225
224
 
226
225
 
226
+ class MapWithLegend:
227
+ """Wrapper around lonboard.Map that adds a legend for Jupyter and HTML exports."""
228
+
229
+ def __init__(
230
+ self,
231
+ map_widget: Any,
232
+ cmap: Any,
233
+ v_min: float,
234
+ v_max: float,
235
+ column_name: str,
236
+ clamped_min: bool = False,
237
+ clamped_max: bool = False,
238
+ ) -> None:
239
+ self._map = map_widget
240
+ self._cmap = cmap
241
+ self._v_min = v_min
242
+ self._v_max = v_max
243
+ self._column_name = column_name
244
+ self._clamped_min = clamped_min
245
+ self._clamped_max = clamped_max
246
+
247
+ def __getattr__(self, name: str) -> Any:
248
+ return getattr(self._map, name)
249
+
250
+ def _generate_legend_div(self, absolute: bool = True) -> str:
251
+ import matplotlib.colors as mcolors
252
+ import numpy as np
253
+
254
+ stops = 5
255
+ style = (
256
+ "position:absolute; bottom:35px; left:20px; z-index:999; "
257
+ if absolute
258
+ else ""
259
+ )
260
+ style += (
261
+ "background:white; padding:8px 12px; border-radius:4px; "
262
+ "box-shadow:0 1px 4px rgba(0,0,0,.3); font-family:sans-serif; font-size:12px;"
263
+ )
264
+
265
+ html = [
266
+ f'<div style="{style}">',
267
+ f'<b style="margin-bottom:4px;display:inline-block;">{self._column_name}</b><br>',
268
+ ]
269
+
270
+ if np.isnan(self._v_min) or np.isnan(self._v_max) or self._v_min >= self._v_max:
271
+ html.append('<span style="color:gray;">No Data</span>')
272
+ else:
273
+ for i in range(stops):
274
+ frac = (stops - 1 - i) / (
275
+ stops - 1
276
+ ) # Reverse order so highest is on top
277
+ val = self._v_min + frac * (self._v_max - self._v_min)
278
+ rgba = self._cmap(frac)
279
+ hex_color = mcolors.to_hex(rgba)
280
+
281
+ prefix = ""
282
+ if i == stops - 1 and self._clamped_min:
283
+ prefix = "&lt; "
284
+ elif i == 0 and self._clamped_max:
285
+ prefix = "&gt; "
286
+
287
+ html.append(
288
+ f'<div style="margin-top:2px;">'
289
+ f'<span style="background:{hex_color};width:12px;height:12px;'
290
+ f"display:inline-block;margin-right:8px;vertical-align:middle;"
291
+ f'border:1px solid #ccc;"></span>'
292
+ f'<span style="vertical-align:middle;">{prefix}{val:.2f}</span></div>'
293
+ )
294
+ html.append("</div>")
295
+ return "".join(html)
296
+
297
+ def _ipython_display_(self) -> None:
298
+ import ipywidgets # type: ignore[import-untyped]
299
+ from IPython.display import display
300
+
301
+ legend_div = self._generate_legend_div(absolute=False)
302
+ legend_html = ipywidgets.HTML(legend_div)
303
+
304
+ display(ipywidgets.VBox([legend_html, self._map])) # type: ignore[no-untyped-call]
305
+
306
+ def to_html(
307
+ self, filename: str | None = None, title: str | None = None
308
+ ) -> str | None:
309
+ """Export the map to a static HTML string or file, injecting the legend."""
310
+ # lonboard.Map to_html doesn't accept title in all versions,
311
+ # so we conditionally pass it if it's not None or we can just omit title if we aren't sure.
312
+ # But wait, we can just use `self._map.to_html()`
313
+ if title is not None:
314
+ try:
315
+ html_str = self._map.to_html(title=title)
316
+ except TypeError:
317
+ html_str = self._map.to_html()
318
+ else:
319
+ html_str = self._map.to_html()
320
+
321
+ legend_div = self._generate_legend_div(absolute=True)
322
+
323
+ # Inject right before closing </body> tag
324
+ html_str = html_str.replace("</body>", legend_div + "</body>")
325
+
326
+ if filename is not None:
327
+ with open(filename, "w", encoding="utf-8") as f:
328
+ f.write(html_str)
329
+ return None
330
+ return str(html_str)
331
+
332
+
227
333
  class PlotNamespace:
228
334
  """Visualisation namespace for :class:`~core_lens.base.result.Result`.
229
335
 
@@ -238,8 +344,12 @@ class PlotNamespace:
238
344
  self.result = result
239
345
 
240
346
  def choropleth(
241
- self, column: str, subplot_on: SubplotOn | None = None
242
- ) -> "lonboard.Map":
347
+ self,
348
+ column: str,
349
+ subplot_on: SubplotOn | None = None,
350
+ v_min: float | None = None,
351
+ v_max: float | None = None,
352
+ ) -> "MapWithLegend":
243
353
  """Render an interactive choropleth map using Lonboard.
244
354
 
245
355
  If the Result does not already have geometry, it will be attached
@@ -312,20 +422,33 @@ class PlotNamespace:
312
422
 
313
423
  with warnings.catch_warnings():
314
424
  warnings.simplefilter("ignore", category=RuntimeWarning)
315
- v_min, v_max = np.nanmin(values), np.nanmax(values)
425
+ actual_min, actual_max = np.nanmin(values), np.nanmax(values)
426
+
427
+ calc_v_min = float(v_min) if v_min is not None else float(actual_min)
428
+ calc_v_max = float(v_max) if v_max is not None else float(actual_max)
429
+
430
+ clamped_min = v_min is not None and actual_min < v_min
431
+ clamped_max = v_max is not None and actual_max > v_max
316
432
 
317
- if np.isnan(v_min) or np.isnan(v_max) or v_min >= v_max:
433
+ if np.isnan(calc_v_min) or np.isnan(calc_v_max) or calc_v_min >= calc_v_max:
318
434
  norm_values = np.where(np.isnan(values), np.nan, 0.0)
319
435
  else:
320
- norm_values = (values - v_min) / (v_max - v_min)
436
+ norm_values = np.clip(
437
+ (values - calc_v_min) / (calc_v_max - calc_v_min), 0.0, 1.0
438
+ )
321
439
 
322
- cmap = mpl.colormaps["plasma"]
440
+ cmap = mpl.colormaps["plasma_r"]
323
441
 
324
442
  layer = lonboard.PolygonLayer(
325
443
  arrow_table,
326
444
  get_fill_color=apply_continuous_cmap(norm_values, cmap),
445
+ get_line_color=[255, 255, 255, 180],
446
+ line_width_min_pixels=0.25,
447
+ )
448
+ map_widget = lonboard.Map(layers=[layer])
449
+ return MapWithLegend(
450
+ map_widget, cmap, calc_v_min, calc_v_max, column, clamped_min, clamped_max
327
451
  )
328
- return lonboard.Map(layers=[layer])
329
452
 
330
453
  def timeseries(
331
454
  self,
@@ -515,7 +515,16 @@ class StatsNamespace:
515
515
  ).alias("pct_change"),
516
516
  ]
517
517
  )
518
- data = joined
518
+
519
+ final_cols = join_keys + [
520
+ "year_from",
521
+ "year_to",
522
+ "value_from",
523
+ "value_to",
524
+ "change",
525
+ "pct_change",
526
+ ]
527
+ data = joined.select(final_cols)
519
528
 
520
529
  else: # trend
521
530
  period_df = df.filter(
@@ -538,6 +547,8 @@ class StatsNamespace:
538
547
  rows.append(
539
548
  {
540
549
  key: eid,
550
+ "year_from": int(from_period),
551
+ "year_to": int(to_period),
541
552
  "slope": float(cast(float, slope)),
542
553
  "r_squared": float(cast(float, r_val) ** 2),
543
554
  "direction": (
@@ -9,7 +9,7 @@ from typing import Any
9
9
  from core_lens.base.entity import BaseEntity
10
10
  from core_lens.base.result import Result
11
11
  from core_lens.schema.profile import Resolution, SchemaProfile
12
- from core_lens.base.namespaces.plot import SubplotOn
12
+ from core_lens.base.namespaces.plot import SubplotOn, MapWithLegend
13
13
 
14
14
 
15
15
  class DummyEntity(BaseEntity):
@@ -79,7 +79,7 @@ def test_choropleth_basic(dummy_result: Result) -> None:
79
79
  """Test choropleth creates a lonboard Map correctly."""
80
80
  plot_map = dummy_result.plot.choropleth("value")
81
81
 
82
- assert isinstance(plot_map, lonboard.Map)
82
+ assert isinstance(plot_map, MapWithLegend)
83
83
  assert len(plot_map.layers) == 1
84
84
  layer = plot_map.layers[0]
85
85
  assert isinstance(layer, lonboard.PolygonLayer)
File without changes
File without changes
File without changes
File without changes