trendify2 2.0.4__py3-none-any.whl

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 (76) hide show
  1. trendify/__about__.py +14 -0
  2. trendify/__init__.py +250 -0
  3. trendify/base/__init__.py +36 -0
  4. trendify/base/helpers.py +76 -0
  5. trendify/base/pen.py +161 -0
  6. trendify/base/record.py +148 -0
  7. trendify/cli.py +403 -0
  8. trendify/color.py +260 -0
  9. trendify/examples.py +474 -0
  10. trendify/formats/__init__.py +28 -0
  11. trendify/formats/format2d.py +128 -0
  12. trendify/formats/table.py +55 -0
  13. trendify/generator/__init__.py +38 -0
  14. trendify/generator/generate.py +167 -0
  15. trendify/generator/histogrammer.py +62 -0
  16. trendify/generator/render.py +195 -0
  17. trendify/generator/table_builder.py +194 -0
  18. trendify/generator/xy_data_plotter.py +195 -0
  19. trendify/log.py +195 -0
  20. trendify/pipeline.py +136 -0
  21. trendify/plotting/__init__.py +48 -0
  22. trendify/plotting/axline.py +122 -0
  23. trendify/plotting/figure.py +389 -0
  24. trendify/plotting/histogram.py +282 -0
  25. trendify/plotting/point.py +89 -0
  26. trendify/plotting/scatter.py +100 -0
  27. trendify/plotting/trace.py +129 -0
  28. trendify/progress.py +43 -0
  29. trendify/store/__init__.py +30 -0
  30. trendify/store/db.py +119 -0
  31. trendify/store/record_store.py +448 -0
  32. trendify/store/tags.py +65 -0
  33. trendify/styling/__init__.py +30 -0
  34. trendify/styling/grid.py +125 -0
  35. trendify/styling/legend.py +217 -0
  36. trendify/styling/marker.py +168 -0
  37. trendify/typing.py +26 -0
  38. trendify/viewer/__init__.py +41 -0
  39. trendify/viewer/app.py +208 -0
  40. trendify/viewer/hydration.py +181 -0
  41. trendify/viewer/plot_config.py +102 -0
  42. trendify/viewer/routes/__init__.py +6 -0
  43. trendify/viewer/routes/api.py +360 -0
  44. trendify/viewer/routes/pages.py +39 -0
  45. trendify/viewer/tag_tree.py +148 -0
  46. trendify/viewer/templates/base.html +63 -0
  47. trendify/viewer/templates/index.html +131 -0
  48. trendify/viewer/templates/partials/__init__.py +1 -0
  49. trendify/viewer/templates/partials/footer.html +15 -0
  50. trendify/viewer/templates/partials/nav.html +181 -0
  51. trendify/viewer/templates/partials/plot_view.html +221 -0
  52. trendify/viewer/templates/partials/sidebar.html +163 -0
  53. trendify/viewer/templates/partials/table_view.html +43 -0
  54. trendify/viewer/templates/static/__init__.py +1 -0
  55. trendify/viewer/templates/static/assets/__init__.py +1 -0
  56. trendify/viewer/templates/static/assets/main-logo-white-bg.svg +75 -0
  57. trendify/viewer/templates/static/assets/main-logo.svg +66 -0
  58. trendify/viewer/templates/static/css/app.css +75 -0
  59. trendify/viewer/templates/static/js/main.js +2 -0
  60. trendify/viewer/templates/static/vendored/__init__.py +1 -0
  61. trendify/viewer/templates/static/vendored/alpine-3.15.12.min.js +5 -0
  62. trendify/viewer/templates/static/vendored/alpine-LICENSE.md +21 -0
  63. trendify/viewer/templates/static/vendored/dataTables-LICENSE.txt +3 -0
  64. trendify/viewer/templates/static/vendored/dataTables.dataTables.min.css +1 -0
  65. trendify/viewer/templates/static/vendored/dataTables.min.js +4 -0
  66. trendify/viewer/templates/static/vendored/jquery-3.7.1.min.js +2 -0
  67. trendify/viewer/templates/static/vendored/jquery-LICENSE.txt +3 -0
  68. trendify/viewer/templates/static/vendored/plotly-3.6.0.min.js +3882 -0
  69. trendify/viewer/templates/static/vendored/plotly-LICENSE.txt +21 -0
  70. trendify/viewer/templates/static/vendored/tailwind-4.3.2.global.js +947 -0
  71. trendify/viewer/templates/static/vendored/tailwind-LICENSE +21 -0
  72. trendify2-2.0.4.dist-info/METADATA +107 -0
  73. trendify2-2.0.4.dist-info/RECORD +76 -0
  74. trendify2-2.0.4.dist-info/WHEEL +4 -0
  75. trendify2-2.0.4.dist-info/entry_points.txt +2 -0
  76. trendify2-2.0.4.dist-info/licenses/LICENSE.txt +21 -0
