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.
- trendify/__about__.py +14 -0
- trendify/__init__.py +250 -0
- trendify/base/__init__.py +36 -0
- trendify/base/helpers.py +76 -0
- trendify/base/pen.py +161 -0
- trendify/base/record.py +148 -0
- trendify/cli.py +403 -0
- trendify/color.py +260 -0
- trendify/examples.py +474 -0
- trendify/formats/__init__.py +28 -0
- trendify/formats/format2d.py +128 -0
- trendify/formats/table.py +55 -0
- trendify/generator/__init__.py +38 -0
- trendify/generator/generate.py +167 -0
- trendify/generator/histogrammer.py +62 -0
- trendify/generator/render.py +195 -0
- trendify/generator/table_builder.py +194 -0
- trendify/generator/xy_data_plotter.py +195 -0
- trendify/log.py +195 -0
- trendify/pipeline.py +136 -0
- trendify/plotting/__init__.py +48 -0
- trendify/plotting/axline.py +122 -0
- trendify/plotting/figure.py +389 -0
- trendify/plotting/histogram.py +282 -0
- trendify/plotting/point.py +89 -0
- trendify/plotting/scatter.py +100 -0
- trendify/plotting/trace.py +129 -0
- trendify/progress.py +43 -0
- trendify/store/__init__.py +30 -0
- trendify/store/db.py +119 -0
- trendify/store/record_store.py +448 -0
- trendify/store/tags.py +65 -0
- trendify/styling/__init__.py +30 -0
- trendify/styling/grid.py +125 -0
- trendify/styling/legend.py +217 -0
- trendify/styling/marker.py +168 -0
- trendify/typing.py +26 -0
- trendify/viewer/__init__.py +41 -0
- trendify/viewer/app.py +208 -0
- trendify/viewer/hydration.py +181 -0
- trendify/viewer/plot_config.py +102 -0
- trendify/viewer/routes/__init__.py +6 -0
- trendify/viewer/routes/api.py +360 -0
- trendify/viewer/routes/pages.py +39 -0
- trendify/viewer/tag_tree.py +148 -0
- trendify/viewer/templates/base.html +63 -0
- trendify/viewer/templates/index.html +131 -0
- trendify/viewer/templates/partials/__init__.py +1 -0
- trendify/viewer/templates/partials/footer.html +15 -0
- trendify/viewer/templates/partials/nav.html +181 -0
- trendify/viewer/templates/partials/plot_view.html +221 -0
- trendify/viewer/templates/partials/sidebar.html +163 -0
- trendify/viewer/templates/partials/table_view.html +43 -0
- trendify/viewer/templates/static/__init__.py +1 -0
- trendify/viewer/templates/static/assets/__init__.py +1 -0
- trendify/viewer/templates/static/assets/main-logo-white-bg.svg +75 -0
- trendify/viewer/templates/static/assets/main-logo.svg +66 -0
- trendify/viewer/templates/static/css/app.css +75 -0
- trendify/viewer/templates/static/js/main.js +2 -0
- trendify/viewer/templates/static/vendored/__init__.py +1 -0
- trendify/viewer/templates/static/vendored/alpine-3.15.12.min.js +5 -0
- trendify/viewer/templates/static/vendored/alpine-LICENSE.md +21 -0
- trendify/viewer/templates/static/vendored/dataTables-LICENSE.txt +3 -0
- trendify/viewer/templates/static/vendored/dataTables.dataTables.min.css +1 -0
- trendify/viewer/templates/static/vendored/dataTables.min.js +4 -0
- trendify/viewer/templates/static/vendored/jquery-3.7.1.min.js +2 -0
- trendify/viewer/templates/static/vendored/jquery-LICENSE.txt +3 -0
- trendify/viewer/templates/static/vendored/plotly-3.6.0.min.js +3882 -0
- trendify/viewer/templates/static/vendored/plotly-LICENSE.txt +21 -0
- trendify/viewer/templates/static/vendored/tailwind-4.3.2.global.js +947 -0
- trendify/viewer/templates/static/vendored/tailwind-LICENSE +21 -0
- trendify2-2.0.4.dist-info/METADATA +107 -0
- trendify2-2.0.4.dist-info/RECORD +76 -0
- trendify2-2.0.4.dist-info/WHEEL +4 -0
- trendify2-2.0.4.dist-info/entry_points.txt +2 -0
- 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
|
+
}
|