Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
1fd5e28
fix(results): stop swapping Cox follow-up time and event indicator
KFilippopolitis Oct 23, 2026
cace962
refactor(results): add shared number formatting for result views
KFilippopolitis Oct 23, 2026
43b6f96
feat(charts): one series palette and house style for every chart
KFilippopolitis Oct 23, 2026
1f353a5
fix(charts): escape labels in chart tooltips
KFilippopolitis Oct 23, 2026
6c0674f
feat(charts): redraw the heatmaps and k-means centres for compare col…
KFilippopolitis Oct 23, 2026
8b788b9
feat(charts): t-test interval, ANOVA ±1 SD whiskers, naive Bayes prof…
KFilippopolitis Oct 23, 2026
8679012
feat(results): label k-means, outlier, SMD and mixed-model results
KFilippopolitis Oct 23, 2026
a5c23ed
feat(results): readable result tables
KFilippopolitis Oct 23, 2026
1364289
feat(results): leave out tables the figures or charts above already show
KFilippopolitis Oct 23, 2026
4f3a03f
feat(results): headline figures for hypothesis tests and cross-valida…
KFilippopolitis Oct 23, 2026
93a6a96
feat(results): coefficient view for regression, mixed and Cox models
KFilippopolitis Oct 23, 2026
981933a
feat(results): lead each result with its coefficient view, figures or…
KFilippopolitis Oct 23, 2026
a1a13ec
refactor(charts): remove the renderers the result views replaced
KFilippopolitis Oct 23, 2026
fb5c4ab
docs: chart house style and the result view layout
KFilippopolitis Oct 23, 2026
803a000
fix(results): address review findings on result views
KFilippopolitis Oct 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,14 @@ Experiment Studio is a dense product workspace, not a marketing site. No landing

### Charts

- Series in order: `#2b33e9`, `#7f9ce8`, `#ffba08`, `#dfefe4` (`grouped-bar-chart.ts`, `charts-renderer.component.ts`). Axis lines and labels `#475569`, split lines `#e2e8f0`, data labels `--chart-label-color`.
- Series in order: `#2b33e9`, `#7f9ce8`, `#ffba08`, `#e34948`, `#4a3aa7`, `#e87ba4`, `#008300`, `#1baf7a` — one list in `visualisations/charts/chart-palette.ts`, used by `charts-renderer.component.ts` and `grouped-bar-chart.ts`. Slots 1–3 are the charter; 4–8 were chosen with the dataviz palette validator so every adjacent pair clears ΔE 15 for both deutan/protan and full colour vision. Assign in order, never cycle: a ninth series folds into "Other" or a table. Axis lines and labels `#475569`, split lines `#e2e8f0`, data labels `--chart-label-color`.
- Only the first series reaches 3:1 on white, so every chart carries a legend or direct labels; colour alone never identifies a series. Orange and green bars get a 1px `#475569` edge.
- Known issue: `#dfefe4` is 1.19:1 on white and a fourth series nearly vanishes. Avoid a fourth series until that slot gets a replacement colour.
- Heatmaps (correlation, PCA weights, confusion matrix, k-means centres) share `DIVERGING_COLORS` (`#e34948` → `#f1f5f9` → `#2b33e9`, or its grey-to-blue half) and `cellLabelColor`: ink on every cell except the deep blue end (≥ 0.7), where white wins. White never reaches 4.5:1 anywhere on the red half.
- The adjacent-pair check covers bars and stacks. Lines that cross or dots side by side compare every pair, and slots 1 and 5 (`#2b33e9`, `#4a3aa7`) fail that at ΔE 11: past four series, name them in rows instead (the k-means centres heatmap) rather than in colour.
- `#7f9ce8`, `#ffba08`, `#e87ba4` and `#1baf7a` sit under 3:1 on white, so a chart that reaches them keeps its legend and the exact-value table under it. `#ffba08` is also lighter than the validator's band — the charter colour stays, with its 1px edge.
- `ChartRendererComponent` applies the house style under every chart's own options: the app font (canvas cannot read `--font-sans`; Alaska is left out), 13px/600 ink titles with 12px muted subtitles, a white tooltip card with a hairline border, and ECharts `aria` descriptions. Renderers set only what differs.
- A chart must fit a 360px compare column: put long category names on the y axis (they wrap), slant crowded x labels, keep captions to short lines.
- Lines are drawn as measured: no default smoothing (it invents bends between discrete points such as an elbow or ROC curve) and no default area fill.

