scistackplotdb 0.1.54__tar.gz → 0.1.57__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.5
2
2
  Name: scistackplotdb
3
- Version: 0.1.54
3
+ Version: 0.1.57
4
4
  Summary: scidb-backed plotting: load variables into long tables and plot them with scistackplot
5
5
  Author: SciStack Contributors
6
6
  License-Expression: MIT
@@ -17,10 +17,10 @@ Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Topic :: Scientific/Engineering :: Visualization
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: >=3.10
20
- Requires-Dist: sciduckdb==0.1.54
21
- Requires-Dist: scistack-db==0.1.54
22
- Requires-Dist: scistacklog==0.1.54
23
- Requires-Dist: scistackplot==0.1.54
20
+ Requires-Dist: sciduckdb==0.1.57
21
+ Requires-Dist: scistack-db==0.1.57
22
+ Requires-Dist: scistacklog==0.1.57
23
+ Requires-Dist: scistackplot==0.1.57
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: matplotlib>=3.6; extra == 'dev'
26
26
  Requires-Dist: pytest-cov>=4.0; extra == 'dev'
@@ -21,6 +21,7 @@ from typing import Any
21
21
 
22
22
  import numpy as np
23
23
  import pandas as pd
24
+ from sciduckdb import merge_tz_columns, stored_tz_companions, tz_column_name
24
25
  from scistacklog import Log
25
26
  from scistackplot import CODE_FACTOR_PREFIX, RUN_FACTOR_PREFIX
26
27
  from scistackplot.framesize import format_extent, frame_extent
@@ -130,6 +131,41 @@ def data_columns_for(db, variable: str) -> list[str]:
130
131
  return list(data_column_types_for(db, variable))
131
132
 
132
133
 
134
+ def _stored_column_types(db, variable: str) -> dict[str, str]:
135
+ """``{column: DuckDB type}`` of the variable's table as stored (zone
136
+ companions included), in ordinal order."""
137
+ table = table_name_for(db, variable)
138
+ rows = db._duck._fetchall(
139
+ "SELECT column_name, data_type FROM information_schema.columns "
140
+ "WHERE table_name = ? AND column_name != 'record_id' "
141
+ "ORDER BY ordinal_position",
142
+ [table],
143
+ )
144
+ return {row[0]: str(row[1] or "") for row in rows}
145
+
146
+
147
+ def datetime_zone_meta(db, variable: str, columns) -> dict:
148
+ """The dtype metadata ``sciduckdb.merge_tz_columns`` needs to fold the
149
+ ``__tz`` zone column of each datetime column in ``columns`` back in:
150
+ ``{"columns": {col: {"python_type": "datetime", "unit": ...}}}``. The
151
+ unit comes from the variable's stored metadata; the companion is found
152
+ in the table schema (owner ``sciduckdb.stored_tz_companions``)."""
153
+ zone_cols = stored_tz_companions(_stored_column_types(db, variable))
154
+ sources = [src for src in zone_cols.values() if src in set(columns)]
155
+ if not sources:
156
+ return {"columns": {}}
157
+ stored = (db.get_dtype_meta(variable) or {}).get("columns", {})
158
+ return {
159
+ "columns": {
160
+ src: {
161
+ **{"python_type": "datetime"},
162
+ **{k: v for k, v in (stored.get(src) or {}).items() if k == "unit"},
163
+ }
164
+ for src in sources
165
+ }
166
+ }
167
+
168
+
133
169
  def data_column_types_for(db, variable: str) -> dict[str, str]:
134
170
  """``{column: declared DuckDB type}``, in ordinal order.
135
171
 
@@ -139,13 +175,7 @@ def data_column_types_for(db, variable: str) -> dict[str, str]:
139
175
  (:func:`is_container_type`). It never accepts one; see that function.
140
176
  """
141
177
  table = table_name_for(db, variable)
142
- rows = db._duck._fetchall(
143
- "SELECT column_name, data_type FROM information_schema.columns "
144
- "WHERE table_name = ? AND column_name != 'record_id' "
145
- "ORDER BY ordinal_position",
146
- [table],
147
- )
148
- types = {row[0]: str(row[1] or "") for row in rows}
178
+ types = _stored_column_types(db, variable)
149
179
 
150
180
  # A data column named after a schema key is an address stored as payload:
151
181
  # a record saved before the spread/collision rules stripped such columns
@@ -171,6 +201,18 @@ def data_column_types_for(db, variable: str) -> dict[str, str]:
171
201
  )
172
202
  for name in shadowed:
173
203
  del types[name]
