flimanalyzer 0.6.0__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.
- flim/__init__.py +2 -0
- flim/_version.py +5 -0
- flim/analysis/__init__.py +1 -0
- flim/analysis/ml/__init__.py +0 -0
- flim/analysis/ml/autoencoder.py +188 -0
- flim/analyzerapp.py +131 -0
- flim/batch.py +235 -0
- flim/configs/__init__.py +77 -0
- flim/configs/analysis.py +554 -0
- flim/configs/data.py +278 -0
- flim/configs/plot.py +543 -0
- flim/configs/workflow.py +204 -0
- flim/core/__init__.py +0 -0
- flim/core/calculator.py +0 -0
- flim/core/config.py +305 -0
- flim/core/execution.py +110 -0
- flim/core/fields.py +226 -0
- flim/core/filter.py +179 -0
- flim/core/graph.py +111 -0
- flim/core/headers.py +72 -0
- flim/core/importer.py +269 -0
- flim/core/parser.py +305 -0
- flim/core/paths.py +66 -0
- flim/core/prefect_server.py +152 -0
- flim/core/preprocessor.py +287 -0
- flim/core/registry.py +108 -0
- flim/core/runner.py +703 -0
- flim/core/schema.py +114 -0
- flim/core/settings.py +419 -0
- flim/core/tool.py +284 -0
- flim/core/tools.py +76 -0
- flim/core/types.py +113 -0
- flim/core/validation.py +272 -0
- flim/core/workflow.py +148 -0
- flim/gui/__init__.py +12 -0
- flim/gui/app.py +953 -0
- flim/gui/configdialog.py +495 -0
- flim/gui/datapanel.py +841 -0
- flim/gui/delimpanel.py +56 -0
- flim/gui/dialogs/__init__.py +473 -0
- flim/gui/dialogs/autoencoder.py +89 -0
- flim/gui/dialogs/categoryorder.py +130 -0
- flim/gui/dialogs/filter.py +86 -0
- flim/gui/dialogs/unpivot.py +72 -0
- flim/gui/dicttablepanel.py +178 -0
- flim/gui/editors.py +1578 -0
- flim/gui/events.py +162 -0
- flim/gui/icons.py +16 -0
- flim/gui/importdlg.py +391 -0
- flim/gui/listcontrol.py +472 -0
- flim/gui/macos.py +77 -0
- flim/gui/plotframe.py +73 -0
- flim/gui/progressdlg.py +290 -0
- flim/resources/aeaugment.png +0 -0
- flim/resources/aefeatureworkflow.png +0 -0
- flim/resources/aerun.png +0 -0
- flim/resources/aetrain.png +0 -0
- flim/resources/aetuneworkflow.png +0 -0
- flim/resources/barplot.png +0 -0
- flim/resources/boxplot.png +0 -0
- flim/resources/categorize.png +0 -0
- flim/resources/concatenate.png +0 -0
- flim/resources/fileopen.png +0 -0
- flim/resources/filter.png +0 -0
- flim/resources/flimanalyzer.ico +0 -0
- flim/resources/flimanalyzer.png +0 -0
- flim/resources/flirrplot.png +0 -0
- flim/resources/flirrworkflow.png +0 -0
- flim/resources/gmm.png +0 -0
- flim/resources/heatmap.png +0 -0
- flim/resources/histogram.png +0 -0
- flim/resources/import.png +0 -0
- flim/resources/kde.png +0 -0
- flim/resources/kmeans.png +0 -0
- flim/resources/ks.png +0 -0
- flim/resources/lineplot.png +0 -0
- flim/resources/merge.png +0 -0
- flim/resources/order.png +0 -0
- flim/resources/pairplot.png +0 -0
- flim/resources/pca.png +0 -0
- flim/resources/pivot.png +0 -0
- flim/resources/randomforest.png +0 -0
- flim/resources/relchange.png +0 -0
- flim/resources/scatter.png +0 -0
- flim/resources/seriesanalysis.png +0 -0
- flim/resources/sort.png +0 -0
- flim/resources/split.png +0 -0
- flim/resources/summary.png +0 -0
- flim/resources/swarmplot.png +0 -0
- flim/resources/unpivot.png +0 -0
- flim/resources/violinplot.png +0 -0
- flim/resources/workflow.png +0 -0
- flim/results.py +70 -0
- flim/tools/__init__.py +7 -0
- flim/tools/autoencoder.py +662 -0
- flim/tools/common.py +62 -0
- flim/tools/data.py +301 -0
- flim/tools/ml.py +311 -0
- flim/tools/plot.py +1439 -0
- flim/tools/statistics.py +348 -0
- flim/utils.py +43 -0
- flim/workflow/__init__.py +3 -0
- flim/workflow/aefeature.py +145 -0
- flim/workflow/aetune.py +163 -0
- flim/workflow/basicflow.py +99 -0
- flimanalyzer-0.6.0.dist-info/METADATA +262 -0
- flimanalyzer-0.6.0.dist-info/RECORD +110 -0
- flimanalyzer-0.6.0.dist-info/WHEEL +4 -0
- flimanalyzer-0.6.0.dist-info/entry_points.txt +5 -0
- flimanalyzer-0.6.0.dist-info/licenses/LICENSE +19 -0
flim/configs/plot.py
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
"""Configs of the Plot tools."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Annotated, ClassVar, Literal, Optional, Union
|
|
5
|
+
|
|
6
|
+
from pydantic import Field, ValidationError, field_validator, model_validator
|
|
7
|
+
|
|
8
|
+
from flim.core.config import BaseConfig, FeatureColumn, TaskConfig
|
|
9
|
+
|
|
10
|
+
LegendPosition = Literal["right", "top", "bottom", "left", "inside", "none"]
|
|
11
|
+
OUTSIDE_POSITIONS = ("right", "top", "bottom", "left", "none") # (and none)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PlotConfig(TaskConfig):
|
|
15
|
+
"""Base of the configs of all plot tools."""
|
|
16
|
+
|
|
17
|
+
legacy_keys = {"legend_position": None}
|
|
18
|
+
# the grouping column whose values the legend shows (its index; e.g. 1: the
|
|
19
|
+
# second, the first being the x axis); None: the tool has no such legend
|
|
20
|
+
legend_grouping: ClassVar[Optional[int]] = 0
|
|
21
|
+
|
|
22
|
+
legend_position: LegendPosition = Field(
|
|
23
|
+
default="right",
|
|
24
|
+
title="Legend",
|
|
25
|
+
description="Outside the plots (right, top, bottom, left), inside, or none.",
|
|
26
|
+
json_schema_extra={"order": 422}, # figure layout: after the features
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def has_legend(cls, values) -> bool:
|
|
31
|
+
if cls.legend_grouping is None:
|
|
32
|
+
return False
|
|
33
|
+
return len(values.get("grouping") or []) > cls.legend_grouping
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def allowed_values(cls, values):
|
|
37
|
+
allowed = super().allowed_values(values)
|
|
38
|
+
if not cls.has_legend(values): # nothing to place
|
|
39
|
+
allowed["legend_position"] = (values.get("legend_position", "right"),)
|
|
40
|
+
return allowed
|
|
41
|
+
|
|
42
|
+
@model_validator(mode="after")
|
|
43
|
+
def _allowed(self):
|
|
44
|
+
self._apply_allowed_values()
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --- categorical plots: one plot per feature, grouped by the category columns ---
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PointSampling(BaseConfig):
|
|
52
|
+
"""Which points are drawn: all, or a random sample that is the same on every run."""
|
|
53
|
+
|
|
54
|
+
mode: Literal["all", "sample"] = Field(
|
|
55
|
+
default="all",
|
|
56
|
+
title="Show",
|
|
57
|
+
description="All points, or a random sample (the same on every run).",
|
|
58
|
+
)
|
|
59
|
+
samples: int = Field(default=20000, ge=1, title="Samples")
|
|
60
|
+
random_seed: int = Field(default=0, title="Random seed")
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def allowed_values(cls, values):
|
|
64
|
+
# the sample's size and seed: only for a sample
|
|
65
|
+
if values.get("mode", "all") == "all":
|
|
66
|
+
return {
|
|
67
|
+
"samples": (values.get("samples"),),
|
|
68
|
+
"random_seed": (values.get("random_seed"),),
|
|
69
|
+
}
|
|
70
|
+
return {}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class TiledPlotConfig(PlotConfig):
|
|
74
|
+
"""Base of the configs of plot tools that draw one figure per feature (or feature
|
|
75
|
+
pair), with the option to draw them as tiles of a single figure instead.
|
|
76
|
+
|
|
77
|
+
Plot tools that always draw one figure (e.g. Heatmap) base their configs on
|
|
78
|
+
PlotConfig. Where a tool cannot tile in some cases (e.g. with more grouping
|
|
79
|
+
columns than ``max_tiled_grouping``, as those become facets), ``allowed_values``
|
|
80
|
+
turns the option off and dialogs hide it.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
legacy_keys = {
|
|
84
|
+
**PlotConfig.legacy_keys,
|
|
85
|
+
"single_figure": None,
|
|
86
|
+
"plots_per_row": None,
|
|
87
|
+
}
|
|
88
|
+
# with more grouping columns, the tool's plots are facets already: no single figure
|
|
89
|
+
max_tiled_grouping: ClassVar[Optional[int]] = None
|
|
90
|
+
|
|
91
|
+
# the figure's layout: after grouping and features (410), before sections (900)
|
|
92
|
+
single_figure: bool = Field(
|
|
93
|
+
default=False,
|
|
94
|
+
title="Single figure",
|
|
95
|
+
description="All plots as tiles of one figure; otherwise one figure each.",
|
|
96
|
+
json_schema_extra={"order": 420},
|
|
97
|
+
)
|
|
98
|
+
plots_per_row: int = Field(
|
|
99
|
+
default=5,
|
|
100
|
+
ge=0,
|
|
101
|
+
title="Plots per row",
|
|
102
|
+
description="Plots per row of the single figure; 0: automatic.",
|
|
103
|
+
json_schema_extra={"order": 421},
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
def allowed_values(cls, values):
|
|
108
|
+
allowed = super().allowed_values(values)
|
|
109
|
+
per_row = (values.get("plots_per_row", 5),)
|
|
110
|
+
limit = cls.max_tiled_grouping
|
|
111
|
+
if limit is not None and len(values.get("grouping") or []) > limit:
|
|
112
|
+
return {**allowed, "single_figure": (False,), "plots_per_row": per_row}
|
|
113
|
+
if not values.get("single_figure"):
|
|
114
|
+
return {**allowed, "plots_per_row": per_row} # only for a single figure
|
|
115
|
+
if "legend_position" not in allowed: # one legend for all tiles: outside
|
|
116
|
+
allowed["legend_position"] = OUTSIDE_POSITIONS
|
|
117
|
+
return allowed
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class CategoricalPlotConfig(TiledPlotConfig):
|
|
121
|
+
# x and hue; further grouping columns become facet columns and rows
|
|
122
|
+
max_tiled_grouping = 2
|
|
123
|
+
legend_grouping = 1 # the hue
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class BarPlotConfig(CategoricalPlotConfig):
|
|
127
|
+
legacy_dropped = frozenset({"display", "ordering"}) # unused
|
|
128
|
+
|
|
129
|
+
max_tiled_grouping = None # bars are grouped in one plot, not in facets
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def allowed_values(cls, values):
|
|
133
|
+
allowed = super().allowed_values(values)
|
|
134
|
+
# stacked bars: all features are in one plot already
|
|
135
|
+
if values.get("bar_type", "single") != "single":
|
|
136
|
+
allowed.update(
|
|
137
|
+
single_figure=(False,), plots_per_row=(values.get("plots_per_row", 5),)
|
|
138
|
+
)
|
|
139
|
+
return allowed
|
|
140
|
+
|
|
141
|
+
orientation: Literal["vertical", "horizontal"] = Field(
|
|
142
|
+
default="vertical", title="Orientation"
|
|
143
|
+
)
|
|
144
|
+
bar_type: Literal["single", "stacked", "100% stacked"] = Field(
|
|
145
|
+
default="single",
|
|
146
|
+
title="Bars",
|
|
147
|
+
description="single: one plot per feature; stacked: the features in one plot.",
|
|
148
|
+
)
|
|
149
|
+
dropna: bool = Field(
|
|
150
|
+
default=True,
|
|
151
|
+
title="Drop missing",
|
|
152
|
+
description="Leave out rows without a value.",
|
|
153
|
+
)
|
|
154
|
+
error_bar: Literal["+/-", "+", "None"] = Field(default="+/-", title="Error bars")
|
|
155
|
+
error_type: Literal["std", "s.e.m."] = Field(default="std", title="Error")
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def _from_legacy(cls, params):
|
|
159
|
+
if params.get("error_type") == "s.e.m":
|
|
160
|
+
params["error_type"] = "s.e.m."
|
|
161
|
+
# the plugin's legend on/off: the legend position "none" for off
|
|
162
|
+
if "legend" in params and not params.pop("legend"):
|
|
163
|
+
return {"legend_position": "none"}
|
|
164
|
+
params.pop("legend", None)
|
|
165
|
+
return {}
|
|
166
|
+
|
|
167
|
+
def _to_legacy(self):
|
|
168
|
+
return {"legend": self.legend_position != "none"}
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def has_legend(cls, values):
|
|
172
|
+
# stacked bars: a legend of the features
|
|
173
|
+
if values.get("bar_type", "single") != "single":
|
|
174
|
+
return True
|
|
175
|
+
return super().has_legend(values)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class AxesRange(BaseConfig):
|
|
179
|
+
"""The value axes: fitted to the data, or including 0."""
|
|
180
|
+
|
|
181
|
+
fit_axes: bool = Field(
|
|
182
|
+
default=True,
|
|
183
|
+
title="Fit axes to the data",
|
|
184
|
+
description="Axes from the smallest to the largest value, with a 5% margin;"
|
|
185
|
+
" otherwise they include 0.",
|
|
186
|
+
json_schema_extra={"order": 419}, # with the figure's layout
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class BoxPlotConfig(AxesRange, CategoricalPlotConfig):
|
|
191
|
+
legacy_keys = {**CategoricalPlotConfig.legacy_keys, "fit_axes": None}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class ViolinPlotConfig(AxesRange, CategoricalPlotConfig):
|
|
195
|
+
legacy_keys = {**CategoricalPlotConfig.legacy_keys, "fit_axes": None}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class SwarmPlotConfig(AxesRange, CategoricalPlotConfig):
|
|
199
|
+
legacy_keys = {
|
|
200
|
+
**CategoricalPlotConfig.legacy_keys,
|
|
201
|
+
"points": None,
|
|
202
|
+
"fit_axes": None,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# a swarm places each point: many points take long
|
|
206
|
+
points: PointSampling = Field(
|
|
207
|
+
default_factory=lambda: PointSampling(samples=2000), title="Points"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class LinePlotConfig(CategoricalPlotConfig):
|
|
212
|
+
legacy_keys = {**CategoricalPlotConfig.legacy_keys, "errorbar": "ci"}
|
|
213
|
+
max_tiled_grouping = 3 # x, hue and style
|
|
214
|
+
legacy_dropped = frozenset({"display", "ordering", "x", "hue", "style", "col"})
|
|
215
|
+
|
|
216
|
+
# confidence interval in percent, standard deviation, or None for no error bars
|
|
217
|
+
errorbar: Union[Annotated[int, Field(gt=0, lt=100)], Literal["sd"], None] = Field(
|
|
218
|
+
default=95,
|
|
219
|
+
title="Error",
|
|
220
|
+
description="Confidence interval in percent, sd, or empty for none.",
|
|
221
|
+
)
|
|
222
|
+
err_style: Literal["bars", "band"] = Field(default="bars", title="Error style")
|
|
223
|
+
markers: bool = Field(default=True, title="Markers")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# --- distributions ---
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class DistributionPlotConfig(TiledPlotConfig):
|
|
230
|
+
"""Plots of each feature's distribution."""
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class KDEConfig(DistributionPlotConfig):
|
|
234
|
+
legacy_keys = {
|
|
235
|
+
**DistributionPlotConfig.legacy_keys,
|
|
236
|
+
"single_figure": "single_plot",
|
|
237
|
+
"plots_per_row": "col_wrap",
|
|
238
|
+
}
|
|
239
|
+
legacy_dropped = frozenset({"return_plot"}) # always true
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
DEFAULT_BINS = 30
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class HistogramRange(BaseConfig):
|
|
246
|
+
"""Range and bins of one feature's histogram; empty min or max: the data's."""
|
|
247
|
+
|
|
248
|
+
min: Optional[float] = Field(default=None, title="Min")
|
|
249
|
+
max: Optional[float] = Field(default=None, title="Max")
|
|
250
|
+
bins: int = Field(default=DEFAULT_BINS, ge=1, title="Bins")
|
|
251
|
+
|
|
252
|
+
@model_validator(mode="after")
|
|
253
|
+
def _check_range(self):
|
|
254
|
+
if self.min is not None and self.max is not None and self.min >= self.max:
|
|
255
|
+
raise ValueError(f"min ({self.min}) >= max ({self.max})")
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class HistogramConfig(DistributionPlotConfig):
|
|
260
|
+
legacy_keys = {
|
|
261
|
+
**DistributionPlotConfig.legacy_keys,
|
|
262
|
+
"table": "datatable",
|
|
263
|
+
"features": None,
|
|
264
|
+
"sync_bins": None,
|
|
265
|
+
"auto_range": None,
|
|
266
|
+
}
|
|
267
|
+
# settings of the features that are the same for all if a field of the config is
|
|
268
|
+
# set: settings field -> config field (dialogs keep them in step)
|
|
269
|
+
synced_settings: ClassVar[dict[str, str]] = {"bins": "sync_bins"}
|
|
270
|
+
# settings of the features that are not used if a field of the config is set:
|
|
271
|
+
# settings field -> config field (dialogs grey them out, the values are kept)
|
|
272
|
+
disabled_settings: ClassVar[dict[str, str]] = {
|
|
273
|
+
"min": "auto_range",
|
|
274
|
+
"max": "auto_range",
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
features: dict[FeatureColumn, HistogramRange] = Field(
|
|
278
|
+
default={},
|
|
279
|
+
title="Features",
|
|
280
|
+
description="Check the features to plot; empty min or max: the data's range.",
|
|
281
|
+
)
|
|
282
|
+
# right above the features table (features: 410)
|
|
283
|
+
auto_range: bool = Field(
|
|
284
|
+
default=False,
|
|
285
|
+
title="Auto min/max range",
|
|
286
|
+
description=(
|
|
287
|
+
"Each feature's min and max in the data; the min and max in the table are"
|
|
288
|
+
" kept, but not used."
|
|
289
|
+
),
|
|
290
|
+
json_schema_extra={"order": 409},
|
|
291
|
+
)
|
|
292
|
+
sync_bins: bool = Field(
|
|
293
|
+
default=False,
|
|
294
|
+
title="Same bins for all",
|
|
295
|
+
description="All features get the same number of bins.",
|
|
296
|
+
json_schema_extra={"order": 409.5},
|
|
297
|
+
)
|
|
298
|
+
density: bool = Field(default=False, title="Density")
|
|
299
|
+
cumulative: bool = Field(default=False, title="Cumulative")
|
|
300
|
+
histtype: Literal["bar", "barstacked", "step", "stepfilled"] = Field(
|
|
301
|
+
default="step", title="Style"
|
|
302
|
+
)
|
|
303
|
+
stacked: bool = Field(
|
|
304
|
+
default=False,
|
|
305
|
+
title="Stacked",
|
|
306
|
+
description="Groups stacked, in percent of all rows (else of each group's).",
|
|
307
|
+
)
|
|
308
|
+
table: bool = Field(
|
|
309
|
+
default=False,
|
|
310
|
+
title="Binned data table",
|
|
311
|
+
description="Also return the binned values as a table.",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
@model_validator(mode="before")
|
|
315
|
+
@classmethod
|
|
316
|
+
def _older_global_bins(cls, data):
|
|
317
|
+
"""Configs with a global "bins" (before 2.0 was released): the bins of the
|
|
318
|
+
features without their own."""
|
|
319
|
+
if isinstance(data, dict) and "bins" in data:
|
|
320
|
+
data = dict(data)
|
|
321
|
+
bins = data.pop("bins")
|
|
322
|
+
features = data.get("features")
|
|
323
|
+
if isinstance(features, dict):
|
|
324
|
+
data["features"] = {
|
|
325
|
+
f: ({"bins": bins, **s} if isinstance(s, dict) else s)
|
|
326
|
+
for f, s in features.items()
|
|
327
|
+
}
|
|
328
|
+
elif isinstance(features, (list, tuple)):
|
|
329
|
+
data["features"] = {f: {"bins": bins} for f in features}
|
|
330
|
+
return data
|
|
331
|
+
|
|
332
|
+
@field_validator("features", mode="before")
|
|
333
|
+
@classmethod
|
|
334
|
+
def _features_from_list(cls, features):
|
|
335
|
+
"""A list of features: each with the default range and bins."""
|
|
336
|
+
if isinstance(features, (list, tuple)):
|
|
337
|
+
return {f: {} for f in features}
|
|
338
|
+
return features
|
|
339
|
+
|
|
340
|
+
@model_validator(mode="after")
|
|
341
|
+
def _same_bins(self):
|
|
342
|
+
"""With a global bin, all features get the first feature's bins."""
|
|
343
|
+
if self.sync_bins and self.features:
|
|
344
|
+
bins = next(iter(self.features.values())).bins
|
|
345
|
+
# through __dict__: assignments would run the validators again
|
|
346
|
+
self.__dict__["features"] = {
|
|
347
|
+
f: r if r.bins == bins else r.model_copy(update={"bins": bins})
|
|
348
|
+
for f, r in self.features.items()
|
|
349
|
+
}
|
|
350
|
+
return self
|
|
351
|
+
|
|
352
|
+
@classmethod
|
|
353
|
+
def _from_legacy(cls, params):
|
|
354
|
+
# the plugin: a list of features, and {"min", "max", "bins"} per feature in
|
|
355
|
+
# "featuresettings". Its defaults there are lists [min, max, bins, groups]
|
|
356
|
+
# that it never read; they are skipped to keep the plots unchanged.
|
|
357
|
+
features = params.pop("features", [])
|
|
358
|
+
if not isinstance(features, list): # "all_features": not a fixed set
|
|
359
|
+
logging.info("Frequency Histogram: 'all features' is not kept")
|
|
360
|
+
features = []
|
|
361
|
+
settings = {}
|
|
362
|
+
for feature, spec in (params.pop("featuresettings", {}) or {}).items():
|
|
363
|
+
if not isinstance(spec, dict):
|
|
364
|
+
continue
|
|
365
|
+
try:
|
|
366
|
+
settings[feature] = HistogramRange(**spec)
|
|
367
|
+
except (TypeError, ValidationError) as err:
|
|
368
|
+
logging.debug(f"Histogram settings of {feature} skipped: {err}")
|
|
369
|
+
params.pop("bins", None) # not used by the plugin
|
|
370
|
+
return {"features": {f: settings.get(f, HistogramRange()) for f in features}}
|
|
371
|
+
|
|
372
|
+
def _to_legacy(self):
|
|
373
|
+
# the plugin uses a feature's settings only if all three are set; its "bins"
|
|
374
|
+
# was not used
|
|
375
|
+
return {
|
|
376
|
+
"features": list(self.features),
|
|
377
|
+
"featuresettings": {
|
|
378
|
+
feature: {"min": r.min, "max": r.max, "bins": r.bins}
|
|
379
|
+
for feature, r in self.features.items()
|
|
380
|
+
if r.min is not None and r.max is not None and not self.auto_range
|
|
381
|
+
},
|
|
382
|
+
"bins": DEFAULT_BINS,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
# --- other plots ---
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class ScatterPlotConfig(AxesRange, TiledPlotConfig):
|
|
390
|
+
"""A scatter plot for each pair of ``features``."""
|
|
391
|
+
|
|
392
|
+
legacy_keys = {
|
|
393
|
+
**TiledPlotConfig.legacy_keys,
|
|
394
|
+
"points": None,
|
|
395
|
+
"x_axis": None,
|
|
396
|
+
"fit_axes": None,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
points: PointSampling = Field(default_factory=PointSampling, title="Points")
|
|
400
|
+
x_axis: Optional[FeatureColumn] = Field(
|
|
401
|
+
default=None,
|
|
402
|
+
title="X axis",
|
|
403
|
+
description="With two features: the one on the x axis (else the first of"
|
|
404
|
+
" each pair).",
|
|
405
|
+
json_schema_extra={"order": 411}, # right after the features
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
@classmethod
|
|
409
|
+
def allowed_values(cls, values):
|
|
410
|
+
allowed = super().allowed_values(values)
|
|
411
|
+
features = values.get("features")
|
|
412
|
+
if isinstance(features, list) and len(set(features)) == 2:
|
|
413
|
+
allowed["x_axis"] = tuple(dict.fromkeys(features)) # first: the default
|
|
414
|
+
else: # one plot per pair: the first of the pair on the x axis
|
|
415
|
+
allowed["x_axis"] = (None,)
|
|
416
|
+
return allowed
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
class PairPlotConfig(PlotConfig):
|
|
420
|
+
"""A grid of scatter plots: one per (row feature, column feature).
|
|
421
|
+
|
|
422
|
+
``features`` is not edited: it is set to the grid's features (columns, then rows).
|
|
423
|
+
Configs that only set ``features`` (older settings) use them as the columns.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
legacy_keys = {
|
|
427
|
+
**PlotConfig.legacy_keys,
|
|
428
|
+
"diagonal": "diag_kind",
|
|
429
|
+
"points": None,
|
|
430
|
+
"symmetric": None,
|
|
431
|
+
}
|
|
432
|
+
hidden_fields = frozenset({"features"})
|
|
433
|
+
|
|
434
|
+
symmetric: bool = Field(
|
|
435
|
+
default=True,
|
|
436
|
+
title="Same rows as columns",
|
|
437
|
+
description="The grid's rows are its columns; otherwise choose them separately.",
|
|
438
|
+
)
|
|
439
|
+
x_vars: list[FeatureColumn] = Field(
|
|
440
|
+
default=[], title="Grid columns", description="The features of the columns."
|
|
441
|
+
)
|
|
442
|
+
y_vars: list[FeatureColumn] = Field(
|
|
443
|
+
default=[],
|
|
444
|
+
title="Grid rows",
|
|
445
|
+
description="The features of the rows.",
|
|
446
|
+
)
|
|
447
|
+
diagonal: Literal["KDE", "Histogram"] = Field(default="Histogram", title="Diagonal")
|
|
448
|
+
corner: bool = Field(
|
|
449
|
+
default=False,
|
|
450
|
+
title="Lower half only",
|
|
451
|
+
description="Omit the upper triangle of the (symmetric) grid.",
|
|
452
|
+
json_schema_extra={"order": 420}, # the figure's layout, after the features
|
|
453
|
+
)
|
|
454
|
+
hist_bins: int = Field(default=20, ge=1, title="Histogram bins")
|
|
455
|
+
points: PointSampling = Field(default_factory=PointSampling, title="Scatter points")
|
|
456
|
+
|
|
457
|
+
@classmethod
|
|
458
|
+
def allowed_values(cls, values):
|
|
459
|
+
allowed = super().allowed_values(values)
|
|
460
|
+
if values.get("symmetric", True): # the rows are the columns
|
|
461
|
+
return {**allowed, "y_vars": (list(values.get("x_vars") or []),)}
|
|
462
|
+
return {**allowed, "corner": (False,)} # a lower half needs a symmetric grid
|
|
463
|
+
|
|
464
|
+
@model_validator(mode="after")
|
|
465
|
+
def _grid_features(self):
|
|
466
|
+
# set through __dict__: assignments would run the validators again
|
|
467
|
+
values = self.__dict__
|
|
468
|
+
if not self.x_vars and isinstance(self.features, list):
|
|
469
|
+
values["x_vars"] = list(self.features)
|
|
470
|
+
self._apply_allowed_values()
|
|
471
|
+
if self.x_vars or self.y_vars:
|
|
472
|
+
values["features"] = list(dict.fromkeys(self.x_vars + self.y_vars))
|
|
473
|
+
return self
|
|
474
|
+
|
|
475
|
+
@classmethod
|
|
476
|
+
def _from_legacy(cls, params):
|
|
477
|
+
points = {}
|
|
478
|
+
for key, field in (
|
|
479
|
+
("plotting", "mode"),
|
|
480
|
+
("samples", "samples"),
|
|
481
|
+
("random_seed", "random_seed"),
|
|
482
|
+
):
|
|
483
|
+
if key in params:
|
|
484
|
+
points[field] = params.pop(key)
|
|
485
|
+
values = {"points": points} if points else {}
|
|
486
|
+
# the plugin's dialog always set the rows to the columns
|
|
487
|
+
x_vars, y_vars = params.get("x_vars") or [], params.get("y_vars") or []
|
|
488
|
+
values["symmetric"] = not y_vars or x_vars == y_vars
|
|
489
|
+
return values
|
|
490
|
+
|
|
491
|
+
def _to_legacy(self):
|
|
492
|
+
return {
|
|
493
|
+
"plotting": self.points.mode,
|
|
494
|
+
"samples": self.points.samples,
|
|
495
|
+
"random_seed": self.points.random_seed,
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class HeatmapConfig(PlotConfig):
|
|
500
|
+
legacy_keys = {
|
|
501
|
+
**PlotConfig.legacy_keys,
|
|
502
|
+
"correlation": "corr_type",
|
|
503
|
+
"annotate": "numbers",
|
|
504
|
+
}
|
|
505
|
+
legend_grouping = None # a colour bar, no legend
|
|
506
|
+
|
|
507
|
+
correlation: Literal["pearson", "spearman", "kendall"] = Field(
|
|
508
|
+
default="pearson", title="Correlation"
|
|
509
|
+
)
|
|
510
|
+
annotate: bool = Field(
|
|
511
|
+
default=False, title="Show values", description="Write the coefficients."
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
class FLIRRPlotConfig(PlotConfig):
|
|
516
|
+
legend_grouping = None # each panel has its own
|
|
517
|
+
# the plotted columns are the three below (not plugin parameters: found by name)
|
|
518
|
+
hidden_fields = frozenset({"features"})
|
|
519
|
+
legacy_keys = {
|
|
520
|
+
**PlotConfig.legacy_keys,
|
|
521
|
+
"flirr": None,
|
|
522
|
+
"fad_a1": None,
|
|
523
|
+
"nadph_a2": None,
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
flirr: Optional[FeatureColumn] = Field(
|
|
527
|
+
default=None,
|
|
528
|
+
title="FLIRR",
|
|
529
|
+
description="Empty: the column whose name contains FLIRR.",
|
|
530
|
+
json_schema_extra={"order": 411},
|
|
531
|
+
)
|
|
532
|
+
fad_a1: Optional[FeatureColumn] = Field(
|
|
533
|
+
default=None,
|
|
534
|
+
title="FAD a1 %",
|
|
535
|
+
description="The x axis of the scatter plot. Empty: found by name (FAD, a1, %).",
|
|
536
|
+
json_schema_extra={"order": 412},
|
|
537
|
+
)
|
|
538
|
+
nadph_a2: Optional[FeatureColumn] = Field(
|
|
539
|
+
default=None,
|
|
540
|
+
title="NAD(P)H a2 %",
|
|
541
|
+
description="The y axis of the scatter plot. Empty: found by name (NAD, a2, %).",
|
|
542
|
+
json_schema_extra={"order": 413},
|
|
543
|
+
)
|