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.
Files changed (110) hide show
  1. flim/__init__.py +2 -0
  2. flim/_version.py +5 -0
  3. flim/analysis/__init__.py +1 -0
  4. flim/analysis/ml/__init__.py +0 -0
  5. flim/analysis/ml/autoencoder.py +188 -0
  6. flim/analyzerapp.py +131 -0
  7. flim/batch.py +235 -0
  8. flim/configs/__init__.py +77 -0
  9. flim/configs/analysis.py +554 -0
  10. flim/configs/data.py +278 -0
  11. flim/configs/plot.py +543 -0
  12. flim/configs/workflow.py +204 -0
  13. flim/core/__init__.py +0 -0
  14. flim/core/calculator.py +0 -0
  15. flim/core/config.py +305 -0
  16. flim/core/execution.py +110 -0
  17. flim/core/fields.py +226 -0
  18. flim/core/filter.py +179 -0
  19. flim/core/graph.py +111 -0
  20. flim/core/headers.py +72 -0
  21. flim/core/importer.py +269 -0
  22. flim/core/parser.py +305 -0
  23. flim/core/paths.py +66 -0
  24. flim/core/prefect_server.py +152 -0
  25. flim/core/preprocessor.py +287 -0
  26. flim/core/registry.py +108 -0
  27. flim/core/runner.py +703 -0
  28. flim/core/schema.py +114 -0
  29. flim/core/settings.py +419 -0
  30. flim/core/tool.py +284 -0
  31. flim/core/tools.py +76 -0
  32. flim/core/types.py +113 -0
  33. flim/core/validation.py +272 -0
  34. flim/core/workflow.py +148 -0
  35. flim/gui/__init__.py +12 -0
  36. flim/gui/app.py +953 -0
  37. flim/gui/configdialog.py +495 -0
  38. flim/gui/datapanel.py +841 -0
  39. flim/gui/delimpanel.py +56 -0
  40. flim/gui/dialogs/__init__.py +473 -0
  41. flim/gui/dialogs/autoencoder.py +89 -0
  42. flim/gui/dialogs/categoryorder.py +130 -0
  43. flim/gui/dialogs/filter.py +86 -0
  44. flim/gui/dialogs/unpivot.py +72 -0
  45. flim/gui/dicttablepanel.py +178 -0
  46. flim/gui/editors.py +1578 -0
  47. flim/gui/events.py +162 -0
  48. flim/gui/icons.py +16 -0
  49. flim/gui/importdlg.py +391 -0
  50. flim/gui/listcontrol.py +472 -0
  51. flim/gui/macos.py +77 -0
  52. flim/gui/plotframe.py +73 -0
  53. flim/gui/progressdlg.py +290 -0
  54. flim/resources/aeaugment.png +0 -0
  55. flim/resources/aefeatureworkflow.png +0 -0
  56. flim/resources/aerun.png +0 -0
  57. flim/resources/aetrain.png +0 -0
  58. flim/resources/aetuneworkflow.png +0 -0
  59. flim/resources/barplot.png +0 -0
  60. flim/resources/boxplot.png +0 -0
  61. flim/resources/categorize.png +0 -0
  62. flim/resources/concatenate.png +0 -0
  63. flim/resources/fileopen.png +0 -0
  64. flim/resources/filter.png +0 -0
  65. flim/resources/flimanalyzer.ico +0 -0
  66. flim/resources/flimanalyzer.png +0 -0
  67. flim/resources/flirrplot.png +0 -0
  68. flim/resources/flirrworkflow.png +0 -0
  69. flim/resources/gmm.png +0 -0
  70. flim/resources/heatmap.png +0 -0
  71. flim/resources/histogram.png +0 -0
  72. flim/resources/import.png +0 -0
  73. flim/resources/kde.png +0 -0
  74. flim/resources/kmeans.png +0 -0
  75. flim/resources/ks.png +0 -0
  76. flim/resources/lineplot.png +0 -0
  77. flim/resources/merge.png +0 -0
  78. flim/resources/order.png +0 -0
  79. flim/resources/pairplot.png +0 -0
  80. flim/resources/pca.png +0 -0
  81. flim/resources/pivot.png +0 -0
  82. flim/resources/randomforest.png +0 -0
  83. flim/resources/relchange.png +0 -0
  84. flim/resources/scatter.png +0 -0
  85. flim/resources/seriesanalysis.png +0 -0
  86. flim/resources/sort.png +0 -0
  87. flim/resources/split.png +0 -0
  88. flim/resources/summary.png +0 -0
  89. flim/resources/swarmplot.png +0 -0
  90. flim/resources/unpivot.png +0 -0
  91. flim/resources/violinplot.png +0 -0
  92. flim/resources/workflow.png +0 -0
  93. flim/results.py +70 -0
  94. flim/tools/__init__.py +7 -0
  95. flim/tools/autoencoder.py +662 -0
  96. flim/tools/common.py +62 -0
  97. flim/tools/data.py +301 -0
  98. flim/tools/ml.py +311 -0
  99. flim/tools/plot.py +1439 -0
  100. flim/tools/statistics.py +348 -0
  101. flim/utils.py +43 -0
  102. flim/workflow/__init__.py +3 -0
  103. flim/workflow/aefeature.py +145 -0
  104. flim/workflow/aetune.py +163 -0
  105. flim/workflow/basicflow.py +99 -0
  106. flimanalyzer-0.6.0.dist-info/METADATA +262 -0
  107. flimanalyzer-0.6.0.dist-info/RECORD +110 -0
  108. flimanalyzer-0.6.0.dist-info/WHEEL +4 -0
  109. flimanalyzer-0.6.0.dist-info/entry_points.txt +5 -0
  110. 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
+ )