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
43 changes: 16 additions & 27 deletions src/component/2d/ft/Contours.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Spectrum2D } from '@zakodium/nmrium-core';
import debounce from 'lodash/debounce.js';
import { memo, useMemo, useRef } from 'react';

Expand All @@ -7,27 +6,25 @@
import { useChartData } from '../../context/ChartContext.js';
import { usePreferences } from '../../context/PreferencesContext.js';
import { useToaster } from '../../context/ToasterContext.js';
import type { SpectrumFTData } from '../../hooks/use2DReducer.tsx';
import { use2DReducer } from '../../hooks/use2DReducer.tsx';
import { useActiveSpectrum } from '../../hooks/useActiveSpectrum.js';
import { PathBuilder } from '../../utility/PathBuilder.js';
import { getSpectraByNucleus } from '../../utility/getSpectraByNucleus.js';
import { useScale2DX, useScale2DY } from '../utilities/scale.js';

interface ContoursPathsProps {
id: string;
color: string;
sign: LevelSign;
spectrum: Spectrum2D;
spectrum: SpectrumFTData;
onTimeout: () => void;
}

interface ContoursInnerProps {
spectra: Spectrum2D[];
spectra: SpectrumFTData[];
}

function usePath(
spectrum: Spectrum2D,
contours: ReturnType<typeof drawContours>['contours'],
) {
function usePath(contours: ReturnType<typeof drawContours>['contours']) {
const scaleX = useScale2DX();
const scaleY = useScale2DY();

Expand All @@ -49,11 +46,14 @@
return pathBuilder.toString();
}

const useContoursLevel = (spectrum: Spectrum2D, sign: LevelSign) => {
const useContoursLevel = (spectrumID: string, sign: LevelSign) => {
const {
display: { contourOptions },
} = spectrum;
return contourOptions?.[sign];
view: {
zoom: { levels },
},
} = useChartData();
const level = levels[spectrumID]?.[sign];
return level;
};

function ContoursPaths({
Expand All @@ -65,7 +65,7 @@
}: ContoursPathsProps) {
const activeSpectrum = useActiveSpectrum();
const preferences = usePreferences();
const level = useContoursLevel(spectrum, sign);
const level = useContoursLevel(spectrumID, sign);

const contours = useMemo(() => {
const { contours, timeout } = drawContours(
Expand All @@ -79,12 +79,12 @@
return contours;
}, [spectrum, level, onTimeout, sign]);

const path = usePath(spectrum, contours);
const path = usePath(contours);

const opacity =
activeSpectrum === null || spectrumID === activeSpectrum.id
? '1'
: // TODO: make sure preferences are not a lie and remove the optional chaining.

Check warning on line 87 in src/component/2d/ft/Contours.tsx

View workflow job for this annotation

GitHub Actions / nodejs / lint-eslint

Unexpected 'todo' comment: 'TODO: make sure preferences are not a...'
(preferences?.current?.general?.dimmedSpectraOpacity ?? 0.1);

return (
Expand Down Expand Up @@ -147,18 +147,7 @@
const MemoizedContours = memo(ContoursInner);

export default function Contours() {
const {
data: spectra,
displayerKey,
view: {
spectra: { activeTab },
},
} = useChartData();
const spectra2d = useMemo<Spectrum2D[]>(() => {
return getSpectraByNucleus(activeTab, spectra).filter(
(datum) => datum.info.isFt,
) as Spectrum2D[];
}, [activeTab, spectra]);
const spectra = use2DReducer();

return <MemoizedContours {...{ spectra: spectra2d, displayerKey }} />;
return <MemoizedContours spectra={spectra} />;
}
140 changes: 140 additions & 0 deletions src/component/hooks/__tests__/reduce2DSpectrum.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { expect, test } from 'vitest';

import { reduce2DSpectrum } from '../reduce2DSpectrum.ts';

test('reduce 1001x1001 matrix', () => {
const size = 11;
const z: Float64Array[] = [];
for (let row = 0; row < size; row++) {
const rowData = new Float64Array(size);
for (let col = 0; col < size; col++) {
rowData[col] = 1000 + col + row;
}
z.push(rowData);
}

const data = {
minX: 1000,
maxX: 2000,
minY: 1000,
maxY: 2000,
minZ: 1000,
maxZ: 2000,
z,
};

const result = reduce2DSpectrum(data, {
fromX: 1000,
toX: 2000,
fromY: 1000,
toY: 2000,
numberOfPoints: 4,
});

expect(result).toStrictEqual({
minX: 1000,
maxX: 2000,
minY: 1000,
maxY: 2000,
minZ: 1000,
maxZ: 2000,
z: [
Float64Array.from([1002, 1005, 1008, 1011]),
Float64Array.from([1005, 1008, 1011, 1014]),
Float64Array.from([1008, 1011, 1014, 1017]),
Float64Array.from([1011, 1014, 1017, 1020]),
],
});
});

test('reduce matrix with negative values uses min when sum is negative', () => {
const size = 8;
const z: Float64Array[] = [];
for (let row = 0; row < size; row++) {
const rowData = new Float64Array(size);
for (let col = 0; col < size; col++) {
rowData[col] = -10 + col + row;
}
z.push(rowData);
}

const data = {
minX: 0,
maxX: 70,
minY: 0,
maxY: 70,
minZ: -10,
maxZ: 4,
z,
};

const result = reduce2DSpectrum(data, {
fromX: 0,
toX: 70,
fromY: 0,
toY: 70,
numberOfPoints: 4,
});

expect(result.z.length).toBe(4);
expect(result.z[0].length).toBe(4);
// top-left rectangle (rows 0-1, cols 0-1) has values -10, -9, -9, -8 → sum < 0 → min = -10
expect(result.z[0][0]).toBe(-10);
// bottom-right rectangle (rows 6-7, cols 6-7) has values 2, 3, 3, 4 → sum > 0 → max = 4
expect(result.z[3][3]).toBe(4);
});

test('keeps original from/to when no range is specified', () => {
const size = 11;
const z: Float64Array[] = [];
for (let row = 0; row < size; row++) {
const rowData = new Float64Array(size);
for (let col = 0; col < size; col++) {
rowData[col] = col + row;
}
z.push(rowData);
}

const data = {
minX: 100,
maxX: 200,
minY: 300,
maxY: 400,
minZ: 0,
maxZ: 20,
z,
};

const result = reduce2DSpectrum(data, { numberOfPoints: 4 });

expect(result.minX).toBe(100);
expect(result.maxX).toBe(200);
expect(result.minY).toBe(300);
expect(result.maxY).toBe(400);
});

test('returns original data when numberOfPoints is larger than matrix size', () => {
const size = 4;
const z: Float64Array[] = [];
for (let row = 0; row < size; row++) {
const rowData = new Float64Array(size);
for (let col = 0; col < size; col++) {
rowData[col] = col + row;
}
z.push(rowData);
}

const data = {
minX: 0,
maxX: 30,
minY: 0,
maxY: 30,
minZ: 0,
maxZ: 6,
z,
};

const result = reduce2DSpectrum(data, { numberOfPoints: 512 });

expect(result).toBe(data);
});
98 changes: 98 additions & 0 deletions src/component/hooks/reduce2DSpectrum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { NmrData2DContent } from 'cheminfo-types';

import type { Reduce2DSpectrumOptions } from './use2DReducer.tsx';

export function reduce2DSpectrum(
data: NmrData2DContent,
options: Reduce2DSpectrumOptions = {},
) {
const {
minY: originalMinY,
minX: originalMinX,
maxY: originalMaxY,
maxX: originalMaxX,
z,
} = data;
const {
numberOfPoints = 512,
fromX = originalMinX,
fromY = originalMinY,
toX = originalMaxX,
toY = originalMaxY,
} = options;
const nbPointsY = z.length;
const nbPointsX = z[0]?.length || 0;

if (nbPointsX <= numberOfPoints && nbPointsY <= numberOfPoints) {
return data;
}

// need to find the indices in X and Y taking care that we are not out of bounds
const deltaX = (originalMaxX - originalMinX) / (nbPointsX - 1);
const deltaY = (originalMaxY - originalMinY) / (nbPointsY - 1);

const indexFromX = Math.max(0, Math.floor((fromX - originalMinX) / deltaX));
const indexToX = Math.min(
nbPointsX - 1,
Math.ceil((toX - originalMinX) / deltaX),
);
const indexFromY = Math.max(0, Math.floor((fromY - originalMinY) / deltaY));
const indexToY = Math.min(
nbPointsY - 1,
Math.ceil((toY - originalMinY) / deltaY),
);

const sourceNbPointsX = indexToX - indexFromX + 1;
const sourceNbPointsY = indexToY - indexFromY + 1;
const newNbPointsX = Math.min(sourceNbPointsX, numberOfPoints);
const newNbPointsY = Math.min(sourceNbPointsY, numberOfPoints);

// create the result object and fill with typed arrays
const reducedMatrix = [];
for (let i = 0; i < newNbPointsY; i++) {
reducedMatrix.push(new Float64Array(newNbPointsX));
}

// Fill the reducedMatrix by aggregating the original values in each rectangle.
// If the sum of values in the rectangle is positive, take the max; otherwise take the min.

for (let newRow = 0; newRow < newNbPointsY; newRow++) {
const srcRowStart =
indexFromY + Math.floor((newRow * sourceNbPointsY) / newNbPointsY);
const srcRowEnd =
indexFromY + Math.floor(((newRow + 1) * sourceNbPointsY) / newNbPointsY);

for (let newCol = 0; newCol < newNbPointsX; newCol++) {
const srcColStart =
indexFromX + Math.floor((newCol * sourceNbPointsX) / newNbPointsX);
const srcColEnd =
indexFromX +
Math.floor(((newCol + 1) * sourceNbPointsX) / newNbPointsX);

let sum = 0;
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;

for (let row = srcRowStart; row < srcRowEnd; row++) {
const rowData = z[row];
for (let col = srcColStart; col < srcColEnd; col++) {
const value = rowData[col];
sum += value;
if (value < min) min = value;
if (value > max) max = value;
}
}

reducedMatrix[newRow][newCol] = sum >= 0 ? max : min;
}
}

return {
...data,
minX: originalMinX + indexFromX * deltaX,
maxX: originalMinX + indexToX * deltaX,
minY: originalMinY + indexFromY * deltaY,
maxY: originalMinY + indexToY * deltaY,
z: reducedMatrix,
};
}
56 changes: 56 additions & 0 deletions src/component/hooks/use2DReducer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type { Spectrum2D } from '@zakodium/nmrium-core';
import type { NmrData2DContent, NmrData2DFt } from 'cheminfo-types';
import { useMemo } from 'react';

import { isFt2DSpectrum } from '../../data/data2d/Spectrum2D/isSpectrum2D.ts';
import { useChartData } from '../context/ChartContext.tsx';
import { getSpectraByNucleus } from '../utility/getSpectraByNucleus.ts';

import { reduce2DSpectrum } from './reduce2DSpectrum.ts';

export interface SpectrumFTData extends Pick<Spectrum2D, 'display' | 'id'> {
data: NmrData2DContent;
}

export function use2DReducer(): SpectrumFTData[] {
const {
xDomain,
yDomain,
view: {
spectra: { activeTab },
},
data,
} = useChartData();
const [fromX, toX] = xDomain;
const [fromY, toY] = yDomain;

return useMemo(() => {
const outputSpectra: SpectrumFTData[] = [];
for (const spectrum of getSpectraByNucleus(activeTab, data).filter(
isFt2DSpectrum,
)) {
const { id, display, data } = spectrum;
const { rr } = data as NmrData2DFt;
const reducedData = reduce2DSpectrum(rr, {
fromX,
fromY,
toX,
toY,
});
outputSpectra.push({
data: reducedData,
id,
display,
});
}
return outputSpectra;
}, [activeTab, data, fromX, fromY, toX, toY]);
}

export interface Reduce2DSpectrumOptions {
numberOfPoints?: number;
fromX?: number;
toX?: number;
fromY?: number;
toY?: number;
}
Loading
Loading