Skip to content

Commit

Permalink
feat: display the tree for similarity
Browse files Browse the repository at this point in the history
close #2905
  • Loading branch information
hamed-musallam committed Feb 13, 2024
1 parent dbd15b2 commit 2a21d17
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 20 deletions.
40 changes: 31 additions & 9 deletions package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -85,9 +85,10 @@
"ml-matrix": "^6.11.0",
"ml-spectra-processing": "^12.10.1",
"ml-stat": "^1.3.3",
"ml-tree-similarity": "^2.0.0",
"multiplet-analysis": "^2.1.2",
"nmr-correlation": "^2.3.3",
"nmr-load-save": "^0.26.0",
"nmr-load-save": "^0.27.0",
"nmr-processing": "^11.10.0",
"nmredata": "^0.9.9",
"numeral": "^2.0.6",
Expand Down
2 changes: 2 additions & 0 deletions src/component/1d/Chart1D.tsx
Expand Up @@ -4,6 +4,7 @@ import { usePreferences } from '../context/PreferencesContext';
import ApodizationLine from './ApodizationLine';
import ExclusionZonesAnnotations from './ExclusionZonesAnnotations';
import LinesSeries from './LinesSeries';
import SimilarityTree from './SimilarityTree';
import SpectraTracker from './SpectraLegends';
import XAxis from './XAxis';
import DatabaseElements from './database/DatabaseElements';
Expand Down Expand Up @@ -60,6 +61,7 @@ function Chart1D({ mode, width, height, margin, displayerKey }) {
<PeaksShapes />
<SpectraTracker />
<SpectrumInfoBlock />
<SimilarityTree />
<g className="container" style={{ pointerEvents: 'none' }}>
<XAxis showGrid mode={mode} />
</g>
Expand Down
110 changes: 110 additions & 0 deletions src/component/1d/SimilarityTree.tsx
@@ -0,0 +1,110 @@
import { createTree } from 'ml-tree-similarity';
import { Spectrum1D } from 'nmr-load-save';
import { CSSProperties, Fragment } from 'react';

import { useChartData } from '../context/ChartContext';
import { useScaleChecked } from '../context/ScaleContext';
import { useFormatNumberByNucleus } from '../hooks/useFormatNumberByNucleus';
import useSpectrum from '../hooks/useSpectrum';

const circleSize = 3;
const marginTop = circleSize + 10;
const circleColor: CSSProperties['color'] = 'red';
const lineColor: CSSProperties['color'] = 'black';
const textColor: CSSProperties['color'] = 'red';
const textPadding = circleSize + 2;
const textSize = 10;
const maxTreeLevels = 25;

export default function SimilarityTree() {
const {
height,
view: {
spectra: { activeTab, showSimilarityTree },
},
} = useChartData();
const spectrum = useSpectrum();
const format = useFormatNumberByNucleus(activeTab);
const { scaleX } = useScaleChecked();
const scaleY = (value: number) => (height * value) / maxTreeLevels;
const treeHeadLength = height / maxTreeLevels;

if (!spectrum || !showSimilarityTree) return null;

const {
data: { x, re },
} = spectrum as Spectrum1D;
const tree = createTree({ x, y: re });
const data = mapTreeToArray(tree, 0);

return (
<g className="similarity-tree" transform={`translate(0,${marginTop})`}>
<g className="tree-lines">
{data.map(({ level, center, parentCenter, parentLevel }) => {
if (parentCenter === undefined) return null;

const startX = scaleX()(parentCenter);
const startY = scaleY(parentLevel);
const midY = startY + treeHeadLength / 2;
const endX = scaleX()(center);
const endY = scaleY(level);
const path = `M ${startX} ${startY} L ${startX} ${midY} L${endX} ${midY} L${endX} ${endY}`;

return <path key={path} d={path} fill="none" stroke={lineColor} />;
})}
</g>
<g className="tree-nodes">
{data.map(({ level, center }) => {
const x = scaleX()(center);
const y = scaleY(level);
return (
<Fragment key={`${level}${center}`}>
<circle cx={x} cy={y} r={circleSize} fill={circleColor} />
<text
x={x + textPadding}
y={y}
textAnchor="start"
alignmentBaseline="middle"
fill={textColor}
fontSize={textSize}
>
{format(center)}
</text>
</Fragment>
);
})}
</g>
</g>
);
}

interface TreeItem {
center: number;
sum: number;
level: number;
parentCenter?: number;
parentLevel?: number;
}

function mapTreeToArray(
node: any,
level: number,
parentCenter = null,
parentLevel = -1,
) {
if (!node) return [];

const { center, sum } = node;
const result: TreeItem[] = [{ center, sum, level }];

for (const obj of result) {
if (parentCenter) {
obj.parentCenter = parentCenter;
obj.parentLevel = parentLevel;
}
}

const left = mapTreeToArray(node.left, level + 1, center, parentLevel + 1);
const right = mapTreeToArray(node.right, level + 1, center, parentLevel + 1);
return result.concat(left, right);
}
1 change: 0 additions & 1 deletion src/component/panels/databasePanel/DatabasePanel.tsx
Expand Up @@ -330,7 +330,6 @@ function DatabasePanelInner({
idCode={idCode}
keywords={keywords}
result={result}
selectedTool={selectedTool}
total={databaseInstance.current?.data.length || 0}
onKeywordsChange={(options) =>
setKeywords((prevKeywords) => ({ ...prevKeywords, ...options }))
Expand Down
37 changes: 29 additions & 8 deletions src/component/panels/databasePanel/DatabaseSearchOptions.tsx
@@ -1,13 +1,16 @@
import { BsHexagon, BsHexagonFill } from 'react-icons/bs';
import { FaICursor } from 'react-icons/fa';
import { IoSearchOutline } from 'react-icons/io5';
import { TbBinaryTree } from 'react-icons/tb';

import { useChartData } from '../../context/ChartContext';
import { useDispatch } from '../../context/DispatchContext';
import ActiveButton from '../../elements/ActiveButton';
import Button from '../../elements/Button';
import { CounterLabel } from '../../elements/CounterLabel';
import Input from '../../elements/Input';
import { PreferencesButton } from '../../elements/PreferencesButton';
import Select from '../../elements/Select';
import ToggleButton from '../../elements/ToggleButton';
import useToolsFunctions from '../../hooks/useToolsFunctions';
import { options } from '../../toolbar/ToolTypes';
import { createFilterLabel } from '../header/DefaultPanelHeader';
Expand All @@ -26,7 +29,6 @@ interface DatabaseSearchOptionsProps {
defaultDatabase: string;
keywords: DatabaseSearchKeywords;
result: DataBaseSearchResultEntry;
selectedTool: string;
idCode?: string;
total: number;
onKeywordsChange: (k: Partial<DatabaseSearchKeywords>) => void;
Expand All @@ -40,7 +42,6 @@ export function DatabaseSearchOptions({
defaultDatabase,
keywords,
result,
selectedTool,
idCode,
total,
onKeywordsChange,
Expand All @@ -49,7 +50,13 @@ export function DatabaseSearchOptions({
onDatabaseChange,
}: DatabaseSearchOptionsProps) {
const { handleChangeOption } = useToolsFunctions();

const {
view: {
spectra: { showSimilarityTree },
},
toolOptions: { selectedTool },
} = useChartData();
const dispatch = useDispatch();
function enableFilterHandler(flag) {
const tool = !flag ? options.zoom.id : options.databaseRangesSelection.id;
handleChangeOption(tool);
Expand All @@ -70,6 +77,10 @@ export function DatabaseSearchOptions({
onKeywordsChange({ searchKeywords: '' });
}

function handleShowSimilarityTree() {
dispatch({ type: 'TOGGLE_SIMILARITY_TREE' });
}

return (
<PanelHeader style={{ flexDirection: 'column', alignItems: 'normal' }}>
<div
Expand All @@ -79,6 +90,16 @@ export function DatabaseSearchOptions({
paddingBottom: '2px',
}}
>
<ActiveButton
popupTitle={`${showSimilarityTree ? 'Hide' : 'Show'} similarity tree`}
popupPlacement="right"
onClick={handleShowSimilarityTree}
value={showSimilarityTree}
style={{ marginRight: '5px' }}
>
<TbBinaryTree style={{ pointerEvents: 'none', fontSize: '12px' }} />
</ActiveButton>

<Select
style={{ flex: 6 }}
items={databases}
Expand Down Expand Up @@ -110,12 +131,12 @@ export function DatabaseSearchOptions({
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<ToggleButton
key={selectedTool}
defaultValue={selectedTool === options.databaseRangesSelection.id}
<ActiveButton
popupTitle="Filter by select ranges"
popupPlacement="right"
onClick={enableFilterHandler}
value={selectedTool === options.databaseRangesSelection.id}
style={{ marginRight: '5px' }}
>
<FaICursor
style={{
Expand All @@ -124,7 +145,7 @@ export function DatabaseSearchOptions({
transform: 'rotate(90deg)',
}}
/>
</ToggleButton>
</ActiveButton>

<Input
value={keywords.searchKeywords}
Expand Down
3 changes: 3 additions & 0 deletions src/component/reducer/Reducer.ts
Expand Up @@ -79,6 +79,7 @@ export function getDefaultViewState(): ViewState {
activeSpectra: {},
activeTab: '',
showLegend: false,
showSimilarityTree: false,
selectReferences: {},
},
zoom: {
Expand Down Expand Up @@ -693,6 +694,8 @@ function innerSpectrumReducer(draft: Draft<State>, action: Action) {
return DatabaseActions.handleResurrectSpectrumFromRanges(draft, action);
case 'RESURRECTING_SPECTRUM_FROM_JCAMP':
return DatabaseActions.handleResurrectSpectrumFromJcamp(draft, action);
case 'TOGGLE_SIMILARITY_TREE':
return DatabaseActions.handleToggleSimilarityTree(draft);

case 'SET_AUTOMATIC_ASSIGNMENTS':
return AssignmentsActions.handleSetAutomaticAssignments(draft, action);
Expand Down
12 changes: 11 additions & 1 deletion src/component/reducer/actions/DatabaseActions.ts
Expand Up @@ -23,6 +23,7 @@ type ResurrectSpectrumFromRangesAction = ActionType<
>;

export type DatabaseActions =
| ActionType<'TOGGLE_SIMILARITY_TREE'>
| ResurrectSpectrumFromJcampAction
| ResurrectSpectrumFromRangesAction;

Expand Down Expand Up @@ -62,4 +63,13 @@ function handleResurrectSpectrumFromRanges(
}
}

export { handleResurrectSpectrumFromRanges, handleResurrectSpectrumFromJcamp };
function handleToggleSimilarityTree(draft: Draft<State>) {
draft.view.spectra.showSimilarityTree =
!draft.view.spectra.showSimilarityTree;
}

export {
handleResurrectSpectrumFromRanges,
handleResurrectSpectrumFromJcamp,
handleToggleSimilarityTree,
};

0 comments on commit 2a21d17

Please sign in to comment.