figrecipe 0.5.0__py3-none-any.whl → 0.7.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figrecipe/__init__.py +220 -819
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +29 -0
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +64 -0
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
- figrecipe/_editor/__init__.py +278 -0
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +258 -0
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +318 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +199 -0
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +152 -0
- figrecipe/_editor/_templates/_html.py +502 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +33 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +92 -110
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +114 -57
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +6 -4
- figrecipe/_utils/_crop.py +10 -4
- figrecipe/_utils/_image_diff.py +37 -33
- figrecipe/_utils/_numpy_io.py +0 -1
- figrecipe/_utils/_units.py +11 -3
- figrecipe/_validator.py +12 -3
- figrecipe/_wrappers/_axes.py +193 -170
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +277 -18
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +12 -11
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +60 -202
- figrecipe/styles/_style_loader.py +73 -121
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
- figrecipe/styles/presets/SCITEX.yaml +181 -0
- figrecipe-0.7.4.dist-info/METADATA +429 -0
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_reproducer.py +0 -358
- figrecipe-0.5.0.dist-info/METADATA +0 -336
- figrecipe-0.5.0.dist-info/RECORD +0 -26
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Style override management with separation of base and manual styles.
|
|
5
|
+
|
|
6
|
+
This module handles the layered style system:
|
|
7
|
+
1. Base style (from preset like SCITEX)
|
|
8
|
+
2. Programmatic style (from code, e.g., fr.load_style())
|
|
9
|
+
3. Manual overrides (from GUI editor)
|
|
10
|
+
|
|
11
|
+
Manual overrides are stored separately with timestamps, allowing:
|
|
12
|
+
- Restoration to original (programmatic) style
|
|
13
|
+
- Tracking when manual edits were made
|
|
14
|
+
- Comparison between original and edited versions
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Dict, Optional
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class StyleOverrides:
|
|
26
|
+
"""
|
|
27
|
+
Manages layered style overrides with timestamp tracking.
|
|
28
|
+
|
|
29
|
+
Attributes
|
|
30
|
+
----------
|
|
31
|
+
base_style : dict
|
|
32
|
+
Original style from preset (e.g., SCITEX values).
|
|
33
|
+
programmatic_style : dict
|
|
34
|
+
Style set programmatically via code.
|
|
35
|
+
manual_overrides : dict
|
|
36
|
+
Overrides from GUI editor.
|
|
37
|
+
call_overrides : dict
|
|
38
|
+
Call parameter overrides {call_id: {param: value}}.
|
|
39
|
+
base_timestamp : str
|
|
40
|
+
When base style was set (ISO format).
|
|
41
|
+
manual_timestamp : str
|
|
42
|
+
When manual overrides were last modified.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
base_style: Dict[str, Any] = field(default_factory=dict)
|
|
46
|
+
programmatic_style: Dict[str, Any] = field(default_factory=dict)
|
|
47
|
+
manual_overrides: Dict[str, Any] = field(default_factory=dict)
|
|
48
|
+
call_overrides: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
|
49
|
+
base_timestamp: Optional[str] = None
|
|
50
|
+
manual_timestamp: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
def get_effective_style(self) -> Dict[str, Any]:
|
|
53
|
+
"""
|
|
54
|
+
Get the final effective style by merging all layers.
|
|
55
|
+
|
|
56
|
+
Priority: base < programmatic < manual
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
dict
|
|
61
|
+
Merged style dictionary.
|
|
62
|
+
"""
|
|
63
|
+
result = {}
|
|
64
|
+
result.update(self.base_style)
|
|
65
|
+
result.update(self.programmatic_style)
|
|
66
|
+
result.update(self.manual_overrides)
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
def get_original_style(self) -> Dict[str, Any]:
|
|
70
|
+
"""
|
|
71
|
+
Get style without manual overrides (for "Restore" function).
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
dict
|
|
76
|
+
Style with only base + programmatic layers.
|
|
77
|
+
"""
|
|
78
|
+
result = {}
|
|
79
|
+
result.update(self.base_style)
|
|
80
|
+
result.update(self.programmatic_style)
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
def set_manual_override(self, key: str, value: Any) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Set a single manual override.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
key : str
|
|
90
|
+
Style property key (e.g., 'axes_width_mm').
|
|
91
|
+
value : Any
|
|
92
|
+
Property value.
|
|
93
|
+
"""
|
|
94
|
+
self.manual_overrides[key] = value
|
|
95
|
+
self.manual_timestamp = datetime.now().isoformat()
|
|
96
|
+
|
|
97
|
+
def update_manual_overrides(self, overrides: Dict[str, Any]) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Update multiple manual overrides at once.
|
|
100
|
+
|
|
101
|
+
Parameters
|
|
102
|
+
----------
|
|
103
|
+
overrides : dict
|
|
104
|
+
Dictionary of overrides to apply.
|
|
105
|
+
"""
|
|
106
|
+
self.manual_overrides.update(overrides)
|
|
107
|
+
self.manual_timestamp = datetime.now().isoformat()
|
|
108
|
+
|
|
109
|
+
def clear_manual_overrides(self, clear_call_overrides: bool = True) -> None:
|
|
110
|
+
"""Clear all manual overrides (restore to original).
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
clear_call_overrides : bool
|
|
115
|
+
Also clear call parameter overrides (default True).
|
|
116
|
+
"""
|
|
117
|
+
self.manual_overrides = {}
|
|
118
|
+
if clear_call_overrides:
|
|
119
|
+
self.call_overrides = {}
|
|
120
|
+
self.manual_timestamp = None
|
|
121
|
+
|
|
122
|
+
def has_manual_overrides(self) -> bool:
|
|
123
|
+
"""Check if any manual overrides exist."""
|
|
124
|
+
return len(self.manual_overrides) > 0
|
|
125
|
+
|
|
126
|
+
def get_diff(self) -> Dict[str, Dict[str, Any]]:
|
|
127
|
+
"""
|
|
128
|
+
Get differences between original and manual overrides.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
dict
|
|
133
|
+
Dictionary showing changes: {key: {'original': val, 'manual': val}}
|
|
134
|
+
"""
|
|
135
|
+
original = self.get_original_style()
|
|
136
|
+
diff = {}
|
|
137
|
+
|
|
138
|
+
for key, manual_val in self.manual_overrides.items():
|
|
139
|
+
original_val = original.get(key)
|
|
140
|
+
if original_val != manual_val:
|
|
141
|
+
diff[key] = {
|
|
142
|
+
"original": original_val,
|
|
143
|
+
"manual": manual_val,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return diff
|
|
147
|
+
|
|
148
|
+
def set_call_override(self, call_id: str, param: str, value: Any) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Set a call parameter override.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
call_id : str
|
|
155
|
+
ID of the call (e.g., 'bp1', 'vp1').
|
|
156
|
+
param : str
|
|
157
|
+
Parameter name.
|
|
158
|
+
value : Any
|
|
159
|
+
Parameter value.
|
|
160
|
+
"""
|
|
161
|
+
if call_id not in self.call_overrides:
|
|
162
|
+
self.call_overrides[call_id] = {}
|
|
163
|
+
self.call_overrides[call_id][param] = value
|
|
164
|
+
self.manual_timestamp = datetime.now().isoformat()
|
|
165
|
+
|
|
166
|
+
def get_call_overrides(self, call_id: str) -> Dict[str, Any]:
|
|
167
|
+
"""Get all overrides for a specific call."""
|
|
168
|
+
return self.call_overrides.get(call_id, {})
|
|
169
|
+
|
|
170
|
+
def has_call_overrides(self) -> bool:
|
|
171
|
+
"""Check if any call overrides exist."""
|
|
172
|
+
return len(self.call_overrides) > 0
|
|
173
|
+
|
|
174
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
175
|
+
"""Convert to dictionary for serialization."""
|
|
176
|
+
return {
|
|
177
|
+
"base_style": self.base_style,
|
|
178
|
+
"programmatic_style": self.programmatic_style,
|
|
179
|
+
"manual_overrides": self.manual_overrides,
|
|
180
|
+
"call_overrides": self.call_overrides,
|
|
181
|
+
"base_timestamp": self.base_timestamp,
|
|
182
|
+
"manual_timestamp": self.manual_timestamp,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_dict(cls, data: Dict[str, Any]) -> "StyleOverrides":
|
|
187
|
+
"""Create from dictionary."""
|
|
188
|
+
return cls(
|
|
189
|
+
base_style=data.get("base_style", {}),
|
|
190
|
+
programmatic_style=data.get("programmatic_style", {}),
|
|
191
|
+
manual_overrides=data.get("manual_overrides", {}),
|
|
192
|
+
call_overrides=data.get("call_overrides", {}),
|
|
193
|
+
base_timestamp=data.get("base_timestamp"),
|
|
194
|
+
manual_timestamp=data.get("manual_timestamp"),
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def get_overrides_path(recipe_path: Path) -> Path:
|
|
199
|
+
"""
|
|
200
|
+
Get the path for storing overrides alongside a recipe.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
recipe_path : Path
|
|
205
|
+
Path to the recipe file.
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
Path
|
|
210
|
+
Path to the overrides file.
|
|
211
|
+
|
|
212
|
+
Examples
|
|
213
|
+
--------
|
|
214
|
+
>>> get_overrides_path(Path('figure.yaml'))
|
|
215
|
+
Path('figure.overrides.json')
|
|
216
|
+
"""
|
|
217
|
+
return recipe_path.with_suffix(".overrides.json")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def save_overrides(
|
|
221
|
+
overrides: StyleOverrides,
|
|
222
|
+
path: Path,
|
|
223
|
+
) -> Path:
|
|
224
|
+
"""
|
|
225
|
+
Save style overrides to JSON file.
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
overrides : StyleOverrides
|
|
230
|
+
Overrides to save.
|
|
231
|
+
path : Path
|
|
232
|
+
Path to save to (or recipe path to derive from).
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
Path
|
|
237
|
+
Path where overrides were saved.
|
|
238
|
+
"""
|
|
239
|
+
# If path is a recipe, derive overrides path
|
|
240
|
+
if path.suffix in (".yaml", ".yml"):
|
|
241
|
+
path = get_overrides_path(path)
|
|
242
|
+
|
|
243
|
+
data = overrides.to_dict()
|
|
244
|
+
data["_version"] = "1.0"
|
|
245
|
+
data["_saved_at"] = datetime.now().isoformat()
|
|
246
|
+
|
|
247
|
+
with open(path, "w") as f:
|
|
248
|
+
json.dump(data, f, indent=2)
|
|
249
|
+
|
|
250
|
+
return path
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def load_overrides(path: Path) -> Optional[StyleOverrides]:
|
|
254
|
+
"""
|
|
255
|
+
Load style overrides from JSON file.
|
|
256
|
+
|
|
257
|
+
Parameters
|
|
258
|
+
----------
|
|
259
|
+
path : Path
|
|
260
|
+
Path to overrides file or recipe file.
|
|
261
|
+
|
|
262
|
+
Returns
|
|
263
|
+
-------
|
|
264
|
+
StyleOverrides or None
|
|
265
|
+
Loaded overrides, or None if file doesn't exist.
|
|
266
|
+
"""
|
|
267
|
+
# If path is a recipe, derive overrides path
|
|
268
|
+
if path.suffix in (".yaml", ".yml"):
|
|
269
|
+
path = get_overrides_path(path)
|
|
270
|
+
|
|
271
|
+
if not path.exists():
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
with open(path) as f:
|
|
275
|
+
data = json.load(f)
|
|
276
|
+
|
|
277
|
+
# Remove metadata
|
|
278
|
+
data.pop("_version", None)
|
|
279
|
+
data.pop("_saved_at", None)
|
|
280
|
+
|
|
281
|
+
return StyleOverrides.from_dict(data)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def create_overrides_from_style(
|
|
285
|
+
base_style: Optional[Dict[str, Any]] = None,
|
|
286
|
+
programmatic_style: Optional[Dict[str, Any]] = None,
|
|
287
|
+
) -> StyleOverrides:
|
|
288
|
+
"""
|
|
289
|
+
Create StyleOverrides from existing style configuration.
|
|
290
|
+
|
|
291
|
+
Parameters
|
|
292
|
+
----------
|
|
293
|
+
base_style : dict, optional
|
|
294
|
+
Base style from preset.
|
|
295
|
+
programmatic_style : dict, optional
|
|
296
|
+
Style set via code.
|
|
297
|
+
|
|
298
|
+
Returns
|
|
299
|
+
-------
|
|
300
|
+
StyleOverrides
|
|
301
|
+
New overrides object.
|
|
302
|
+
"""
|
|
303
|
+
return StyleOverrides(
|
|
304
|
+
base_style=base_style or {},
|
|
305
|
+
programmatic_style=programmatic_style or {},
|
|
306
|
+
manual_overrides={},
|
|
307
|
+
base_timestamp=datetime.now().isoformat(),
|
|
308
|
+
manual_timestamp=None,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
__all__ = [
|
|
313
|
+
"StyleOverrides",
|
|
314
|
+
"get_overrides_path",
|
|
315
|
+
"save_overrides",
|
|
316
|
+
"load_overrides",
|
|
317
|
+
"create_overrides_from_style",
|
|
318
|
+
]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""User preferences management for the figure editor.
|
|
4
|
+
|
|
5
|
+
Preferences are stored in ~/.figrecipe/preferences.json and persist
|
|
6
|
+
across sessions. This allows users to set defaults like dark mode.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict
|
|
12
|
+
|
|
13
|
+
# Default preferences
|
|
14
|
+
DEFAULT_PREFERENCES = {
|
|
15
|
+
"dark_mode": False,
|
|
16
|
+
"default_style": "SCITEX",
|
|
17
|
+
"auto_save": True,
|
|
18
|
+
"show_hit_regions": False,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Preferences file location
|
|
22
|
+
PREFERENCES_DIR = Path.home() / ".figrecipe"
|
|
23
|
+
PREFERENCES_FILE = PREFERENCES_DIR / "preferences.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_preferences_path() -> Path:
|
|
27
|
+
"""Get the path to the preferences file."""
|
|
28
|
+
return PREFERENCES_FILE
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_preferences() -> Dict[str, Any]:
|
|
32
|
+
"""Load user preferences from disk.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
dict
|
|
37
|
+
User preferences merged with defaults.
|
|
38
|
+
"""
|
|
39
|
+
prefs = DEFAULT_PREFERENCES.copy()
|
|
40
|
+
|
|
41
|
+
if PREFERENCES_FILE.exists():
|
|
42
|
+
try:
|
|
43
|
+
with open(PREFERENCES_FILE, "r") as f:
|
|
44
|
+
saved = json.load(f)
|
|
45
|
+
prefs.update(saved)
|
|
46
|
+
except (json.JSONDecodeError, IOError):
|
|
47
|
+
# If file is corrupted, use defaults
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
return prefs
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def save_preferences(prefs: Dict[str, Any]) -> bool:
|
|
54
|
+
"""Save user preferences to disk.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
prefs : dict
|
|
59
|
+
Preferences to save.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
bool
|
|
64
|
+
True if save was successful.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
PREFERENCES_DIR.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
with open(PREFERENCES_FILE, "w") as f:
|
|
69
|
+
json.dump(prefs, f, indent=2)
|
|
70
|
+
return True
|
|
71
|
+
except IOError:
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_preference(key: str, default: Any = None) -> Any:
|
|
76
|
+
"""Get a single preference value.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
key : str
|
|
81
|
+
Preference key.
|
|
82
|
+
default : Any, optional
|
|
83
|
+
Default value if key not found.
|
|
84
|
+
|
|
85
|
+
Returns
|
|
86
|
+
-------
|
|
87
|
+
Any
|
|
88
|
+
Preference value.
|
|
89
|
+
"""
|
|
90
|
+
prefs = load_preferences()
|
|
91
|
+
return prefs.get(key, default)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def set_preference(key: str, value: Any) -> bool:
|
|
95
|
+
"""Set a single preference value.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
key : str
|
|
100
|
+
Preference key.
|
|
101
|
+
value : Any
|
|
102
|
+
Value to set.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
bool
|
|
107
|
+
True if save was successful.
|
|
108
|
+
"""
|
|
109
|
+
prefs = load_preferences()
|
|
110
|
+
prefs[key] = value
|
|
111
|
+
return save_preferences(prefs)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def reset_preferences() -> bool:
|
|
115
|
+
"""Reset all preferences to defaults.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
bool
|
|
120
|
+
True if reset was successful.
|
|
121
|
+
"""
|
|
122
|
+
return save_preferences(DEFAULT_PREFERENCES.copy())
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = [
|
|
126
|
+
"DEFAULT_PREFERENCES",
|
|
127
|
+
"get_preferences_path",
|
|
128
|
+
"load_preferences",
|
|
129
|
+
"save_preferences",
|
|
130
|
+
"get_preference",
|
|
131
|
+
"set_preference",
|
|
132
|
+
"reset_preferences",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# EOF
|