Skip to content

Commit efadbcd

Browse files
Improve chart and table display API (#171)
* Add display UX ADR and plan * Rename display settings category to rendering * Add grouped display facade * Move constraint display and standardize CIF helpers * Refine display pattern routing * Implement state-aware pattern display * Update display UX documentation * Complete display UX verification and test updates * Address display UX review fixes * Fix single-crystal display availability checks
1 parent 3467c72 commit efadbcd

81 files changed

Lines changed: 3228 additions & 990 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

β€Ždocs/dev/adr_display-ux.mdβ€Ž

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
# ADR: Display UX Facade
2+
3+
## Status
4+
5+
Accepted.
6+
7+
## Context
8+
9+
The current user-facing display API mixes presentation actions, analysis
10+
reports, and renderer configuration:
11+
12+
```python
13+
project.display.plotter.plot_meas(expt_name='hrpt')
14+
project.display.plotter.plot_calc(expt_name='hrpt')
15+
project.display.plotter.plot_meas_vs_calc(expt_name='hrpt')
16+
project.display.plotter.plot_param_correlations()
17+
project.display.plotter.plot_posterior_pairs()
18+
project.display.plotter.plot_param_distribution(param)
19+
project.display.plotter.plot_posterior_predictive(expt_name='hrpt')
20+
21+
project.analysis.display.free_params()
22+
project.analysis.display.fit_results()
23+
```
24+
25+
This has several UX problems:
26+
27+
- `plot_` is redundant below any display or chart object.
28+
- `plotter` and `tabler` expose backend implementation language to
29+
scientists using notebooks.
30+
- `project.display` and `project.analysis.display` overlap without a
31+
clear user-facing rule.
32+
- `plot_meas`, `plot_calc`, and `plot_meas_vs_calc` force users to
33+
choose a plot state that the project can often infer.
34+
- Bayesian and deterministic chart names are not systematic.
35+
- The existing `project.display` category is serialized to CIF, so it
36+
should not also become a broad transient display facade.
37+
38+
EasyDiffraction is aimed at scientists, often non-programmers, so the
39+
display API should prioritize discoverability, clear names, and safe
40+
defaults.
41+
42+
## Decision
43+
44+
Use `project.display` as the user-facing facade for display actions.
45+
Move serialized renderer settings out of that facade and into a separate
46+
project category named `project.rendering`.
47+
48+
Renderer settings:
49+
50+
```python
51+
project.rendering.chart_engine = 'plotly'
52+
project.rendering.table_engine = 'pandas'
53+
project.rendering.show_chart_engines()
54+
project.rendering.show_table_engines()
55+
project.rendering.show_config()
56+
```
57+
58+
Suggested CIF names:
59+
60+
- `_rendering.chart_engine`
61+
- `_rendering.table_engine`
62+
63+
No legacy loader is required for `_display.plotter_type` or
64+
`_display.tabler_type`. The project is in beta, so this cleanup may
65+
break old project files rather than carrying compatibility code.
66+
67+
The selected display API is grouped:
68+
69+
```python
70+
project.display.pattern(expt_name='hrpt')
71+
72+
project.display.parameters.free()
73+
project.display.parameters.fittable()
74+
project.display.parameters.all()
75+
project.display.parameters.access()
76+
project.display.parameters.cif_uids()
77+
78+
project.display.fit.results()
79+
project.display.fit.correlations()
80+
project.display.fit.series(param, versus=temperature)
81+
82+
project.display.posterior.pairs()
83+
project.display.posterior.distribution(param)
84+
project.display.posterior.predictive(expt_name='hrpt')
85+
86+
project.display.show_pattern_options(expt_name='hrpt')
87+
```
88+
89+
`project.analysis.display` is removed from the primary public API. Its
90+
current responsibilities move to clearer homes:
91+
92+
| Current method | New home |
93+
| ---------------------------- | -------------------------------------------------------------- |
94+
| `all_params()` | `project.display.parameters.all()` |
95+
| `fittable_params()` | `project.display.parameters.fittable()` |
96+
| `free_params()` | `project.display.parameters.free()` |
97+
| `how_to_access_parameters()` | `project.display.parameters.access()` |
98+
| `parameter_cif_uids()` | `project.display.parameters.cif_uids()` |
99+
| `fit_results()` | `project.display.fit.results()` |
100+
| `constraints()` | `project.analysis.constraints.show()` |
101+
| `as_cif()` | `project.analysis.as_cif` and `project.analysis.show_as_cif()` |
102+
103+
`project.analysis` and `project.info` should follow the same CIF display
104+
pattern as structures and experiments:
105+
106+
- `as_cif` is a read-only property returning CIF text as a string.
107+
- `show_as_cif()` pretty-prints the CIF text with a header.
108+
109+
## Pattern Display
110+
111+
Use `pattern()` as the main experiment chart:
112+
113+
```python
114+
project.display.pattern(expt_name='hrpt')
115+
project.display.pattern(expt_name='hrpt', x_min=40, x_max=55)
116+
```
117+
118+
By default, `pattern()` uses `include='auto'` and displays as much
119+
useful information as the project state supports:
120+
121+
- measured data if present
122+
- calculated data if a model/calculation is available
123+
- background if defined and relevant
124+
- Bragg ticks if phases/reflections are available
125+
- residual if both measured and calculated data are available and the
126+
experiment type supports a residual panel
127+
- excluded regions if available
128+
- uncertainty bands where posterior predictive data exists
129+
130+
Specific subsets are selected with `include`:
131+
132+
```python
133+
project.display.pattern(expt_name='hrpt', include='auto')
134+
project.display.pattern(expt_name='hrpt', include='measured')
135+
project.display.pattern(expt_name='hrpt', include='calculated')
136+
project.display.pattern(
137+
expt_name='hrpt',
138+
include=('measured', 'calculated', 'background', 'residual', 'bragg'),
139+
)
140+
```
141+
142+
`include` was chosen over alternatives:
143+
144+
| Name | Reason not selected |
145+
| ------------- | ----------------------------------------------- |
146+
| `layers` | Sounds graphical rather than user intent. |
147+
| `components` | Precise, but longer. |
148+
| `content` | Too broad. |
149+
| `view` | Better for presets than arbitrary combinations. |
150+
| `series` | Does not fit residual rows or Bragg ticks well. |
151+
| boolean flags | Explicit, but scales poorly. |
152+
153+
Add discovery for supported pattern content:
154+
155+
```python
156+
project.display.show_pattern_options(expt_name='hrpt')
157+
```
158+
159+
The table should show option name, description, availability for the
160+
experiment, whether `include='auto'` includes it, and the reason an
161+
option is unavailable.
162+
163+
Initial option names:
164+
165+
- `auto`
166+
- `measured`
167+
- `calculated`
168+
- `background`
169+
- `residual`
170+
- `bragg`
171+
- `excluded`
172+
- `uncertainty`
173+
174+
`uncertainty` should be implemented immediately where posterior
175+
predictive data exists. It should be unavailable, with a clear reason,
176+
when no posterior predictive data is present.
177+
178+
## Deterministic And Bayesian Consistency
179+
180+
Use these naming rules:
181+
182+
- `pattern()` shows the current point-estimate experiment view.
183+
- `fit.results()` reports the latest fit result.
184+
- `fit.correlations()` shows parameter relationships from the latest
185+
fit.
186+
- `fit.series(param, versus=...)` shows fitted parameter values across a
187+
sequence of fit results or experiments.
188+
- `posterior.*` names are used only when posterior samples are required.
189+
190+
## Rejected Alternatives
191+
192+
Flat display facade:
193+
194+
```python
195+
project.display.pattern(expt_name='hrpt')
196+
project.display.parameters(scope='free')
197+
project.display.fit_results()
198+
project.display.correlations()
199+
project.display.parameter_series(param, versus=temperature)
200+
project.display.posterior_pairs()
201+
project.display.posterior_distribution(param)
202+
project.display.posterior_predictive(expt_name='hrpt')
203+
```
204+
205+
This is shorter but would make `project.display` grow into a long flat
206+
list.
207+
208+
Separate `charts` and `tables` namespaces were also rejected because
209+
users should not need to decide the output type before asking for
210+
information. Some outputs may render as a chart or a table depending on
211+
backend and state.
212+
213+
Separate `measured()` and `calculated()` methods were rejected because
214+
they duplicate `pattern(..., include=...)`.
215+
216+
## Consequences
217+
218+
- The main display workflow becomes more discoverable through grouped
219+
namespaces and tab completion.
220+
- Renderer configuration becomes clearly separate from display actions.
221+
- Existing tutorials and public API docs must be updated to the selected
222+
API.
223+
- Constraints remain owned by the analysis constraints category.
224+
- There is no legacy CIF compatibility path for `_display.plotter_type`
225+
or `_display.tabler_type`.
226+
- `project.analysis` and `project.info` need CIF access cleanup for
227+
consistency with structure and experiment objects.

β€Ždocs/dev/architecture.mdβ€Ž

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -879,15 +879,16 @@ project = ed.Project(name='my_project')
879879

880880
It owns and coordinates all components:
881881

882-
| Property | Type | Description |
883-
| --------------------- | ------------- | ---------------------------------------- |
884-
| `project.info` | `ProjectInfo` | Metadata: name, title, description, path |
885-
| `project.structures` | `Structures` | Collection of structure datablocks |
886-
| `project.experiments` | `Experiments` | Collection of experiment datablocks |
887-
| `project.display` | `Display` | Plot/table engine selection and facades |
888-
| `project.analysis` | `Analysis` | Minimiser, fitting, aliases, constraints |
889-
| `project.summary` | `Summary` | Report generation |
890-
| `project.verbosity` | `str` | Console output level (full/short/silent) |
882+
| Property | Type | Description |
883+
| --------------------- | ---------------- | ---------------------------------------- |
884+
| `project.info` | `ProjectInfo` | Metadata: name, title, description, path |
885+
| `project.structures` | `Structures` | Collection of structure datablocks |
886+
| `project.experiments` | `Experiments` | Collection of experiment datablocks |
887+
| `project.rendering` | `Rendering` | Plot/table engine selection |
888+
| `project.display` | `ProjectDisplay` | Pattern/report facade |
889+
| `project.analysis` | `Analysis` | Minimiser, fitting, aliases, constraints |
890+
| `project.summary` | `Summary` | Report generation |
891+
| `project.verbosity` | `str` | Console output level (full/short/silent) |
891892

892893
### 7.1 Data Flow
893894

@@ -922,7 +923,7 @@ project_dir/
922923
```
923924

924925
`project.cif` carries both the `_project.*` metadata and the
925-
`_display.*` engine preferences (`plotter_type`, `tabler_type`), so a
926+
`_rendering.*` engine preferences (`chart_engine`, `table_engine`), so a
926927
saved project re-opens with the same display backends. Per-experiment
927928
calculator selection (`_calculation.calculator_type`) lives in each
928929
experiment file, and fit configuration (`_fit.minimizer_type`,
@@ -1064,7 +1065,7 @@ project.experiments['hrpt'].calculation.calculator_type = 'cryspy'
10641065
project.analysis.fit.minimizer_type = 'lmfit'
10651066

10661067
# Plot before fitting
1067-
project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
1068+
project.display.pattern(expt_name='hrpt')
10681069

10691070
# Select free parameters
10701071
project.structures['lbco'].cell.length_a.free = True
@@ -1073,14 +1074,14 @@ project.experiments['hrpt'].instrument.calib_twotheta_offset.free = True
10731074
project.experiments['hrpt'].background['10'].y.free = True
10741075

10751076
# Inspect free parameters
1076-
project.analysis.display.free_params()
1077+
project.display.parameters.free()
10771078

10781079
# Fit and show results
10791080
project.analysis.fit()
1080-
project.analysis.display.fit_results()
1081+
project.display.fit.results()
10811082

10821083
# Plot after fitting
1083-
project.display.plotter.plot_meas_vs_calc(expt_name='hrpt', show_residual=True)
1084+
project.display.pattern(expt_name='hrpt')
10841085

10851086
# Save
10861087
project.save()
@@ -1100,11 +1101,11 @@ project.analysis.fit.minimizer.parallel = 0
11001101
project.analysis.fit(random_seed=11)
11011102

11021103
# Runtime-only Bayesian summaries and plots
1103-
project.analysis.display.fit_results()
1104-
project.display.plotter.plot_param_correlations()
1105-
project.display.plotter.plot_posterior_pairs()
1106-
project.display.plotter.plot_param_distribution(param)
1107-
project.display.plotter.plot_posterior_predictive(expt_name='hrpt')
1104+
project.display.fit.results()
1105+
project.display.fit.correlations()
1106+
project.display.posterior.pairs()
1107+
project.display.posterior.distribution(param)
1108+
project.display.posterior.predictive(expt_name='hrpt')
11081109
```
11091110

11101111
### 8.5 TOF Experiment (tutorial ed-7)
@@ -1225,8 +1226,8 @@ that owns calculator selection β€”
12251226
`experiment.calculation.calculator_type` and
12261227
`experiment.calculation.show_calculator_types()` β€” instead of the
12271228
selector being exposed at the experiment owner level. The same pattern
1228-
applies to `display` on `Project`, which owns `plotter_type` and
1229-
`tabler_type` (see Β§9.4.1).
1229+
applies to `display` on `Project`, which owns `chart_engine` and
1230+
`table_engine` (see Β§9.4.1).
12301231

12311232
**Design decisions:**
12321233

@@ -1248,14 +1249,14 @@ recognises three distinct selector families. They share a similar
12481249
`<name>_type` shape so the user can inspect and set them uniformly, but
12491250
their intent and ownership differ:
12501251

1251-
| Family | User intent | Examples | CIF |
1252-
| ---------------------------------- | ------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
1253-
| Backend selector | Pick an execution backend | `fit.minimizer_type`, `calculation.calculator_type`, `display.plotter_type` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_display.plotter_type` |
1254-
| Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` |
1255-
| Semantic value selector | Pick a scientific/analysis mode | `fit.mode` | `_fit.mode` |
1252+
| Family | User intent | Examples | CIF |
1253+
| ---------------------------------- | ------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
1254+
| Backend selector | Pick an execution backend | `fit.minimizer_type`, `calculation.calculator_type`, `rendering.chart_engine` | `_fit.minimizer_type`, `_calculation.calculator_type`, `_rendering.chart_engine` |
1255+
| Switchable-category impl. selector | Swap a category implementation | `experiment.background_type`, `experiment.peak_profile_type` | category-owned type tag such as `_peak.profile_type` |
1256+
| Semantic value selector | Pick a scientific/analysis mode | `fit.mode` | `_fit.mode` |
12561257

12571258
Backend selectors and semantic value selectors live on a dedicated
1258-
configuration category (`fit`, `calculation`, `display`). Switchable-
1259+
configuration category (`fit`, `calculation`, `rendering`). Switchable-
12591260
category implementation selectors are owned by the host (typically the
12601261
experiment) because switching them replaces the category instance, as
12611262
described in Β§9.3.
@@ -1272,8 +1273,8 @@ expt.calculation.show_calculator_types()
12721273
expt.show_extinction_types()
12731274
project.analysis.fit.show_minimizer_types()
12741275
project.analysis.fit.show_modes()
1275-
project.display.show_plotter_types()
1276-
project.display.show_tabler_types()
1276+
project.rendering.show_chart_engines()
1277+
project.rendering.show_table_engines()
12771278
```
12781279

12791280
Available calculators are filtered by `engine_imported` (whether the

β€Ždocs/dev/package-structure-full.mdβ€Ž

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -402,14 +402,20 @@
402402
β”‚ └── πŸ“„ ascii.py
403403
β”œβ”€β”€ πŸ“ project
404404
β”‚ β”œβ”€β”€ πŸ“ categories
405-
β”‚ β”‚ β”œβ”€β”€ πŸ“ display
405+
β”‚ β”‚ β”œβ”€β”€ πŸ“ rendering
406406
β”‚ β”‚ β”‚ β”œβ”€β”€ πŸ“„ __init__.py
407407
β”‚ β”‚ β”‚ β”œβ”€β”€ πŸ“„ default.py
408-
β”‚ β”‚ β”‚ β”‚ └── 🏷️ class Display
408+
β”‚ β”‚ β”‚ β”‚ └── 🏷️ class Rendering
409409
β”‚ β”‚ β”‚ └── πŸ“„ factory.py
410-
β”‚ β”‚ β”‚ └── 🏷️ class DisplayFactory
410+
β”‚ β”‚ β”‚ └── 🏷️ class RenderingFactory
411411
β”‚ β”‚ └── πŸ“„ __init__.py
412412
β”‚ β”œβ”€β”€ πŸ“„ __init__.py
413+
β”‚ β”œβ”€β”€ πŸ“„ display.py
414+
β”‚ β”‚ β”œβ”€β”€ 🏷️ class PatternOptionStatus
415+
β”‚ β”‚ β”œβ”€β”€ 🏷️ class ParameterDisplay
416+
β”‚ β”‚ β”œβ”€β”€ 🏷️ class FitDisplay
417+
β”‚ β”‚ β”œβ”€β”€ 🏷️ class PosteriorDisplay
418+
β”‚ β”‚ └── 🏷️ class ProjectDisplay
413419
β”‚ β”œβ”€β”€ πŸ“„ project.py
414420
β”‚ β”‚ └── 🏷️ class Project
415421
β”‚ └── πŸ“„ project_info.py

β€Ždocs/dev/package-structure-short.mdβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,13 @@
198198
β”‚ └── πŸ“„ ascii.py
199199
β”œβ”€β”€ πŸ“ project
200200
β”‚ β”œβ”€β”€ πŸ“ categories
201-
β”‚ β”‚ β”œβ”€β”€ πŸ“ display
201+
β”‚ β”‚ β”œβ”€β”€ πŸ“ rendering
202202
β”‚ β”‚ β”‚ β”œβ”€β”€ πŸ“„ __init__.py
203203
β”‚ β”‚ β”‚ β”œβ”€β”€ πŸ“„ default.py
204204
β”‚ β”‚ β”‚ └── πŸ“„ factory.py
205205
β”‚ β”‚ └── πŸ“„ __init__.py
206206
β”‚ β”œβ”€β”€ πŸ“„ __init__.py
207+
β”‚ β”œβ”€β”€ πŸ“„ display.py
207208
β”‚ β”œβ”€β”€ πŸ“„ project.py
208209
β”‚ └── πŸ“„ project_info.py
209210
β”œβ”€β”€ πŸ“ summary

0 commit comments

Comments
Β (0)