Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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: 5 additions & 4 deletions docs/d3-upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@ Restarting explicitly refreshes those caches after changing pseudoknot strength
or removing exterior helpers. Old link arrays are cleared before replacing
nodes. Stopping does not emit convergence or restart a cancelled simulation.

Drag movement is measured in the zoomed plot's coordinates. Selection modifiers
and prior fixed state are preserved. Disposal detaches active mouse gestures,
restores browser text selection, and prevents late callbacks from restarting
the old simulation. Programmatic fitting updates D3's stored zoom transform,
The subsequent #74 interaction update measures pointer movement through each
element's zoomed and rotated coordinate frame. Ctrl-click and rectangle selection
include nucleotides, numbering, and text. Dragged graph nodes remain fixed for the
current rendering; text positions update the model. Disposal detaches active
gestures and prevents late callbacks from restarting the old simulation. Programmatic fitting updates D3's stored zoom transform,
so subsequent wheel and pan gestures start at the fitted view.

## Verification and visual review
Expand Down
6 changes: 5 additions & 1 deletion docs/force-field-graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,11 @@ Linear RRI and intramolecular options construct two-rail helix templates from
the validated pair structure. Projection preserves handedness, handles bulges
and interior loops, and respects fixed nodes. Index labels receive a bounded
outward adjustment. RRI horizontalization is a rigid graph rotation, and the
viewport refits once at force convergence.
viewport refits once at force convergence. Both linear options also work with
Force layout disabled: the same constraints settle for at most 60 synchronous
ticks, then the force and its constraint listeners stop before rendering is
shown. This creates a static linear layout without fixing its nodes; dragging,
release, reset, and undo remain available.

## Drawing and lifecycle

