import styled from '@emotion/styled';
import type { Range } from '@zakodium/nmr-types';
import type { Spectrum1D } from '@zakodium/nmrium-core';
import { xFindClosestIndex } from 'ml-spectra-processing';
import { useRef } from 'react';
import { isSpectrum1D } from '../../../data/data1d/Spectrum1D/index.js';
import type { AssignmentsData } from '../../assignment/AssignmentsContext.js';
import { useAssignment } from '../../assignment/AssignmentsContext.js';
import { useChartData } from '../../context/ChartContext.js';
import { useDispatch } from '../../context/DispatchContext.js';
import { useScaleChecked } from '../../context/ScaleContext.js';
import { ActionsButtonsPopover } from '../../elements/ActionsButtonsPopover.js';
import { useHighlight } from '../../highlight/index.js';
import { useActiveSpectrum } from '../../hooks/useActiveSpectrum.js';
import useSpectrum from '../../hooks/useSpectrum.js';
import { useIsInset } from '../inset/InsetProvider.js';
import { useAssignmentsPopoverActionsButtons } from '../ranges/useAssignmentsPopoverActionsButtons.js';
import type { TreeNodes } from './generateTreeNodes.js';
import { generateTreeNodes } from './generateTreeNodes.js';
const Group = styled.g<{ isActive: boolean; isHighlighted: boolean }>`
cursor: default;
opacity: ${({ isHighlighted }) => (isHighlighted ? '1' : '0.6')};
stroke-width: ${({ isHighlighted }) => (isHighlighted ? '1.5' : '1')}px;
`;
interface MultiplicityTreeProps {
range: Range;
}
const treeLevelsColors: string[] = ['red', 'green', 'blue', 'magenta'];
const marginBottom = 20;
const headTextMargin = 5;
const tailLength = 10;
const boxPadding = 20;
const headerTextSize = 12;
export default function MultiplicityTree(props: MultiplicityTreeProps) {
const { range } = props;
const spectrum = useSpectrum();
const { scaleY } = useScaleChecked();
if (!spectrum || !isSpectrum1D(spectrum)) return null;
const { from, to } = range;
const maxY = getMaxY(spectrum, { from, to });
const startY = scaleY(spectrum.id)(maxY) - marginBottom;
const tree = generateTreeNodes(range, spectrum);
return tree.map((treeItem, signalIndex) => {
const { rangeKey, signalKey } = treeItem;
return (
);
});
}
interface TreeProps {
startY: number;
treeNodes: TreeNodes;
signalIndex: number;
range: Range;
}
function Tree(props: TreeProps) {
const {
signalIndex,
range,
startY: originStartY,
treeNodes: {
multiplicity = '',
nodes,
min,
max,
signalKey,
diaIDs,
assignment: assignmentLabel,
},
} = props;
const { from, to } = range;
const { width } = useChartData();
const { scaleX, shiftY } = useScaleChecked();
const dispatch = useDispatch();
const isInset = useIsInset();
const isAssignBtnTrigged = useRef(false);
const activeSpectrum = useActiveSpectrum();
const assignment = useAssignment(signalKey);
const highlight = useHighlight(extractID(signalKey, assignment), {
type: 'SIGNAL',
});
let widthRatio: number;
let treeWidth: number;
if (nodes?.length > 1) {
treeWidth = scaleX()(min) - scaleX()(max);
widthRatio = treeWidth / width;
} else {
treeWidth = 4;
widthRatio = (scaleX()(from) - scaleX()(to)) / width;
}
const isRationLabelsVisible = widthRatio > 0.1;
const isMassive = ['m', 's'].includes(multiplicity);
const levelLength = isMassive ? tailLength : tailLength * 2;
const treeHeight = multiplicity.split('').length * levelLength + tailLength;
const startY = originStartY - treeHeight;
const [{ x: head }, ...otherNodes] = nodes;
const headX = scaleX()(head);
const paths = useTreePaths(otherNodes, {
isMassive,
levelLength,
tailLength,
startY,
});
const hasDiaIDs = Array.isArray(diaIDs) && diaIDs.length > 0;
const isAssignmentActive = assignment.isActive;
const actionsButtons = useAssignmentsPopoverActionsButtons({
isUnAssignButtonVisible: isAssignmentActive || hasDiaIDs,
isAssignLabelButtonVisible: !assignmentLabel,
isUnAssignLabelButtonVisible: !!assignmentLabel,
onAssign: assignHandler,
onUnAssign: unAssignHandler,
rangeId: range.id,
});
if (!multiplicity) return null;
function assignHandler() {
assignment.activate('x');
isAssignBtnTrigged.current = true;
}
function unAssignHandler() {
dispatch({
type: 'UNASSIGN_1D_SIGNAL',
payload: {
rangeKey: range.id,
signalIndex,
},
});
}
const isHighlighted = highlight.isActive || isAssignmentActive;
const padding = boxPadding * widthRatio;
const x = scaleX()(max) - padding;
const y = startY - headTextMargin - headerTextSize - padding;
const boxWidth = treeWidth + padding * 2;
const boxHeight = treeHeight + headTextMargin + headerTextSize + padding * 2;
const isOpen = isAssignBtnTrigged.current ? isAssignmentActive : undefined;
return (
{
isAssignBtnTrigged.current = false;
}}
>
{
assignment.highlight('x');
highlight.show();
}}
onMouseLeave={() => {
assignment.clearHighlight();
highlight.hide();
}}
pointerEvents="bounding-box"
>
{multiplicity}
{paths.map((path, level) => {
const levelColor = isMassive
? 'blue'
: treeLevelsColors[level % treeLevelsColors.length];
return (
);
})}
{!isMassive &&
isRationLabelsVisible &&
otherNodes.map((node, index) => {
const { x, level, ratio } = node;
const x1 = scaleX()(x);
const y = levelLength * level + tailLength + tailLength / 2;
const levelColor =
treeLevelsColors[level % treeLevelsColors.length];
return (
{ratio}
);
})}
);
}
function useTreePaths(
otherNodes: TreeNodes['nodes'],
options: {
tailLength: number;
levelLength: number;
startY: number;
isMassive: boolean;
},
) {
const { tailLength, levelLength, startY, isMassive } = options;
const { scaleX } = useScaleChecked();
const paths: string[][] = [];
for (const node of otherNodes) {
const { x, parentX = 0, level } = node;
const baseX = scaleX()(parentX);
const x1 = scaleX()(x);
let y = tailLength;
if (!isMassive) {
y = levelLength * level + tailLength;
}
const path = `M ${baseX} ${startY + y} L ${x1} ${startY + y + (isMassive ? 0 : tailLength)} l 0 ${tailLength}`;
if (!paths?.[level]) {
paths[level] = [path];
} else {
paths[level].push(path);
}
}
return paths;
}
function getMaxY(spectrum: Spectrum1D, options: { from: number; to: number }) {
const { from, to } = options;
const {
data: { re, x },
} = spectrum;
const fromIndex = xFindClosestIndex(x, from);
const toIndex = xFindClosestIndex(x, to);
let max = Number.NEGATIVE_INFINITY;
for (const value of re.slice(fromIndex, toIndex)) {
if (value > max) {
max = value;
}
}
return max;
}
function extractID(id: string, assignment: AssignmentsData) {
return [id].concat(assignment.assignedDiaIds?.x || []);
}