triplot 1.1.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.
- dscpanel/__init__.py +4 -0
- dscpanel/__main__.py +155 -0
- dscpanel/branding.py +142 -0
- dscpanel/core/__init__.py +0 -0
- dscpanel/core/arrange.py +143 -0
- dscpanel/core/chem.py +252 -0
- dscpanel/core/dtg.py +135 -0
- dscpanel/core/export.py +649 -0
- dscpanel/core/figure.py +171 -0
- dscpanel/core/labels.py +430 -0
- dscpanel/core/loader.py +202 -0
- dscpanel/core/log.py +147 -0
- dscpanel/core/measure.py +636 -0
- dscpanel/core/model.py +1993 -0
- dscpanel/core/molar.py +344 -0
- dscpanel/core/numbers.py +208 -0
- dscpanel/core/ops.py +161 -0
- dscpanel/core/presets.py +312 -0
- dscpanel/core/profile.py +15 -0
- dscpanel/core/session.py +667 -0
- dscpanel/core/shades.py +54 -0
- dscpanel/core/style.py +528 -0
- dscpanel/core/trios_analysis.py +636 -0
- dscpanel/core/trios_io.py +1311 -0
- dscpanel/core/undo.py +230 -0
- dscpanel/core/units.py +220 -0
- dscpanel/register.py +284 -0
- dscpanel/ui/__init__.py +0 -0
- dscpanel/ui/appearance.py +146 -0
- dscpanel/ui/colour.py +629 -0
- dscpanel/ui/dialogs.py +3639 -0
- dscpanel/ui/loading.py +95 -0
- dscpanel/ui/numbox.py +103 -0
- dscpanel/ui/outliner.py +818 -0
- dscpanel/ui/palette.py +193 -0
- dscpanel/ui/plot.py +8349 -0
- dscpanel/ui/settings.py +256 -0
- dscpanel/ui/window.py +4129 -0
- triplot-1.1.0.dist-info/METADATA +315 -0
- triplot-1.1.0.dist-info/RECORD +44 -0
- triplot-1.1.0.dist-info/WHEEL +5 -0
- triplot-1.1.0.dist-info/entry_points.txt +5 -0
- triplot-1.1.0.dist-info/licenses/LICENSE +22 -0
- triplot-1.1.0.dist-info/top_level.txt +1 -0
dscpanel/core/session.py
ADDED
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
"""Saving and reopening an arrangement.
|
|
2
|
+
|
|
3
|
+
A figure made of eight scans is half an hour of dragging, colouring and
|
|
4
|
+
typing molar masses, and it is worth nothing if it cannot be reopened. So the
|
|
5
|
+
arrangement is a file: which measurements, which segments of them, where each
|
|
6
|
+
one sits, what colour it is, what M it was given, where the arrow is.
|
|
7
|
+
|
|
8
|
+
**The measurements themselves are NOT copied in.** A session stores paths and
|
|
9
|
+
reads the files again, which keeps a session small and keeps one copy of the
|
|
10
|
+
data on disk - and means a re-run of the same sample, saved over the same
|
|
11
|
+
path, is picked up by reopening the session. A file that has moved is
|
|
12
|
+
reported by name instead of failing the whole load, because losing seven
|
|
13
|
+
scans over one missing path would be the worst possible behaviour.
|
|
14
|
+
|
|
15
|
+
UI-free: `load` takes the reader as an argument, so this module never imports
|
|
16
|
+
the reader or a window and is testable with a stub.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
from . import dtg
|
|
23
|
+
from . import figure as figure_module
|
|
24
|
+
from . import labels
|
|
25
|
+
from . import measure
|
|
26
|
+
from . import model
|
|
27
|
+
from . import numbers
|
|
28
|
+
from . import style
|
|
29
|
+
from . import units
|
|
30
|
+
|
|
31
|
+
FORMAT = "dscpanel-session"
|
|
32
|
+
#: 2: sizes are None until chosen (the house style fills them in), the
|
|
33
|
+
#: figure carries its own `style`, and analyses have a `flush`.
|
|
34
|
+
#: 3: an axis's `label_gap` is measured from its NUMBERS (it was from the
|
|
35
|
+
#: axis line for x and from the window's edge for y), None until chosen.
|
|
36
|
+
VERSION = 8
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def view_to_state(view):
|
|
40
|
+
"""The framing as plain data (lists), or None."""
|
|
41
|
+
if not view:
|
|
42
|
+
return None
|
|
43
|
+
return {"x": list(view["x"]) if view.get("x") else None,
|
|
44
|
+
"y": list(view["y"]) if view.get("y") else None,
|
|
45
|
+
"y2": list(view["y2"]) if view.get("y2") else None,
|
|
46
|
+
# The weight range is a number of % or of mg: which, beside it
|
|
47
|
+
# (the plot restores it only in that unit). A range saved
|
|
48
|
+
# without it never came back.
|
|
49
|
+
"y2_unit": view.get("y2_unit") if view.get("y2") else None,
|
|
50
|
+
"context": list(view.get("context") or ())}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _view_from(saved):
|
|
54
|
+
"""The framing back as the plot keeps it (tuples), or None."""
|
|
55
|
+
if not isinstance(saved, dict):
|
|
56
|
+
return None
|
|
57
|
+
try:
|
|
58
|
+
return {"x": tuple(float(v) for v in saved["x"]) if saved.get("x")
|
|
59
|
+
else None,
|
|
60
|
+
"y": tuple(float(v) for v in saved["y"]) if saved.get("y")
|
|
61
|
+
else None,
|
|
62
|
+
"y2": tuple(float(v) for v in saved["y2"])
|
|
63
|
+
if saved.get("y2") else None,
|
|
64
|
+
"y2_unit": (saved.get("y2_unit")
|
|
65
|
+
if saved.get("y2_unit") in model.WEIGHT_UNITS
|
|
66
|
+
else None),
|
|
67
|
+
"context": tuple(saved.get("context") or ())}
|
|
68
|
+
except (TypeError, ValueError, KeyError):
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _marker_at(value):
|
|
73
|
+
"""A stored marker place: None, ("i", sample) or ("T", celsius)."""
|
|
74
|
+
try:
|
|
75
|
+
kind, number = value
|
|
76
|
+
if kind == "i":
|
|
77
|
+
return ("i", int(number))
|
|
78
|
+
if kind == "T":
|
|
79
|
+
return ("T", float(number))
|
|
80
|
+
except (TypeError, ValueError):
|
|
81
|
+
pass
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _number_or_none(value):
|
|
86
|
+
try:
|
|
87
|
+
return None if value is None else float(value)
|
|
88
|
+
except (TypeError, ValueError):
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _chosen(value, key, version):
|
|
93
|
+
"""A stored size, or None where the file did not really choose it.
|
|
94
|
+
|
|
95
|
+
A version-1 file wrote EVERY size, chosen or not, because there was no
|
|
96
|
+
house style to fall back on. Read literally, each of those would pin the
|
|
97
|
+
object and the user's defaults would never reach a figure saved before
|
|
98
|
+
they existed. So in a version-1 file a value equal to the built-in one is
|
|
99
|
+
read as "not chosen" - it is what the object carried when nobody had
|
|
100
|
+
touched it. Later files store None themselves.
|
|
101
|
+
"""
|
|
102
|
+
setting = style.BY_KEY[key]
|
|
103
|
+
value = setting.clean(value)
|
|
104
|
+
if (value is not None and version < 2
|
|
105
|
+
and value == _VERSION_1_DEFAULTS.get(key)):
|
|
106
|
+
return None
|
|
107
|
+
return value
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
#: What every object carried in a version-1 file when nobody had touched it:
|
|
111
|
+
#: the built-ins of THAT time, not today's, which have since moved.
|
|
112
|
+
_VERSION_1_DEFAULTS = {"analysis_size": 9.0, "caption_size": 10.0,
|
|
113
|
+
"tick_size": 8.0, "legend_size": 9.0,
|
|
114
|
+
"label_size": 10.0, "line_width": 1.0}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _analysis_state(analysis):
|
|
118
|
+
"""One analysis: how it is drawn, and - if it was made HERE - how to
|
|
119
|
+
make it again.
|
|
120
|
+
|
|
121
|
+
A file's own analyses come back with the file, so only their styling is
|
|
122
|
+
stored, matched up again by `key`. One measured in the panel exists
|
|
123
|
+
nowhere else, so its model and its two cursors are stored too, and it is
|
|
124
|
+
RECOMPUTED from the re-read file on load - the same rule as the curves:
|
|
125
|
+
a session keeps the arrangement, never a copy of the numbers.
|
|
126
|
+
"""
|
|
127
|
+
state = {"key": analysis.key(), "visible": analysis.visible,
|
|
128
|
+
"colour": analysis.colour, "label": analysis.label,
|
|
129
|
+
"label_dy": analysis.label_dy, "shade": analysis.shade,
|
|
130
|
+
"shading": analysis.shading,
|
|
131
|
+
"label_size": analysis.label_size, "flush": analysis.flush,
|
|
132
|
+
"show_interval": analysis.show_interval,
|
|
133
|
+
"interval_size": analysis.interval_size,
|
|
134
|
+
"construction": analysis.construction,
|
|
135
|
+
"show_peak": analysis.show_peak,
|
|
136
|
+
"number_format": analysis.number_format,
|
|
137
|
+
"unit": analysis.unit,
|
|
138
|
+
"label_at": analysis.label_at, "z": analysis.z,
|
|
139
|
+
"attribution": analysis.attribution,
|
|
140
|
+
"source": analysis.source}
|
|
141
|
+
if analysis.source == "panel":
|
|
142
|
+
state["model"] = analysis.model_name
|
|
143
|
+
state["cursors"] = analysis.cursors()
|
|
144
|
+
state["span"] = list(analysis.span) if analysis.span else None
|
|
145
|
+
return state
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _restore_analysis(analysis, saved, version):
|
|
149
|
+
analysis.visible = bool(saved.get("visible", False))
|
|
150
|
+
analysis.colour = saved.get("colour", "auto")
|
|
151
|
+
analysis.label = saved.get("label")
|
|
152
|
+
stored_dy = saved.get("label_dy", analysis.label_dy)
|
|
153
|
+
analysis.label_dy = None if stored_dy is None else float(stored_dy)
|
|
154
|
+
analysis.shade = bool(saved.get("shade", True))
|
|
155
|
+
shading = saved.get("shading")
|
|
156
|
+
analysis.shading = shading if shading in style.SHADINGS else None
|
|
157
|
+
peak = saved.get("show_peak")
|
|
158
|
+
analysis.show_peak = peak if peak in style.PEAKS else None
|
|
159
|
+
analysis.label_size = _chosen(saved.get("label_size"), "analysis_size",
|
|
160
|
+
version)
|
|
161
|
+
flush = saved.get("flush")
|
|
162
|
+
analysis.flush = flush if flush in style.FLUSHES else None
|
|
163
|
+
analysis.show_interval = bool(saved.get("show_interval", True))
|
|
164
|
+
analysis.interval_size = style.BY_KEY["interval_tick"].clean(
|
|
165
|
+
saved.get("interval_size"))
|
|
166
|
+
if "construction" in saved:
|
|
167
|
+
lines = saved.get("construction")
|
|
168
|
+
analysis.construction = lines if lines in style.LINES else None
|
|
169
|
+
else:
|
|
170
|
+
# Saved before an onset's lines had a switch of their own:
|
|
171
|
+
# "Show interval markers" was the dashes AND the lines, so one saved
|
|
172
|
+
# with it off had no lines either, and opens that way. No version
|
|
173
|
+
# bump: the missing key says it.
|
|
174
|
+
analysis.construction = (None if analysis.show_interval
|
|
175
|
+
else style.LINES_NONE)
|
|
176
|
+
if analysis.attribution != model.CURVE_NOT_STATED:
|
|
177
|
+
# What the FILE does not say stays unsaid, whatever a session
|
|
178
|
+
# written before that was known recorded.
|
|
179
|
+
analysis.attribution = saved.get("attribution", analysis.attribution)
|
|
180
|
+
# With its unit, if it names one (`%.0f degF`): a unit-less read
|
|
181
|
+
# dropped such a format on every reopen.
|
|
182
|
+
analysis.number_format = labels.normalise_format(
|
|
183
|
+
saved.get("number_format"))
|
|
184
|
+
analysis.label_at = _number_or_none(saved.get("label_at"))
|
|
185
|
+
analysis.z = _number_or_none(saved.get("z"))
|
|
186
|
+
unit = labels.canonical_unit(saved.get("unit"))
|
|
187
|
+
analysis.unit = (unit if unit in labels.units_of(analysis.quantity)
|
|
188
|
+
else None)
|
|
189
|
+
if version < 5 and measure.is_legacy_label(analysis.label):
|
|
190
|
+
# A panel analysis was once GIVEN a label with its number written
|
|
191
|
+
# in ("*T*_{onset} = 61.1 degC"), and a file before version 5 can
|
|
192
|
+
# still hold one whose number went stale. Nobody typed that shape,
|
|
193
|
+
# so it goes back to the default template, whose number follows the
|
|
194
|
+
# measurement; any other label stays the user's.
|
|
195
|
+
analysis.label = None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def to_state(doc):
|
|
199
|
+
"""The document as plain data, ready for `json.dump`."""
|
|
200
|
+
samples = []
|
|
201
|
+
for sample in doc.samples:
|
|
202
|
+
samples.append({
|
|
203
|
+
"path": sample.path,
|
|
204
|
+
"molar_mass": sample.molar_mass,
|
|
205
|
+
"exo": sample.exo,
|
|
206
|
+
"exo_source": sample.exo_source,
|
|
207
|
+
"title": sample.title,
|
|
208
|
+
"composition": sample.composition,
|
|
209
|
+
})
|
|
210
|
+
scans = []
|
|
211
|
+
for scan in doc.scans:
|
|
212
|
+
scans.append({
|
|
213
|
+
"path": scan.sample.path,
|
|
214
|
+
"seg": scan.seg,
|
|
215
|
+
"signal": scan.signal,
|
|
216
|
+
"dtg_window": scan.dtg_window if scan.is_dtg else None,
|
|
217
|
+
"colour": scan.colour,
|
|
218
|
+
"offset": scan.offset,
|
|
219
|
+
"line_width": scan.line_width,
|
|
220
|
+
"keep": list(scan.keep),
|
|
221
|
+
"label": scan.label,
|
|
222
|
+
"visible": scan.visible,
|
|
223
|
+
"analyses": [_analysis_state(a) for a in scan.analysis_objects],
|
|
224
|
+
"z": scan.z,
|
|
225
|
+
"marker": {"z": scan.marker.z,"at": (list(scan.marker.at) if scan.marker.at
|
|
226
|
+
else None),
|
|
227
|
+
"dy": scan.marker.dy,
|
|
228
|
+
"number_format": scan.marker.number_format,
|
|
229
|
+
"size": scan.marker.size,
|
|
230
|
+
"colour": scan.marker.colour,
|
|
231
|
+
"visible": scan.marker.visible},
|
|
232
|
+
})
|
|
233
|
+
arrow = doc.arrow
|
|
234
|
+
axes = {}
|
|
235
|
+
for which, axis in doc.axes.items():
|
|
236
|
+
axes[which] = {"label": axis.label, "show_grid": axis.show_grid,
|
|
237
|
+
"minor_ticks": axis.minor_ticks,
|
|
238
|
+
"ticks_inward": axis.ticks_inward,
|
|
239
|
+
"label_size": axis.label_size,
|
|
240
|
+
"tick_size": axis.tick_size,
|
|
241
|
+
"label_along": axis.label_along,
|
|
242
|
+
"number_format": axis.number_format,
|
|
243
|
+
"mirror": axis.mirror,
|
|
244
|
+
"mirror_ticks": axis.mirror_ticks,
|
|
245
|
+
"major_step": axis.major_step,
|
|
246
|
+
"minor_count": axis.minor_count,
|
|
247
|
+
"tick_length": axis.tick_length,
|
|
248
|
+
"minor_length": axis.minor_length,
|
|
249
|
+
"side": axis.side,
|
|
250
|
+
"show_numbers": axis.show_numbers,
|
|
251
|
+
"visible": axis.visible,
|
|
252
|
+
"label_gap": axis.label_gap,
|
|
253
|
+
"lock": (list(axis.lock) if axis.lock else None),
|
|
254
|
+
"lock_context": (list(axis.lock_context)
|
|
255
|
+
if axis.lock_context else None),
|
|
256
|
+
"hidden_numbers": [float(v) for v in
|
|
257
|
+
axis.hidden_numbers or ()],
|
|
258
|
+
"hidden_context": (list(axis.hidden_context)
|
|
259
|
+
if axis.hidden_numbers
|
|
260
|
+
and axis.hidden_context else None)}
|
|
261
|
+
labels = []
|
|
262
|
+
for lb in doc.labels:
|
|
263
|
+
labels.append({"text": lb.text, "x": lb.x, "y": lb.y,
|
|
264
|
+
"colour": lb.colour, "size": lb.size, "bold": lb.bold,
|
|
265
|
+
"visible": lb.visible, "space": lb.space,
|
|
266
|
+
"anchor": lb.anchor, "rotation": lb.rotation,
|
|
267
|
+
"z": lb.z,
|
|
268
|
+
"scan": (None if lb.scan is None
|
|
269
|
+
else [lb.scan.sample.path, lb.scan.seg]),
|
|
270
|
+
"parent_offset": lb.parent_offset,
|
|
271
|
+
"at": (list(lb.at) if lb.at else None),
|
|
272
|
+
"dx": lb.dx, "dy": lb.dy,
|
|
273
|
+
"leader": lb.leader,
|
|
274
|
+
"leader_from": lb.leader_from,
|
|
275
|
+
"leader_colour": lb.leader_colour,
|
|
276
|
+
"flush": lb.flush,
|
|
277
|
+
"vline": lb.vline, "line_dashed": lb.line_dashed})
|
|
278
|
+
return {
|
|
279
|
+
"format": FORMAT,
|
|
280
|
+
"version": VERSION,
|
|
281
|
+
"axes": axes,
|
|
282
|
+
"labels": labels,
|
|
283
|
+
"structures": [{"smiles": m.smiles, "atoms": m.atoms,
|
|
284
|
+
"bonds": m.bonds, "x": m.x, "y": m.y,
|
|
285
|
+
"space": m.space, "anchor": m.anchor,
|
|
286
|
+
"rotation": m.rotation, "z": m.z,
|
|
287
|
+
"visible": m.visible, "colour": m.colour,
|
|
288
|
+
"bond_length": m.bond_length,
|
|
289
|
+
"bond_width": m.bond_width,
|
|
290
|
+
"label_size": m.label_size,
|
|
291
|
+
"upright_labels": m.upright_labels,
|
|
292
|
+
"label_font": m.label_font,
|
|
293
|
+
"colour_by_element": m.colour_by_element}
|
|
294
|
+
for m in doc.structures],
|
|
295
|
+
"images": [{"png": im.png, "x": im.x, "y": im.y,
|
|
296
|
+
"space": im.space, "anchor": im.anchor,
|
|
297
|
+
"rotation": im.rotation, "width": im.width,
|
|
298
|
+
"z": im.z, "visible": im.visible,
|
|
299
|
+
"mirror_h": im.mirror_h, "mirror_v": im.mirror_v}
|
|
300
|
+
for im in doc.images],
|
|
301
|
+
"x_axis": doc.x_axis,
|
|
302
|
+
"x_unit": doc.x_unit,
|
|
303
|
+
"y_unit": doc.y_unit,
|
|
304
|
+
"weight_unit": doc.weight_unit,
|
|
305
|
+
"dtg_unit": doc.dtg_unit,
|
|
306
|
+
"theme": doc.theme,
|
|
307
|
+
"background": doc.background,
|
|
308
|
+
"follow_zoom": bool(doc.follow_zoom),
|
|
309
|
+
"offset_markers": bool(doc.offset_markers),
|
|
310
|
+
"view": view_to_state(doc.view),
|
|
311
|
+
"style": doc.style.chosen(),
|
|
312
|
+
"figure": doc.figure.to_state(),
|
|
313
|
+
"figure_grown": dict(doc.figure.grown),
|
|
314
|
+
"legend": {"visible": doc.legend.visible, "size": doc.legend.size,
|
|
315
|
+
"show_frame": doc.legend.show_frame,
|
|
316
|
+
"sample": doc.legend.sample,
|
|
317
|
+
"spacing": doc.legend.spacing,
|
|
318
|
+
"colour": doc.legend.colour, "x": doc.legend.x,
|
|
319
|
+
"rotation": doc.legend.rotation, "z": doc.legend.z,
|
|
320
|
+
"line_width": doc.legend.line_width,
|
|
321
|
+
"y": doc.legend.y, "space": doc.legend.space,
|
|
322
|
+
"anchor": doc.legend.anchor},
|
|
323
|
+
"arrow": {"word": arrow.word, "direction": arrow.direction,
|
|
324
|
+
"x": arrow.x, "y": arrow.y,
|
|
325
|
+
"head_length": arrow.head_length,
|
|
326
|
+
"head_width": arrow.head_width,
|
|
327
|
+
"tail_width": arrow.tail_width,
|
|
328
|
+
"tail_length": arrow.tail_length,
|
|
329
|
+
"lock": arrow.lock, "size": arrow.size, "z": arrow.z,
|
|
330
|
+
"colour": arrow.colour, "visible": arrow.visible,
|
|
331
|
+
"space": arrow.space, "anchor": arrow.anchor},
|
|
332
|
+
"samples": samples,
|
|
333
|
+
"scans": scans,
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def save(doc, path):
|
|
338
|
+
"""Write the session. The document remembers where it went."""
|
|
339
|
+
state = to_state(doc)
|
|
340
|
+
with open(path, "w", encoding="utf-8") as fh:
|
|
341
|
+
json.dump(state, fh, indent=1)
|
|
342
|
+
doc.path = str(path)
|
|
343
|
+
return path
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def load(path, read_sample):
|
|
347
|
+
"""Rebuild a document from a session file.
|
|
348
|
+
|
|
349
|
+
Returns `(document, problems)`. `problems` names every file that could not
|
|
350
|
+
be read, so the window can say so in one line and still show everything
|
|
351
|
+
else.
|
|
352
|
+
"""
|
|
353
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
354
|
+
state = json.load(fh)
|
|
355
|
+
if state.get("format") != FORMAT:
|
|
356
|
+
raise ValueError("not a {} file".format(FORMAT))
|
|
357
|
+
doc = model.Document()
|
|
358
|
+
version = int(state.get("version", 1) or 1)
|
|
359
|
+
#: Which version it was read from: before 7, a decorator's place was a
|
|
360
|
+
#: fraction of the VIEW, not of the home frame (`PlotWidget.rehome`).
|
|
361
|
+
doc.loaded_version = version
|
|
362
|
+
problems = []
|
|
363
|
+
by_path = {}
|
|
364
|
+
saved_style = dict(state.get("style") or {})
|
|
365
|
+
if version < 6:
|
|
366
|
+
# The fit margins were percent of the data's range until version 6.
|
|
367
|
+
style.convert_old_fit(saved_style)
|
|
368
|
+
for key, raw in saved_style.items():
|
|
369
|
+
if key in style.BY_KEY and style.BY_KEY[key].figure:
|
|
370
|
+
setattr(doc.style, key, style.BY_KEY[key].clean(raw))
|
|
371
|
+
# A session keeps its OWN figure size; one saved before there was such
|
|
372
|
+
# a thing follows the window, as it did then - not today's default.
|
|
373
|
+
doc.figure = figure_module.FigureLayout().load_state(
|
|
374
|
+
state.get("figure") or {"mode": figure_module.MODE_WINDOW})
|
|
375
|
+
doc.figure.grown = figure_module.clean_grown(state.get("figure_grown"))
|
|
376
|
+
for entry in state.get("samples", []):
|
|
377
|
+
try:
|
|
378
|
+
sample = read_sample(entry["path"])
|
|
379
|
+
except Exception as exc:
|
|
380
|
+
problems.append("{}: {}".format(
|
|
381
|
+
os.path.basename(entry.get("path", "?")), exc))
|
|
382
|
+
continue
|
|
383
|
+
if entry.get("molar_mass"):
|
|
384
|
+
sample.molar_mass = float(entry["molar_mass"])
|
|
385
|
+
if entry.get("exo") in (units.EXO_DOWN, units.EXO_UP):
|
|
386
|
+
sample.exo = entry["exo"]
|
|
387
|
+
sample.exo_source = entry.get("exo_source", sample.exo_source)
|
|
388
|
+
title = entry.get("title")
|
|
389
|
+
sample.title = str(title) if title else None
|
|
390
|
+
made_of = entry.get("composition")
|
|
391
|
+
sample.composition = str(made_of) if made_of else None
|
|
392
|
+
doc.samples.append(sample)
|
|
393
|
+
by_path[os.path.normcase(sample.path)] = sample
|
|
394
|
+
# An older session kept a scan's weight as a dashed extra on it; a
|
|
395
|
+
# weight shown there opens as a mass scan of its own, AFTER the saved
|
|
396
|
+
# scans (labels name their scan by its place in the list).
|
|
397
|
+
legacy_mass = []
|
|
398
|
+
for entry in state.get("scans", []):
|
|
399
|
+
sample = by_path.get(os.path.normcase(entry.get("path", "")))
|
|
400
|
+
if sample is None:
|
|
401
|
+
continue
|
|
402
|
+
seg = int(entry.get("seg", 0))
|
|
403
|
+
if seg >= sample.segment_count():
|
|
404
|
+
problems.append("{}: segment {} is gone".format(sample.name,
|
|
405
|
+
seg + 1))
|
|
406
|
+
continue
|
|
407
|
+
signal = (entry.get("signal") if entry.get("signal") in model.SIGNALS
|
|
408
|
+
else model.SIGNAL_HEAT)
|
|
409
|
+
scan = model.Scan(doc._next_id(), sample, seg,
|
|
410
|
+
entry.get("colour")
|
|
411
|
+
or model.PALETTE[len(doc.scans) % len(model.PALETTE)],
|
|
412
|
+
signal)
|
|
413
|
+
scan.offset = float(entry.get("offset", 0.0))
|
|
414
|
+
window = _number_or_none(entry.get("dtg_window"))
|
|
415
|
+
if window is not None and window >= 0:
|
|
416
|
+
scan.dtg_window = window
|
|
417
|
+
scan.line_width = _chosen(entry.get("line_width"), "line_width",
|
|
418
|
+
version)
|
|
419
|
+
keep = entry.get("keep") or (0.0, 1.0)
|
|
420
|
+
try:
|
|
421
|
+
start, end = float(keep[0]), float(keep[1])
|
|
422
|
+
except (TypeError, ValueError, IndexError):
|
|
423
|
+
start, end = 0.0, 1.0
|
|
424
|
+
if 0.0 <= start < end <= 1.0:
|
|
425
|
+
scan.keep = (start, end)
|
|
426
|
+
scan.label = entry.get("label")
|
|
427
|
+
scan.visible = bool(entry.get("visible", True))
|
|
428
|
+
weight = entry.get("weight")
|
|
429
|
+
if (isinstance(weight, dict) and weight.get("visible", True)
|
|
430
|
+
and signal == model.SIGNAL_HEAT and scan.has_weight()):
|
|
431
|
+
legacy_mass.append((sample, seg, scan.colour))
|
|
432
|
+
stored = entry.get("analyses") or []
|
|
433
|
+
wanted = {item.get("key"): item for item in stored
|
|
434
|
+
if item.get("source") != "panel"}
|
|
435
|
+
for analysis in scan.analysis_objects:
|
|
436
|
+
saved = wanted.get(analysis.key())
|
|
437
|
+
if saved:
|
|
438
|
+
_restore_analysis(analysis, saved, version)
|
|
439
|
+
for saved in stored:
|
|
440
|
+
if saved.get("source") != "panel":
|
|
441
|
+
continue
|
|
442
|
+
cursors = saved.get("cursors") or []
|
|
443
|
+
made = None
|
|
444
|
+
if len(cursors) == 2:
|
|
445
|
+
made = measure.run(saved.get("model", ""), scan,
|
|
446
|
+
float(cursors[0]), float(cursors[1]),
|
|
447
|
+
span=saved.get("span"))
|
|
448
|
+
if made is None:
|
|
449
|
+
problems.append("{}: {} could not be measured again".format(
|
|
450
|
+
scan.display_name(), saved.get("model", "an analysis")))
|
|
451
|
+
continue
|
|
452
|
+
_restore_analysis(made, saved, version)
|
|
453
|
+
# A scan has no molar mass of its own: one an older session gave
|
|
454
|
+
# a scan goes to its file, if the file had none.
|
|
455
|
+
own = _number_or_none(entry.get("molar_mass_override"))
|
|
456
|
+
if own and own > 0 and not sample.molar_mass:
|
|
457
|
+
sample.molar_mass = float(own)
|
|
458
|
+
scan.z = _number_or_none(entry.get("z"))
|
|
459
|
+
marker = entry.get("marker") or {}
|
|
460
|
+
scan.marker.at = _marker_at(marker.get("at"))
|
|
461
|
+
scan.marker.number_format = labels.normalise_format(
|
|
462
|
+
marker.get("number_format"))
|
|
463
|
+
scan.marker.dy = _number_or_none(marker.get("dy"))
|
|
464
|
+
scan.marker.size = _chosen(marker.get("size"), "offset_marker_size",
|
|
465
|
+
version)
|
|
466
|
+
scan.marker.colour = marker.get("colour", "auto")
|
|
467
|
+
scan.marker.visible = bool(marker.get("visible", True))
|
|
468
|
+
scan.marker.z = _number_or_none(marker.get("z"))
|
|
469
|
+
sample.scans.append(scan)
|
|
470
|
+
doc.scans.append(scan)
|
|
471
|
+
for sample, seg, colour in legacy_mass:
|
|
472
|
+
if any(s.sample is sample and s.seg == seg and s.is_mass
|
|
473
|
+
for s in doc.scans):
|
|
474
|
+
continue
|
|
475
|
+
mass = model.Scan(doc._next_id(), sample, seg, colour,
|
|
476
|
+
model.SIGNAL_MASS)
|
|
477
|
+
sample.scans.append(mass)
|
|
478
|
+
doc.scans.append(mass)
|
|
479
|
+
arrow = state.get("arrow") or {}
|
|
480
|
+
doc.arrow.word = arrow.get("word", doc.arrow.word)
|
|
481
|
+
doc.arrow.direction = arrow.get("direction", doc.arrow.direction)
|
|
482
|
+
doc.arrow.x = float(arrow.get("x", doc.arrow.x))
|
|
483
|
+
doc.arrow.y = float(arrow.get("y", doc.arrow.y))
|
|
484
|
+
# Its dimensions are in points. An older session's `length`
|
|
485
|
+
# (a fraction of the plot height) has no honest conversion, so the
|
|
486
|
+
# template's proportions are what it opens with.
|
|
487
|
+
for name in ("head_length", "head_width", "tail_width", "tail_length"):
|
|
488
|
+
value = _number_or_none(arrow.get(name))
|
|
489
|
+
if value is not None and value > 0:
|
|
490
|
+
setattr(doc.arrow, name, value)
|
|
491
|
+
if arrow.get("lock") in model.ARROW_LOCKS:
|
|
492
|
+
doc.arrow.lock = arrow["lock"]
|
|
493
|
+
doc.arrow.size = _chosen(arrow.get("size"), "arrow_size", version)
|
|
494
|
+
doc.arrow.z = _number_or_none(arrow.get("z"))
|
|
495
|
+
doc.arrow.colour = arrow.get("colour", doc.arrow.colour)
|
|
496
|
+
doc.arrow.visible = bool(arrow.get("visible", True))
|
|
497
|
+
doc.arrow.space = arrow.get("space", doc.arrow.space)
|
|
498
|
+
doc.arrow.anchor = arrow.get("anchor", doc.arrow.anchor)
|
|
499
|
+
for which, saved in (state.get("axes") or {}).items():
|
|
500
|
+
axis = doc.axes.get(which)
|
|
501
|
+
if axis is None:
|
|
502
|
+
continue
|
|
503
|
+
for name, value in saved.items():
|
|
504
|
+
if hasattr(axis, name):
|
|
505
|
+
setattr(axis, name, value)
|
|
506
|
+
axis.number_format = numbers.normalise(saved.get("number_format"))
|
|
507
|
+
step = _number_or_none(saved.get("major_step"))
|
|
508
|
+
axis.major_step = step if step and step > 0 else None
|
|
509
|
+
try:
|
|
510
|
+
axis.minor_count = max(1, int(saved.get("minor_count", 5)))
|
|
511
|
+
except (TypeError, ValueError):
|
|
512
|
+
axis.minor_count = 5
|
|
513
|
+
for name, default in (("tick_length", 7.0), ("minor_length", 3.0)):
|
|
514
|
+
value = _number_or_none(saved.get(name))
|
|
515
|
+
setattr(axis, name, value if value is not None and value >= 0
|
|
516
|
+
else default)
|
|
517
|
+
axis.mirror = bool(saved.get("mirror", True))
|
|
518
|
+
axis.mirror_ticks = bool(saved.get("mirror_ticks", True))
|
|
519
|
+
axis.label_size = _chosen(saved.get("label_size"), "caption_size",
|
|
520
|
+
version)
|
|
521
|
+
# Before version 3 the gap was measured from somewhere else, so an
|
|
522
|
+
# old number would put the caption in the wrong place: dropped.
|
|
523
|
+
axis.label_gap = (style.BY_KEY["caption_gap"].clean(
|
|
524
|
+
saved.get("label_gap")) if version >= 3 else None)
|
|
525
|
+
if saved.get("side") not in (("bottom", "top") if which == "x"
|
|
526
|
+
else ("left", "right")):
|
|
527
|
+
axis.side = "bottom" if which == "x" else "left"
|
|
528
|
+
axis.tick_size = _chosen(saved.get("tick_size"), "tick_size", version)
|
|
529
|
+
lock = saved.get("lock")
|
|
530
|
+
try:
|
|
531
|
+
lock = [float(lock[0]), float(lock[1])] if lock else None
|
|
532
|
+
except (TypeError, ValueError, IndexError):
|
|
533
|
+
lock = None
|
|
534
|
+
axis.lock = lock if lock and lock[1] > lock[0] else None
|
|
535
|
+
context = saved.get("lock_context")
|
|
536
|
+
axis.lock_context = (list(context) if axis.lock and
|
|
537
|
+
isinstance(context, list) else None)
|
|
538
|
+
hidden = []
|
|
539
|
+
for value in saved.get("hidden_numbers") or ():
|
|
540
|
+
value = _number_or_none(value)
|
|
541
|
+
if value is not None and value == value and abs(value) != \
|
|
542
|
+
float("inf"):
|
|
543
|
+
hidden.append(value)
|
|
544
|
+
context = saved.get("hidden_context")
|
|
545
|
+
axis.hidden_context = (list(context) if hidden
|
|
546
|
+
and isinstance(context, list) else None)
|
|
547
|
+
axis.hidden_numbers = hidden if axis.hidden_context else []
|
|
548
|
+
for saved in state.get("labels") or []:
|
|
549
|
+
owner = None
|
|
550
|
+
owned = saved.get("scan")
|
|
551
|
+
if owned:
|
|
552
|
+
for scan in doc.scans:
|
|
553
|
+
if (os.path.normcase(scan.sample.path)
|
|
554
|
+
== os.path.normcase(owned[0])
|
|
555
|
+
and scan.seg == owned[1]):
|
|
556
|
+
owner = scan
|
|
557
|
+
break
|
|
558
|
+
label = doc.add_label(saved.get("text", "Label"),
|
|
559
|
+
float(saved.get("x", 0.5)),
|
|
560
|
+
float(saved.get("y", 0.5)), owner)
|
|
561
|
+
label.colour = saved.get("colour", "auto")
|
|
562
|
+
label.size = _chosen(saved.get("size"), "label_size", version)
|
|
563
|
+
label.bold = bool(saved.get("bold", False))
|
|
564
|
+
label.visible = bool(saved.get("visible", True))
|
|
565
|
+
label.space = saved.get("space", label.space)
|
|
566
|
+
label.anchor = saved.get("anchor", label.anchor)
|
|
567
|
+
label.rotation = float(_number_or_none(saved.get("rotation")) or 0.0)
|
|
568
|
+
label.z = _number_or_none(saved.get("z"))
|
|
569
|
+
# Where its scan stood when it was placed. A session from before
|
|
570
|
+
# labels followed their scan has none: it stays where it was drawn.
|
|
571
|
+
followed = _number_or_none(saved.get("parent_offset"))
|
|
572
|
+
if owner is not None and followed is not None:
|
|
573
|
+
label.parent_offset = followed
|
|
574
|
+
# A note's arrow: the point it names, [degC, heat flow].
|
|
575
|
+
leader = saved.get("leader")
|
|
576
|
+
if (isinstance(leader, (list, tuple)) and len(leader) == 2
|
|
577
|
+
and all(_number_or_none(v) is not None for v in leader)):
|
|
578
|
+
label.leader = [float(leader[0]), float(leader[1])]
|
|
579
|
+
start = saved.get("leader_from", "auto")
|
|
580
|
+
label.leader_from = start if start in model.ANCHORS else "auto"
|
|
581
|
+
label.leader_colour = saved.get("leader_colour") or "auto"
|
|
582
|
+
flush = saved.get("flush")
|
|
583
|
+
label.flush = flush if flush in ("left", "right", "center") else None
|
|
584
|
+
label.vline = _number_or_none(saved.get("vline"))
|
|
585
|
+
# Hanging from its curve; an older one is attached
|
|
586
|
+
# where it is drawn once the session is on screen.
|
|
587
|
+
at = saved.get("at")
|
|
588
|
+
if (owner is not None and isinstance(at, (list, tuple))
|
|
589
|
+
and len(at) == 2 and at[0] == "i"
|
|
590
|
+
and _number_or_none(at[1]) is not None):
|
|
591
|
+
label.at = ("i", int(at[1]))
|
|
592
|
+
label.dx = float(_number_or_none(saved.get("dx")) or 0.0)
|
|
593
|
+
label.dy = _number_or_none(saved.get("dy"))
|
|
594
|
+
label.line_dashed = bool(saved.get("line_dashed", True))
|
|
595
|
+
for saved in state.get("structures") or []:
|
|
596
|
+
if not saved.get("atoms"):
|
|
597
|
+
continue
|
|
598
|
+
structure = model.MoleculeArtist(
|
|
599
|
+
doc._next_id(), saved.get("smiles", ""),
|
|
600
|
+
{"atoms": saved["atoms"], "bonds": saved.get("bonds", [])},
|
|
601
|
+
float(saved.get("x", 0.5)), float(saved.get("y", 0.5)))
|
|
602
|
+
for name in ("space", "anchor", "colour"):
|
|
603
|
+
if saved.get(name) is not None:
|
|
604
|
+
setattr(structure, name, saved[name])
|
|
605
|
+
for name in ("rotation", "bond_length", "bond_width", "label_size"):
|
|
606
|
+
value = _number_or_none(saved.get(name))
|
|
607
|
+
if value is not None:
|
|
608
|
+
setattr(structure, name, value)
|
|
609
|
+
structure.z = _number_or_none(saved.get("z"))
|
|
610
|
+
structure.visible = bool(saved.get("visible", True))
|
|
611
|
+
structure.upright_labels = bool(saved.get("upright_labels", True))
|
|
612
|
+
structure.label_font = saved.get("label_font") or None
|
|
613
|
+
structure.colour_by_element = bool(saved.get("colour_by_element",
|
|
614
|
+
False))
|
|
615
|
+
doc.structures.append(structure)
|
|
616
|
+
for saved in state.get("images") or []:
|
|
617
|
+
if not saved.get("png"):
|
|
618
|
+
continue
|
|
619
|
+
image = model.ImageArtist(doc._next_id(), saved["png"],
|
|
620
|
+
float(saved.get("x", 0.5)),
|
|
621
|
+
float(saved.get("y", 0.5)),
|
|
622
|
+
float(saved.get("width", 160.0)))
|
|
623
|
+
image.space = saved.get("space", image.space)
|
|
624
|
+
image.anchor = saved.get("anchor", image.anchor)
|
|
625
|
+
image.rotation = float(_number_or_none(saved.get("rotation")) or 0.0)
|
|
626
|
+
image.z = _number_or_none(saved.get("z"))
|
|
627
|
+
image.visible = bool(saved.get("visible", True))
|
|
628
|
+
image.mirror_h = bool(saved.get("mirror_h", False))
|
|
629
|
+
image.mirror_v = bool(saved.get("mirror_v", False))
|
|
630
|
+
doc.images.append(image)
|
|
631
|
+
for name, value in (state.get("legend") or {}).items():
|
|
632
|
+
if hasattr(doc.legend, name):
|
|
633
|
+
setattr(doc.legend, name, value)
|
|
634
|
+
doc.legend.rotation = float(
|
|
635
|
+
_number_or_none((state.get("legend") or {}).get("rotation")) or 0.0)
|
|
636
|
+
doc.legend.z = _number_or_none((state.get("legend") or {}).get("z"))
|
|
637
|
+
if version < 5:
|
|
638
|
+
# The frame was on by default before version 5, so an older file's
|
|
639
|
+
# True was the default and not a choice; it follows the new one.
|
|
640
|
+
doc.legend.show_frame = False
|
|
641
|
+
doc.legend.size = _chosen((state.get("legend") or {}).get("size"),
|
|
642
|
+
"legend_size", version)
|
|
643
|
+
doc.x_axis = state.get("x_axis", doc.x_axis)
|
|
644
|
+
if state.get("x_unit") in units.TEMPERATURE_UNITS:
|
|
645
|
+
doc.x_unit = state["x_unit"]
|
|
646
|
+
doc.y_unit = state.get("y_unit", doc.y_unit)
|
|
647
|
+
if state.get("weight_unit") in model.WEIGHT_UNITS:
|
|
648
|
+
doc.weight_unit = state["weight_unit"]
|
|
649
|
+
if state.get("dtg_unit") in dtg.UNITS:
|
|
650
|
+
doc.dtg_unit = state["dtg_unit"]
|
|
651
|
+
doc.theme = state.get("theme", doc.theme)
|
|
652
|
+
background = state.get("background")
|
|
653
|
+
# Version 7 placed decorators in the home frame (they followed the
|
|
654
|
+
# zoom, the only way then); 8 says which. The window converts a
|
|
655
|
+
# version-7 figure's places to the page's once it is on screen.
|
|
656
|
+
doc.follow_zoom = bool(state.get("follow_zoom", False))
|
|
657
|
+
doc.background = (str(background) if isinstance(background, str)
|
|
658
|
+
and background.startswith("#") else None)
|
|
659
|
+
doc.offset_markers = bool(state.get("offset_markers", False))
|
|
660
|
+
doc.view = _view_from(state.get("view"))
|
|
661
|
+
if doc.view and doc.view.get("y2") and not doc.view.get("y2_unit"):
|
|
662
|
+
# Written with the range and not its unit: it was the weight unit
|
|
663
|
+
# saved with it (a change of unit fits the range
|
|
664
|
+
# again, so a range is always in the unit in force).
|
|
665
|
+
doc.view["y2_unit"] = doc.weight_unit
|
|
666
|
+
doc.path = str(path)
|
|
667
|
+
return doc, problems
|