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/__about__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Looks up the installed version of this package."""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = version("trendify2")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
__version__ = "0.0.0-dev"
|
|
10
|
+
|
|
11
|
+
__license__ = "MIT"
|
|
12
|
+
__summary__ = "Tools for generating data products, storing, and interacting with them."
|
|
13
|
+
__copyright__ = f"© {date.today().year} Talbot Knighton and David Gable"
|
|
14
|
+
__url__ = "https://github.com/Hydrowelder/trendify"
|
trendify/__init__.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools for generating, storing, and interacting with tagged records (points, traces, tables,
|
|
3
|
+
histograms) produced by batch-processing many run directories.
|
|
4
|
+
|
|
5
|
+
Write a `RecordGenerator` that returns records for one run directory, run it over many runs
|
|
6
|
+
with [`TrendifyPipeline`][trendify.pipeline.TrendifyPipeline] or the `trendify` CLI, then browse
|
|
7
|
+
the results in the viewer or render them as plots and tables.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from trendify import (
|
|
11
|
+
base,
|
|
12
|
+
cli,
|
|
13
|
+
color,
|
|
14
|
+
examples,
|
|
15
|
+
formats,
|
|
16
|
+
generator,
|
|
17
|
+
log,
|
|
18
|
+
pipeline,
|
|
19
|
+
plotting,
|
|
20
|
+
store,
|
|
21
|
+
styling,
|
|
22
|
+
typing,
|
|
23
|
+
viewer,
|
|
24
|
+
)
|
|
25
|
+
from trendify.base import (
|
|
26
|
+
HashableBase,
|
|
27
|
+
Pen,
|
|
28
|
+
R,
|
|
29
|
+
Record,
|
|
30
|
+
RecordGenerator,
|
|
31
|
+
RecordList,
|
|
32
|
+
RecordType,
|
|
33
|
+
Tag,
|
|
34
|
+
Tags,
|
|
35
|
+
helpers,
|
|
36
|
+
pen,
|
|
37
|
+
record,
|
|
38
|
+
)
|
|
39
|
+
from trendify.cli import (
|
|
40
|
+
app as cli_app,
|
|
41
|
+
)
|
|
42
|
+
from trendify.color import (
|
|
43
|
+
Color,
|
|
44
|
+
)
|
|
45
|
+
from trendify.examples import (
|
|
46
|
+
example_record_generator,
|
|
47
|
+
make_example_data,
|
|
48
|
+
)
|
|
49
|
+
from trendify.formats import (
|
|
50
|
+
AxisScale,
|
|
51
|
+
Format2D,
|
|
52
|
+
PlottableData2D,
|
|
53
|
+
Rastered,
|
|
54
|
+
TableEntry,
|
|
55
|
+
Vector,
|
|
56
|
+
XYData,
|
|
57
|
+
format2d,
|
|
58
|
+
table,
|
|
59
|
+
)
|
|
60
|
+
from trendify.generator import (
|
|
61
|
+
Histogrammer,
|
|
62
|
+
TableBuilder,
|
|
63
|
+
XYDataPlotter,
|
|
64
|
+
generate,
|
|
65
|
+
generate_records,
|
|
66
|
+
get_sorted_dirs,
|
|
67
|
+
histogrammer,
|
|
68
|
+
render,
|
|
69
|
+
render_assets,
|
|
70
|
+
table_builder,
|
|
71
|
+
xy_data_plotter,
|
|
72
|
+
)
|
|
73
|
+
from trendify.log import (
|
|
74
|
+
create_queue_listener,
|
|
75
|
+
get_logger,
|
|
76
|
+
set_log_level,
|
|
77
|
+
setup_logger,
|
|
78
|
+
worker_init,
|
|
79
|
+
)
|
|
80
|
+
from trendify.pipeline import (
|
|
81
|
+
TrendifyPipeline,
|
|
82
|
+
)
|
|
83
|
+
from trendify.plotting import (
|
|
84
|
+
AxLine,
|
|
85
|
+
HistogramEntry,
|
|
86
|
+
HistogramStyle,
|
|
87
|
+
LineOrientation,
|
|
88
|
+
PlotlyFigure,
|
|
89
|
+
Point2D,
|
|
90
|
+
Scatter2D,
|
|
91
|
+
SingleAxisFigure,
|
|
92
|
+
Trace2D,
|
|
93
|
+
axline,
|
|
94
|
+
figure,
|
|
95
|
+
histogram,
|
|
96
|
+
point,
|
|
97
|
+
scatter,
|
|
98
|
+
trace,
|
|
99
|
+
)
|
|
100
|
+
from trendify.store import (
|
|
101
|
+
SCHEMA_VERSION,
|
|
102
|
+
RecordStore,
|
|
103
|
+
connect,
|
|
104
|
+
db,
|
|
105
|
+
decode_tag,
|
|
106
|
+
encode_tag,
|
|
107
|
+
record_store,
|
|
108
|
+
tag_to_path_parts,
|
|
109
|
+
tags,
|
|
110
|
+
)
|
|
111
|
+
from trendify.styling import (
|
|
112
|
+
Grid,
|
|
113
|
+
GridAxis,
|
|
114
|
+
GridTheme,
|
|
115
|
+
Legend,
|
|
116
|
+
LegendLocation,
|
|
117
|
+
Marker,
|
|
118
|
+
grid,
|
|
119
|
+
legend,
|
|
120
|
+
marker,
|
|
121
|
+
)
|
|
122
|
+
from trendify.typing import (
|
|
123
|
+
MatN,
|
|
124
|
+
VecN,
|
|
125
|
+
)
|
|
126
|
+
from trendify.viewer import (
|
|
127
|
+
HoverMode,
|
|
128
|
+
InterpMode,
|
|
129
|
+
LineMode,
|
|
130
|
+
PlotConfig,
|
|
131
|
+
TagNode,
|
|
132
|
+
api,
|
|
133
|
+
build_tag_tree,
|
|
134
|
+
camel_case_dict,
|
|
135
|
+
create_app,
|
|
136
|
+
create_app_from_env,
|
|
137
|
+
pages,
|
|
138
|
+
plot_config,
|
|
139
|
+
routes,
|
|
140
|
+
tag_tree,
|
|
141
|
+
viewer_app,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
__all__ = [
|
|
145
|
+
"SCHEMA_VERSION",
|
|
146
|
+
"AxLine",
|
|
147
|
+
"AxisScale",
|
|
148
|
+
"Color",
|
|
149
|
+
"Format2D",
|
|
150
|
+
"Grid",
|
|
151
|
+
"GridAxis",
|
|
152
|
+
"GridTheme",
|
|
153
|
+
"HashableBase",
|
|
154
|
+
"HistogramEntry",
|
|
155
|
+
"HistogramStyle",
|
|
156
|
+
"Histogrammer",
|
|
157
|
+
"HoverMode",
|
|
158
|
+
"InterpMode",
|
|
159
|
+
"Legend",
|
|
160
|
+
"LegendLocation",
|
|
161
|
+
"LineMode",
|
|
162
|
+
"LineOrientation",
|
|
163
|
+
"Marker",
|
|
164
|
+
"MatN",
|
|
165
|
+
"Pen",
|
|
166
|
+
"PlotConfig",
|
|
167
|
+
"PlotlyFigure",
|
|
168
|
+
"PlottableData2D",
|
|
169
|
+
"Point2D",
|
|
170
|
+
"R",
|
|
171
|
+
"Rastered",
|
|
172
|
+
"Record",
|
|
173
|
+
"RecordGenerator",
|
|
174
|
+
"RecordList",
|
|
175
|
+
"RecordStore",
|
|
176
|
+
"RecordType",
|
|
177
|
+
"Scatter2D",
|
|
178
|
+
"SingleAxisFigure",
|
|
179
|
+
"TableBuilder",
|
|
180
|
+
"TableEntry",
|
|
181
|
+
"Tag",
|
|
182
|
+
"TagNode",
|
|
183
|
+
"Tags",
|
|
184
|
+
"Trace2D",
|
|
185
|
+
"TrendifyPipeline",
|
|
186
|
+
"VecN",
|
|
187
|
+
"Vector",
|
|
188
|
+
"XYData",
|
|
189
|
+
"XYDataPlotter",
|
|
190
|
+
"api",
|
|
191
|
+
"axline",
|
|
192
|
+
"base",
|
|
193
|
+
"build_tag_tree",
|
|
194
|
+
"camel_case_dict",
|
|
195
|
+
"cli",
|
|
196
|
+
"cli_app",
|
|
197
|
+
"color",
|
|
198
|
+
"connect",
|
|
199
|
+
"create_app",
|
|
200
|
+
"create_app_from_env",
|
|
201
|
+
"create_queue_listener",
|
|
202
|
+
"db",
|
|
203
|
+
"decode_tag",
|
|
204
|
+
"encode_tag",
|
|
205
|
+
"example_record_generator",
|
|
206
|
+
"examples",
|
|
207
|
+
"figure",
|
|
208
|
+
"format2d",
|
|
209
|
+
"formats",
|
|
210
|
+
"generate",
|
|
211
|
+
"generate_records",
|
|
212
|
+
"generator",
|
|
213
|
+
"get_logger",
|
|
214
|
+
"get_sorted_dirs",
|
|
215
|
+
"grid",
|
|
216
|
+
"helpers",
|
|
217
|
+
"histogram",
|
|
218
|
+
"histogrammer",
|
|
219
|
+
"legend",
|
|
220
|
+
"log",
|
|
221
|
+
"make_example_data",
|
|
222
|
+
"marker",
|
|
223
|
+
"pages",
|
|
224
|
+
"pen",
|
|
225
|
+
"pipeline",
|
|
226
|
+
"plot_config",
|
|
227
|
+
"plotting",
|
|
228
|
+
"point",
|
|
229
|
+
"record",
|
|
230
|
+
"record_store",
|
|
231
|
+
"render",
|
|
232
|
+
"render_assets",
|
|
233
|
+
"routes",
|
|
234
|
+
"scatter",
|
|
235
|
+
"set_log_level",
|
|
236
|
+
"setup_logger",
|
|
237
|
+
"store",
|
|
238
|
+
"styling",
|
|
239
|
+
"table",
|
|
240
|
+
"table_builder",
|
|
241
|
+
"tag_to_path_parts",
|
|
242
|
+
"tag_tree",
|
|
243
|
+
"tags",
|
|
244
|
+
"trace",
|
|
245
|
+
"typing",
|
|
246
|
+
"viewer",
|
|
247
|
+
"viewer_app",
|
|
248
|
+
"worker_init",
|
|
249
|
+
"xy_data_plotter",
|
|
250
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Core building blocks: the `Record` base class, `Pen` styling, and shared type helpers."""
|
|
2
|
+
|
|
3
|
+
from trendify.base import helpers
|
|
4
|
+
from trendify.base import pen
|
|
5
|
+
from trendify.base import record
|
|
6
|
+
|
|
7
|
+
from trendify.base.helpers import (
|
|
8
|
+
HashableBase,
|
|
9
|
+
R,
|
|
10
|
+
RecordType,
|
|
11
|
+
Tag,
|
|
12
|
+
Tags,
|
|
13
|
+
)
|
|
14
|
+
from trendify.base.pen import (
|
|
15
|
+
Pen,
|
|
16
|
+
)
|
|
17
|
+
from trendify.base.record import (
|
|
18
|
+
Record,
|
|
19
|
+
RecordGenerator,
|
|
20
|
+
RecordList,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"HashableBase",
|
|
25
|
+
"Pen",
|
|
26
|
+
"R",
|
|
27
|
+
"Record",
|
|
28
|
+
"RecordGenerator",
|
|
29
|
+
"RecordList",
|
|
30
|
+
"RecordType",
|
|
31
|
+
"Tag",
|
|
32
|
+
"Tags",
|
|
33
|
+
"helpers",
|
|
34
|
+
"pen",
|
|
35
|
+
"record",
|
|
36
|
+
]
|
trendify/base/helpers.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Small shared building blocks used across the rest of the package: the `Tag`/`Tags` type
|
|
3
|
+
aliases every `Record` and the store's tag-encoding logic build on, `HashableBase` for
|
|
4
|
+
pydantic models that need to go in a `set`, and the `RecordType` enum.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from enum import StrEnum
|
|
11
|
+
from typing import TYPE_CHECKING, TypeVar
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from trendify.base.record import Record
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"HashableBase",
|
|
22
|
+
"R",
|
|
23
|
+
"RecordType",
|
|
24
|
+
"Tag",
|
|
25
|
+
"Tags",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
R = TypeVar("R", bound="Record")
|
|
29
|
+
"""Bound to `Record`: used by `RecordStore.get_records`/`get_records_of_type` so a
|
|
30
|
+
call like `get_records_of_type(Point2D)` returns `list[Point2D]` rather than `list[Record]`.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
Tag = str | int | tuple[str | int, ...]
|
|
34
|
+
"""
|
|
35
|
+
Determines what types can be used to define a tag.
|
|
36
|
+
|
|
37
|
+
Tags must be reliably JSON-encodable (str/int, or tuples thereof) because the store
|
|
38
|
+
canonicalizes a tag into a `tag_key` via `json.dumps` for indexed lookup (see
|
|
39
|
+
`trendify.store.tags`).
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
Tags = list[Tag]
|
|
43
|
+
"""
|
|
44
|
+
List of tags
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class HashableBase(BaseModel):
|
|
49
|
+
"""
|
|
50
|
+
Defines a base for hashable pydantic data classes so that they can be reduced to a minimal set through type-casting.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def __hash__(self):
|
|
54
|
+
"""
|
|
55
|
+
Defines hash function
|
|
56
|
+
"""
|
|
57
|
+
return hash((type(self), *tuple(self.__dict__.values())))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RecordType(StrEnum):
|
|
61
|
+
"""Defines all record types. Used to type-cast URL info in server to validate."""
|
|
62
|
+
|
|
63
|
+
XY_DATA = "xy_data"
|
|
64
|
+
"""XY_DATA name"""
|
|
65
|
+
|
|
66
|
+
TRACE_2D = "trace_2d"
|
|
67
|
+
"""TRACE_2D name"""
|
|
68
|
+
|
|
69
|
+
POINT_2D = "point_2d"
|
|
70
|
+
"""POINT_2D name"""
|
|
71
|
+
|
|
72
|
+
TABLE_ENTRY = "table_entry"
|
|
73
|
+
"""TABLE_ENTRY name"""
|
|
74
|
+
|
|
75
|
+
HISTOGRAM_ENTRY = "histogram_entry"
|
|
76
|
+
"""HISTOGRAM_ENTRY name"""
|
trendify/base/pen.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`Pen`: shared line/marker styling (color, size, alpha, linestyle, zorder, label) used by
|
|
3
|
+
`Trace2D`, `AxLine`, and anything else that draws a line to matplotlib or Plotly.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from matplotlib.colors import to_rgba
|
|
11
|
+
from pydantic import ConfigDict
|
|
12
|
+
|
|
13
|
+
from trendify.base.helpers import HashableBase
|
|
14
|
+
|
|
15
|
+
__all__ = ["Pen"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Pen(HashableBase):
|
|
19
|
+
"""Defines the pen drawing style."""
|
|
20
|
+
|
|
21
|
+
color: tuple[float, float, float] | tuple[float, float, float, float] | str = "k"
|
|
22
|
+
"""Color of line"""
|
|
23
|
+
|
|
24
|
+
size: float = 1
|
|
25
|
+
"""Line width"""
|
|
26
|
+
|
|
27
|
+
alpha: float = 1
|
|
28
|
+
"""Opacity from 0 to 1 (inclusive)"""
|
|
29
|
+
|
|
30
|
+
zorder: float = 0
|
|
31
|
+
"""Prioritization of trace line relative to other plotted data"""
|
|
32
|
+
|
|
33
|
+
linestyle: str | tuple[int, tuple[int, ...]] | None = "-"
|
|
34
|
+
"""Linestyle to plot. Supports `str` or `tuple` definition ([matplotlib documentation](https://matplotlib.org/stable/gallery/lines_bars_and_markers/linestyles.html)).
|
|
35
|
+
Use `None` to draw no line at all, for a markers-only trace."""
|
|
36
|
+
|
|
37
|
+
label: str | None = None
|
|
38
|
+
"""Legend label"""
|
|
39
|
+
|
|
40
|
+
model_config = ConfigDict(extra="forbid")
|
|
41
|
+
|
|
42
|
+
def as_scatter_plot_kwargs(self) -> dict[str, Any]:
|
|
43
|
+
"""
|
|
44
|
+
Returns kwargs dictionary for passing to [matplotlib plot][matplotlib.axes.Axes.plot] method
|
|
45
|
+
"""
|
|
46
|
+
return {
|
|
47
|
+
"color": self.color,
|
|
48
|
+
"linewidth": self.size,
|
|
49
|
+
# matplotlib's `Line2D` treats `linestyle=None` as "use the default style", not
|
|
50
|
+
# "no line" (only the string "none" means that), so `None` is translated here to
|
|
51
|
+
# keep `Pen.linestyle = None` meaning "no line" from `has_line`'s point of view.
|
|
52
|
+
"linestyle": self.linestyle if self.linestyle is not None else "none",
|
|
53
|
+
"alpha": self.alpha,
|
|
54
|
+
"zorder": self.zorder,
|
|
55
|
+
"label": self.label,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def has_line(self) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Whether this pen draws a visible line. `False` when `linestyle` is `None`, which
|
|
62
|
+
callers like `Trace2D.add_to_plotly` use to render markers only, with no connecting
|
|
63
|
+
line.
|
|
64
|
+
"""
|
|
65
|
+
return self.linestyle is not None
|
|
66
|
+
|
|
67
|
+
def _convert_linestyle_to_plotly(self) -> str:
|
|
68
|
+
"""Convert matplotlib linestyle to plotly dash style"""
|
|
69
|
+
# `None` means "no line" (see `has_line`); callers exclude "lines" from the Plotly
|
|
70
|
+
# trace's `mode` in that case, so this value is never actually rendered, but return
|
|
71
|
+
# something valid regardless.
|
|
72
|
+
if self.linestyle is None:
|
|
73
|
+
return "solid"
|
|
74
|
+
|
|
75
|
+
# Handle string styles
|
|
76
|
+
style_map = {
|
|
77
|
+
"-": "solid",
|
|
78
|
+
"--": "dash",
|
|
79
|
+
":": "dot",
|
|
80
|
+
"-.": "dashdot",
|
|
81
|
+
}
|
|
82
|
+
if isinstance(self.linestyle, str):
|
|
83
|
+
return style_map.get(self.linestyle, "solid")
|
|
84
|
+
|
|
85
|
+
# Handle tuple styles - convert to 'dash' as approximation
|
|
86
|
+
return "dash"
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def rgba(self) -> str:
|
|
90
|
+
"""
|
|
91
|
+
Convert the pen's color to rgba string format.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
str: Color in 'rgba(r,g,b,a)' format where r,g,b are 0-255 and a is 0-1
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
# Handle different color input formats
|
|
98
|
+
if isinstance(self.color, tuple):
|
|
99
|
+
if len(self.color) == 3: # RGB tuple
|
|
100
|
+
r, g, b = self.color
|
|
101
|
+
a = self.alpha
|
|
102
|
+
else: # RGBA tuple
|
|
103
|
+
r, g, b, a = self.color
|
|
104
|
+
# Convert 0-1 range to 0-255 for RGB
|
|
105
|
+
r, g, b = int(r * 255), int(g * 255), int(b * 255)
|
|
106
|
+
else: # String color (name or hex)
|
|
107
|
+
# Use matplotlib's color converter
|
|
108
|
+
rgba_vals = to_rgba(self.color, self.alpha)
|
|
109
|
+
# Convert 0-1 range to 0-255 for RGB
|
|
110
|
+
r, g, b = [int(x * 255) for x in rgba_vals[:3]]
|
|
111
|
+
a = rgba_vals[3]
|
|
112
|
+
|
|
113
|
+
return f"rgba({r}, {g}, {b}, {a})"
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def rgb(self) -> str:
|
|
117
|
+
return ", ".join(self.rgba.split(",")[0:-1]) + ")"
|
|
118
|
+
|
|
119
|
+
def get_contrast_color(self, background_luminance: float = 1.0) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Returns 'white' or 'black' to provide the best contrast against the pen's color,
|
|
122
|
+
taking into account the alpha (transparency) value of the line.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
background_luminance (float): The luminance of the background (default is 1.0 for white).
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
str: 'white' or 'black'
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
# Convert the pen's color to RGB (0-255 range) and get alpha
|
|
132
|
+
if isinstance(self.color, tuple):
|
|
133
|
+
if len(self.color) == 3: # RGB tuple
|
|
134
|
+
r, g, b = self.color
|
|
135
|
+
a = self.alpha
|
|
136
|
+
else: # RGBA tuple
|
|
137
|
+
r, g, b, a = self.color
|
|
138
|
+
r, g, b = int(r * 255), int(g * 255), int(b * 255)
|
|
139
|
+
else: # String color (name or hex)
|
|
140
|
+
rgba_vals = to_rgba(self.color, self.alpha)
|
|
141
|
+
r, g, b = [int(x * 255) for x in rgba_vals[:3]]
|
|
142
|
+
a = rgba_vals[3]
|
|
143
|
+
|
|
144
|
+
# Calculate relative luminance of the pen's color
|
|
145
|
+
def luminance(channel: float) -> float:
|
|
146
|
+
channel /= 255.0
|
|
147
|
+
return (
|
|
148
|
+
channel / 12.92
|
|
149
|
+
if channel <= 0.03928
|
|
150
|
+
else ((channel + 0.055) / 1.055) ** 2.4
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
color_luminance = (
|
|
154
|
+
0.2126 * luminance(r) + 0.7152 * luminance(g) + 0.0722 * luminance(b)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Blend the color luminance with the background luminance based on alpha
|
|
158
|
+
blended_luminance = (1 - a) * background_luminance + a * color_luminance
|
|
159
|
+
|
|
160
|
+
# Return white for dark blended colors, black for light blended colors
|
|
161
|
+
return "white" if blended_luminance < 0.5 else "black"
|
trendify/base/record.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base `Record` pydantic model, the `RecordGenerator`/`RecordList` type aliases user code
|
|
3
|
+
implements against, and the subclass registry `RecordStore` uses to deserialize a stored
|
|
4
|
+
`record_type` string back into its concrete pydantic class.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Self
|
|
13
|
+
|
|
14
|
+
from pydantic import (
|
|
15
|
+
BaseModel,
|
|
16
|
+
ConfigDict,
|
|
17
|
+
InstanceOf,
|
|
18
|
+
SerializeAsAny,
|
|
19
|
+
computed_field,
|
|
20
|
+
model_validator,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from trendify.base.helpers import Tags
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
__all__ = ["Record", "RecordGenerator", "RecordList"]
|
|
28
|
+
|
|
29
|
+
_record_subclass_registry: dict[str, type[Record]] = {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Record(BaseModel):
|
|
33
|
+
"""
|
|
34
|
+
Base class for records to be generated and handled.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
tags: Tags
|
|
38
|
+
"""Tags to be used for sorting data."""
|
|
39
|
+
|
|
40
|
+
metadata: dict[str, str] = {}
|
|
41
|
+
"""A dictionary of metadata to be used as a tool tip for mouseover in Grafana."""
|
|
42
|
+
|
|
43
|
+
@model_validator(mode="before")
|
|
44
|
+
@classmethod
|
|
45
|
+
def _remove_computed_fields(cls, data: dict[str, Any]) -> dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Removes computed fields before passing data to constructor.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
data (dict[str, Any]): Raw data to be validated before passing to pydantic class constructor.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
(dict[str, Any]): Sanitized data to be passed to class constructor.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
for f in cls.model_computed_fields:
|
|
57
|
+
data.pop(f, None)
|
|
58
|
+
return data
|
|
59
|
+
|
|
60
|
+
@computed_field
|
|
61
|
+
@property
|
|
62
|
+
def record_type(self) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Returns:
|
|
65
|
+
(str): Record type should be the same as the class name.
|
|
66
|
+
The record type is used to search for records from the store.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
return type(self).__name__
|
|
70
|
+
|
|
71
|
+
def __init_subclass__(cls, **kwargs: Any) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Registers child subclasses to be able to parse them from JSON payloads using the
|
|
74
|
+
[deserialize][trendify.base.record.Record.deserialize] method
|
|
75
|
+
"""
|
|
76
|
+
super().__init_subclass__(**kwargs)
|
|
77
|
+
_record_subclass_registry[cls.__name__] = cls
|
|
78
|
+
logger.debug(f"Registered Record subclass {cls.__name__!r}")
|
|
79
|
+
|
|
80
|
+
model_config = ConfigDict(extra="allow")
|
|
81
|
+
|
|
82
|
+
def append_to_list(self, to_add_to: list) -> Self:
|
|
83
|
+
"""
|
|
84
|
+
Appends self to list.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
to_add_to (List): list to which `self` will be appended
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
(Self): returns instance of `self`
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
to_add_to.append(self)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def registry(cls) -> dict[str, type[Record]]:
|
|
98
|
+
"""
|
|
99
|
+
Returns:
|
|
100
|
+
(dict[str, type[Record]]): a copy of the record_type -> class registry,
|
|
101
|
+
used by the store to resolve which leaf `record_type` names correspond to a
|
|
102
|
+
given (possibly non-leaf) type when filtering by `object_type`.
|
|
103
|
+
|
|
104
|
+
"""
|
|
105
|
+
return dict(_record_subclass_registry)
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def deserialize(cls, record_type: str, payload: str) -> Record:
|
|
109
|
+
"""
|
|
110
|
+
Loads a stored JSON payload into the pydantic dataclass registered under `record_type`.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
record_type (str): the `record_type` discriminator string (same as the class name)
|
|
114
|
+
payload (str): the raw JSON text (e.g. from the store's `records.payload` column)
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
(Record): the reconstructed, correctly-typed record instance
|
|
118
|
+
|
|
119
|
+
"""
|
|
120
|
+
try:
|
|
121
|
+
duck_type = _record_subclass_registry[record_type]
|
|
122
|
+
except KeyError:
|
|
123
|
+
logger.error(
|
|
124
|
+
f"No registered Record subclass named {record_type!r}. Known types: "
|
|
125
|
+
f"{sorted(_record_subclass_registry)}. This usually means the module "
|
|
126
|
+
f"defining that subclass hasn't been imported in this process."
|
|
127
|
+
)
|
|
128
|
+
raise
|
|
129
|
+
return duck_type.model_validate_json(payload)
|
|
130
|
+
|
|
131
|
+
def set_metadata(self, new: dict[str, str]) -> Self:
|
|
132
|
+
self.metadata = new
|
|
133
|
+
return self
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
RecordList = list[SerializeAsAny[InstanceOf[Record]]]
|
|
137
|
+
"""List of serializable [Record][trendify.base.record.Record] or child classes thereof"""
|
|
138
|
+
|
|
139
|
+
RecordGenerator = Callable[[Path], RecordList]
|
|
140
|
+
"""
|
|
141
|
+
Callable method type. Users must provide a `RecordGenerator` to map over raw data.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
path (Path): Workdir holding raw data (Should be one per run from a batch)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
(RecordList): List of records to be sorted and used to produce assets
|
|
148
|
+
"""
|