## Adjacent-panel test

Expand Down
1 change: 0 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"allowedCommonJsDependencies": [
"canvg",
"core-js",
"echarts-gl",
"html2canvas",
"raf",
"rgbcolor"
Expand Down
4 changes: 2 additions & 2 deletions docs/context/module-index.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ Notes: Browser validation often requires a backend with data models, algorithms,

## `src/app/pages/experiment-studio/visualisations`
Purpose: Chart/table rendering for algorithm outputs, histograms, metadata browsers, and auto-rendered results.
Key files: `charts/chart-registry.ts`, `charts/chart-builder.service.ts`, `auto-renderer/algorithm-table-registry.ts`, renderer files under `charts/renderers`.
Key files: `charts/chart-registry.ts` (builder + section title per algorithm), `charts/chart-builder.service.ts`, `charts/chart-palette.ts` (the one 8-slot series palette), `auto-renderer/algorithm-table-registry.ts`, renderer files under `charts/renderers`, `regression/regression-view.ts` + `regression-result.component` (coefficient view for linear/logistic regression, LMM, binary GLMM), `summary/summary-figures.ts` + `key-figures.component` (headline figures for tests and cross-validated models), `result-format.ts` (p-value, ratio and number formatting, χ² tail).
Used by: Algorithm result panels, dashboard detail/compare, and statistics views.
Rules: Register new algorithm outputs in the existing registries; preserve legacy aliases when changing algorithm keys.
Rules: Register new algorithm outputs in the existing registries; preserve legacy aliases when changing algorithm keys. `AlgorithmResultComponent` picks one layout: the regression view when `buildRegressionView` returns one (linear/logistic regression, LMM, GLMMs, Cox; it then replaces charts and tables), otherwise key figures from `buildSummaryFigures`, then the registry charts, then the tables. A table is never repeated under a summary that already shows its values: `tablesShownAsFigures` (summary-figures) works out from the result which rows or columns the figures actually print, and each chart entry's `replaces` names the chart (`byChart`, its title text) that stands in for a table, applied through `tablesReplacedByCharts` only when that chart rendered. Both are `TableTrim`s — a title alone covers the whole table, `columns`/`rows` only those — and `trimTable` applies them on screen; the result title goes on the builder's first table before anything is trimmed. Before covering a table, move any value it has beyond the summary into the figures or chart. "Show all values as tables" under a result brings every covered table back whole (the accessible path to values a chart only draws); exports always keep every table, with exact values: a cell the screen rounds is a `DisplayCell` (`text` shown, `value` exported) and a table that merges values on screen carries an `export` with one exact value per column; CSV, ZIP and PDF all go through `exportTable`. The first column and name columns (groups, classes, categories) print as sent, never digit-grouped. View builders are pure functions with specs; components only render. The PDF export reads the table registry, so tables stay complete even where a summary view leads on screen.
Tests: Registry and renderer specs live in this subtree.
Notes: Registry keys and legacy aliases are compatibility-sensitive; check stored experiment data before removing an alias.

Expand Down
2 changes: 1 addition & 1 deletion e2e/experiment-studio.flows.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ test.describe('run and save lifecycle', () => {
await expect(result).toContainText('R²');
await expect(result).toContainText('0.042');
await expect(result).toContainText('F(1, 98) = 4.31');
const sexRow = result.getByRole('row', { name: /Sex\[Male\]/ });
const sexRow = result.getByRole('row', { name: /Sex · Male/ });
await expect(sexRow).toContainText('2.05');
await expect(sexRow).toContainText('0.090 to 4.01');
await expect(result.getByRole('row', { name: /Intercept/ })).toContainText('71.20');
Expand Down
19 changes: 0 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"@fortawesome/fontawesome-free": "^7.3.1",
"d3": "^7.9.0",
"echarts": "6.1.0",
"echarts-gl": "2.1.0",
"html2canvas": "^1.4.1",
"jspdf": "^4.2.1",
"jspdf-autotable": "^5.0.9",
Expand Down
51 changes: 51 additions & 0 deletions src/app/core/algorithm-result-enum-mapper.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,55 @@ describe('mapAlgorithmResultEnums', () => {
expect(mapped.x_labels).toEqual(['Alzheimer disease', 'Other diagnosis']);
expect(mapped.y_labels).toEqual(['Female', 'Male']);
});

it('maps standardized mean difference groups using the x-variable enum map', () => {
const mapped = mapAlgorithmResultEnums(
'standardized_mean_difference',
{ comparisons: [{ group1: '1', group2: '2', smd: -0.277 }] },
{ biol_sex: { '1': 'male', '2': 'female' } },
{ y: 'age', x: 'biol_sex' }
);

expect(mapped.comparisons).toEqual([{ group1: 'male', group2: 'female', smd: -0.277 }]);
});

it('labels k-means variables and the matching centre keys', () => {
const mapped = mapAlgorithmResultEnums(
'kmeans',
{ variables: ['age', 'nihss_24h'], clusters: [{ label: 'Cluster 0', center: { age: 79.5, nihss_24h: 2 } }] },
{},
{},
{ age: 'Age', nihss_24h: '24h score' }
);

expect(mapped.variables).toEqual(['Age', '24h score']);
expect(mapped.clusters[0].center).toEqual({ Age: 79.5, '24h score': 2 });
});

it('labels the variable that opens each k-means profile line', () => {
const mapped = mapAlgorithmResultEnums(
'kmeans',
{
variables: ['nihss', 'nihss_24h'],
clusters: [{ label: 'Cluster 0', center: {}, profile: ['nihss_24h is above the overall mean', 'nihss is below the overall mean'] }],
},
{},
{},
{ nihss: 'Admission score', nihss_24h: '24h score' }
);

expect(mapped.clusters[0].profile).toEqual(['24h score is above the overall mean', 'Admission score is below the overall mean']);
});

it('labels a mixed model grouping variable sent as a list', () => {
const mapped = mapAlgorithmResultEnums(
'lmm',
{ grouping_var: ['hospit_unit'], indep_vars: ['Intercept'] },
{},
{},
{ hospit_unit: 'Hospitalization unit' }
);

expect(mapped.grouping_var).toEqual(['Hospitalization unit']);
});
});
48 changes: 48 additions & 0 deletions src/app/core/algorithm-result-enum-mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ function mapMixedEffectsResult(

if (typeof mapped?.grouping_var === 'string') {
next.grouping_var = labelMap?.[mapped.grouping_var] ?? mapped.grouping_var;
} else if (Array.isArray(mapped?.grouping_var)) {
next.grouping_var = mapped.grouping_var.map((code: string) => labelMap?.[code] ?? code);
}

if (Array.isArray(mapped?.category_order)) {
Expand Down Expand Up @@ -403,6 +405,13 @@ export function mapAlgorithmResultEnums(
const confusionMapped = mapConfusionMatrix(classesMapped.result, safeEnumMaps, yVar);
const effectiveMap = confusionMapped.map ?? classesMapped.map;

// Gaussian only: the categorical table looks category_count up by feature code.
if (labelMap && Array.isArray(confusionMapped.result?.feature_names) && !confusionMapped.result?.category_count) {
confusionMapped.result = {
...confusionMapped.result,
feature_names: confusionMapped.result.feature_names.map((code: string) => labelMap[code] ?? code),
};
}
if (effectiveMap && confusionMapped.result?.classification_summary) {
return {
...confusionMapped.result,
Expand All @@ -429,6 +438,45 @@ export function mapAlgorithmResultEnums(
}
case 'histogram':
return mapMultipleHistograms(result, safeEnumMaps, yVar);
case 'outlier_report': {
if (!labelMap || !Array.isArray(result?.featurewise)) return result;
return {
...result,
featurewise: result.featurewise.map((row: any) => ({ ...row, variable: labelMap[row?.variable] ?? row?.variable })),
};
}
case 'kmeans': {
if (!labelMap || !Array.isArray(result?.variables)) return result;
// Each profile line starts with a variable code: "nihss_adm_score is below the overall mean".
const profileLine = (line: unknown) => {
const text = String(line ?? '');
const code = result.variables.find((v: string) => text.startsWith(`${v} `));
return code && labelMap[code] ? labelMap[code] + text.slice(code.length) : text;
};
return {
...result,
variables: result.variables.map((code: string) => labelMap[code] ?? code),
clusters: Array.isArray(result.clusters)
? result.clusters.map((cluster: any) => ({
...cluster,
center: mapObjectKeys(cluster?.center ?? {}, labelMap),
...(Array.isArray(cluster?.profile) ? { profile: cluster.profile.map(profileLine) } : {}),
}))
: result.clusters,
};
}
case 'standardized_mean_difference': {
const map = getEnumMapForVar(safeEnumMaps, xVar);
if (!map || !Array.isArray(result?.comparisons)) return result;
return {
...result,
comparisons: result.comparisons.map((row: any) => ({
...row,
group1: map[normalizeKey(row.group1)] ?? row.group1,
group2: map[normalizeKey(row.group2)] ?? row.group2,
})),
};
}
case 'linear_regression':
case 'linear_regression_cv':
case 'logistic_regression':
Expand Down
7 changes: 4 additions & 3 deletions src/app/core/html.utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Escapes a value for safe interpolation into strings bound with [innerHTML]. */
export function escapeHtml(value: string): string {
return value
/** Escapes a value for safe interpolation into strings bound with [innerHTML] or returned by an
* ECharts tooltip formatter (also HTML). */
export function escapeHtml(value: unknown): string {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
Expand Down
5 changes: 3 additions & 2 deletions src/app/core/result-label.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ export function formulaLine(y: string[], x: string[], xSeparator = ', '): string
return [...y, ...x].join(', ');
}

/** A result's `n_obs` as a grouped count, or '' when absent or not a positive number. */
/** A result's `n_obs` as a count grouped like the result figures (27 308, narrow no-break space),
* or '' when absent or not a positive number. */
export function rowsUsedLabel(nObs: unknown): string {
if (typeof nObs !== 'number' || !Number.isFinite(nObs) || nObs <= 0) return '';
return nObs.toLocaleString('en-US');
return nObs.toLocaleString('en-US').replace(/,/g, '\u202f');
}

/** Top-level `n_obs`, or the first nested one (one-way ANOVA keeps it on `anova_table`). */
Expand Down
5 changes: 2 additions & 3 deletions src/app/models/algorithm-visualization-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,8 @@ This document describes what the frontend currently renders for each algorithm a
### K-Means (`kmeans`)

- Charts:
- 2D scatter when the result has 2 variables.
- 3D scatter when the result has 3 variables.
- Parallel coordinates fallback for any other number of variables.
- Bubble chart of the centres when the result has 2 variables (the Cluster Centers table stays).
- Heatmap of the centres, values in the cells, for 3 or more variables (it replaces the Cluster Centers table on screen).
- Elbow curve when `elbow` is present.
- Tables:
- K-Means summary (k selection, selected k, observations, initialization, convergence).
Expand Down
5 changes: 2 additions & 3 deletions src/app/models/visualization-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,8 @@ Data includes:
- `n_obs_interval`, `center_definition`, `privacy_note`, `intended_use`, `warnings`, `limitations`

Proposed visualization:
- For 2 variables: Scatter plot of cluster centers.
- For 3 variables: 3D scatter plot of cluster centers.
- For any other number of variables: Parallel coordinates plot.
- For 2 variables: Bubble plot of cluster centers, bubble area by cluster size.
- For 3 or more variables: Heatmap of cluster centers (clusters × variables), each cell printing the centre's value.
- When `elbow` is present: Elbow curve of `inertia_by_k` with the selected k marked.
- Supporting tables:
- K-Means summary (k selection, k, observations, initialization, convergence).
Expand Down
Loading
Loading