trendify/examples.py ADDED
@@ -0,0 +1,474 @@
1
+ """
2
+ Sample data + a `RecordGenerator` used as a fixture for exercising the `trendify` pipeline end to end.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+ from typing import cast
9
+
10
+ import numpy as np
11
+ import polars as pl
12
+
13
+ import trendify
14
+
15
+ __all__ = ["example_record_generator", "make_example_data"]
16
+
17
+
18
+ # results.csv has a `time` column followed by `wave_1`, `wave_2`, ... counting upward. Only the
19
+ # first N_PLOTTED_WAVES of those are plotted by `example_record_generator`.
20
+ N_PLOTTED_WAVES = 3
21
+
22
+
23
+ def make_example_data(workdir: Path, n_folders: int = 10, n_cols: int = 3) -> None:
24
+ """
25
+ Makes a highly diverse, varied sample dataset featuring distinct waveform signatures.
26
+
27
+ Args:
28
+ workdir (Path): Directory in which the sample data is to be generated
29
+ n_folders (int): Number of sample data files to generate (in separate subfolders).
30
+ n_cols (int): Number of data columns in each `results.csv` (not counting `time`), named
31
+ `wave_1`, `wave_2`, ... counting upward. The first three are the waveforms
32
+ `example_record_generator` plots. Any beyond that (`wave_4` onward) are unused
33
+ filler that exist only to make reading each file more expensive.
34
+
35
+ """
36
+ if n_cols < N_PLOTTED_WAVES:
37
+ raise ValueError(f"n_cols must be at least {N_PLOTTED_WAVES}, got {n_cols}")
38
+ models_dir = workdir.joinpath("models")
39
+ models_dir.mkdir(parents=True, exist_ok=True)
40
+
41
+ if not (models_dir / ".gitignore").exists():
42
+ (models_dir / ".gitignore").write_text("*")
43
+
44
+ for n in range(n_folders):
45
+ subdir = models_dir.joinpath(str(n))
46
+ subdir.mkdir(exist_ok=True, parents=True)
47
+
48
+ rng = np.random.default_rng(seed=n)
49
+
50
+ n_samples = rng.integers(
51
+ low=60, high=80
52
+ ) # Bumped slightly to resolve sharp shapes beautifully
53
+ t = np.linspace(0, 1, n_samples)
54
+ amplitudes = rng.uniform(low=0.8, high=1.5, size=3)
55
+ noise_level = 0.03
56
+
57
+ # --- 1. WAVE_1: Damped Harmonic Oscillation ---
58
+ decay_rate = rng.uniform(1.5, 3.5)
59
+ freq = rng.uniform(3.0, 5.0)
60
+ wave_1 = amplitudes[0] * np.sin(2 * np.pi * freq * t) * np.exp(
61
+ -decay_rate * t
62
+ ) + noise_level * rng.normal(size=len(t))
63
+
64
+ # --- 2. WAVE_2: Sharp Step / Discontinuity ---
65
+ step_time = rng.uniform(0.3, 0.6)
66
+ base_level = rng.uniform(-0.2, 0.2)
67
+ wave_2 = np.where(
68
+ t < step_time, base_level, base_level + amplitudes[1]
69
+ ) + noise_level * rng.normal(size=len(t))
70
+
71
+ # --- 3. WAVE_3: Logistic Saturation (S-Curve) ---
72
+ midpoint = rng.uniform(0.4, 0.6)
73
+ steepness = rng.uniform(10.0, 16.0)
74
+ wave_3 = amplitudes[2] / (
75
+ 1.0 + np.exp(-steepness * (t - midpoint))
76
+ ) + noise_level * rng.normal(size=len(t))
77
+
78
+ # Collect metadata parameters uniquely for this run to write to stdin.csv
79
+ inputs: dict[str, int | float] = {
80
+ "n_samples": int(n_samples),
81
+ "w1_decay": float(decay_rate),
82
+ "w1_freq": float(freq),
83
+ "w2_step_time": float(step_time),
84
+ "w3_midpoint": float(midpoint),
85
+ }
86
+
87
+ pl.DataFrame(
88
+ {"key": list(inputs.keys()), "value": [str(v) for v in inputs.values()]}
89
+ ).write_csv(subdir.joinpath("stdin.csv"), include_header=False)
90
+
91
+ # Package channels cleanly matching your original structure
92
+ columns = {"time": t, "wave_1": wave_1, "wave_2": wave_2, "wave_3": wave_3}
93
+
94
+ # Filler columns are drawn last, so the waveforms above (and every file written with
95
+ # the default n_cols) are identical whether or not filler is requested.
96
+ n_extra = n_cols - N_PLOTTED_WAVES
97
+ if n_extra:
98
+ filler = rng.normal(size=(len(t), n_extra))
99
+ for j in range(n_extra):
100
+ columns[f"wave_{N_PLOTTED_WAVES + 1 + j}"] = filler[:, j]
101
+
102
+ pl.DataFrame(columns).write_csv(subdir.joinpath("results.csv"))
103
+
104
+
105
+ def transform(data: np.ndarray, scale: trendify.AxisScale) -> np.ndarray:
106
+ if scale == trendify.AxisScale.LINEAR:
107
+ return data
108
+ elif scale == trendify.AxisScale.LOG:
109
+ return np.exp(data) # ensures positivity and preserves shape
110
+ else:
111
+ raise ValueError(f"Unsupported scale: {scale}")
112
+
113
+
114
+ def example_record_generator(workdir: Path) -> trendify.RecordList:
115
+ """
116
+ Processes the generated sample data in given workdir returning several types of records.
117
+
118
+ Args:
119
+ workdir (Path): Directory containing sample data.
120
+
121
+ """
122
+ records = []
123
+
124
+ df = pl.read_csv(workdir.joinpath("results.csv"))
125
+ time = df["time"].to_numpy()
126
+ # Only the first N_PLOTTED_WAVES columns, not "every column but time": results.csv may carry
127
+ # unused filler (see `make_example_data`'s `n_cols`) that must not be plotted or tabulated.
128
+ # The whole file is still read above, so filler still costs read time.
129
+ value_columns = [f"wave_{i}" for i in range(1, N_PLOTTED_WAVES + 1)]
130
+
131
+ colors = ["#FF0000", "#000B81", "#FFAA00"]
132
+ alphas = [1.0, 0.3, 1.0]
133
+ linestyles = ["-", ":", (0, (3, 1, 1, 1))]
134
+
135
+ run_num = workdir.name
136
+
137
+ trendify.Format2D(
138
+ tags=[("an_xy_plot", "trace_plot")],
139
+ grid=trendify.Grid.from_theme(trendify.GridTheme.MATLAB),
140
+ scale_x=trendify.AxisScale.LINEAR,
141
+ scale_y=trendify.AxisScale.LINEAR,
142
+ ).append_to_list(records)
143
+ traces = [
144
+ trendify.Trace2D(
145
+ x=time,
146
+ y=df[col].to_numpy(),
147
+ tags=[("an_xy_plot", "trace_plot")],
148
+ pen=trendify.Pen(
149
+ label=col,
150
+ color=colors[i],
151
+ linestyle=linestyles[i % len(linestyles)],
152
+ alpha=alphas[i],
153
+ ),
154
+ )
155
+ .append_to_list(records)
156
+ .set_metadata({"run_num": run_num})
157
+ for i, col in enumerate(value_columns)
158
+ ]
159
+
160
+ trendify.Format2D(
161
+ tags=[("an_xy_plot", "another_trace_plot")],
162
+ grid=trendify.Grid.from_theme(trendify.GridTheme.MATLAB),
163
+ scale_x=trendify.AxisScale.LINEAR,
164
+ scale_y=trendify.AxisScale.LINEAR,
165
+ ).append_to_list(records)
166
+ traces = [
167
+ trendify.Trace2D(
168
+ x=time,
169
+ y=df[col].to_numpy(),
170
+ tags=[("an_xy_plot", "another_trace_plot")],
171
+ pen=trendify.Pen(
172
+ label=col,
173
+ color=colors[i],
174
+ linestyle=linestyles[i % len(linestyles)],
175
+ alpha=alphas[i],
176
+ ),
177
+ )
178
+ .append_to_list(records)
179
+ .set_metadata({"run_num": run_num})
180
+ for i, col in enumerate(value_columns)
181
+ ]
182
+
183
+ trendify.Format2D(
184
+ tags=[("another_xy_plot", "trace_plot")],
185
+ grid=trendify.Grid.from_theme(trendify.GridTheme.MATLAB),
186
+ scale_x=trendify.AxisScale.LINEAR,
187
+ scale_y=trendify.AxisScale.LINEAR,
188
+ legend=trendify.Legend(loc=trendify.LegendLocation.LOWER_CENTER),
189
+ figure_width=8,
190
+ figure_height=4,
191
+ ).append_to_list(records)
192
+ traces = [
193
+ trendify.Trace2D(
194
+ x=time,
195
+ y=df[col].to_numpy(),
196
+ tags=[("another_xy_plot", "trace_plot")],
197
+ pen=trendify.Pen(
198
+ label=col,
199
+ color=colors[i],
200
+ linestyle=linestyles[i % len(linestyles)],
201
+ alpha=alphas[i],
202
+ ),
203
+ )
204
+ .append_to_list(records)
205
+ .set_metadata({"run_num": run_num})
206
+ for i, col in enumerate(value_columns)
207
+ ]
208
+ trendify.AxLine(
209
+ tags=[("another_xy_plot", "trace_plot")],
210
+ value=0.5,
211
+ orientation=trendify.LineOrientation.VERTICAL,
212
+ pen=trendify.Pen(zorder=11, color="k"),
213
+ ).append_to_list(records)
214
+ trendify.AxLine(
215
+ tags=[("another_xy_plot", "trace_plot")],
216
+ value=0.45,
217
+ orientation=trendify.LineOrientation.VERTICAL,
218
+ pen=trendify.Pen(zorder=9, color="r"),
219
+ ).append_to_list(records)
220
+
221
+ trendify.Format2D(
222
+ tags=["trace_plot_log_y"],
223
+ legend=trendify.Legend(
224
+ title="example",
225
+ loc=trendify.LegendLocation.CENTER_RIGHT,
226
+ framealpha=0,
227
+ ),
228
+ grid=trendify.Grid.from_theme(trendify.GridTheme.MATLAB),
229
+ scale_x=trendify.AxisScale.LINEAR,
230
+ scale_y=trendify.AxisScale.LOG,
231
+ ).append_to_list(records)
232
+ traces = [
233
+ trendify.Trace2D(
234
+ x=time,
235
+ y=transform(df[col].to_numpy(), trendify.AxisScale.LOG),
236
+ tags=["trace_plot_log_y"],
237
+ pen=trendify.Pen(
238
+ label=col,
239
+ color=colors[i],
240
+ linestyle=linestyles[i % len(linestyles)],
241
+ alpha=alphas[i],
242
+ ),
243
+ )
244
+ .append_to_list(records)
245
+ .set_metadata({"run_num": run_num})
246
+ for i, col in enumerate(value_columns)
247
+ ]
248
+
249
+ trendify.Format2D(
250
+ tags=["trace_plot_log_xy"],
251
+ legend=trendify.Legend(
252
+ fancybox=False,
253
+ loc=trendify.LegendLocation.CENTER,
254
+ edgecolor="red",
255
+ framealpha=1,
256
+ ),
257
+ lim_y=(0.1, 10),
258
+ grid=trendify.Grid.from_theme(trendify.GridTheme.MATLAB),
259
+ scale_x=trendify.AxisScale.LOG,
260
+ scale_y=trendify.AxisScale.LOG,
261
+ ).append_to_list(records)
262
+ traces = [
263
+ trendify.Trace2D(
264
+ x=transform(time, trendify.AxisScale.LOG),
265
+ y=transform(df[col].to_numpy(), trendify.AxisScale.LOG),
266
+ tags=["trace_plot_log_xy"],
267
+ pen=trendify.Pen(
268
+ label=col,
269
+ color=colors[i],
270
+ linestyle="-",
271
+ alpha=alphas[i],
272
+ zorder=[1, 1, 2][i],
273
+ size=5,
274
+ ),
275
+ )
276
+ .append_to_list(records)
277
+ .set_metadata({"run_num": run_num})
278
+ for i, col in enumerate(value_columns)
279
+ ]
280
+ trendify.AxLine(
281
+ tags=["trace_plot_log_xy"],
282
+ value=2.5,
283
+ orientation=trendify.LineOrientation.HORIZONTAL,
284
+ pen=trendify.Pen(
285
+ alpha=0.5, color="r", linestyle="-", label="test line", zorder=1
286
+ ),
287
+ ).append_to_list(records)
288
+
289
+ trendify.Format2D(
290
+ tags=["trace_plot_log_x"],
291
+ legend=trendify.Legend(visible=False),
292
+ grid=trendify.Grid.from_theme(trendify.GridTheme.MATLAB),
293
+ scale_x=trendify.AxisScale.LOG,
294
+ scale_y=trendify.AxisScale.LINEAR,
295
+ ).append_to_list(records)
296
+ traces = [
297
+ trendify.Trace2D(
298
+ x=transform(time, trendify.AxisScale.LOG),
299
+ y=df[col].to_numpy(),
300
+ tags=["trace_plot_log_x"],
301
+ pen=trendify.Pen(
302
+ label=col,
303
+ color=colors[i],
304
+ linestyle=linestyles[i % len(linestyles)],
305
+ alpha=alphas[i],
306
+ ),
307
+ )
308
+ .append_to_list(records)
309
+ .set_metadata({"run_num": run_num})
310
+ for i, col in enumerate(value_columns)
311
+ ]
312
+
313
+ trendify.Format2D(tags=["scatter_plot"], title_fig="N Points").append_to_list(
314
+ records
315
+ )
316
+ for i, trace in enumerate(traces):
317
+ trendify.Point2D(
318
+ x=workdir.name,
319
+ y=len(trace.x),
320
+ marker=trendify.Marker(
321
+ size=10,
322
+ label=trace.pen.label,
323
+ color=trace.pen.color,
324
+ alpha=alphas[i],
325
+ ),
326
+ tags=["scatter_plot"],
327
+ ).append_to_list(records).set_metadata({"run_num": run_num})
328
+
329
+ trendify.Format2D(
330
+ tags=[("nested_plots", "group_a", "deep_trace")],
331
+ grid=trendify.Grid.from_theme(trendify.GridTheme.MATLAB),
332
+ ).append_to_list(records)
333
+ trendify.Trace2D(
334
+ x=time,
335
+ y=df[value_columns[0]].to_numpy(),
336
+ tags=[("nested_plots", "group_a", "deep_trace")],
337
+ pen=trendify.Pen(label=value_columns[0], color=colors[0]),
338
+ ).append_to_list(records).set_metadata({"run_num": run_num})
339
+ trendify.Format2D(
340
+ tags=[("nested_plots", "group_b", "deep_trace")],
341
+ grid=trendify.Grid.from_theme(trendify.GridTheme.MATLAB),
342
+ ).append_to_list(records)
343
+ trendify.Trace2D(
344
+ x=time,
345
+ y=df[value_columns[-1]].to_numpy(),
346
+ tags=[("nested_plots", "group_b", "deep_trace")],
347
+ pen=trendify.Pen(label=value_columns[-1], color=colors[-1]),
348
+ ).append_to_list(records).set_metadata({"run_num": run_num})
349
+
350
+ trendify.Format2D(
351
+ tags=["histogram"],
352
+ title_ax="Idk lol",
353
+ title_fig="Idk lol2",
354
+ legend=trendify.Legend(
355
+ loc=trendify.LegendLocation.UPPER_LEFT,
356
+ bbox_to_anchor=(1.05, 1),
357
+ ),
358
+ label_x="Series value",
359
+ label_y="Counts",
360
+ grid=trendify.Grid.from_theme(trendify.GridTheme.MATLAB),
361
+ ).append_to_list(records)
362
+
363
+ for col in value_columns:
364
+ series = df[col]
365
+ trendify.TableEntry(
366
+ row=workdir.name,
367
+ col=col,
368
+ value=series.len(),
369
+ tags=[("tables", "lengths")],
370
+ unit=None,
371
+ ).append_to_list(records)
372
+ trendify.TableEntry(
373
+ row=workdir.name,
374
+ col=col,
375
+ value=cast(float, series.mean()),
376
+ tags=[("tables", "means")],
377
+ unit=None,
378
+ ).append_to_list(records)
379
+ trendify.TableEntry(
380
+ row=workdir.name,
381
+ col=col,
382
+ value=cast(float, series.std()),
383
+ tags=[("tables", "std_devs")],
384
+ unit=None,
385
+ ).append_to_list(records)
386
+ trendify.TableEntry(
387
+ row=workdir.name,
388
+ col=col,
389
+ value=cast(float, series.max()),
390
+ tags=[("extrema", "max")],
391
+ unit=None,
392
+ ).append_to_list(records)
393
+ trendify.TableEntry(
394
+ row=workdir.name,
395
+ col=col,
396
+ value=cast(float, series.min()),
397
+ tags=[("extrema", "min")],
398
+ unit=None,
399
+ ).append_to_list(records)
400
+
401
+ mean = cast(float, series.mean())
402
+ trendify.HistogramEntry(
403
+ tags=["histogram"],
404
+ value=mean,
405
+ style=trendify.HistogramStyle(
406
+ alpha_face=0.75,
407
+ alpha_edge=1,
408
+ bins=6,
409
+ label="A histogram entry",
410
+ ),
411
+ ).append_to_list(records)
412
+
413
+ trendify.AxLine(
414
+ tags=["histogram"],
415
+ value=mean,
416
+ orientation=trendify.LineOrientation.VERTICAL,
417
+ pen=trendify.Pen(color="r", label="mean", zorder=2),
418
+ ).append_to_list(records)
419
+
420
+ return records
421
+
422
+
423
+ def make_sample_data() -> None:
424
+ """
425
+ Generates sample data to run the trendify code on
426
+ """
427
+ import argparse
428
+
429
+ parser = argparse.ArgumentParser(
430
+ prog="make_sample_data_for_trendify",
431
+ )
432
+ parser.add_argument(
433
+ "-wd",
434
+ "--working-directory",
435
+ required=True,
436
+ help="Directory to be created and filled with sample data from a batch run",
437
+ )
438
+ parser.add_argument(
439
+ "-n",
440
+ "--number-of-data-sets",
441
+ type=int,
442
+ default=5,
443
+ help="Number of sample data sets to generate",
444
+ )
445
+ args = parser.parse_args()
446
+ make_example_data(
447
+ workdir=Path(args.working_directory),
448
+ n_folders=args.number_of_data_sets,
449
+ )
450
+
451
+
452
+ def _main() -> None:
453
+ """
454
+ Makes sample data and runs the generate pipeline against it.
455
+ """
456
+ here = Path(__file__).parent
457
+ workdir = here.joinpath("workdir")
458
+
459
+ make_example_data(workdir=workdir, n_folders=100)
460
+
461
+ process_dirs = list(workdir.joinpath("models").glob("*/"))
462
+ db_path = workdir.joinpath("trendify.db")
463
+ n_procs = 30
464
+
465
+ trendify.generate_records(
466
+ record_generator=example_record_generator,
467
+ data_dirs=process_dirs,
468
+ db_path=db_path,
469
+ n_procs=n_procs,
470
+ )
471
+
472
+
473
+ if __name__ == "__main__":
474
+ _main()
@@ -0,0 +1,28 @@
1
+ """Plot formatting (`Format2D`) and table entry records."""
2
+
3
+ from trendify.formats import format2d
4
+ from trendify.formats import table
5
+
6
+ from trendify.formats.format2d import (
7
+ AxisScale,
8
+ Format2D,
9
+ PlottableData2D,
10
+ Rastered,
11
+ Vector,
12
+ XYData,
13
+ )
14
+ from trendify.formats.table import (
15
+ TableEntry,
16
+ )
17
+
18
+ __all__ = [
19
+ "AxisScale",
20
+ "Format2D",
21
+ "PlottableData2D",
22
+ "Rastered",
23
+ "TableEntry",
24
+ "Vector",
25
+ "XYData",
26
+ "format2d",
27
+ "table",
28
+ ]
@@ -0,0 +1,128 @@
1
+ """
2
+ `Format2D` (axis/legend/grid/scale/limit settings for a plot) and the `PlottableData2D`/
3
+ `XYData` base classes that know how to draw themselves onto a Plotly figure.
4
+
5
+ `Format2D` is a `Record` in its own right, written once per tag (like any other
6
+ record) rather than embedded in every plotted record; see `RecordStore.write_run`'s
7
+ upsert-by-tag handling for it, and `render.py`'s single per-tag lookup.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from abc import ABC, abstractmethod
14
+ from enum import StrEnum
15
+ from typing import TYPE_CHECKING, Literal
16
+
17
+ from pydantic import BaseModel, ConfigDict, Field
18
+
19
+ from trendify.base.record import Record
20
+ from trendify.styling.grid import Grid
21
+ from trendify.styling.legend import Legend
22
+
23
+ if TYPE_CHECKING:
24
+ from trendify.plotting.figure import PlotlyFigure
25
+ logger = logging.getLogger(__name__)
26
+
27
+ __all__ = ["AxisScale", "Format2D", "PlottableData2D", "Rastered", "Vector", "XYData"]
28
+
29
+
30
+ class AxisScale(StrEnum):
31
+ LINEAR = "linear"
32
+ """Format axis as linear"""
33
+ LOG = "log"
34
+ """Format axis with log base 10"""
35
+
36
+
37
+ class Rastered(BaseModel):
38
+ """Renders to a raster image format (matplotlib's `dpi` setting applies)."""
39
+
40
+ type: Literal["rastered"] = "rastered"
41
+ """Discriminator identifying this renderer variant."""
42
+
43
+ filetype: str = ".jpg"
44
+ """File extension used when saving the rendered image."""
45
+
46
+ dpi: int = 500
47
+ """Resolution (dots per inch) used when saving the rendered image."""
48
+
49
+
50
+ class Vector(BaseModel):
51
+ """Renders to a vector image format (matplotlib's `dpi` setting doesn't apply)."""
52
+
53
+ type: Literal["vector"] = "vector"
54
+ """Discriminator identifying this renderer variant."""
55
+
56
+ filetype: str = ".svg"
57
+ """File extension used when saving the rendered image."""
58
+
59
+
60
+ class Format2D(Record):
61
+ """
62
+ Formatting data for matplotlib figure and axes. Written once per tag.
63
+ """
64
+
65
+ title_fig: str | None = None
66
+ """Sets the [figure title][matplotlib.figure.Figure.suptitle]."""
67
+
68
+ legend: Legend | None = Legend()
69
+ """Sets the [legend style][trendify.styling.legend.Legend]."""
70
+
71
+ title_ax: str | None = None
72
+ """Sets the [axis title][matplotlib.axes.Axes.set_title]."""
73
+
74
+ label_x: str | None = None
75
+ """Sets the [x-axis label][matplotlib.axes.Axes.set_xlabel]."""
76
+
77
+ label_y: str | None = None
78
+ """Sets the [y-axis label][matplotlib.axes.Axes.set_ylabel]."""
79
+
80
+ lim_x: tuple[float | None, float | None] = (None, None)
81
+ """x-axis (lower, upper) bound. Either side `None` means autofit that side to whatever's
82
+ plotted."""
83
+
84
+ lim_y: tuple[float | None, float | None] = (None, None)
85
+ """y-axis (lower, upper) bound, same semantics as `lim_x`."""
86
+
87
+ grid: Grid | None = None
88
+ """Sets the [grid][matplotlib.pyplot.grid]."""
89
+
90
+ scale_x: AxisScale = AxisScale.LINEAR
91
+ """Sets the x axis scale to an option from [AxisScale][trendify.formats.format2d.AxisScale]."""
92
+
93
+ scale_y: AxisScale = AxisScale.LINEAR
94
+ """Sets the y axis scale to an option from [AxisScale][trendify.formats.format2d.AxisScale]."""
95
+
96
+ figure_width: float = 6.4
97
+ """Sets the width of the rendered figure in inches."""
98
+
99
+ figure_height: float = 4.8
100
+ """Sets the height of the rendered figure in inches."""
101
+
102
+ renderer: Rastered | Vector = Field(default_factory=Rastered, discriminator="type")
103
+ """Chooses the saved file format: `Rastered` for a raster image at a configurable `dpi`,
104
+ or `Vector` for a vector image."""
105
+
106
+ model_config = ConfigDict(extra="forbid")
107
+
108
+
109
+ class PlottableData2D(Record, ABC):
110
+ """
111
+ Base class for children of Record to be plotted ax xy data on a 2D plot
112
+ """
113
+
114
+ @abstractmethod
115
+ def add_to_plotly(self, plotly_figure: PlotlyFigure) -> PlotlyFigure:
116
+ """
117
+ Add this record to a plotly figure
118
+
119
+ Args:
120
+ plotly_figure (PlotlyFigure): Plotly figure to add data to
121
+
122
+ """
123
+
124
+
125
+ class XYData(PlottableData2D):
126
+ """
127
+ Base class for children of Record to be plotted ax xy data on a 2D plot
128
+ """
@@ -0,0 +1,55 @@
1
+ """
2
+ `TableEntry`: the one `Record` subclass the store gives real SQL columns instead of an
3
+ opaque JSON payload, since its whole purpose (pivoting into a wide table, then computing
4
+ per-column stats) is naturally SQL-shaped. See `trendify.generator.table_builder`.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ from pydantic import ConfigDict
12
+
13
+ from trendify.base.record import Record
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ __all__ = ["TableEntry"]
18
+
19
+
20
+ class TableEntry(Record):
21
+ """
22
+ Defines an entry to be collected into a table.
23
+
24
+ Collected table entries will be printed in three forms when possible: melted, pivot (when
25
+ possible), and stats (on pivot columns, when possible).
26
+ """
27
+
28
+ row: float | str
29
+ """Row label"""
30
+
31
+ col: float | str
32
+ """Column label"""
33
+
34
+ value: float | str | bool
35
+ """Value"""
36
+
37
+ unit: str | None = None
38
+ """Units for value"""
39
+
40
+ model_config = ConfigDict(extra="forbid")
41
+
42
+ def get_entry_dict(self) -> dict[str, float | str | bool | None]:
43
+ """
44
+ Returns a dictionary of entries to be used in creating a table.
45
+
46
+ Returns:
47
+ (dict[str, str | float]): Dictionary of entries to be used in creating a melted table
48
+
49
+ """
50
+ return {
51
+ "row": self.row,
52
+ "col": self.col,
53
+ "value": self.value,
54
+ "unit": self.unit,
55
+ }