Loading frontend/src/components/task-view/analysis/batch/visualization/BatchVisualizationPanel.tsx +81 −142 Original line number Diff line number Diff line import React, { useCallback, useMemo, useRef } from 'react'; import React, { useMemo } from 'react'; import { Button, Grid, Loading @@ -12,15 +12,9 @@ import { Help } from '@mui/icons-material'; import CanvasHeatmap from '../../../../visualization/CanvasHeatmap'; import useHeatmapManagement from '../../../../../hooks/visualization/useHeatmapManagement'; import { binHeight, binWidth } from '../../../../visualization/visualizationConfig'; import { binHeight } from '../../../../visualization/visualizationConfig'; import SelectPrimaryFaceDialog from '../../../../projects-view/dialogs/SelectPrimaryFaceDialog'; import { LinkageStrategy, Transform } from '../../../../../types/BatchVisualizationTypes'; import { LinkageStrategy } from '../../../../../types/BatchVisualizationTypes'; import OpenSingleTaskConfirmationDialog from '../../../../projects-view/dialogs/OpenSingleTaskConfirmationDialog'; const BatchVisualizationPanel = () => { Loading @@ -30,54 +24,38 @@ const BatchVisualizationPanel = () => { scaleAxisX, scaleAxisY, colorScale, hiddenRowFaceNames, hiddenColumnFaceNames, selectedHiddenRows, selectedHiddenColumns, setSelectedHiddenRows, setSelectedHiddenColumns, selectionMode, toggleSelectionMode, draggedRowIndex, targetRowIndex, draggedColumnIndex, targetColumnIndex, handleRowMouseDown, handleColumnMouseDown, handleMouseUp, setTargetRowIndex, setTargetColumnIndex, showHidden, handleShowFaces, openSelectPrimaryFace, setOpenSelectPrimaryFace, filesPair, handleRowOrColumnClick, handleOpenPairTask, calculateRowPositions, clusteringSteps, handleShowDendrogram, localLinkageStrategy, setLocalLinkageStrategy, lastClickedRowName, lastClickedColumnName, hideRowAndColumn, openSingleTaskConfirmation, setOpenSingleTaskConfirmation, hiddenItems, rowState, columnState, onCellClick, transform, handleHeatmapDrag, finalizeDrag, openSelectPrimaryFace, localLinkageStrategy, filesPair, handleOpenSingleTask, lastClickedRowIndex, lastClickedColumnIndex, isPairClicked, handleCellClick, handleKeyDown, openSingleTaskConfirmation, clusteringSteps, distance, calculateRowPositions, showHidden, hideBoth, distance setHiddenItems, toggleSelectionMode, setOpenSelectPrimaryFace, setLocalLinkageStrategy, setOpenSingleTaskConfirmation, handleShowDendrogram } = useHeatmapManagement(); const transform = useRef<Transform>({ scale: 1, translateX: 0, translateY: 0 }).current; console.log('rendering BatchVisualizationPanel'); const rowPositions = useMemo( () => calculateRowPositions(heatmapRows, scaleAxisY, binHeight), [heatmapRows] Loading @@ -88,62 +66,6 @@ const BatchVisualizationPanel = () => { [clusteringSteps] ); const handleMouseMoveInternal = useCallback( (event: React.MouseEvent<HTMLDivElement>) => { if (selectionMode) { const rect = event.currentTarget.getBoundingClientRect(); if (draggedRowIndex !== null) { const yPosition = (event.clientY - rect.top - transform.translateY) / transform.scale; const newIndex = Math.floor((yPosition - 100) / binHeight); if (newIndex >= 0 && newIndex < heatmapRows.length) { if (newIndex === draggedRowIndex) { setTargetRowIndex(null); } else { setTargetRowIndex(newIndex); } } } if (draggedColumnIndex !== null) { const xPosition = (event.clientX - rect.left - transform.translateX) / transform.scale; const newColumnIndex = Math.floor((xPosition - 100) / binWidth); if ( newColumnIndex >= 0 && newColumnIndex < heatmapRows[0].columns.length ) { if (newColumnIndex === draggedColumnIndex) { setTargetColumnIndex(null); } else { setTargetColumnIndex(newColumnIndex); } } } } }, [ selectionMode, draggedRowIndex, draggedColumnIndex, transform.scale, transform.translateX, transform.translateY, heatmapRows ] ); const handleKeyDown = useCallback( (event: React.KeyboardEvent<HTMLDivElement>) => { if (event.key === 'S' || event.key === 's') { toggleSelectionMode(); } }, [] ); if (!heatmapRows.length) { return ( <Grid container> Loading @@ -167,8 +89,13 @@ const BatchVisualizationPanel = () => { <InputLabel id="row-select-label">Hidden Rows</InputLabel> <Select multiple value={selectedHiddenRows} onChange={e => setSelectedHiddenRows(e.target.value as string[])} value={hiddenItems.selectedRows} onChange={e => setHiddenItems({ ...hiddenItems, selectedRows: e.target.value as string[] }) } sx={{ width: '150px', height: '30px' }} displayEmpty renderValue={selected => (selected.length > 0 ? selected[0] : '')} Loading @@ -181,7 +108,7 @@ const BatchVisualizationPanel = () => { } }} > {hiddenRowFaceNames.map(rowFaceName => ( {hiddenItems.rows.map(rowFaceName => ( <MenuItem key={rowFaceName} value={rowFaceName}> {rowFaceName} </MenuItem> Loading @@ -193,9 +120,12 @@ const BatchVisualizationPanel = () => { <InputLabel id="column-select-label">Hidden Columns</InputLabel> <Select multiple value={selectedHiddenColumns} value={hiddenItems.selectedColumns} onChange={e => setSelectedHiddenColumns(e.target.value as string[]) setHiddenItems({ ...hiddenItems, selectedColumns: e.target.value as string[] }) } sx={{ width: '150px', height: '30px' }} displayEmpty Loading @@ -209,7 +139,7 @@ const BatchVisualizationPanel = () => { } }} > {hiddenColumnFaceNames.map(colFaceName => ( {hiddenItems.columns.map(colFaceName => ( <MenuItem key={colFaceName} value={colFaceName}> {colFaceName} </MenuItem> Loading @@ -221,7 +151,10 @@ const BatchVisualizationPanel = () => { <Button variant="contained" onClick={() => showHidden(selectedHiddenRows, selectedHiddenColumns) showHidden( hiddenItems.selectedRows, hiddenItems.selectedColumns ) } sx={{ height: '30px', marginTop: '22px' }} > Loading @@ -229,14 +162,16 @@ const BatchVisualizationPanel = () => { </Button> </Grid> {lastClickedRowName && ( {rowState.lastClickedName && ( <Grid item> <InputLabel style={{ fontWeight: 'bold', width: '115px' }}> {lastClickedRowName} {rowState.lastClickedName} </InputLabel> <Button variant="contained" onClick={() => hideRowAndColumn(lastClickedRowName, true)} onClick={() => hideRowAndColumn(rowState.lastClickedName as string, true) } sx={{ height: '30px', width: '115px' }} > Hide face Loading @@ -244,26 +179,31 @@ const BatchVisualizationPanel = () => { </Grid> )} {lastClickedColumnName && ( {columnState.lastClickedName !== null && ( <Grid item> <InputLabel style={{ fontWeight: 'bold', width: '115px' }}> {lastClickedColumnName} {columnState.lastClickedName} </InputLabel> <Button variant="contained" onClick={() => hideRowAndColumn(lastClickedColumnName, false)} onClick={() => hideRowAndColumn(columnState.lastClickedName as string, false) } sx={{ height: '30px', width: '115px' }} > Hide face </Button> </Grid> )} {lastClickedRowName && lastClickedColumnName && ( {rowState.lastClickedName && columnState.lastClickedName && ( <Grid item> <Button variant="contained" onClick={() => hideBoth(lastClickedRowName, lastClickedColumnName) hideBoth( rowState.lastClickedName as string, columnState.lastClickedName as string ) } sx={{ height: '30px', Loading Loading @@ -312,31 +252,35 @@ const BatchVisualizationPanel = () => { Make clustering </Button> </Grid> {lastClickedRowName && ( {rowState.lastClickedName && ( <Grid item> <Button variant="contained" onClick={() => handleOpenSingleTask(lastClickedRowName)} onClick={() => handleOpenSingleTask(rowState.lastClickedName as string) } sx={{ height: '30px', width: '115px' }} > Open task </Button> </Grid> )} {lastClickedColumnName && ( {columnState.lastClickedName && ( <Grid item> <Button variant="contained" onClick={() => handleOpenSingleTask(lastClickedColumnName)} onClick={() => handleOpenSingleTask(columnState.lastClickedName as string) } sx={{ height: '30px', width: '115px' }} > Open task </Button> </Grid> )} {lastClickedRowName && lastClickedColumnName && lastClickedRowName !== lastClickedColumnName && ( {rowState.lastClickedName && columnState.lastClickedName && rowState.lastClickedName !== columnState.lastClickedName && ( <Grid item> <Button variant="contained" Loading Loading @@ -400,12 +344,13 @@ const BatchVisualizationPanel = () => { ) : ( <div style={{ width: '800px', height: '600px', position: 'relative' width: '100%', height: 'calc(100vh - 325px)', display: 'flex', outline: 'none' }} onMouseMove={handleMouseMoveInternal} onMouseUp={handleMouseUp} onMouseMove={handleHeatmapDrag} onMouseUp={finalizeDrag} onKeyDown={handleKeyDown} role="button" tabIndex={0} Loading @@ -415,20 +360,14 @@ const BatchVisualizationPanel = () => { scaleAxisX={scaleAxisX} scaleAxisY={scaleAxisY} colorScale={colorScale} canvasWidth={800} canvasHeight={600} handleRowMouseDown={handleRowMouseDown} handleColumnMouseDown={handleColumnMouseDown} targetRowIndex={targetRowIndex} targetColumnIndex={targetColumnIndex} handleShowFaces={handleShowFaces} handleCellClick={handleCellClick} handleRowOrColumnClick={handleRowOrColumnClick} onCellClick={onCellClick} rowState={rowState} columnState={columnState} selectionMode={selectionMode} transform={transform} clusteringSteps={memoizedClusteringSteps} rowPositions={rowPositions} lastClickedRowIndex={lastClickedRowIndex} lastClickedColumnIndex={lastClickedColumnIndex} /> </div> )} Loading frontend/src/components/visualization/CanvasHeatmap.tsx +43 −49 Original line number Diff line number Diff line Loading @@ -2,8 +2,10 @@ import React, { useRef, useEffect, useCallback } from 'react'; import { ScaleLinear } from 'd3-scale'; import { ColumnState, HeatmapRowData, PairStep, RowState, Transform } from '../../types/BatchVisualizationTypes'; Loading @@ -14,26 +16,24 @@ type CanvasHeatmapProps = { scaleAxisX: ScaleLinear<number, number>; scaleAxisY: ScaleLinear<number, number>; colorScale: (value: number) => string; canvasWidth: number; canvasHeight: number; handleRowMouseDown: (rowIndex: number, rowName: string) => void; handleColumnMouseDown: (columnIndex: number, columnName: string) => void; targetRowIndex: number | null; targetColumnIndex: number | null; handleShowFaces: (rowFaceName: string, columnFaceName: string) => void; handleCellClick: ( handleRowOrColumnClick: ( type: 'row' | 'column', rowIndex: number, rowName: string ) => void; onCellClick: ( rowFaceName: string, rowIndex: number, columnFaceName: string, columnIndex: number, distance: number ) => void; rowState: RowState; columnState: ColumnState; selectionMode: boolean; transform: Transform; clusteringSteps: PairStep[] | null; rowPositions: Record<string, number>; lastClickedRowIndex: number | null; lastClickedColumnIndex: number | null; }; const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ Loading @@ -41,21 +41,16 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ scaleAxisX, scaleAxisY, colorScale, canvasWidth, canvasHeight, handleRowMouseDown, handleColumnMouseDown, targetRowIndex, targetColumnIndex, handleShowFaces, handleCellClick, handleRowOrColumnClick, onCellClick, rowState, columnState, selectionMode, transform, clusteringSteps, rowPositions, lastClickedRowIndex, lastClickedColumnIndex rowPositions }) => { console.log('rendering CanvasHeatmap'); const canvasRef = useRef<HTMLCanvasElement>(null); const isDragging = useRef(false); const lastPosition = useRef({ x: 0, y: 0 }); Loading Loading @@ -113,8 +108,8 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ const highlightTargetRowsColumns = (context: CanvasRenderingContext2D) => { const labelPaddingX = 100; const labelPaddingY = 100; if (targetRowIndex !== null) { const targetY = scaleAxisY(targetRowIndex) + labelPaddingY; if (rowState.targetIndex !== null) { const targetY = scaleAxisY(rowState.targetIndex) + labelPaddingY; context.fillStyle = 'rgba(0, 0, 255, 0.3)'; context.fillRect( labelPaddingX, Loading @@ -123,8 +118,8 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ binHeight ); } if (targetColumnIndex !== null) { const targetX = scaleAxisX(targetColumnIndex) + labelPaddingX; if (columnState.targetIndex !== null) { const targetX = scaleAxisX(columnState.targetIndex) + labelPaddingX; context.fillStyle = 'rgba(0, 255, 0, 0.3)'; context.fillRect( targetX, Loading @@ -133,8 +128,8 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ heatmapRows.length * binHeight ); } if (lastClickedRowIndex !== null) { const targetY = scaleAxisY(lastClickedRowIndex) + labelPaddingY; if (rowState.lastClickedIndex !== null) { const targetY = scaleAxisY(rowState.lastClickedIndex) + labelPaddingY; context.strokeStyle = 'rgba(0, 0, 255, 1)'; context.lineWidth = 3; context.strokeRect( Loading @@ -145,8 +140,8 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ ); } if (lastClickedColumnIndex !== null) { const targetX = scaleAxisX(lastClickedColumnIndex) + labelPaddingX; if (columnState.lastClickedIndex !== null) { const targetX = scaleAxisX(columnState.lastClickedIndex) + labelPaddingX; context.strokeStyle = 'rgba(0, 255, 0, 1)'; context.lineWidth = 3; context.strokeRect( Loading Loading @@ -270,7 +265,7 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ const context = canvas.getContext('2d'); if (!context) return; context.clearRect(0, 0, canvasWidth, canvasHeight); context.clearRect(0, 0, 1000, 1000); context.save(); context.translate(transform.translateX, transform.translateY); context.scale(transform.scale, transform.scale); Loading @@ -292,12 +287,10 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ scaleAxisX, scaleAxisY, colorScale, canvasWidth, canvasHeight, targetRowIndex, targetColumnIndex, lastClickedRowIndex, lastClickedColumnIndex, rowState.targetIndex, columnState.targetIndex, rowState.lastClickedIndex, columnState.lastClickedIndex, clusteringSteps ]); Loading Loading @@ -374,7 +367,7 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ heatmapRows.forEach((row, rowIndex) => { const y = scaleAxisY(rowIndex) + labelPaddingY; if (clickX < labelPaddingX && clickY > y && clickY < y + binHeight) { handleRowMouseDown(rowIndex, row.faceName); handleRowOrColumnClick('row', rowIndex, row.faceName); rowClicked = true; } }); Loading @@ -390,7 +383,7 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ heatmapRows[0].columns.forEach((col, colIndex) => { const x = scaleAxisX(colIndex) + labelPaddingX; if (clickY < labelPaddingY && clickX > x && clickX < x + binWidth) { handleColumnMouseDown(colIndex, col.faceName); handleRowOrColumnClick('column', colIndex, col.faceName); } }); } Loading @@ -411,7 +404,7 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ clickY > y && clickY < y + binHeight ) { handleCellClick( onCellClick( row.faceName, rowIndex, col.faceName, Loading Loading @@ -440,14 +433,7 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ handleRectangleClick(clickX, clickY); }, [ heatmapRows, scaleAxisY, scaleAxisX, handleRowMouseDown, handleColumnMouseDown, handleShowFaces ] [heatmapRows, scaleAxisY, scaleAxisX, handleRowOrColumnClick, selectionMode] ); useEffect(() => { Loading @@ -467,9 +453,17 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ return ( <canvas width={ canvasRef.current?.parentElement ? canvasRef.current.parentElement.offsetWidth - 2 : 800 } height={ canvasRef.current?.parentElement ? canvasRef.current.parentElement.offsetHeight - 2 : 600 } ref={canvasRef} width={canvasWidth} height={canvasHeight} style={{ border: '1px solid black' }} onMouseDown={e => { handleCanvasMouseDown(e); Loading frontend/src/hooks/visualization/useDendrogramManagament.ts 0 → 100644 +166 −0 Original line number Diff line number Diff line import { useCallback } from 'react'; import { ScaleLinear } from 'd3-scale'; import { BatchDistancesDto } from '../../types/BatchProcessingTypes'; import { HeatmapRowData, LinkageStrategy, PairStep, SerializableClusterNode } from '../../types/BatchVisualizationTypes'; import { BatchProcessingTaskService } from '../../services/BatchProcessingTaskService'; function useDendrogram( filteredHeatmapRows: HeatmapRowData[], setFilteredHeatmapRows: (rows: HeatmapRowData[]) => void, setClusteringSteps: (steps: PairStep[]) => void, batchDistancesDto: BatchDistancesDto | null, localLinkageStrategy: LinkageStrategy ) { const calculateRowPositions = useCallback( ( heatmapRows: HeatmapRowData[], scaleAxisY: ScaleLinear<number, number>, binHeight: number ): Record<string, number> => { const positions: Record<string, number> = {}; heatmapRows.forEach((row, rowIndex) => { const y = scaleAxisY(rowIndex) + binHeight / 2; positions[row.faceName] = y; }); return positions; }, [filteredHeatmapRows] ); const prepareDataForDendrogram = (): BatchDistancesDto | null => { if (!filteredHeatmapRows || !batchDistancesDto) { return null; } const heatmapRowData = filteredHeatmapRows; const validFaceNames = new Set(heatmapRowData.map(row => row.faceName)); const validIndexes = new Set( heatmapRowData.flatMap(row => row.columns.map(column => column.faceIndex)) ); // Filter BatchDistancesDto to only include faceDistances with faceNames in HeatmapRowData const filteredFaceDistances = batchDistancesDto.faceDistances .filter(faceDistance => validFaceNames.has(faceDistance.faceName)) .map(faceDistance => ({ ...faceDistance, // Filter faceDistances array to only include distances at valid indexes faceDistances: faceDistance.faceDistances.filter((_, index) => validIndexes.has(index) ), faceDeviations: faceDistance.faceDeviations.filter((_, index) => validIndexes.has(index) ) })); return { ...batchDistancesDto, faceDistances: filteredFaceDistances, linkageStrategy: localLinkageStrategy }; }; const getClusterSteps = (node: SerializableClusterNode): PairStep[] => { const steps: PairStep[] = []; const traverse = (currentNode: SerializableClusterNode): string => { if (currentNode.children.length === 2) { const [leftChild, rightChild] = currentNode.children; const leftName = traverse(leftChild); const rightName = traverse(rightChild); if (currentNode.distance !== null) { steps.push({ pair: [leftName, rightName], resultName: currentNode.name, distance: currentNode.distance }); } return currentNode.name; } return currentNode.name; }; traverse(node); steps.sort((a, b) => a.distance - b.distance); return steps; }; const reorderRows = (clusteringSteps: PairStep[]) => { type RowType = (typeof filteredHeatmapRows)[0]; const faceMap = new Map<string, RowType>(); filteredHeatmapRows.forEach(row => { faceMap.set(row.faceName, row); }); const visited = new Set<string>(); const collectRows = (clusterName: string): RowType[] => { const step = clusteringSteps.find( step => step.resultName === clusterName ); if (!step) { if (visited.has(clusterName)) return []; const row = faceMap.get(clusterName); if (row) { visited.add(clusterName); return [row]; } return []; } const [left, right] = step.pair; return [...collectRows(left), ...collectRows(right)]; }; const orderedRows: RowType[] = []; clusteringSteps.forEach(step => { step.pair.forEach(item => { if (!visited.has(item)) { orderedRows.push(...collectRows(item)); } }); }); const orderedFaceNames = orderedRows.map(row => row.faceName); orderedRows.forEach(row => { row.columns.sort( (a, b) => orderedFaceNames.indexOf(a.faceName) - orderedFaceNames.indexOf(b.faceName) ); }); setFilteredHeatmapRows(orderedRows); }; const handleShowDendrogram = async () => { const actualDistances = prepareDataForDendrogram(); if (!actualDistances) return; const clusterRoot = await BatchProcessingTaskService.prepareDataForDendrogram( actualDistances ); const clusteringSteps = getClusterSteps(clusterRoot); reorderRows(clusteringSteps); setClusteringSteps(clusteringSteps); }; return { calculateRowPositions, handleShowDendrogram }; } export default useDendrogram; frontend/src/hooks/visualization/useHeatmapManagement.ts +330 −383 File changed.Preview size limit exceeded, changes collapsed. Show changes frontend/src/types/BatchVisualizationTypes.ts +23 −0 Original line number Diff line number Diff line Loading @@ -28,3 +28,26 @@ export enum LinkageStrategy { COMPLETE = 'COMPLETE', AVERAGE = 'AVERAGE' } export type RowState = { draggedIndex: number | null; targetIndex: number | null; clickedName: string | null; lastClickedName: string | null; lastClickedIndex: number | null; }; export type ColumnState = { draggedIndex: number | null; targetIndex: number | null; clickedName: string | null; lastClickedName: string | null; lastClickedIndex: number | null; }; export type HiddenItems = { rows: string[]; columns: string[]; selectedRows: string[]; selectedColumns: string[]; }; Loading
frontend/src/components/task-view/analysis/batch/visualization/BatchVisualizationPanel.tsx +81 −142 Original line number Diff line number Diff line import React, { useCallback, useMemo, useRef } from 'react'; import React, { useMemo } from 'react'; import { Button, Grid, Loading @@ -12,15 +12,9 @@ import { Help } from '@mui/icons-material'; import CanvasHeatmap from '../../../../visualization/CanvasHeatmap'; import useHeatmapManagement from '../../../../../hooks/visualization/useHeatmapManagement'; import { binHeight, binWidth } from '../../../../visualization/visualizationConfig'; import { binHeight } from '../../../../visualization/visualizationConfig'; import SelectPrimaryFaceDialog from '../../../../projects-view/dialogs/SelectPrimaryFaceDialog'; import { LinkageStrategy, Transform } from '../../../../../types/BatchVisualizationTypes'; import { LinkageStrategy } from '../../../../../types/BatchVisualizationTypes'; import OpenSingleTaskConfirmationDialog from '../../../../projects-view/dialogs/OpenSingleTaskConfirmationDialog'; const BatchVisualizationPanel = () => { Loading @@ -30,54 +24,38 @@ const BatchVisualizationPanel = () => { scaleAxisX, scaleAxisY, colorScale, hiddenRowFaceNames, hiddenColumnFaceNames, selectedHiddenRows, selectedHiddenColumns, setSelectedHiddenRows, setSelectedHiddenColumns, selectionMode, toggleSelectionMode, draggedRowIndex, targetRowIndex, draggedColumnIndex, targetColumnIndex, handleRowMouseDown, handleColumnMouseDown, handleMouseUp, setTargetRowIndex, setTargetColumnIndex, showHidden, handleShowFaces, openSelectPrimaryFace, setOpenSelectPrimaryFace, filesPair, handleRowOrColumnClick, handleOpenPairTask, calculateRowPositions, clusteringSteps, handleShowDendrogram, localLinkageStrategy, setLocalLinkageStrategy, lastClickedRowName, lastClickedColumnName, hideRowAndColumn, openSingleTaskConfirmation, setOpenSingleTaskConfirmation, hiddenItems, rowState, columnState, onCellClick, transform, handleHeatmapDrag, finalizeDrag, openSelectPrimaryFace, localLinkageStrategy, filesPair, handleOpenSingleTask, lastClickedRowIndex, lastClickedColumnIndex, isPairClicked, handleCellClick, handleKeyDown, openSingleTaskConfirmation, clusteringSteps, distance, calculateRowPositions, showHidden, hideBoth, distance setHiddenItems, toggleSelectionMode, setOpenSelectPrimaryFace, setLocalLinkageStrategy, setOpenSingleTaskConfirmation, handleShowDendrogram } = useHeatmapManagement(); const transform = useRef<Transform>({ scale: 1, translateX: 0, translateY: 0 }).current; console.log('rendering BatchVisualizationPanel'); const rowPositions = useMemo( () => calculateRowPositions(heatmapRows, scaleAxisY, binHeight), [heatmapRows] Loading @@ -88,62 +66,6 @@ const BatchVisualizationPanel = () => { [clusteringSteps] ); const handleMouseMoveInternal = useCallback( (event: React.MouseEvent<HTMLDivElement>) => { if (selectionMode) { const rect = event.currentTarget.getBoundingClientRect(); if (draggedRowIndex !== null) { const yPosition = (event.clientY - rect.top - transform.translateY) / transform.scale; const newIndex = Math.floor((yPosition - 100) / binHeight); if (newIndex >= 0 && newIndex < heatmapRows.length) { if (newIndex === draggedRowIndex) { setTargetRowIndex(null); } else { setTargetRowIndex(newIndex); } } } if (draggedColumnIndex !== null) { const xPosition = (event.clientX - rect.left - transform.translateX) / transform.scale; const newColumnIndex = Math.floor((xPosition - 100) / binWidth); if ( newColumnIndex >= 0 && newColumnIndex < heatmapRows[0].columns.length ) { if (newColumnIndex === draggedColumnIndex) { setTargetColumnIndex(null); } else { setTargetColumnIndex(newColumnIndex); } } } } }, [ selectionMode, draggedRowIndex, draggedColumnIndex, transform.scale, transform.translateX, transform.translateY, heatmapRows ] ); const handleKeyDown = useCallback( (event: React.KeyboardEvent<HTMLDivElement>) => { if (event.key === 'S' || event.key === 's') { toggleSelectionMode(); } }, [] ); if (!heatmapRows.length) { return ( <Grid container> Loading @@ -167,8 +89,13 @@ const BatchVisualizationPanel = () => { <InputLabel id="row-select-label">Hidden Rows</InputLabel> <Select multiple value={selectedHiddenRows} onChange={e => setSelectedHiddenRows(e.target.value as string[])} value={hiddenItems.selectedRows} onChange={e => setHiddenItems({ ...hiddenItems, selectedRows: e.target.value as string[] }) } sx={{ width: '150px', height: '30px' }} displayEmpty renderValue={selected => (selected.length > 0 ? selected[0] : '')} Loading @@ -181,7 +108,7 @@ const BatchVisualizationPanel = () => { } }} > {hiddenRowFaceNames.map(rowFaceName => ( {hiddenItems.rows.map(rowFaceName => ( <MenuItem key={rowFaceName} value={rowFaceName}> {rowFaceName} </MenuItem> Loading @@ -193,9 +120,12 @@ const BatchVisualizationPanel = () => { <InputLabel id="column-select-label">Hidden Columns</InputLabel> <Select multiple value={selectedHiddenColumns} value={hiddenItems.selectedColumns} onChange={e => setSelectedHiddenColumns(e.target.value as string[]) setHiddenItems({ ...hiddenItems, selectedColumns: e.target.value as string[] }) } sx={{ width: '150px', height: '30px' }} displayEmpty Loading @@ -209,7 +139,7 @@ const BatchVisualizationPanel = () => { } }} > {hiddenColumnFaceNames.map(colFaceName => ( {hiddenItems.columns.map(colFaceName => ( <MenuItem key={colFaceName} value={colFaceName}> {colFaceName} </MenuItem> Loading @@ -221,7 +151,10 @@ const BatchVisualizationPanel = () => { <Button variant="contained" onClick={() => showHidden(selectedHiddenRows, selectedHiddenColumns) showHidden( hiddenItems.selectedRows, hiddenItems.selectedColumns ) } sx={{ height: '30px', marginTop: '22px' }} > Loading @@ -229,14 +162,16 @@ const BatchVisualizationPanel = () => { </Button> </Grid> {lastClickedRowName && ( {rowState.lastClickedName && ( <Grid item> <InputLabel style={{ fontWeight: 'bold', width: '115px' }}> {lastClickedRowName} {rowState.lastClickedName} </InputLabel> <Button variant="contained" onClick={() => hideRowAndColumn(lastClickedRowName, true)} onClick={() => hideRowAndColumn(rowState.lastClickedName as string, true) } sx={{ height: '30px', width: '115px' }} > Hide face Loading @@ -244,26 +179,31 @@ const BatchVisualizationPanel = () => { </Grid> )} {lastClickedColumnName && ( {columnState.lastClickedName !== null && ( <Grid item> <InputLabel style={{ fontWeight: 'bold', width: '115px' }}> {lastClickedColumnName} {columnState.lastClickedName} </InputLabel> <Button variant="contained" onClick={() => hideRowAndColumn(lastClickedColumnName, false)} onClick={() => hideRowAndColumn(columnState.lastClickedName as string, false) } sx={{ height: '30px', width: '115px' }} > Hide face </Button> </Grid> )} {lastClickedRowName && lastClickedColumnName && ( {rowState.lastClickedName && columnState.lastClickedName && ( <Grid item> <Button variant="contained" onClick={() => hideBoth(lastClickedRowName, lastClickedColumnName) hideBoth( rowState.lastClickedName as string, columnState.lastClickedName as string ) } sx={{ height: '30px', Loading Loading @@ -312,31 +252,35 @@ const BatchVisualizationPanel = () => { Make clustering </Button> </Grid> {lastClickedRowName && ( {rowState.lastClickedName && ( <Grid item> <Button variant="contained" onClick={() => handleOpenSingleTask(lastClickedRowName)} onClick={() => handleOpenSingleTask(rowState.lastClickedName as string) } sx={{ height: '30px', width: '115px' }} > Open task </Button> </Grid> )} {lastClickedColumnName && ( {columnState.lastClickedName && ( <Grid item> <Button variant="contained" onClick={() => handleOpenSingleTask(lastClickedColumnName)} onClick={() => handleOpenSingleTask(columnState.lastClickedName as string) } sx={{ height: '30px', width: '115px' }} > Open task </Button> </Grid> )} {lastClickedRowName && lastClickedColumnName && lastClickedRowName !== lastClickedColumnName && ( {rowState.lastClickedName && columnState.lastClickedName && rowState.lastClickedName !== columnState.lastClickedName && ( <Grid item> <Button variant="contained" Loading Loading @@ -400,12 +344,13 @@ const BatchVisualizationPanel = () => { ) : ( <div style={{ width: '800px', height: '600px', position: 'relative' width: '100%', height: 'calc(100vh - 325px)', display: 'flex', outline: 'none' }} onMouseMove={handleMouseMoveInternal} onMouseUp={handleMouseUp} onMouseMove={handleHeatmapDrag} onMouseUp={finalizeDrag} onKeyDown={handleKeyDown} role="button" tabIndex={0} Loading @@ -415,20 +360,14 @@ const BatchVisualizationPanel = () => { scaleAxisX={scaleAxisX} scaleAxisY={scaleAxisY} colorScale={colorScale} canvasWidth={800} canvasHeight={600} handleRowMouseDown={handleRowMouseDown} handleColumnMouseDown={handleColumnMouseDown} targetRowIndex={targetRowIndex} targetColumnIndex={targetColumnIndex} handleShowFaces={handleShowFaces} handleCellClick={handleCellClick} handleRowOrColumnClick={handleRowOrColumnClick} onCellClick={onCellClick} rowState={rowState} columnState={columnState} selectionMode={selectionMode} transform={transform} clusteringSteps={memoizedClusteringSteps} rowPositions={rowPositions} lastClickedRowIndex={lastClickedRowIndex} lastClickedColumnIndex={lastClickedColumnIndex} /> </div> )} Loading
frontend/src/components/visualization/CanvasHeatmap.tsx +43 −49 Original line number Diff line number Diff line Loading @@ -2,8 +2,10 @@ import React, { useRef, useEffect, useCallback } from 'react'; import { ScaleLinear } from 'd3-scale'; import { ColumnState, HeatmapRowData, PairStep, RowState, Transform } from '../../types/BatchVisualizationTypes'; Loading @@ -14,26 +16,24 @@ type CanvasHeatmapProps = { scaleAxisX: ScaleLinear<number, number>; scaleAxisY: ScaleLinear<number, number>; colorScale: (value: number) => string; canvasWidth: number; canvasHeight: number; handleRowMouseDown: (rowIndex: number, rowName: string) => void; handleColumnMouseDown: (columnIndex: number, columnName: string) => void; targetRowIndex: number | null; targetColumnIndex: number | null; handleShowFaces: (rowFaceName: string, columnFaceName: string) => void; handleCellClick: ( handleRowOrColumnClick: ( type: 'row' | 'column', rowIndex: number, rowName: string ) => void; onCellClick: ( rowFaceName: string, rowIndex: number, columnFaceName: string, columnIndex: number, distance: number ) => void; rowState: RowState; columnState: ColumnState; selectionMode: boolean; transform: Transform; clusteringSteps: PairStep[] | null; rowPositions: Record<string, number>; lastClickedRowIndex: number | null; lastClickedColumnIndex: number | null; }; const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ Loading @@ -41,21 +41,16 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ scaleAxisX, scaleAxisY, colorScale, canvasWidth, canvasHeight, handleRowMouseDown, handleColumnMouseDown, targetRowIndex, targetColumnIndex, handleShowFaces, handleCellClick, handleRowOrColumnClick, onCellClick, rowState, columnState, selectionMode, transform, clusteringSteps, rowPositions, lastClickedRowIndex, lastClickedColumnIndex rowPositions }) => { console.log('rendering CanvasHeatmap'); const canvasRef = useRef<HTMLCanvasElement>(null); const isDragging = useRef(false); const lastPosition = useRef({ x: 0, y: 0 }); Loading Loading @@ -113,8 +108,8 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ const highlightTargetRowsColumns = (context: CanvasRenderingContext2D) => { const labelPaddingX = 100; const labelPaddingY = 100; if (targetRowIndex !== null) { const targetY = scaleAxisY(targetRowIndex) + labelPaddingY; if (rowState.targetIndex !== null) { const targetY = scaleAxisY(rowState.targetIndex) + labelPaddingY; context.fillStyle = 'rgba(0, 0, 255, 0.3)'; context.fillRect( labelPaddingX, Loading @@ -123,8 +118,8 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ binHeight ); } if (targetColumnIndex !== null) { const targetX = scaleAxisX(targetColumnIndex) + labelPaddingX; if (columnState.targetIndex !== null) { const targetX = scaleAxisX(columnState.targetIndex) + labelPaddingX; context.fillStyle = 'rgba(0, 255, 0, 0.3)'; context.fillRect( targetX, Loading @@ -133,8 +128,8 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ heatmapRows.length * binHeight ); } if (lastClickedRowIndex !== null) { const targetY = scaleAxisY(lastClickedRowIndex) + labelPaddingY; if (rowState.lastClickedIndex !== null) { const targetY = scaleAxisY(rowState.lastClickedIndex) + labelPaddingY; context.strokeStyle = 'rgba(0, 0, 255, 1)'; context.lineWidth = 3; context.strokeRect( Loading @@ -145,8 +140,8 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ ); } if (lastClickedColumnIndex !== null) { const targetX = scaleAxisX(lastClickedColumnIndex) + labelPaddingX; if (columnState.lastClickedIndex !== null) { const targetX = scaleAxisX(columnState.lastClickedIndex) + labelPaddingX; context.strokeStyle = 'rgba(0, 255, 0, 1)'; context.lineWidth = 3; context.strokeRect( Loading Loading @@ -270,7 +265,7 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ const context = canvas.getContext('2d'); if (!context) return; context.clearRect(0, 0, canvasWidth, canvasHeight); context.clearRect(0, 0, 1000, 1000); context.save(); context.translate(transform.translateX, transform.translateY); context.scale(transform.scale, transform.scale); Loading @@ -292,12 +287,10 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ scaleAxisX, scaleAxisY, colorScale, canvasWidth, canvasHeight, targetRowIndex, targetColumnIndex, lastClickedRowIndex, lastClickedColumnIndex, rowState.targetIndex, columnState.targetIndex, rowState.lastClickedIndex, columnState.lastClickedIndex, clusteringSteps ]); Loading Loading @@ -374,7 +367,7 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ heatmapRows.forEach((row, rowIndex) => { const y = scaleAxisY(rowIndex) + labelPaddingY; if (clickX < labelPaddingX && clickY > y && clickY < y + binHeight) { handleRowMouseDown(rowIndex, row.faceName); handleRowOrColumnClick('row', rowIndex, row.faceName); rowClicked = true; } }); Loading @@ -390,7 +383,7 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ heatmapRows[0].columns.forEach((col, colIndex) => { const x = scaleAxisX(colIndex) + labelPaddingX; if (clickY < labelPaddingY && clickX > x && clickX < x + binWidth) { handleColumnMouseDown(colIndex, col.faceName); handleRowOrColumnClick('column', colIndex, col.faceName); } }); } Loading @@ -411,7 +404,7 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ clickY > y && clickY < y + binHeight ) { handleCellClick( onCellClick( row.faceName, rowIndex, col.faceName, Loading Loading @@ -440,14 +433,7 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ handleRectangleClick(clickX, clickY); }, [ heatmapRows, scaleAxisY, scaleAxisX, handleRowMouseDown, handleColumnMouseDown, handleShowFaces ] [heatmapRows, scaleAxisY, scaleAxisX, handleRowOrColumnClick, selectionMode] ); useEffect(() => { Loading @@ -467,9 +453,17 @@ const CanvasHeatmap: React.FC<CanvasHeatmapProps> = ({ return ( <canvas width={ canvasRef.current?.parentElement ? canvasRef.current.parentElement.offsetWidth - 2 : 800 } height={ canvasRef.current?.parentElement ? canvasRef.current.parentElement.offsetHeight - 2 : 600 } ref={canvasRef} width={canvasWidth} height={canvasHeight} style={{ border: '1px solid black' }} onMouseDown={e => { handleCanvasMouseDown(e); Loading
frontend/src/hooks/visualization/useDendrogramManagament.ts 0 → 100644 +166 −0 Original line number Diff line number Diff line import { useCallback } from 'react'; import { ScaleLinear } from 'd3-scale'; import { BatchDistancesDto } from '../../types/BatchProcessingTypes'; import { HeatmapRowData, LinkageStrategy, PairStep, SerializableClusterNode } from '../../types/BatchVisualizationTypes'; import { BatchProcessingTaskService } from '../../services/BatchProcessingTaskService'; function useDendrogram( filteredHeatmapRows: HeatmapRowData[], setFilteredHeatmapRows: (rows: HeatmapRowData[]) => void, setClusteringSteps: (steps: PairStep[]) => void, batchDistancesDto: BatchDistancesDto | null, localLinkageStrategy: LinkageStrategy ) { const calculateRowPositions = useCallback( ( heatmapRows: HeatmapRowData[], scaleAxisY: ScaleLinear<number, number>, binHeight: number ): Record<string, number> => { const positions: Record<string, number> = {}; heatmapRows.forEach((row, rowIndex) => { const y = scaleAxisY(rowIndex) + binHeight / 2; positions[row.faceName] = y; }); return positions; }, [filteredHeatmapRows] ); const prepareDataForDendrogram = (): BatchDistancesDto | null => { if (!filteredHeatmapRows || !batchDistancesDto) { return null; } const heatmapRowData = filteredHeatmapRows; const validFaceNames = new Set(heatmapRowData.map(row => row.faceName)); const validIndexes = new Set( heatmapRowData.flatMap(row => row.columns.map(column => column.faceIndex)) ); // Filter BatchDistancesDto to only include faceDistances with faceNames in HeatmapRowData const filteredFaceDistances = batchDistancesDto.faceDistances .filter(faceDistance => validFaceNames.has(faceDistance.faceName)) .map(faceDistance => ({ ...faceDistance, // Filter faceDistances array to only include distances at valid indexes faceDistances: faceDistance.faceDistances.filter((_, index) => validIndexes.has(index) ), faceDeviations: faceDistance.faceDeviations.filter((_, index) => validIndexes.has(index) ) })); return { ...batchDistancesDto, faceDistances: filteredFaceDistances, linkageStrategy: localLinkageStrategy }; }; const getClusterSteps = (node: SerializableClusterNode): PairStep[] => { const steps: PairStep[] = []; const traverse = (currentNode: SerializableClusterNode): string => { if (currentNode.children.length === 2) { const [leftChild, rightChild] = currentNode.children; const leftName = traverse(leftChild); const rightName = traverse(rightChild); if (currentNode.distance !== null) { steps.push({ pair: [leftName, rightName], resultName: currentNode.name, distance: currentNode.distance }); } return currentNode.name; } return currentNode.name; }; traverse(node); steps.sort((a, b) => a.distance - b.distance); return steps; }; const reorderRows = (clusteringSteps: PairStep[]) => { type RowType = (typeof filteredHeatmapRows)[0]; const faceMap = new Map<string, RowType>(); filteredHeatmapRows.forEach(row => { faceMap.set(row.faceName, row); }); const visited = new Set<string>(); const collectRows = (clusterName: string): RowType[] => { const step = clusteringSteps.find( step => step.resultName === clusterName ); if (!step) { if (visited.has(clusterName)) return []; const row = faceMap.get(clusterName); if (row) { visited.add(clusterName); return [row]; } return []; } const [left, right] = step.pair; return [...collectRows(left), ...collectRows(right)]; }; const orderedRows: RowType[] = []; clusteringSteps.forEach(step => { step.pair.forEach(item => { if (!visited.has(item)) { orderedRows.push(...collectRows(item)); } }); }); const orderedFaceNames = orderedRows.map(row => row.faceName); orderedRows.forEach(row => { row.columns.sort( (a, b) => orderedFaceNames.indexOf(a.faceName) - orderedFaceNames.indexOf(b.faceName) ); }); setFilteredHeatmapRows(orderedRows); }; const handleShowDendrogram = async () => { const actualDistances = prepareDataForDendrogram(); if (!actualDistances) return; const clusterRoot = await BatchProcessingTaskService.prepareDataForDendrogram( actualDistances ); const clusteringSteps = getClusterSteps(clusterRoot); reorderRows(clusteringSteps); setClusteringSteps(clusteringSteps); }; return { calculateRowPositions, handleShowDendrogram }; } export default useDendrogram;
frontend/src/hooks/visualization/useHeatmapManagement.ts +330 −383 File changed.Preview size limit exceeded, changes collapsed. Show changes
frontend/src/types/BatchVisualizationTypes.ts +23 −0 Original line number Diff line number Diff line Loading @@ -28,3 +28,26 @@ export enum LinkageStrategy { COMPLETE = 'COMPLETE', AVERAGE = 'AVERAGE' } export type RowState = { draggedIndex: number | null; targetIndex: number | null; clickedName: string | null; lastClickedName: string | null; lastClickedIndex: number | null; }; export type ColumnState = { draggedIndex: number | null; targetIndex: number | null; clickedName: string | null; lastClickedName: string | null; lastClickedIndex: number | null; }; export type HiddenItems = { rows: string[]; columns: string[]; selectedRows: string[]; selectedColumns: string[]; };