Expand Down
4 changes: 2 additions & 2 deletions docs/sharing-and-input.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ See the [viewer guide](viewer-guide.md) and the [Input Format Reference](#input-
| **`colorRriNodes` / `colorRriRegion` / `colorBasepair`** | Hex color codes for nucleotide highlights, background highlights, and base pairs. |
| **`distinctBpTypes`** | Toggle display of G-U Wobble base pairs as dashed lines (`true` / `false`). |
| **`forceLayout`** | Enable or disable the force-layout physics simulation (`true` / `false`). |
| **`forceLayoutLinearRRI`** | Enforce a linear horizontal layout of all noncrossing RRI helices. Enabling it also enables `forceLayout`. |
| **`forceLayoutLinearStructure`** | Enforce a linear layout of intramolecular stems containing bulges or interior loops. Enabling it also enables `forceLayout`. |
| **`forceLayoutLinearRRI`** | Enforce a linear horizontal layout of all noncrossing RRI helices. Independent of `forceLayout`; when it is off, the linear layout settles briefly and then stops. |
| **`forceLayoutLinearStructure`** | Enforce a linear layout of intramolecular stems containing bulges or interior loops. Independent of `forceLayout`; when it is off, the linear layout settles briefly and then stops. |
| **`textAnnotations`** | JSON definitions with text, styles, positions, and sequence-name identities. An explicit `[]` leaves names unpositioned; use Share Link to generate this value. |

To simplify sequence and structure input validation, sequence and structure inputs are highlighted with the chosen strand-specific colors.
Expand Down
41 changes: 41 additions & 0 deletions docs/static-linear-layout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Static linear layouts (issue #101)

Both linear options are independent of Force layout in the UI, URL state,
examples, and standalone rendering API. They appear above Force layout in
Visualization Settings. Disabling Force layout keeps either linear option
selected; free trailing ends and pseudoknot pulling remain force-only options.

The existing rail projection and D3 engine are unchanged. With animation off,
applicable helices settle for at most 60 synchronous force ticks. The renderer
then removes linear tick/end listeners and stops the force before returning
control. Annotation placement and viewport fitting use the settled coordinates.
No nodes are fixed by this initialization, and no background overlay animation
is started. Manual moves, release, reset, undo, exports, cancellation, and
rerendering keep their static-layout behavior.

## Geometry review

Compared ordinary static rendering against both linear options enabled with
Force layout off on 7 October 2026, using Chromium 153.0.8010.12, an 800×600
render-only viewport, seed 83, and each example's existing cropping/annotations.
The source is based on `a6743a2` with the changes in this issue's pull request.
Screenshots and measurements were saved under
`output/playwright/static-linear-review/` for inspection, not as new baselines.

- `2mol`: the RRI axis becomes horizontal; the intramolecular stem straightens.
All 47 graph nucleotides and the colored annotations remain visible.
- `coronel-tellez-2022`: the bent interaction becomes horizontal; all 92 graph
nucleotides, probability colors, labels, and region overlays remain present.
- `wu-2024`: the interaction straightens while retaining the large unpaired loop,
all 61 graph nucleotides, signed biological labels, and mutation labels.
- `IntaRNA-seeds`: the long interaction becomes horizontal with its unpaired
loops and seed highlights retained; all 127 graph nucleotides remain present.
- `crossing-rri`: the existing crossing-pair restriction leaves the 30-node graph
unchanged because it has no applicable noncrossing rail template.

All ten compared scenes stayed motionless after rendering. The original five
visual regression fixtures still match exactly with linear options disabled;
no baselines were replaced. Automated tests additionally check each linear
option separately, both together, unpaired/crossing inputs, rail spacing,
listener cleanup, cancellation, native/bundle parity, static release/undo,
SVG output, and URL/example/control state.
17 changes: 10 additions & 7 deletions docs/viewer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,11 @@ Open the **Example** dropdown to choose another RNA-RNA interaction. Each open m
| **Base pair color** | Chose the color used for all base pairs (intra- and intermolecular). |
| **Color Choice** | Use the color pickers to customize the highlighting colors. |
| **G-U basepairs dashed** | When checked, G-U basepairs are drawn with a dashed stroke. |
| **Linear horizontal RRI layout** | Keeps all noncrossing intermolecular helixes on straight linear layout and rotates the complete two-molecule interaction so its RRI axis is horizontal. Works with or without Force layout; when it is off, the linear layout settles briefly and then stops. |
| **Linear intramolecular stem layout** | Keeps intramolecular stems (i.e. helices containing bulges or interior loops) on straight linear layout. Stems stop at multiloops or crossing pairs. Works with or without Force layout; when it is off, the linear layout settles briefly and then stops. |
| **Force layout** | When checked, the rendered structure is shown in an interactive force-directed layout. When unchecked, the structure is drawn in a fixed layout. |
| - **Linear horizontal RRI layout** | Keeps all noncrossing intermolecular helixes on straight linear layout and rotates the complete two-molecule interaction so its RRI axis is horizontal. Enabling it also enables the force layout. |
| - **Free trailing ends** | When checked, the trailing ends of the sequences are not fixed in the force-directed layout and can move freely. |
| - **Pull Pseudoknot Basepairs** | Pulls pseudoknot basepairs together and stabilizes stacked pairs in the force-directed layout. |
| - **Linear intramolecular stem layout** | Keeps intramolecular stems (i.e. helices containing bulges or interior loops) on straight linear layout. Stems stop at multiloops or crossing pairs. Enabling it also enables the force layout. |
| - **Pull Pseudoknot Basepairs** | When checked, pulls pseudoknot basepairs together and stabilizes stacked pairs in the force-directed layout. |

### Region Highlights

Expand Down Expand Up @@ -214,12 +214,15 @@ both positioned and unpositioned labels, including their formatting.
### Additional Features

- **Zooming**: Use the mouse wheel to zoom in and out.
- **Panning**: Click and drag the visualisation to pan around. This is useful when zoomed in to focus on a certain region of the structure.
- **Panning**: Click and drag the empty background to pan around. This is useful when zoomed in to focus on a certain region of the structure.
- **Rotation**: Use the *Rotation* slider below the visualisation to rotate the structure. Rotation preserves text orientation and is useful to align the structure for better visibility or to match a certain orientation in a publication figure.
- **Cropping**: Use the *Crop* slider to reduce the unpaired nucleotides at the ends of each sequence to the given number. This is useful to focus on the interaction region and reduce the size of the visualisation. A value of `-1` disables cropping and shows the full sequences.
- **Nucleotide Nodes**
- .. can be dragged to new positions in the force-directed layout mode.
- .. show a tooltip with the nucleotide index and probability value (if present) when hovered over.
- **Selection**: Ctrl-click a nucleotide, numbering label, or text label to add it to or remove it from the selection. Ctrl-drag the background to replace the selection with all elements fully inside the rectangle. An empty rectangle, Ctrl-click on the background, or a plain click on an element clears the selection. Command also works on macOS.
- **Moving elements**: Drag any nucleotide, numbering label, or text label in either layout mode. Drag a selected element to move the whole selection; dragging an unselected element clears the selection and moves that element alone. Released nodes and numbering labels stay in place until the next render, including in force mode.
- **Manual positions**: The **Moved** button beside Crop counts manually positioned nucleotides and numbering labels; click it to select them all. **Release** unfixes selected nodes at their current positions, allowing them to move again when Force layout is enabled, and removes them from the Moved count. **Reset** restores selected manual nodes to their positions and fixation before the first edit. Text labels are not counted, released, or reset. **Undo** (or Ctrl+Z / Command+Z while the canvas is focused) reverses the last drag, selection rotation, release, or reset. Undo a release before resetting that node to its original position. History is kept for the current rendering. The **?** button opens the full interaction guide.
- **Selection rotation**: With at least two elements selected, hold Ctrl (or Command) and use the mouse wheel to rotate them around the pointer. The ordinary wheel still zooms.
- **Saving positions**: SVG and PNG exports preserve the current layout and omit selection outlines. Share links save text-label positions; manually moved nucleotide and numbering positions last only for the current rendering.
- **Nucleotide tooltips**: Hover to see the nucleotide index and probability value (if present).
- **Resize Canvas**: The visualisation canvas size can be adjusted by dragging the bottom-right corner of the canvas. This is useful when visualizing large interactions on large screens, or when preparing figures for publication. The canvas size is preserved when exporting the visualisation.

### Export
Expand Down
69 changes: 69 additions & 0 deletions scripts/browser-canvas-help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';

/** The panel must supply working help even when its host knows only ViewerPanel. */
export async function exerciseStandaloneCanvasHelp(page, origin) {
const appURL = origin + '/src/ui/components/app.js';
await page.route(appURL, route => route.fulfill({ contentType: 'text/javascript', body: `
import { viewerComponent } from './context.js';
import ViewerPanel from './viewer-panel.js';
export default viewerComponent('ViewerHost', '<ViewerPanel />', { ViewerPanel });
` }));
// Reproduce the review screenshot: the general dialog theme is cached without
// the newer help-specific rules. Component styles must still center the help.
const cssURL = origin + '/src/ui/styles/dialogs.css';
const oldCSS = fs.readFileSync(new URL('../src/ui/styles/dialogs.css', import.meta.url), 'utf8')
.split('dialog.canvas-help-dialog')[0];
await page.route(cssURL, route => route.fulfill({ contentType: 'text/css', body: oldCSS }));
try {
await page.goto(origin + '/index.html?showRenderingOnly=1&sequence=AAAA%26UUUU&structure=((..%26..))&forceLayout=0',
{ waitUntil: 'networkidle' });
await page.waitForFunction(() => document.querySelector('#rendering-canvas circle') &&
document.getElementById('rendering-canvas').style.visibility !== 'hidden');
const help = page.getByRole('button', { name: 'Canvas interaction help', exact: true });
await help.focus();
await page.keyboard.press('Enter');
const dialog = page.getByRole('dialog', { name: 'Canvas interactions', exact: true });
assert.equal(await dialog.count(), 1, 'The help button supplies exactly one accessible dialog');
assert.equal(await dialog.isVisible(), true, 'Keyboard activation opens visible help');
assert.equal(await dialog.evaluate(element => element.matches(':modal')), true, 'Help enters the modal top layer');
await checkCanvasHelpStyles(page);
await page.keyboard.press('Escape');
assert.equal(await help.evaluate(element => element === document.activeElement), true);
await help.click();
await dialog.getByRole('button', { name: 'Close', exact: true }).click();
assert.equal(await dialog.count(), 0, 'Close dismisses help from the accessibility tree');
} finally {
await page.unroute(appURL);
await page.unroute(cssURL);
}
}

/** Check geometry and recognizable, noninteractive button names in the help. */
export async function checkCanvasHelpStyles(page) {
const dialog = page.locator('#canvasHelpDialog');
const box = await dialog.boundingBox();
const viewport = page.viewportSize();
assert.ok(box.width <= 542 && box.width <= viewport.width - 30, 'Help has a bounded width');
assert.ok(Math.abs(box.x + box.width / 2 - viewport.width / 2) < 2, 'Help is horizontally centered');
assert.ok(Math.abs(box.y + box.height / 2 - viewport.height / 2) < 2, 'Help is vertically centered');
assert.equal(await dialog.locator('dt').first().evaluate(el => getComputedStyle(el).fontWeight), '700');
const keys = dialog.locator('dt .canvas-position-key');
assert.deepEqual(await keys.allTextContents(), ['Moved: X', 'Release', 'Reset', 'Undo']);
const appearances = await keys.evaluateAll(elements => elements.map(el => {
const style = getComputedStyle(el);
return { border: style.border, radius: style.borderRadius, padding: style.padding,
height: style.height, fontSize: style.fontSize, weight: style.fontWeight, tag: el.tagName };
}));
// Enable a toolbar button only to compare its normal appearance with the key.
const expected = await page.locator('#selectMovedBtn').evaluate(el => {
const disabled = el.disabled;
el.disabled = false;
const style = getComputedStyle(el);
const result = { border: style.border, radius: style.borderRadius, padding: style.padding,
height: style.height, fontSize: style.fontSize, weight: '700', tag: 'SPAN' };
el.disabled = disabled;
return result;
});
appearances.forEach(appearance => assert.deepEqual(appearance, expected));
}
Loading
Loading