majoplot 0.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.
- majoplot/__init__.py +0 -0
- majoplot/__main__.py +25 -0
- majoplot/app/__init__.py +0 -0
- majoplot/app/cli.py +259 -0
- majoplot/app/gui.py +6 -0
- majoplot/config.json +11 -0
- majoplot/domain/base.py +433 -0
- majoplot/domain/importers/PPMS_Resistivity.py +128 -0
- majoplot/domain/importers/VSM.py +109 -0
- majoplot/domain/importers/XRD.py +62 -0
- majoplot/domain/muti_axes_spec.py +172 -0
- majoplot/domain/scenarios/PPMS_Resistivity/RT.py +119 -0
- majoplot/domain/scenarios/VSM/MT.py +131 -0
- majoplot/domain/scenarios/VSM/MT_insert.py +135 -0
- majoplot/domain/scenarios/VSM/MT_reliability_analysis.py +145 -0
- majoplot/domain/scenarios/XRD/Compare.py +104 -0
- majoplot/domain/utils.py +87 -0
- majoplot/gui/__init__.py +0 -0
- majoplot/gui/main.py +529 -0
- majoplot/infra/plotters/matplot.py +337 -0
- majoplot/infra/plotters/origin.py +1006 -0
- majoplot/infra/plotters/origin_utils/originlab_type_library.py +403 -0
- majoplot-0.1.0.dist-info/METADATA +81 -0
- majoplot-0.1.0.dist-info/RECORD +27 -0
- majoplot-0.1.0.dist-info/WHEEL +4 -0
- majoplot-0.1.0.dist-info/entry_points.txt +2 -0
- majoplot-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from itertools import cycle
|
|
4
|
+
from typing import Iterable, Optional, Sequence, Tuple
|
|
5
|
+
|
|
6
|
+
import matplotlib.figure as mfig
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
|
|
9
|
+
from ...domain.base import *
|
|
10
|
+
from ...domain.muti_axes_spec import *
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Inset axes should look slightly lighter/smaller than the main axes.
|
|
14
|
+
# Only scale what the user explicitly requested: marker, linewidth, axis-title fontsize.
|
|
15
|
+
INSET_STYLE_SCALE = 0.75
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _nonempty_cycle(values: Sequence, fallback: Sequence) -> Iterable:
|
|
19
|
+
"""Return an infinite cycle; if values is empty, cycle fallback instead."""
|
|
20
|
+
if values:
|
|
21
|
+
return cycle(values)
|
|
22
|
+
return cycle(fallback)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _safe_arrowstyle(value: object) -> str:
|
|
26
|
+
"""ArrowSpec.arrowstyle may accidentally be a 1-tuple due to a trailing comma."""
|
|
27
|
+
if isinstance(value, tuple) and len(value) == 1:
|
|
28
|
+
return str(value[0])
|
|
29
|
+
return str(value)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _apply_ticks(ax: plt.Axes, tick: TickSpec, labelsize: float) -> None:
|
|
33
|
+
"""Apply TickSpec to an Axes."""
|
|
34
|
+
ax.tick_params(
|
|
35
|
+
axis=tick.axis,
|
|
36
|
+
direction=tick.direction,
|
|
37
|
+
length=tick.length,
|
|
38
|
+
width=tick.width,
|
|
39
|
+
top=tick.top,
|
|
40
|
+
left=tick.left,
|
|
41
|
+
labelsize=labelsize,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _apply_grid(ax: plt.Axes, grid: GridSpec, which: str) -> None:
|
|
46
|
+
"""Apply GridSpec to an Axes."""
|
|
47
|
+
ax.grid(
|
|
48
|
+
True,
|
|
49
|
+
which=which,
|
|
50
|
+
linestyle=grid.linestyle,
|
|
51
|
+
linewidth=grid.linewidth,
|
|
52
|
+
color=grid.color,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _draw_axes_legend(
|
|
57
|
+
ax: plt.Axes,
|
|
58
|
+
handles: list,
|
|
59
|
+
labels: list,
|
|
60
|
+
legend: Optional[LegendSpec],
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Draw a legend on an Axes using an optional LegendSpec."""
|
|
63
|
+
if not handles:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if legend is None:
|
|
67
|
+
ax.legend(handles, labels)
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
ax.legend(
|
|
71
|
+
handles,
|
|
72
|
+
labels,
|
|
73
|
+
loc=legend.loc,
|
|
74
|
+
bbox_to_anchor=(legend.anchor_x, legend.anchor_y),
|
|
75
|
+
fontsize=legend.fontsize,
|
|
76
|
+
frameon=legend.frameon,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _draw_figure_legend(
|
|
81
|
+
matfig: mfig.Figure,
|
|
82
|
+
handles: list,
|
|
83
|
+
labels: list,
|
|
84
|
+
legend: Optional[LegendSpec],
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Draw a figure-level legend using an optional LegendSpec."""
|
|
87
|
+
if not handles:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if legend is None:
|
|
91
|
+
matfig.legend(handles, labels)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
matfig.legend(
|
|
95
|
+
handles,
|
|
96
|
+
labels,
|
|
97
|
+
loc=legend.loc,
|
|
98
|
+
bbox_to_anchor=(legend.anchor_x, legend.anchor_y),
|
|
99
|
+
fontsize=legend.fontsize,
|
|
100
|
+
frameon=legend.frameon,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _maybe_set_log_scale(ax: plt.Axes, axes_obj: Axes) -> None:
|
|
105
|
+
"""Enable log scale when limits are positive and span exceeds thresholds."""
|
|
106
|
+
xl = axes_obj.xlim[0]
|
|
107
|
+
xr = axes_obj.xlim[1]
|
|
108
|
+
yl = axes_obj.ylim[0]
|
|
109
|
+
yr = axes_obj.ylim[1]
|
|
110
|
+
x_span_min = axes_obj.spec.x_log_scale_min_span
|
|
111
|
+
y_span_min = axes_obj.spec.y_log_scale_min_span
|
|
112
|
+
|
|
113
|
+
if xl > 0 and xr > 0 and (xr / xl) >= x_span_min:
|
|
114
|
+
ax.set_xscale("log")
|
|
115
|
+
|
|
116
|
+
if yl > 0 and yr > 0 and (yr / yl) >= y_span_min:
|
|
117
|
+
ax.set_yscale("log")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def plot(myfig: Figure) -> mfig.Figure:
|
|
121
|
+
"""
|
|
122
|
+
Render a FigureSpec-driven Figure into a matplotlib Figure (OO-style).
|
|
123
|
+
|
|
124
|
+
Guarantees:
|
|
125
|
+
- All drawing-related Specs in base.py are respected
|
|
126
|
+
- Global style cycles are shared across all Axes in the same Figure
|
|
127
|
+
- If there are more series than cycle elements, cycles repeat automatically
|
|
128
|
+
- Returns matplotlib.figure.Figure without saving
|
|
129
|
+
"""
|
|
130
|
+
# Ensure every Data has an overall label (used for legends)
|
|
131
|
+
fig_spec: FigureSpec = myfig.spec
|
|
132
|
+
global_fontsize: float = getattr(fig_spec, "global_fontsize", 8.0)
|
|
133
|
+
|
|
134
|
+
# Global style cycles (shared across the entire Figure)
|
|
135
|
+
ls_cycle = _nonempty_cycle(getattr(fig_spec, "linestyle_cycle", ("-",)), ("-",))
|
|
136
|
+
lc_cycle = _nonempty_cycle(getattr(fig_spec, "linecolor_cycle", ("black",)), ("black",))
|
|
137
|
+
mk_cycle = _nonempty_cycle(getattr(fig_spec, "linemarker_cycle", ("o",)), ("o",))
|
|
138
|
+
al_cycle = _nonempty_cycle(getattr(fig_spec, "alpa_cycle", (1.0,)), (1.0,))
|
|
139
|
+
|
|
140
|
+
# ---------- Layout dispatch ----------
|
|
141
|
+
muti_spec = myfig.muti_axes_spec
|
|
142
|
+
mpl_axes: list[plt.Axes] = []
|
|
143
|
+
|
|
144
|
+
if muti_spec is None:
|
|
145
|
+
matfig, ax = plt.subplots(figsize=fig_spec.figsize, dpi=fig_spec.dpi)
|
|
146
|
+
mpl_axes = [ax]
|
|
147
|
+
|
|
148
|
+
elif isinstance(muti_spec, InsertAxesSpec):
|
|
149
|
+
matfig = plt.figure(figsize=fig_spec.figsize, dpi=fig_spec.dpi)
|
|
150
|
+
main_ax = matfig.add_subplot(111)
|
|
151
|
+
inset_ax = main_ax.inset_axes([muti_spec.x, muti_spec.y, muti_spec.width, muti_spec.height])
|
|
152
|
+
mpl_axes = [main_ax, inset_ax]
|
|
153
|
+
|
|
154
|
+
elif isinstance(muti_spec, DualYAxesSpec):
|
|
155
|
+
matfig = plt.figure(figsize=fig_spec.figsize, dpi=fig_spec.dpi)
|
|
156
|
+
left_ax = matfig.add_subplot(111)
|
|
157
|
+
right_ax = left_ax.twinx()
|
|
158
|
+
mpl_axes = [left_ax, right_ax]
|
|
159
|
+
|
|
160
|
+
elif isinstance(muti_spec, StackAxesSpec):
|
|
161
|
+
matfig, axes = plt.subplots(
|
|
162
|
+
muti_spec.nrows,
|
|
163
|
+
muti_spec.ncols,
|
|
164
|
+
figsize=fig_spec.figsize,
|
|
165
|
+
dpi=fig_spec.dpi,
|
|
166
|
+
)
|
|
167
|
+
mpl_axes = [axes] if isinstance(axes, plt.Axes) else list(axes.flatten())
|
|
168
|
+
|
|
169
|
+
else:
|
|
170
|
+
raise TypeError(f"Unsupported MutiAxesSpec: {type(muti_spec)}")
|
|
171
|
+
|
|
172
|
+
# Figure-level properties
|
|
173
|
+
matfig.set_facecolor(fig_spec.facecolor)
|
|
174
|
+
if getattr(fig_spec, "title", None):
|
|
175
|
+
matfig.suptitle(fig_spec.title, fontsize=global_fontsize)
|
|
176
|
+
|
|
177
|
+
per_ax_info: list[Tuple[plt.Axes, list, list, AxesSpec]] = []
|
|
178
|
+
|
|
179
|
+
# ---------- Draw each Axes ----------
|
|
180
|
+
for ax_idx, (ax, axes_obj) in enumerate(zip(mpl_axes, myfig.axes_pool)):
|
|
181
|
+
axes_spec: AxesSpec = axes_obj.spec
|
|
182
|
+
|
|
183
|
+
# for stack, every axes has its own cycle
|
|
184
|
+
if isinstance(myfig.muti_axes_spec, StackAxesSpec):
|
|
185
|
+
ls_cycle = _nonempty_cycle(getattr(fig_spec, "linestyle_cycle", ("-",)), ("-",))
|
|
186
|
+
lc_cycle = _nonempty_cycle(getattr(fig_spec, "linecolor_cycle", ("black",)), ("black",))
|
|
187
|
+
mk_cycle = _nonempty_cycle(getattr(fig_spec, "linemarker_cycle", ("o",)), ("o",))
|
|
188
|
+
al_cycle = _nonempty_cycle(getattr(fig_spec, "alpa_cycle", (1.0,)), (1.0,))
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# Determine if this axes is an inset axes that should be slightly scaled down
|
|
192
|
+
is_inset = isinstance(muti_spec, InsertAxesSpec) and (ax_idx == 1)
|
|
193
|
+
style_scale = INSET_STYLE_SCALE if is_inset else 1.0
|
|
194
|
+
|
|
195
|
+
# Apply per-data plotting
|
|
196
|
+
for data in axes_obj.data_pool:
|
|
197
|
+
if getattr(data, "ignore", False):
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
points = data.points_for_plot
|
|
201
|
+
|
|
202
|
+
# Default x/y selection: column 0 and 1
|
|
203
|
+
xs = points[:,0]
|
|
204
|
+
ys = points[:,1]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
ls = next(ls_cycle)
|
|
208
|
+
lc = next(lc_cycle)
|
|
209
|
+
mk = next(mk_cycle) # still consume, even if not used (keeps cycles aligned)
|
|
210
|
+
al = next(al_cycle)
|
|
211
|
+
|
|
212
|
+
lw = getattr(axes_spec, "linewidth", 1.0) * style_scale
|
|
213
|
+
ms = getattr(axes_spec, "marker_size", 4.0) * style_scale
|
|
214
|
+
|
|
215
|
+
if ls == "|":
|
|
216
|
+
# Stick plot: draw a vertical line at each x, from baseline to y.
|
|
217
|
+
baseline = axes_spec.y_left_lim if axes_spec.y_left_lim is not None else 0.0
|
|
218
|
+
ax.vlines(
|
|
219
|
+
xs,
|
|
220
|
+
baseline,
|
|
221
|
+
ys,
|
|
222
|
+
colors=lc,
|
|
223
|
+
linestyles="--",
|
|
224
|
+
linewidth=lw,
|
|
225
|
+
alpha=al,
|
|
226
|
+
label=data.labels.brief_summary,
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
ax.plot(
|
|
230
|
+
xs,
|
|
231
|
+
ys,
|
|
232
|
+
linestyle=ls,
|
|
233
|
+
color=lc,
|
|
234
|
+
marker=mk,
|
|
235
|
+
alpha=al,
|
|
236
|
+
linewidth=lw,
|
|
237
|
+
markersize=ms,
|
|
238
|
+
label=data.labels.brief_summary,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# Axis labels (axis-title fontsize should be slightly smaller on inset axes)
|
|
243
|
+
axis_title_fs = getattr(axes_spec, "axis_title_font_size", global_fontsize) * style_scale
|
|
244
|
+
ax.set_xlabel(axes_spec.x_axis_title, fontsize=axis_title_fs)
|
|
245
|
+
ax.set_ylabel(axes_spec.y_axis_title, fontsize=axis_title_fs)
|
|
246
|
+
|
|
247
|
+
# Limits
|
|
248
|
+
if axes_spec.x_left_lim is not None or axes_spec.x_right_lim is not None:
|
|
249
|
+
ax.set_xlim(axes_spec.x_left_lim, axes_spec.x_right_lim)
|
|
250
|
+
if axes_spec.y_left_lim is not None or axes_spec.y_right_lim is not None:
|
|
251
|
+
ax.set_ylim(axes_spec.y_left_lim, axes_spec.y_right_lim)
|
|
252
|
+
|
|
253
|
+
# Log scale
|
|
254
|
+
_maybe_set_log_scale(ax, axes_obj)
|
|
255
|
+
|
|
256
|
+
# Ticks
|
|
257
|
+
if getattr(axes_spec, "major_tick", None):
|
|
258
|
+
_apply_ticks(ax, axes_spec.major_tick, global_fontsize)
|
|
259
|
+
else:
|
|
260
|
+
ax.tick_params(labelsize=global_fontsize)
|
|
261
|
+
|
|
262
|
+
if getattr(axes_spec, "minor_tick", None):
|
|
263
|
+
ax.minorticks_on()
|
|
264
|
+
_apply_ticks(ax, axes_spec.minor_tick, global_fontsize)
|
|
265
|
+
|
|
266
|
+
# Grids
|
|
267
|
+
if getattr(axes_spec, "major_grid", None):
|
|
268
|
+
_apply_grid(ax, axes_spec.major_grid, "major")
|
|
269
|
+
if getattr(axes_spec, "minor_grid", None):
|
|
270
|
+
ax.minorticks_on()
|
|
271
|
+
_apply_grid(ax, axes_spec.minor_grid, "minor")
|
|
272
|
+
|
|
273
|
+
# Annotations
|
|
274
|
+
if getattr(axes_spec, "annotation", None):
|
|
275
|
+
for ann in axes_spec.annotation:
|
|
276
|
+
arrowprops = None
|
|
277
|
+
xy = (ann.text_x, ann.text_y)
|
|
278
|
+
|
|
279
|
+
if ann.arrow:
|
|
280
|
+
arrowprops = dict(
|
|
281
|
+
arrowstyle=_safe_arrowstyle(ann.arrow.arrowstyle),
|
|
282
|
+
color=ann.arrow.color,
|
|
283
|
+
linewidth=ann.arrow.linewidth,
|
|
284
|
+
)
|
|
285
|
+
xy = (ann.arrow.point_x, ann.arrow.point_y)
|
|
286
|
+
|
|
287
|
+
ax.annotate(
|
|
288
|
+
ann.text,
|
|
289
|
+
xy=xy,
|
|
290
|
+
xytext=(ann.text_x, ann.text_y),
|
|
291
|
+
fontsize=ann.fontsize,
|
|
292
|
+
arrowprops=arrowprops,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Collect legend handles/labels (legend placement is handled later)
|
|
296
|
+
h, l = ax.get_legend_handles_labels()
|
|
297
|
+
per_ax_info.append((ax, h, l, axes_spec))
|
|
298
|
+
|
|
299
|
+
# ---------- Legend handling ----------
|
|
300
|
+
def resolve_legend_spec(axes_spec: AxesSpec) -> Optional[LegendSpec]:
|
|
301
|
+
# FigureSpec.legend is the global override/fallback (as you required earlier)
|
|
302
|
+
return getattr(axes_spec, "legend", None) or getattr(fig_spec, "legend", None)
|
|
303
|
+
|
|
304
|
+
if muti_spec is None:
|
|
305
|
+
# Default: each subplot draws its own legend
|
|
306
|
+
for ax, h, l, axes_spec in per_ax_info:
|
|
307
|
+
_draw_axes_legend(ax, h, l, resolve_legend_spec(axes_spec))
|
|
308
|
+
|
|
309
|
+
elif isinstance(muti_spec, StackAxesSpec):
|
|
310
|
+
for ax, h, l, axes_spec in per_ax_info:
|
|
311
|
+
_draw_axes_legend(ax, h, l, axes_spec.legend)
|
|
312
|
+
|
|
313
|
+
elif isinstance(muti_spec, InsertAxesSpec):
|
|
314
|
+
# InsertAxesSpec uses exactly the first two Axes for plotting (main + inset)
|
|
315
|
+
holder = muti_spec.legend_holder
|
|
316
|
+
if len(per_ax_info) >= 2:
|
|
317
|
+
(ax0, h0, l0, spec0), (ax1, h1, l1, spec1) = per_ax_info[:2]
|
|
318
|
+
handles = h0 + h1
|
|
319
|
+
labels = l0 + l1
|
|
320
|
+
|
|
321
|
+
if holder == "first axes":
|
|
322
|
+
_draw_axes_legend(ax0, handles, labels, resolve_legend_spec(spec0))
|
|
323
|
+
elif holder == "last axes":
|
|
324
|
+
# IMPORTANT: all legends must appear on the last plotting axes (the inset axes here)
|
|
325
|
+
_draw_axes_legend(ax1, handles, labels, resolve_legend_spec(spec1))
|
|
326
|
+
elif holder == "figure":
|
|
327
|
+
_draw_figure_legend(matfig, handles, labels, getattr(fig_spec, "legend", None))
|
|
328
|
+
|
|
329
|
+
elif isinstance(muti_spec, DualYAxesSpec):
|
|
330
|
+
# DualYAxesSpec: bind x-axis; only first two Axes are used.
|
|
331
|
+
if len(per_ax_info) >= 2:
|
|
332
|
+
(ax0, h0, l0, spec0), (_ax1, h1, l1, _spec1) = per_ax_info[:2]
|
|
333
|
+
handles = h0 + h1
|
|
334
|
+
labels = l0 + l1
|
|
335
|
+
_draw_axes_legend(ax0, handles, labels, resolve_legend_spec(spec0))
|
|
336
|
+
|
|
337
|
+
return matfig
|