Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

## Interface and handling

- select the first browser result and show its interaction visualization
automatically; reuse CSV help text as result-header tooltips and store bundled
examples in directly importable/exportable parameter JSON (#315)

- explain all browser input, CopomuS ranking and candidate-filter controls with
tooltips; annotate compensatory mutations in one vaRRI view and compare all
four-state energies in an adjacent Plotly chart (#313)
Expand Down Expand Up @@ -341,6 +345,17 @@ energies, restricted partition sums, and trackers.
################################################################################
################################################################################

261027 Alexander Mitrofanov
* gui/{results.js,sessions.js,index.html}, scripts/wasm/test-browser.py
* Select the first energy-sorted interaction when a computation completes and
load its vaRRI visualization; retain the selection through later table
sorting and filtering.
* Apply each CSV catalog description to both its result-header cell and sort
button, matching the corresponding CSV-selection tooltip (issue #315).
* gui/{app.js,examples/,tests/parameter-file.test.mjs}, gui/docs/
* Move example display metadata into the example index so every example file
is an ordinary Store/Load parameter document with no wrapper (issue #315).

261027 Alexander Mitrofanov
* gui/{index.html,app.js,options.js,copomus-options.js,styles.css}
+ Add explanatory tooltips to every input label and all CopomuS ranking
Expand Down
16 changes: 9 additions & 7 deletions gui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -357,15 +357,17 @@ async function loadExamples() {
const response = await fetch(new URL('./examples/index.json', import.meta.url));
if (!response.ok) throw new Error('Example list is unavailable.');
const files = await response.json();
if (!Array.isArray(files) || !files.length || files.some((name) => typeof name !== 'string' || !/^[a-z0-9-]+\.json$/.test(name))) throw new Error('Invalid example list.');
const examples = await Promise.all(files.map(async (filename) => {
const response = await fetch(new URL(`./examples/${filename}`, import.meta.url));
if (!Array.isArray(files) || !files.length || files.some((example) =>
typeof example?.file !== 'string' || !/^[a-z0-9-]+\.json$/.test(example.file)
|| typeof example.name !== 'string' || !example.name.trim()
|| typeof example.description !== 'string' || !example.description.trim()
)) throw new Error('Invalid example list.');
const examples = await Promise.all(files.map(async (metadata) => {
const response = await fetch(new URL(`./examples/${metadata.file}`, import.meta.url));
if (!response.ok) throw new Error('An example is unavailable.');
const example = await response.json();
if (typeof example.name !== 'string' || typeof example.description !== 'string') throw new Error('Invalid example description.');
const request = importParameters(example.configuration);
const request = importParameters(await response.json());
prepareRequest(request);
return { ...example, request };
return { ...metadata, request };
}));
container.replaceChildren();
for (const example of examples) {
Expand Down
14 changes: 8 additions & 6 deletions gui/docs/json-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ fields show the short demonstration sequences as placeholders only. The Examples
disclosure menu follows the Input Parameters label and lists only examples for
the selected Prediction, Evaluation or Mutation Design task.

Maintainers edit `gui/examples/index.json` (an array of local JSON filenames).
Each listed file contains `name`, `description`, and `configuration`, where the
configuration is the same native JSON document used by Load. Three replaceable
examples cover prediction, evaluation and mutation design. Loading validates their settings and
sequences through the same importer and computation validator. Missing or invalid
examples show an error in the menu; they never overwrite the form.
Maintainers edit `gui/examples/index.json`, whose entries provide a local JSON
filename, display name and description. Each listed example file is directly the
same native JSON parameter document used by Store and Load: a maintainer can set
up an input in the GUI, download it with Store and replace an example file without
adding an example-only wrapper. Three replaceable examples cover prediction,
evaluation and mutation design. Loading validates their settings and sequences
through the same importer and computation validator. Missing or invalid examples
show an error in the menu; they never overwrite the form.

## Local FASTA files

Expand Down
34 changes: 18 additions & 16 deletions gui/docs/step4-results-and-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,16 @@ computed **mfe Δ** is `E - min(reported E for the same query–target pair)`, r
to hundredths. Evaluation uses the best supplied structure as its reference.
This is relative suboptimality, not a free-energy change or probability.

Every heading is a keyboard-operable sort button with `aria-sort`. Search covers
all returned values. A separate checklist shows/hides table columns after the
run; it does not change selected CSV output. All identifiers, output previews
and diagnostics render with `textContent`. The table has a 500 px maximum height
with vertical scrolling for longer results and a sticky heading; CSV downloads
still contain every selected row. Selected extra files appear below the
table with their original filename, covered sequence/sequence-pair names, a
header-level download, and bounded text previews, including SVG source.
Empty output is successful and still permits CSV/accessibility downloads.
Every heading is a keyboard-operable sort button with `aria-sort`; its hover
tooltip repeats the matching CSV-selection description. Search covers all
returned values. A separate checklist shows/hides table columns after the run;
it does not change selected CSV output. All identifiers, output previews and
diagnostics render with `textContent`. The table has a 500 px maximum height with
vertical scrolling for longer results and a sticky heading; CSV downloads still
contain every selected row. Selected extra files appear below the table with
their original filename, covered sequence/sequence-pair names, a header-level
download, and bounded text previews, including SVG source. Empty output is
successful and still permits CSV/accessibility downloads.

## Additional output visualizations

Expand All @@ -52,10 +53,11 @@ deleted. Non-plot formats keep the direct content view.
## Selected interaction in vaRRI

Each row has a keyboard-accessible View button; clicking a row also selects it.
Selection survives table sorting, filtering and visibility changes within each
session. Input edits, new runs, errors and cancellation preserve prior sessions
and their archive snapshots. Folding a session unloads its viewer; reopening
restores its selected interaction.
When computation returns interactions, the first row in the initial energy-sorted
table is selected and visualized automatically. Selection survives table sorting,
filtering and visibility changes within each session. Input edits, new runs, errors
and cancellation preserve prior sessions and their archive snapshots. Folding a
session unloads its viewer; reopening restores its selected interaction.
`visualization.js` uses native seq1 = target, seq2 = query and maps the native
CSV identifiers to the completed run's normalized FASTA records (including
semicolon-to-underscore ID conversion). It pads the interaction dot-bracket
Expand All @@ -73,9 +75,9 @@ viewer. The local status always identifies the selected pair and coordinates.
Zero-based CLI start indices remain accepted for computation/export, but vaRRI
requires nonzero start indices; choosing View for such a run explains this limit.

No external viewer loads until a row is selected. The interface explains that
selection shares that pair and structure through vaRRI's URL. Computation and
ZIP generation remain local. CSP permits frames only at the vaRRI deployment;
The first successful interaction result loads the external viewer automatically;
choosing another row shares that pair and structure through vaRRI's URL. Computation
and ZIP generation remain local. CSP permits frames only at the vaRRI deployment;
the iframe sends no referrer. Viewer availability depends on that external site;
the full-viewer link permits retry, and scientific outputs remain downloadable.

Expand Down
26 changes: 11 additions & 15 deletions gui/examples/evaluation.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
{
"name": "Evaluate a duplex",
"description": "Evaluate the energy of a supplied ten-base-pair RNA interaction.",
"configuration": {
"schema": "IntaRNA-parameters-v1",
"parameters": {
"personality": "IntaRNAeval",
"query": {
"text": ">ncRNA1\nAGGAUGGGGGAAACCCCAUACUCCUCACACACCAAAUCGCCCGAUUUAUCGGGCUUUUUU\n"
},
"target": {
"text": ">mRNA1\nUUUAAAUUAAAAAAUCAUAGAAAAAGUAUCGUUUGAUACUUGUGAUUAUACUCAGUUAUA\nCAGUAUCUUAAGGUGUUAUUAAUAGUGGUGAGGAGAAUUUAUGAAGCUUUUCAAAAGCUU\nGCUUGUGGCACCUGCAACUCUUGGUCUUUUAGCACCAAUGACCGCUACUGCUAAU\n"
},
"rri": "85|||||||||||&21||||||||.|||",
"outCsvCols": "id1,start1,end1,id2,start2,end2,subseqDP,hybridDP,E,Etotal,ED1,ED2,E_init,E_loops,E_dangleL,E_dangleR,E_endL,E_endR,E_hybrid,E_norm,E_hybridNorm,E_add,Eall,Eall1,Eall2,EallTotal"
}
"schema": "IntaRNA-parameters-v1",
"parameters": {
"personality": "IntaRNAeval",
"query": {
"text": ">ncRNA1\nAGGAUGGGGGAAACCCCAUACUCCUCACACACCAAAUCGCCCGAUUUAUCGGGCUUUUUU\n"
},
"target": {
"text": ">mRNA1\nUUUAAAUUAAAAAAUCAUAGAAAAAGUAUCGUUUGAUACUUGUGAUUAUACUCAGUUAUA\nCAGUAUCUUAAGGUGUUAUUAAUAGUGGUGAGGAGAAUUUAUGAAGCUUUUCAAAAGCUU\nGCUUGUGGCACCUGCAACUCUUGGUCUUUUAGCACCAAUGACCGCUACUGCUAAU\n"
},
"rri": "85|||||||||||&21||||||||.|||",
"outCsvCols": "id1,start1,end1,id2,start2,end2,subseqDP,hybridDP,E,Etotal,ED1,ED2,E_init,E_loops,E_dangleL,E_dangleR,E_endL,E_endR,E_hybrid,E_norm,E_hybridNorm,E_add,Eall,Eall1,Eall2,EallTotal"
}
}
18 changes: 17 additions & 1 deletion gui/examples/index.json
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
["prediction.json", "evaluation.json", "mutation-design.json"]
[
{
"file": "prediction.json",
"name": "Short duplex",
"description": "Predict interactions between two complementary ten-nucleotide RNAs."
},
{
"file": "evaluation.json",
"name": "Evaluate a duplex",
"description": "Evaluate the energy of a supplied ten-base-pair RNA interaction."
},
{
"file": "mutation-design.json",
"name": "Compensatory duplex mutation",
"description": "Rank an explicit compensatory mutation for a short RNA duplex."
}
]
40 changes: 18 additions & 22 deletions gui/examples/mutation-design.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
{
"name": "Compensatory duplex mutation",
"description": "Rank an explicit compensatory mutation for a short RNA duplex.",
"configuration": {
"schema": "IntaRNA-parameters-v1",
"parameters": {
"query": {
"text": ">query\nGGGAGGAGGG\n"
},
"target": {
"text": ">target\nCCCUCCUCCC\n"
}
"schema": "IntaRNA-parameters-v1",
"parameters": {
"query": {
"text": ">query\nGGGAGGAGGG\n"
},
"gui": {
"task": "mutation",
"copomus": {
"measures": ["mfeCover", "E", "minDeltaE"],
"candidateSelection": "mfe",
"candidateFilters": [],
"generator": "flip",
"mutationEncoding": "G1C&C10G",
"alpha": 1,
"beta": 1
}
"target": {
"text": ">target\nCCCUCCUCCC\n"
}
},
"gui": {
"task": "mutation",
"copomus": {
"measures": ["mfeCover", "E", "minDeltaE"],
"candidateSelection": "mfe",
"candidateFilters": [],
"generator": "flip",
"mutationEncoding": "G1C&C10G",
"alpha": 1,
"beta": 1
}
}
}
24 changes: 10 additions & 14 deletions gui/examples/prediction.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
{
"name": "Short duplex",
"description": "Predict interactions between two complementary ten-nucleotide RNAs.",
"configuration": {
"schema": "IntaRNA-parameters-v1",
"parameters": {
"query": {
"text": ">ncRNA1\nAGGAUGGGGGAAACCCCAUACUCCUCACACACCAAAUCGCCCGAUUUAUCGGGCUUUUUU\n>ncRNA2\nACUGAGGUACGAUCGUGGCAGGGCCUUUGACUGACUUUCGUACUUU"
},
"target": {
"text": ">mRNA1\nUUUAAAUUAAAAAAUCAUAGAAAAAGUAUCGUUUGAUACUUGUGAUUAUACUCAGUUAUA\nCAGUAUCUUAAGGUGUUAUUAAUAGUGGUGAGGAGAAUUUAUGAAGCUUUUCAAAAGCUU\nGCUUGUGGCACCUGCAACUCUUGGUCUUUUAGCACCAAUGACCGCUACUGCUAAU\n>mRNA2\nUGCUUCUUAGUAAUGGCCAGCUCGGGGAUUUGGAACUGGUGGUUGUUAAUUCUAGGCAAA\nUAGAUUAAAAUUAAAUUACUCAAAUUGUGUGACGAGUUUUAUGAAGCUUUUUAAAAGCUU\nGUUGGUAGCUCCAGCAACGAUUGGGCUACUAGCUCCAUUUUCAACGUUUGCUGGC\n"
},
"outNumber": 10,
"outCsvCols": "id1,start1,end1,id2,start2,end2,subseqDP,hybridDP,E,ED1,ED2,seedE,seedStart1,seedStart2"
}
"schema": "IntaRNA-parameters-v1",
"parameters": {
"query": {
"text": ">ncRNA1\nAGGAUGGGGGAAACCCCAUACUCCUCACACACCAAAUCGCCCGAUUUAUCGGGCUUUUUU\n>ncRNA2\nACUGAGGUACGAUCGUGGCAGGGCCUUUGACUGACUUUCGUACUUU"
},
"target": {
"text": ">mRNA1\nUUUAAAUUAAAAAAUCAUAGAAAAAGUAUCGUUUGAUACUUGUGAUUAUACUCAGUUAUA\nCAGUAUCUUAAGGUGUUAUUAAUAGUGGUGAGGAGAAUUUAUGAAGCUUUUCAAAAGCUU\nGCUUGUGGCACCUGCAACUCUUGGUCUUUUAGCACCAAUGACCGCUACUGCUAAU\n>mRNA2\nUGCUUCUUAGUAAUGGCCAGCUCGGGGAUUUGGAACUGGUGGUUGUUAAUUCUAGGCAAA\nUAGAUUAAAAUUAAAUUACUCAAAUUGUGUGACGAGUUUUAUGAAGCUUUUUAAAAGCUU\nGUUGGUAGCUCCAGCAACGAUUGGGCUACUAGCUCCAUUUUCAACGUUUGCUGGC\n"
},
"outNumber": 10,
"outCsvCols": "id1,start1,end1,id2,start2,end2,subseqDP,hybridDP,E,ED1,ED2,seedE,seedStart1,seedStart2"
}
}
2 changes: 1 addition & 1 deletion gui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ <h3 id="mutation-options-title">Compensatory mutation settings</h3>
<section data-role="visualization" aria-label="Interaction visualization">
<div class="visualization-heading"><h3 data-role="visualization-title">Interaction visualization</h3><a data-role="varri-link" target="_blank" rel="noopener noreferrer" hidden>Open in vaRRI</a></div>
<p data-role="visualization-status" role="status">Select View on an interaction to visualize it.</p>
<p class="input-help">Selecting View loads vaRRI with that sequence pair and structure in its URL.</p>
<p class="input-help">The first interaction is selected automatically. Selecting another View loads vaRRI with that sequence pair and structure in its URL.</p>
<iframe data-role="varri-frame" title="Selected RNA interaction in vaRRI" referrerpolicy="no-referrer" hidden></iframe>
</section>
</div>
Expand Down
12 changes: 9 additions & 3 deletions gui/results.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,17 @@ export class ResultsView {
label.append(input, document.createTextNode(column.label)); this.picker.append(label);
}
this.search.value = ''; this.key = columns.includes('E') ? 'E' : columns[0]; this.direction = 1;
this.root.hidden = false; this.render(); return this.rows.length;
this.root.hidden = false; this.render(true); return this.rows.length;
}
render() {
render(selectFirst = false) {
const rows = selectRows(this.rows, this.search.value, this.key, this.direction);
const columns = this.columns.filter((column) => this.visible.has(column.key));
// The completed run opens on the same interaction shown at the top of the
// initial table; later sorting and filtering retain that selection.
if (selectFirst && rows.length) {
this.selected = rows[0];
this.visualization.show(this.selected, this.request);
}
this.header.replaceChildren();
const viewHeading = document.createElement('th'); viewHeading.scope = 'col'; viewHeading.setAttribute('aria-label', 'Interaction visualization');
this.header.append(viewHeading);
Expand All @@ -111,7 +117,7 @@ export class ResultsView {
const split = column.numeric ? column.label.indexOf(' ') : -1;
if (split > 0) button.append(document.createTextNode(column.label.slice(0, split)), document.createElement('br'), document.createTextNode(column.label.slice(split + 1)));
else button.textContent = column.label;
button.title = column.description ?? column.label;
cell.title = button.title = column.description ?? column.label;
const active = column.key === this.key, direction = this.direction === 1 ? 'ascending' : 'descending';
cell.setAttribute('aria-sort', active ? direction : 'none');
button.setAttribute('aria-label', `Sort by ${column.label}${active ? `, currently ${direction}` : ''}`);
Expand Down
5 changes: 3 additions & 2 deletions gui/sessions.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ export class SessionsView {
session.toggle.setAttribute('aria-expanded', String(expanded));
session.toggle.textContent = expanded ? 'Hide' : 'Show';
// Unload third-party viewers while folded. The table selection is retained
// and can be visualized again without recomputing the prediction.
// and can be visualized again without recomputing the prediction. A newly
// rendered result already has its initial viewer URL and must not reload it.
if (!expanded) session.view.visualization.clear();
else {
if (session.view.selected) session.view.visualization.show(session.view.selected, session.view.request);
if (session.view.selected && !session.view.visualization.frame.hasAttribute('src')) session.view.visualization.show(session.view.selected, session.view.request);
for (const fileView of session.fileViews) fileView.resize();
}
}
Expand Down
12 changes: 7 additions & 5 deletions gui/tests/parameter-file.test.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { DEFAULTS, EVALUATION_EXAMPLE, prepareRequest, CSV_COLUMNS } from '../protocol.js';
import { DEFAULTS, EVALUATION_EXAMPLE, prepareRequest, CSV_COLUMNS, JSON_SCHEMA } from '../protocol.js';
import { FIELDS } from '../options.js';
import { exportParameters, serializeParameters, importParameters } from '../parameter-file.js';
const provenance = 'IntaRNA test\nViennaRNA test\n';
Expand Down Expand Up @@ -50,10 +50,12 @@ test('maintainer examples use the same JSON import and prediction validation', a
const examples = JSON.parse(await readFile(new URL('../examples/index.json', import.meta.url)));
assert.equal(examples.length, 3);
const tasks = new Set();
for (const file of examples) {
const example = JSON.parse(await readFile(new URL(`../examples/${file}`, import.meta.url)));
assert.ok(example.name && example.description);
tasks.add(prepareRequest(importParameters(example.configuration)).task);
for (const metadata of examples) {
assert.ok(metadata.file && metadata.name && metadata.description);
const example = JSON.parse(await readFile(new URL(`../examples/${metadata.file}`, import.meta.url)));
assert.equal(example.schema, JSON_SCHEMA);
assert.equal(example.configuration, undefined);
tasks.add(prepareRequest(importParameters(example)).task);
}
assert.deepEqual([...tasks].sort(), ['evaluation', 'mutation', 'prediction']);
});
Loading
Loading