scitex 2.4.2__py3-none-any.whl → 2.5.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.
- scitex/__version__.py +1 -1
- scitex/browser/__init__.py +53 -0
- scitex/browser/debugging/__init__.py +56 -0
- scitex/browser/debugging/_failure_capture.py +372 -0
- scitex/browser/debugging/_sync_session.py +259 -0
- scitex/browser/debugging/_test_monitor.py +284 -0
- scitex/browser/debugging/_visual_cursor.py +432 -0
- scitex/io/_load.py +5 -0
- scitex/io/_load_modules/_canvas.py +171 -0
- scitex/io/_save.py +8 -0
- scitex/io/_save_modules/_canvas.py +356 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +77 -22
- scitex/plt/docs/FIGURE_ARCHITECTURE.md +257 -0
- scitex/plt/utils/__init__.py +10 -0
- scitex/plt/utils/_collect_figure_metadata.py +14 -12
- scitex/plt/utils/_csv_column_naming.py +237 -0
- scitex/scholar/citation_graph/database.py +9 -2
- scitex/scholar/config/ScholarConfig.py +23 -3
- scitex/scholar/config/default.yaml +55 -0
- scitex/scholar/core/Paper.py +102 -0
- scitex/scholar/core/__init__.py +44 -0
- scitex/scholar/core/journal_normalizer.py +524 -0
- scitex/scholar/core/oa_cache.py +285 -0
- scitex/scholar/core/open_access.py +457 -0
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +137 -0
- scitex/scholar/pdf_download/strategies/__init__.py +6 -0
- scitex/scholar/pdf_download/strategies/open_access_download.py +186 -0
- scitex/scholar/pipelines/ScholarPipelineSearchParallel.py +18 -3
- scitex/scholar/pipelines/ScholarPipelineSearchSingle.py +15 -2
- scitex/session/_decorator.py +13 -1
- scitex/vis/README.md +246 -615
- scitex/vis/__init__.py +138 -78
- scitex/vis/canvas.py +423 -0
- scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
- scitex/vis/editor/__init__.py +1 -1
- scitex/vis/editor/_dearpygui_editor.py +1830 -0
- scitex/vis/editor/_defaults.py +40 -1
- scitex/vis/editor/_edit.py +54 -18
- scitex/vis/editor/_flask_editor.py +37 -0
- scitex/vis/editor/_qt_editor.py +865 -0
- scitex/vis/editor/flask_editor/__init__.py +21 -0
- scitex/vis/editor/flask_editor/bbox.py +216 -0
- scitex/vis/editor/flask_editor/core.py +152 -0
- scitex/vis/editor/flask_editor/plotter.py +130 -0
- scitex/vis/editor/flask_editor/renderer.py +184 -0
- scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
- scitex/vis/editor/flask_editor/templates/html.py +295 -0
- scitex/vis/editor/flask_editor/templates/scripts.py +614 -0
- scitex/vis/editor/flask_editor/templates/styles.py +549 -0
- scitex/vis/editor/flask_editor/utils.py +81 -0
- scitex/vis/io/__init__.py +84 -21
- scitex/vis/io/canvas.py +226 -0
- scitex/vis/io/data.py +204 -0
- scitex/vis/io/directory.py +202 -0
- scitex/vis/io/export.py +460 -0
- scitex/vis/io/panel.py +424 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/METADATA +9 -2
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/RECORD +61 -32
- scitex/vis/DJANGO_INTEGRATION.md +0 -677
- scitex/vis/editor/_web_editor.py +0 -1440
- scitex/vis/tmp.txt +0 -239
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/WHEEL +0 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.4.2.dist-info → scitex-2.5.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<!-- ---
|
|
2
|
+
!-- Timestamp: 2025-12-08 16:05:55
|
|
3
|
+
!-- Author: ywatanabe
|
|
4
|
+
!-- File: /home/ywatanabe/proj/scitex-code/src/scitex/plt/docs/FIGURE_ARCHITECTURE.md
|
|
5
|
+
!-- --- -->
|
|
6
|
+
|
|
7
|
+
# Figure Architecture for scitex.plt
|
|
8
|
+
|
|
9
|
+
## Terminology
|
|
10
|
+
|
|
11
|
+
| Term | Meaning | In Code |
|
|
12
|
+
|------------|------------------------------|--------------------------------|
|
|
13
|
+
| **Figure** | A matplotlib figure object | `fig, ax = stx.plt.subplots()` |
|
|
14
|
+
| **Axes** | A single subplot/axes | `ax.plot(x, y)` |
|
|
15
|
+
| **Panel** | Used in `scitex.vis` context | See CANVAS_ARCHITECTURE.md |
|
|
16
|
+
|
|
17
|
+
## Output Format
|
|
18
|
+
|
|
19
|
+
`stx.plt` outputs **3 files per figure**:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
output_dir/
|
|
23
|
+
├── 01_plot.png # Rendered image
|
|
24
|
+
├── 01_plot.json # Metadata (dimensions, axes, traces, styles)
|
|
25
|
+
└── 01_plot.csv # Raw data (columns referenced in JSON)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Save Patterns
|
|
29
|
+
|
|
30
|
+
**Flat (default):**
|
|
31
|
+
```python
|
|
32
|
+
fig.savefig("./01_plot.png")
|
|
33
|
+
# Creates: 01_plot.png, 01_plot.json, 01_plot.csv
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Organized by extension:**
|
|
37
|
+
```python
|
|
38
|
+
fig.savefig("./png/01_plot.png")
|
|
39
|
+
# Creates: png/01_plot.png, json/01_plot.json, csv/01_plot.csv
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## JSON Schema (panel.json)
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"metadata_version": "1.1.0",
|
|
47
|
+
"scitex": {
|
|
48
|
+
"version": "2.4.3",
|
|
49
|
+
"created_at": "2025-12-08T15:40:58.453762",
|
|
50
|
+
"created_with": "scitex.plt.subplots (mm-control)",
|
|
51
|
+
"mode": "publication",
|
|
52
|
+
"axes_size_mm": [40, 28],
|
|
53
|
+
"position_in_grid": [0, 0],
|
|
54
|
+
"style_mm": {
|
|
55
|
+
"axis_thickness_mm": 0.2,
|
|
56
|
+
"tick_length_mm": 0.8,
|
|
57
|
+
"tick_thickness_mm": 0.2,
|
|
58
|
+
"trace_thickness_mm": 0.2,
|
|
59
|
+
"marker_size_mm": 0.8,
|
|
60
|
+
"axis_font_size_pt": 7,
|
|
61
|
+
"tick_font_size_pt": 7,
|
|
62
|
+
"title_font_size_pt": 8,
|
|
63
|
+
"legend_font_size_pt": 6,
|
|
64
|
+
"font_family": "Arial",
|
|
65
|
+
"n_ticks": 4
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"matplotlib": {
|
|
69
|
+
"version": "3.10.3"
|
|
70
|
+
},
|
|
71
|
+
"id": "01_plot",
|
|
72
|
+
"dimensions": {
|
|
73
|
+
"figure_size_mm": [80.0, 68.0],
|
|
74
|
+
"figure_size_inch": [3.15, 2.68],
|
|
75
|
+
"figure_size_px": [944, 803],
|
|
76
|
+
"axes_size_mm": [40.0, 28.0],
|
|
77
|
+
"axes_size_inch": [1.57, 1.10],
|
|
78
|
+
"axes_size_px": [472, 330],
|
|
79
|
+
"axes_position": [0.25, 0.29, 0.5, 0.41],
|
|
80
|
+
"dpi": 300
|
|
81
|
+
},
|
|
82
|
+
"margins_mm": {
|
|
83
|
+
"left": 20.0,
|
|
84
|
+
"bottom": 20.0,
|
|
85
|
+
"right": 20.0,
|
|
86
|
+
"top": 20.0
|
|
87
|
+
},
|
|
88
|
+
"axes_bbox_px": {
|
|
89
|
+
"x0": 236,
|
|
90
|
+
"y0": 236,
|
|
91
|
+
"x1": 708,
|
|
92
|
+
"y1": 566,
|
|
93
|
+
"width": 472,
|
|
94
|
+
"height": 330
|
|
95
|
+
},
|
|
96
|
+
"axes_bbox_mm": {
|
|
97
|
+
"x0": 20.0,
|
|
98
|
+
"y0": 20.0,
|
|
99
|
+
"x1": 60.0,
|
|
100
|
+
"y1": 48.0,
|
|
101
|
+
"width": 40.0,
|
|
102
|
+
"height": 28.0
|
|
103
|
+
},
|
|
104
|
+
"axes": {
|
|
105
|
+
"x": {
|
|
106
|
+
"label": "Time",
|
|
107
|
+
"unit": "s",
|
|
108
|
+
"scale": "linear",
|
|
109
|
+
"lim": [-0.31, 6.60],
|
|
110
|
+
"n_ticks": 4
|
|
111
|
+
},
|
|
112
|
+
"y": {
|
|
113
|
+
"label": "Amplitude",
|
|
114
|
+
"unit": "a.u.",
|
|
115
|
+
"scale": "linear",
|
|
116
|
+
"lim": [-1.10, 1.10],
|
|
117
|
+
"n_ticks": 4
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
"title": "ax.plot(x, y)",
|
|
121
|
+
"plot_type": "line",
|
|
122
|
+
"method": "plot",
|
|
123
|
+
"traces": [
|
|
124
|
+
{
|
|
125
|
+
"id": "sine",
|
|
126
|
+
"label": "sin(x)",
|
|
127
|
+
"color": "#0000ff",
|
|
128
|
+
"linestyle": "-",
|
|
129
|
+
"linewidth": 0.57,
|
|
130
|
+
"csv_columns": {
|
|
131
|
+
"x": "ax_00_sine_plot_x",
|
|
132
|
+
"y": "ax_00_sine_plot_y"
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "cosine",
|
|
137
|
+
"label": "cos(x)",
|
|
138
|
+
"color": "#ff0000",
|
|
139
|
+
"linestyle": "--",
|
|
140
|
+
"linewidth": 0.57,
|
|
141
|
+
"csv_columns": {
|
|
142
|
+
"x": "ax_00_cosine_plot_x",
|
|
143
|
+
"y": "ax_00_cosine_plot_y"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
],
|
|
147
|
+
"legend": {
|
|
148
|
+
"visible": true,
|
|
149
|
+
"loc": 0,
|
|
150
|
+
"frameon": false,
|
|
151
|
+
"labels": ["sin(x)", "cos(x)"]
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## CSV Format
|
|
157
|
+
|
|
158
|
+
Column naming convention: `ax_{ax_idx}_{trace_id}_{method}_{dim}`
|
|
159
|
+
|
|
160
|
+
```csv
|
|
161
|
+
ax_00_sine_plot_x,ax_00_sine_plot_y,ax_00_cosine_plot_x,ax_00_cosine_plot_y
|
|
162
|
+
0.0,0.0,0.0,1.0
|
|
163
|
+
0.063,0.063,0.063,0.998
|
|
164
|
+
0.127,0.127,0.127,0.992
|
|
165
|
+
...
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Key Features
|
|
169
|
+
|
|
170
|
+
### Publication Mode
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
fig, ax = stx.plt.subplots(
|
|
174
|
+
fig_mm={"width": 80, "height": 68},
|
|
175
|
+
axes_mm={"width": 40, "height": 28},
|
|
176
|
+
mode="publication"
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
- All dimensions in **millimeters** for publication standards
|
|
181
|
+
- Consistent styling across figures
|
|
182
|
+
- Automatic metadata embedding
|
|
183
|
+
|
|
184
|
+
### Automatic Export
|
|
185
|
+
|
|
186
|
+
On `fig.savefig()`:
|
|
187
|
+
1. PNG/PDF/SVG rendered
|
|
188
|
+
2. JSON metadata exported
|
|
189
|
+
3. CSV data exported
|
|
190
|
+
|
|
191
|
+
### Trace Tracking
|
|
192
|
+
|
|
193
|
+
All plotting calls are tracked:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
ax.plot(x, y, id="sine", label="sin(x)") # Tracked
|
|
197
|
+
ax.scatter(x, y, id="points") # Tracked
|
|
198
|
+
ax.stx_line(x, y, id="trace") # Tracked
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Supported Plot Methods
|
|
202
|
+
|
|
203
|
+
### Standard Matplotlib
|
|
204
|
+
- `plot`, `scatter`, `bar`, `barh`
|
|
205
|
+
- `hist`, `hist2d`, `hexbin`
|
|
206
|
+
- `boxplot`, `violinplot`
|
|
207
|
+
- `fill_between`, `fill_betweenx`
|
|
208
|
+
- `errorbar`, `contour`, `contourf`
|
|
209
|
+
- `imshow`, `matshow`, `pie`
|
|
210
|
+
- `quiver`, `streamplot`
|
|
211
|
+
- `stem`, `step`, `eventplot`
|
|
212
|
+
|
|
213
|
+
### SciTeX Extensions
|
|
214
|
+
- `stx_line`, `stx_shaded_line`
|
|
215
|
+
- `stx_mean_std`, `stx_mean_ci`, `stx_median_iqr`
|
|
216
|
+
- `stx_kde`, `stx_ecdf`
|
|
217
|
+
- `stx_box`, `stx_violin`
|
|
218
|
+
- `stx_bar`, `stx_barh`
|
|
219
|
+
- `stx_scatter`, `stx_scatter_hist`
|
|
220
|
+
- `stx_heatmap`, `stx_conf_mat`
|
|
221
|
+
- `stx_image`, `stx_imshow`
|
|
222
|
+
- `stx_fillv`, `stx_contour`
|
|
223
|
+
- `stx_raster`, `stx_joyplot`
|
|
224
|
+
- `stx_rectangle`
|
|
225
|
+
|
|
226
|
+
### Seaborn Integration
|
|
227
|
+
- `sns_lineplot`, `sns_scatterplot`
|
|
228
|
+
- `sns_barplot`, `sns_boxplot`, `sns_violinplot`
|
|
229
|
+
- `sns_stripplot`, `sns_swarmplot`
|
|
230
|
+
- `sns_histplot`, `sns_kdeplot`
|
|
231
|
+
- `sns_heatmap`, `sns_jointplot`, `sns_pairplot`
|
|
232
|
+
|
|
233
|
+
## Integration with scitex.vis
|
|
234
|
+
|
|
235
|
+
stx.plt outputs can be used as `scitex` type panels in a canvas:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
canvas/panels/panel_a/
|
|
239
|
+
├── panel.json # Renamed from 01_plot.json
|
|
240
|
+
├── panel.csv # Renamed from 01_plot.csv
|
|
241
|
+
└── panel.png # Renamed from 01_plot.png
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The JSON structure is compatible - canvas.json references the panel data via relative paths with hash verification.
|
|
245
|
+
|
|
246
|
+
## Summary
|
|
247
|
+
|
|
248
|
+
| Aspect | Description |
|
|
249
|
+
|-------------|------------------------------------|
|
|
250
|
+
| Output | PNG + JSON + CSV per figure |
|
|
251
|
+
| Units | Millimeters for publication |
|
|
252
|
+
| Tracking | All traces tracked with IDs |
|
|
253
|
+
| Metadata | Dimensions, styles, axes info |
|
|
254
|
+
| Data | CSV with column references in JSON |
|
|
255
|
+
| Integration | Direct use as canvas panels |
|
|
256
|
+
|
|
257
|
+
<!-- EOF -->
|
scitex/plt/utils/__init__.py
CHANGED
|
@@ -22,6 +22,12 @@ from ._figure_from_axes_mm import (
|
|
|
22
22
|
)
|
|
23
23
|
from ._units import inch_to_mm, mm_to_inch, mm_to_pt, pt_to_mm
|
|
24
24
|
from ._collect_figure_metadata import collect_figure_metadata
|
|
25
|
+
from ._csv_column_naming import (
|
|
26
|
+
get_csv_column_name,
|
|
27
|
+
get_csv_column_prefix,
|
|
28
|
+
parse_csv_column_name,
|
|
29
|
+
sanitize_trace_id,
|
|
30
|
+
)
|
|
25
31
|
|
|
26
32
|
__all__ = [
|
|
27
33
|
"HistogramBinManager",
|
|
@@ -47,6 +53,8 @@ __all__ = [
|
|
|
47
53
|
"cross_ref",
|
|
48
54
|
"enhance_scitex_save_with_captions",
|
|
49
55
|
"export_captions",
|
|
56
|
+
"get_csv_column_name",
|
|
57
|
+
"get_csv_column_prefix",
|
|
50
58
|
"get_dimension_info",
|
|
51
59
|
"get_scitex_config",
|
|
52
60
|
"histogram_bin_manager",
|
|
@@ -57,9 +65,11 @@ __all__ = [
|
|
|
57
65
|
"mk_patches",
|
|
58
66
|
"mm_to_inch",
|
|
59
67
|
"mm_to_pt",
|
|
68
|
+
"parse_csv_column_name",
|
|
60
69
|
"print_dimension_info",
|
|
61
70
|
"pt_to_mm",
|
|
62
71
|
"quick_caption",
|
|
72
|
+
"sanitize_trace_id",
|
|
63
73
|
"save_with_caption",
|
|
64
74
|
"view_dimensions",
|
|
65
75
|
]
|
|
@@ -349,14 +349,15 @@ def _extract_traces(ax) -> list:
|
|
|
349
349
|
and csv_columns mapping
|
|
350
350
|
"""
|
|
351
351
|
import matplotlib.colors as mcolors
|
|
352
|
+
from ._csv_column_naming import get_csv_column_name, sanitize_trace_id
|
|
352
353
|
|
|
353
354
|
traces = []
|
|
354
355
|
|
|
355
356
|
# Get axes position for CSV column naming
|
|
356
|
-
|
|
357
|
+
ax_row, ax_col = 0, 0 # Default for single axes
|
|
357
358
|
if hasattr(ax, '_scitex_metadata') and 'position_in_grid' in ax._scitex_metadata:
|
|
358
359
|
pos = ax._scitex_metadata['position_in_grid']
|
|
359
|
-
|
|
360
|
+
ax_row, ax_col = pos[0], pos[1]
|
|
360
361
|
|
|
361
362
|
for i, line in enumerate(ax.lines):
|
|
362
363
|
trace = {}
|
|
@@ -369,14 +370,16 @@ def _extract_traces(ax) -> list:
|
|
|
369
370
|
label = line.get_label()
|
|
370
371
|
|
|
371
372
|
# Determine trace_id for CSV column matching
|
|
373
|
+
# Use index-based ID to match CSV export (single source of truth)
|
|
374
|
+
trace_id_for_csv = None # Will use trace_index in get_csv_column_name
|
|
375
|
+
|
|
376
|
+
# Store display id/label separately
|
|
372
377
|
if scitex_id:
|
|
373
|
-
|
|
378
|
+
trace["id"] = scitex_id
|
|
374
379
|
elif not label.startswith('_'):
|
|
375
|
-
|
|
380
|
+
trace["id"] = label
|
|
376
381
|
else:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
trace["id"] = trace_id
|
|
382
|
+
trace["id"] = f"line_{i}"
|
|
380
383
|
|
|
381
384
|
# Label (for legend) - use label if not internal
|
|
382
385
|
if not label.startswith('_'):
|
|
@@ -404,12 +407,11 @@ def _extract_traces(ax) -> list:
|
|
|
404
407
|
trace["marker"] = marker
|
|
405
408
|
trace["markersize"] = line.get_markersize()
|
|
406
409
|
|
|
407
|
-
# CSV column mapping -
|
|
408
|
-
#
|
|
409
|
-
# The id should match the id= kwarg passed to ax.plot()
|
|
410
|
+
# CSV column mapping - use single source of truth
|
|
411
|
+
# Uses trace_index to match what _export_as_csv generates
|
|
410
412
|
trace["csv_columns"] = {
|
|
411
|
-
"x":
|
|
412
|
-
"y":
|
|
413
|
+
"x": get_csv_column_name("plot_x", ax_row, ax_col, trace_index=i),
|
|
414
|
+
"y": get_csv_column_name("plot_y", ax_row, ax_col, trace_index=i),
|
|
413
415
|
}
|
|
414
416
|
|
|
415
417
|
traces.append(trace)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: 2025-12-08
|
|
4
|
+
# File: ./src/scitex/plt/utils/_csv_column_naming.py
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Single source of truth for CSV column naming in scitex.
|
|
8
|
+
|
|
9
|
+
This module ensures consistent column naming between:
|
|
10
|
+
- CSV export (_export_as_csv)
|
|
11
|
+
- JSON metadata (_collect_figure_metadata)
|
|
12
|
+
- GUI editors (reading CSV data back)
|
|
13
|
+
|
|
14
|
+
Column naming convention:
|
|
15
|
+
ax_{row}{col}_{trace_id}_{data_type}
|
|
16
|
+
|
|
17
|
+
Where:
|
|
18
|
+
- row, col: axes position in grid (e.g., "00" for single axes)
|
|
19
|
+
- trace_id: unique identifier for the trace (from label, id kwarg, or index)
|
|
20
|
+
- data_type: type of data (e.g., "plot_x", "plot_y", "hist_bins", etc.)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
'get_csv_column_name',
|
|
25
|
+
'get_csv_column_prefix',
|
|
26
|
+
'parse_csv_column_name',
|
|
27
|
+
'sanitize_trace_id',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def sanitize_trace_id(trace_id: str) -> str:
|
|
32
|
+
"""Sanitize trace ID for use in CSV column names.
|
|
33
|
+
|
|
34
|
+
Removes or replaces characters that could cause issues in column names.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
trace_id : str
|
|
39
|
+
Raw trace identifier (label, id kwarg, or generated)
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
str
|
|
44
|
+
Sanitized trace ID safe for CSV column names
|
|
45
|
+
"""
|
|
46
|
+
if not trace_id:
|
|
47
|
+
return "unnamed"
|
|
48
|
+
|
|
49
|
+
# Replace problematic characters
|
|
50
|
+
sanitized = str(trace_id)
|
|
51
|
+
# Keep alphanumeric, underscore, hyphen; replace others with underscore
|
|
52
|
+
result = []
|
|
53
|
+
for char in sanitized:
|
|
54
|
+
if char.isalnum() or char in ('_', '-'):
|
|
55
|
+
result.append(char)
|
|
56
|
+
elif char in (' ', '(', ')', '[', ']', '{', '}', '/', '\\', '.'):
|
|
57
|
+
result.append('_')
|
|
58
|
+
# Skip other characters
|
|
59
|
+
|
|
60
|
+
sanitized = ''.join(result)
|
|
61
|
+
|
|
62
|
+
# Remove consecutive underscores
|
|
63
|
+
while '__' in sanitized:
|
|
64
|
+
sanitized = sanitized.replace('__', '_')
|
|
65
|
+
|
|
66
|
+
# Remove leading/trailing underscores
|
|
67
|
+
sanitized = sanitized.strip('_')
|
|
68
|
+
|
|
69
|
+
return sanitized if sanitized else "unnamed"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_csv_column_prefix(ax_row: int = 0, ax_col: int = 0, trace_id: str = None, trace_index: int = None) -> str:
|
|
73
|
+
"""Get CSV column prefix for a trace.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
ax_row : int
|
|
78
|
+
Row position of axes in grid (default: 0)
|
|
79
|
+
ax_col : int
|
|
80
|
+
Column position of axes in grid (default: 0)
|
|
81
|
+
trace_id : str, optional
|
|
82
|
+
Trace identifier (from label or id kwarg). If None, uses trace_index.
|
|
83
|
+
trace_index : int, optional
|
|
84
|
+
Index of trace when no trace_id is provided (default: 0)
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
str
|
|
89
|
+
Column prefix like "ax_00_sin_x_" or "ax_01_plot_0_"
|
|
90
|
+
"""
|
|
91
|
+
ax_pos = f"{ax_row}{ax_col}"
|
|
92
|
+
|
|
93
|
+
if trace_id:
|
|
94
|
+
safe_id = sanitize_trace_id(trace_id)
|
|
95
|
+
elif trace_index is not None:
|
|
96
|
+
safe_id = f"plot_{trace_index}"
|
|
97
|
+
else:
|
|
98
|
+
safe_id = "plot_0"
|
|
99
|
+
|
|
100
|
+
return f"ax_{ax_pos}_{safe_id}_"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_csv_column_name(
|
|
104
|
+
data_type: str,
|
|
105
|
+
ax_row: int = 0,
|
|
106
|
+
ax_col: int = 0,
|
|
107
|
+
trace_id: str = None,
|
|
108
|
+
trace_index: int = None,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Get full CSV column name for a data field.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
data_type : str
|
|
115
|
+
Type of data (e.g., "plot_x", "plot_y", "hist_bins", "bar_heights")
|
|
116
|
+
ax_row : int
|
|
117
|
+
Row position of axes in grid (default: 0)
|
|
118
|
+
ax_col : int
|
|
119
|
+
Column position of axes in grid (default: 0)
|
|
120
|
+
trace_id : str, optional
|
|
121
|
+
Trace identifier (from label or id kwarg)
|
|
122
|
+
trace_index : int, optional
|
|
123
|
+
Index of trace when no trace_id is provided
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
str
|
|
128
|
+
Full column name like "ax_00_sin_x_plot_x" or "ax_01_plot_0_plot_y"
|
|
129
|
+
|
|
130
|
+
Examples
|
|
131
|
+
--------
|
|
132
|
+
>>> get_csv_column_name("plot_x", trace_id="sin(x)")
|
|
133
|
+
'ax_00_sin_x_plot_x'
|
|
134
|
+
>>> get_csv_column_name("plot_y", ax_row=1, ax_col=2, trace_index=0)
|
|
135
|
+
'ax_12_plot_0_plot_y'
|
|
136
|
+
"""
|
|
137
|
+
prefix = get_csv_column_prefix(ax_row, ax_col, trace_id, trace_index)
|
|
138
|
+
return f"{prefix}{data_type}"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def parse_csv_column_name(column_name: str) -> dict:
|
|
142
|
+
"""Parse CSV column name to extract components.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
column_name : str
|
|
147
|
+
Full column name (e.g., "ax_00_sin_x_plot_x")
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
dict
|
|
152
|
+
Dictionary with keys:
|
|
153
|
+
- ax_row: int
|
|
154
|
+
- ax_col: int
|
|
155
|
+
- trace_id: str
|
|
156
|
+
- data_type: str
|
|
157
|
+
- valid: bool (True if parsing succeeded)
|
|
158
|
+
|
|
159
|
+
Examples
|
|
160
|
+
--------
|
|
161
|
+
>>> parse_csv_column_name("ax_00_sin_x_plot_x")
|
|
162
|
+
{'ax_row': 0, 'ax_col': 0, 'trace_id': 'sin_x', 'data_type': 'plot_x', 'valid': True}
|
|
163
|
+
"""
|
|
164
|
+
result = {
|
|
165
|
+
'ax_row': 0,
|
|
166
|
+
'ax_col': 0,
|
|
167
|
+
'trace_id': '',
|
|
168
|
+
'data_type': '',
|
|
169
|
+
'valid': False,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if not column_name or not column_name.startswith('ax_'):
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
parts = column_name.split('_')
|
|
176
|
+
if len(parts) < 4:
|
|
177
|
+
return result
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Parse ax position (e.g., "00" from "ax_00_...")
|
|
181
|
+
ax_pos = parts[1]
|
|
182
|
+
if len(ax_pos) >= 2:
|
|
183
|
+
result['ax_row'] = int(ax_pos[0])
|
|
184
|
+
result['ax_col'] = int(ax_pos[1])
|
|
185
|
+
|
|
186
|
+
# Last two parts are typically data_type (e.g., "plot_x", "hist_bins")
|
|
187
|
+
# Everything in between is the trace_id
|
|
188
|
+
data_type_parts = parts[-2:] # e.g., ["plot", "x"]
|
|
189
|
+
result['data_type'] = '_'.join(data_type_parts)
|
|
190
|
+
|
|
191
|
+
# Trace ID is everything between ax_pos and data_type
|
|
192
|
+
trace_parts = parts[2:-2]
|
|
193
|
+
result['trace_id'] = '_'.join(trace_parts) if trace_parts else 'plot_0'
|
|
194
|
+
|
|
195
|
+
result['valid'] = True
|
|
196
|
+
|
|
197
|
+
except (ValueError, IndexError):
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_trace_columns_from_df(df, trace_id: str = None, trace_index: int = None, ax_row: int = 0, ax_col: int = 0) -> dict:
|
|
204
|
+
"""Find CSV columns for a specific trace in a DataFrame.
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
df : pandas.DataFrame
|
|
209
|
+
DataFrame with CSV data
|
|
210
|
+
trace_id : str, optional
|
|
211
|
+
Trace identifier to search for
|
|
212
|
+
trace_index : int, optional
|
|
213
|
+
Trace index to search for (if trace_id not provided)
|
|
214
|
+
ax_row : int
|
|
215
|
+
Row position of axes
|
|
216
|
+
ax_col : int
|
|
217
|
+
Column position of axes
|
|
218
|
+
|
|
219
|
+
Returns
|
|
220
|
+
-------
|
|
221
|
+
dict
|
|
222
|
+
Dictionary mapping data types to column names, e.g.:
|
|
223
|
+
{'plot_x': 'ax_00_sin_x_plot_x', 'plot_y': 'ax_00_sin_x_plot_y'}
|
|
224
|
+
"""
|
|
225
|
+
result = {}
|
|
226
|
+
prefix = get_csv_column_prefix(ax_row, ax_col, trace_id, trace_index)
|
|
227
|
+
|
|
228
|
+
for col in df.columns:
|
|
229
|
+
if col.startswith(prefix):
|
|
230
|
+
# Extract data_type from column name
|
|
231
|
+
data_type = col[len(prefix):]
|
|
232
|
+
result[data_type] = col
|
|
233
|
+
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# EOF
|
|
@@ -44,9 +44,16 @@ class CitationDatabase:
|
|
|
44
44
|
read_only: If True, open in read-only mode (default)
|
|
45
45
|
"""
|
|
46
46
|
if read_only:
|
|
47
|
-
self.conn = sqlite3.connect(
|
|
47
|
+
self.conn = sqlite3.connect(
|
|
48
|
+
f"file:{self.db_path}?mode=ro",
|
|
49
|
+
uri=True,
|
|
50
|
+
check_same_thread=False # Allow multi-threaded access (e.g., Django)
|
|
51
|
+
)
|
|
48
52
|
else:
|
|
49
|
-
self.conn = sqlite3.connect(
|
|
53
|
+
self.conn = sqlite3.connect(
|
|
54
|
+
self.db_path,
|
|
55
|
+
check_same_thread=False
|
|
56
|
+
)
|
|
50
57
|
|
|
51
58
|
self.conn.row_factory = sqlite3.Row
|
|
52
59
|
|
|
@@ -29,8 +29,22 @@ logger = getLogger(__name__)
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
class ScholarConfig:
|
|
32
|
-
def __init__(
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
config_path: Optional[Union[str, Path]] = None,
|
|
35
|
+
scholar_dir: Optional[Union[str, Path]] = None,
|
|
36
|
+
):
|
|
37
|
+
"""Initialize ScholarConfig.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
config_path: Path to custom config YAML file
|
|
41
|
+
scholar_dir: Direct path to scholar directory (e.g., /data/users/alice/.scitex)
|
|
42
|
+
This bypasses SCITEX_DIR env var for thread-safe multi-user usage.
|
|
43
|
+
Use this in Django/multi-user environments to avoid race conditions.
|
|
44
|
+
"""
|
|
33
45
|
self.name = self.__class__.__name__
|
|
46
|
+
self._explicit_scholar_dir = scholar_dir # Store for thread-safe access
|
|
47
|
+
|
|
34
48
|
if config_path and Path(config_path).exists():
|
|
35
49
|
config_data = self.load_yaml(config_path)
|
|
36
50
|
else:
|
|
@@ -114,8 +128,14 @@ class ScholarConfig:
|
|
|
114
128
|
|
|
115
129
|
# Path Management ----------------------------------------
|
|
116
130
|
def _setup_path_manager(self, scholar_dir=None):
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
# Priority: explicit parameter > env var > config > default
|
|
132
|
+
if self._explicit_scholar_dir:
|
|
133
|
+
# Use explicitly provided path (thread-safe for multi-user)
|
|
134
|
+
base_path = Path(self._explicit_scholar_dir).expanduser() / "scholar"
|
|
135
|
+
else:
|
|
136
|
+
# Fall back to cascade resolution (uses SCITEX_DIR env var)
|
|
137
|
+
scholar_dir = self.cascade.resolve("scholar_dir", default="~/.scitex")
|
|
138
|
+
base_path = Path(scholar_dir).expanduser() / "scholar"
|
|
119
139
|
self.path_manager = PathManager(scholar_dir=base_path)
|
|
120
140
|
|
|
121
141
|
@property
|