204
+
205
+ # A datetime column's __tz zone companion is storage, not a column to
206
+ # plot (owner sciduckdb.datetimes). load_variable selects it anyway and
207
+ # folds it back into its datetime column (datetime_zone_meta).
208
+ zone_cols = stored_tz_companions(types)
209
+ if zone_cols:
210
+ Log.debug(
211
+ "variable %r: datetime zone column(s) %s left out of the data columns",
212
+ variable, sorted(zone_cols), layer=LAYER,
213
+ )
214
+ for name in zone_cols:
215
+ del types[name]
174
216
  return types
175
217
 
176
218
 
@@ -442,7 +484,12 @@ def load_variable(
442
484
  schema_select = "".join(
443
485
  f', CAST(s."{key}" AS VARCHAR) AS "{key}"' for key in keys
444
486
  )
445
- data_select = "".join(f', t."{column}"' for column in columns)
487
+ # Datetime columns bring their __tz zone column, folded back right
488
+ # after the fetch (sciduckdb.merge_tz_columns): without it every
489
+ # aware value reached a plot as a naive UTC instant.
490
+ zone_meta = datetime_zone_meta(db, variable, columns) if columns else {"columns": {}}
491
+ zone_select = [tz_column_name(c) for c in zone_meta["columns"]]
492
+ data_select = "".join(f', t."{column}"' for column in [*columns, *zone_select])
446
493
  query = (
447
494
  f"SELECT t.record_id{data_select}{schema_select} "
448
495
  f'FROM "{table}" t '
@@ -464,6 +511,9 @@ def load_variable(
464
511
  # regression to per-sample boxing is visible in the log, not inferred.
465
512
  with timer.phase("fetch"):
466
513
  frame = db._duck._fetchdf(query, [variable])
514
+ if zone_meta["columns"]:
515
+ with timer.phase("datetime_zones"):
516
+ merge_tz_columns(frame, zone_meta, where=f"load_variable({variable})")
467
517
 
468
518
  with timer.phase("dataframe"):
469
519
  frame = frame[["record_id", *columns, *keys]].copy()
@@ -28,6 +28,7 @@ from scistackplot.dedup import SingleFlight
28
28
  from scistackplot.framesize import format_extent, frame_extent
29
29
  from scistackplot.sources import BaseSource, UnknownMeasureError
30
30
  from scistackplot.variants import VARIABLE_COLUMN
31
+ from scistackplot.temporal import is_temporal_column
31
32
 
32
33
  from .hierarchy import join_frames, joinable, joined_levels
33
34
  from .load import (
@@ -75,7 +76,16 @@ def _as_levels(column):
75
76
  synthetic-factors invariant 4 exists to prevent. The order matters:
76
77
  ``astype(str)`` first would turn every missing value into the string
77
78
  ``"nan"``.
79
+
80
+ A DATETIME column is the exception: its values stay datetimes (missing
81
+ ones named), and ``LongTable.from_frame`` gives each its identity label
82
+ in time order (``scistackplot.temporal.level_labels``) — text that says
83
+ when, not ``2026-07-01 14:00:00+02:00`` sorted as text. The generated code
84
+ emits the same labels (``temporal.factor_label_code``).
78
85
  """
86
+ if is_temporal_column(column):
87
+ out = column.astype(object).where(column.notna(), MISSING_LEVEL)
88
+ return out
79
89
  return column.fillna(MISSING_LEVEL).astype(str)
80
90
 
81
91
 
@@ -1380,6 +1390,20 @@ class ScidbSource(BaseSource):
1380
1390
  usable.append(column)
1381
1391
  else:
1382
1392
  skipped.append(column)
1393
+ # Datetime fields melt into ONE value column with the rest, which is a
1394
+ # time axis only if every field is a datetime (scistackplot.temporal).
1395
+ # Beside numeric fields they would read as numbers (NaN), so they are
1396
+ # left out and named — group by one (Variable.column) instead.
1397
+ datetimes = [c for c in usable if is_temporal_column(frame[c])]
1398
+ if datetimes and len(datetimes) < len(usable):
1399
+ Log.warn(
1400
+ "variable %r: datetime field(s) %s cannot share one axis with its "
1401
+ "numeric field(s) and were left out — group by one instead",
1402
+ variable_frame.name,
1403
+ datetimes,
1404
+ layer=LAYER,
1405
+ )
1406
+ usable = [c for c in usable if c not in datetimes]
1383
1407
  if skipped:
1384
1408
  Log.warn(
1385
1409
  "variable %r: %d field(s) are not plottable and were dropped: %s",