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
19 changes: 16 additions & 3 deletions components/CircularPlot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@ interface CircularPlotProps {
imageProperties: ImagePropertiesConfig;
onViewStateChange?: (state: PlotViewState) => void;
squarePlot?: boolean;
centreViewSignal?: number;
}

interface TooltipInfo extends PlotTooltip {
x: number;
y: number;
}

export default function CircularPlot({ data, imageProperties, onViewStateChange, squarePlot = false }: CircularPlotProps) {
export default function CircularPlot({ data, imageProperties, onViewStateChange, squarePlot = false, centreViewSignal = 0 }: CircularPlotProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const rendererRef = useRef<CanvasPlotRenderer | null>(null);
const [tooltip, setTooltip] = useState<TooltipInfo | null>(null);
Expand All @@ -40,6 +41,7 @@ export default function CircularPlot({ data, imageProperties, onViewStateChange,
panRef.current = pan;

const onViewStateChangeRef = useRef(onViewStateChange);
const previousCentreViewSignal = useRef(centreViewSignal);
onViewStateChangeRef.current = onViewStateChange;

/** Emit current view state to parent (for SVG export). */
Expand Down Expand Up @@ -130,6 +132,15 @@ export default function CircularPlot({ data, imageProperties, onViewStateChange,
emitViewState();
}, [zoom, pan, emitViewState]);

// A fullscreen transition should not preserve a plot dragged partly off-canvas.
// Keep the user's zoom and legend placement, but centre the circular map.
useEffect(() => {
if (previousCentreViewSignal.current === centreViewSignal) return;
previousCentreViewSignal.current = centreViewSignal;
setPan({ x: 0, y: 0 });
setTooltip(null);
}, [centreViewSignal]);

const getCanvasCoords = (e: React.MouseEvent) => {
const canvas = canvasRef.current;
if (!canvas) return { canvasX: 0, canvasY: 0 };
Expand Down Expand Up @@ -274,11 +285,13 @@ export default function CircularPlot({ data, imageProperties, onViewStateChange,
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
onWheel={handleWheel}
style={{ cursor: draggingLegend ? 'move' : isDragging ? 'grabbing' : 'grab', background: 'white' }}
data-plot-pan-x={Math.round(pan.x)}
data-plot-pan-y={Math.round(pan.y)}
style={{ cursor: draggingLegend ? 'move' : isDragging ? 'grabbing' : 'grab', background: 'var(--gx-bg-alt)' }}
>
<canvas
ref={canvasRef}
style={{ maxWidth: '100%', maxHeight: '100%' }}
style={{ display: 'block', maxWidth: '100%', maxHeight: '100%', background: 'white', boxShadow: '0 0 0 1px var(--gx-border)' }}
/>
{tooltip && tooltip.x != null && tooltip.y != null && (
<div
Expand Down
9 changes: 7 additions & 2 deletions src/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export default function Home() {
const sessionUrl = searchParams.get('url');
const loadedSessionUrlRef = useRef<string | null>(null);
const [plotViewState, setPlotViewState] = useReactState<PlotViewState | null>(null);
const [plotCentreSignal, setPlotCentreSignal] = useReactState(0);
const [bugReportOpen, setBugReportOpen] = useReactState(false);
const handleViewStateChange = useCallback((state: PlotViewState) => {
setPlotViewState(previous => samePlotViewState(previous, state) ? previous : state);
Expand All @@ -78,6 +79,10 @@ export default function Home() {
loadedSessionUrlRef.current = sessionUrl;
void handleLoadSessionUrl(sessionUrl);
}, [handleLoadSessionUrl, sessionUrl]);
const togglePlotExpanded = () => {
if (!plotExpanded) setPlotCentreSignal(signal => signal + 1);
setPlotExpanded(value => !value);
};

return (
<>
Expand Down Expand Up @@ -116,7 +121,7 @@ export default function Home() {
<div className="flex justify-between items-center mb-6">
<h2 className="section-title mb-0">Circular Plot</h2>
<div className="flex items-center gap-2">
<button onClick={() => setPlotExpanded(value => !value)} className="btn-secondary text-xs px-2 py-1" title={plotExpanded ? 'Shrink plot' : 'Expand plot'}>
<button onClick={togglePlotExpanded} className="btn-secondary text-xs px-2 py-1" title={plotExpanded ? 'Shrink plot' : 'Expand plot'}>
{plotExpanded ? (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /></svg>
) : (
Expand All @@ -130,7 +135,7 @@ export default function Home() {
{displayedPlotData ? (
<div className={plotExpanded ? 'flex-1 min-h-0' : ''}>
<ErrorBoundary>
<CircularPlot data={displayedPlotData} imageProperties={imageProperties} onViewStateChange={handleViewStateChange} />
<CircularPlot data={displayedPlotData} imageProperties={imageProperties} onViewStateChange={handleViewStateChange} centreViewSignal={plotCentreSignal} />
</ErrorBoundary>
</div>
) : (
Expand Down
35 changes: 35 additions & 0 deletions tests/e2e/brigx.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,13 +484,48 @@ test.describe('BRIGX e2e — circular genome plot', () => {
})

test('expanded plot controls remain clickable and SVG downloads', async ({ page }) => {
await page.setViewportSize({ width: 2048, height: 722 })
await page.goto('/app')

await page.getByLabel('Reference genome file').setInputFiles(REFERENCE)
await expect(page.getByRole('heading', { name: 'Statistics' })).toBeVisible({ timeout: 30_000 })

const inlinePlotArea = page.getByTestId('plot-area')
await inlinePlotArea.scrollIntoViewIfNeeded()
const inlineCanvasBox = await inlinePlotArea.locator('canvas').boundingBox()
expect(inlineCanvasBox).not.toBeNull()
const dragY = Math.max(1, Math.min(721, inlineCanvasBox!.y + inlineCanvasBox!.height / 2))
await page.mouse.move(inlineCanvasBox!.x + inlineCanvasBox!.width / 2, dragY)
await page.mouse.down()
await page.mouse.move(inlineCanvasBox!.x + inlineCanvasBox!.width / 2 + 180, dragY, { steps: 6 })
await page.mouse.up()
await expect(inlinePlotArea).not.toHaveAttribute('data-plot-pan-x', '0')

await page.getByRole('button', { name: 'Expand plot' }).click()
await expect(page.getByRole('button', { name: 'Shrink plot' })).toBeVisible()
const expandedPlotArea = page.getByTestId('plot-area')
await expect(expandedPlotArea).toHaveAttribute('data-plot-pan-x', '0')
await expect(expandedPlotArea).toHaveAttribute('data-plot-pan-y', '0')
await expect.poll(async () => {
const canvasRect = await expandedPlotArea.locator('canvas').evaluate(canvas => canvas.getBoundingClientRect())
return Math.abs(canvasRect.width - canvasRect.height)
}).toBeLessThan(2)
const expandedLayout = await expandedPlotArea.evaluate(element => {
const canvas = element.querySelector('canvas')!;
const wrapperRect = element.getBoundingClientRect();
const canvasRect = canvas.getBoundingClientRect();
return {
wrapperCentre: wrapperRect.left + wrapperRect.width / 2,
canvasCentre: canvasRect.left + canvasRect.width / 2,
canvasWidth: canvasRect.width,
canvasHeight: canvasRect.height,
wrapperBackground: getComputedStyle(element).backgroundColor,
canvasBackground: getComputedStyle(canvas).backgroundColor,
};
})
expect(Math.abs(expandedLayout.canvasWidth - expandedLayout.canvasHeight)).toBeLessThan(2)
expect(Math.abs(expandedLayout.wrapperCentre - expandedLayout.canvasCentre)).toBeLessThan(2)
expect(expandedLayout.wrapperBackground).not.toBe(expandedLayout.canvasBackground)
await page.getByRole('button', { name: 'Zoom in (or scroll up)' }).click()
await page.getByRole('button', { name: 'Zoom in (or scroll up)' }).click()

Expand Down
Loading