Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: sort spectra by specific parameter #3272

Merged
merged 6 commits into from
Oct 22, 2024
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
2 changes: 1 addition & 1 deletion src/component/1d/LinesSeries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const style = css`
`;

function LinesSeries() {
const { data, displayerKey, xDomains } = useChartData();
const { displayerKey, xDomains, data } = useChartData();
const activeSpectra = useActiveSpectra();
const spectra = (data?.filter(
(d) => isSpectrum1D(d) && d.display.isVisible && xDomains[d.id],
Expand Down
2 changes: 0 additions & 2 deletions src/component/context/DispatchContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,13 @@ import type { PeaksActions } from '../reducer/actions/PeaksActions.js';
import type { PreferencesActions } from '../reducer/actions/PreferencesActions.js';
import type { RangesActions } from '../reducer/actions/RangesActions.js';
import type { SpectrumActions } from '../reducer/actions/SpectraActions.js';
import type { SpectraAnalysisActions } from '../reducer/actions/SpectraAnalysisAction.js';
import type { ToolsActions } from '../reducer/actions/ToolsActions.js';
import type { ZonesActions } from '../reducer/actions/ZonesActions.js';
import type { ActionType } from '../reducer/types/ActionType.js';

export type Action =
| ToolsActions
| SpectrumActions
| SpectraAnalysisActions
| LoadActions
| IntegralsActions
| RangesActions
Expand Down
273 changes: 273 additions & 0 deletions src/component/context/SortSpectraContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import lodashGet from 'lodash/get.js';
import type { Spectrum } from 'nmr-load-save';
import type { ReactNode } from 'react';
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from 'react';

import nucleusToString from '../utility/nucleusToString.js';

type SortDirection = 'asc' | 'desc' | null;
type SpectraReference = { id: string } & Partial<Record<string, any>>;

interface BaseSortOptions {
sortDirection?: SortDirection;
}

interface SortByReferenceOptions extends BaseSortOptions {
path: string;
sortByReferences: SpectraReference[];
sortType: 'sortByReference';
}
interface SortByReferenceIndexesOptions {
sortByReferences: SpectraReference[];
sortType: 'sortByReferenceIndexes';
}

interface SortByPathOptions extends BaseSortOptions {
path: string;
nucleus?: string;
sortType: 'sortByPath';
}

type SortOptions =
| SortByPathOptions
| SortByReferenceOptions
| SortByReferenceIndexesOptions;

type SortFuncOptions = SortOptions & { activeSort?: string };

interface SortSpectraContextState {
sortOptions: SortOptions | null;
sort: (options: SortFuncOptions) => void;
reset: () => void;
activeSort: string | null;
}

const SortSpectraContext = createContext<SortSpectraContextState | null>(null);

export function useSortSpectra(): SortSpectraContextState {
const context = useContext(SortSpectraContext);
if (!context) {
throw new Error('useSpectra must be used within a SpectraProvider');
}

return context;
}

interface SpectraProviderProps {
children: ReactNode;
}

function sortSpectraByPath(
originSpectra: Spectrum[],
sortOptions: SortByPathOptions,
) {
const outSpectra = [...originSpectra];
const { path, sortDirection, nucleus } = sortOptions;

const sortedSpectra: SortItem[] = [];
const originIndexes: number[] = [];

for (let index = 0; index < originSpectra.length; index++) {
const spectrum = originSpectra[index];
if (
!nucleus ||
(nucleus && nucleusToString(spectrum.info.nucleus) === nucleus)
) {
sortedSpectra.push({
spectrum,
sortValue: lodashGet(spectrum, path),
});
originIndexes.push(index);
}
}

sortArray(sortedSpectra, sortDirection);

for (let index = 0; index < sortedSpectra.length; index++) {
outSpectra[originIndexes[index]] = sortedSpectra[index].spectrum;
}
return outSpectra;
}

function sortSpectraByReferences(
originSpectra: Spectrum[],
sortOptions: SortByReferenceOptions,
) {
const outSpectra = [...originSpectra];
const { path, sortDirection, sortByReferences } = sortOptions;

const spectraReferenceMap = new Map<string, SpectraReference>();
const sortedSpectra: SortItem[] = [];
const originIndexes: number[] = [];

for (const reference of sortByReferences || []) {
spectraReferenceMap.set(reference.id, reference);
}

for (let index = 0; index < originSpectra.length; index++) {
const spectrum = originSpectra[index];
if (spectraReferenceMap.size > 0 && spectraReferenceMap.has(spectrum.id)) {
sortedSpectra.push({
spectrum,
sortValue: lodashGet(spectraReferenceMap.get(spectrum.id), path),
});
originIndexes.push(index);
}
}
sortArray(sortedSpectra, sortDirection);

for (let index = 0; index < sortedSpectra.length; index++) {
outSpectra[originIndexes[index]] = sortedSpectra[index].spectrum;
}
return outSpectra;
}
function sortSpectraByReferenceIndexes(
originSpectra: Spectrum[],
sortOptions: SortByReferenceIndexesOptions,
) {
const { sortByReferences } = sortOptions;
const outSpectra = [...originSpectra];
const spectraReferenceMap = new Map<string, SpectraReference>();
const originIndexes: number[] = [];
const sortedSpectra = new Map<string, Spectrum>();

for (const reference of sortByReferences || []) {
spectraReferenceMap.set(reference.id, reference);
}

for (let index = 0; index < originSpectra.length; index++) {
const spectrum = originSpectra[index];
if (spectraReferenceMap.size > 0 && spectraReferenceMap.has(spectrum.id)) {
originIndexes.push(index);
sortedSpectra.set(spectrum.id, spectrum);
}
}

for (let index = 0; index < originIndexes.length; index++) {
const spectrum = sortedSpectra.get(sortByReferences[index].id);
if (spectrum) {
outSpectra[originIndexes[index]] = spectrum;
}
}
return outSpectra;
}

interface SortItem {
spectrum: Spectrum;
sortValue: string | number | boolean | null | undefined;
}

function sortArray(data: SortItem[], sortDirection?: SortDirection) {
const direction = sortDirection === 'asc' ? 1 : -1;

if (!sortDirection) {
return data;
}

data.sort((a, b) => {
const valueA = a.sortValue;
const valueB = b.sortValue;

if (valueA == null && valueB != null) return -1 * direction;
if (valueB == null && valueA != null) return 1 * direction;
if (valueA == null && valueB == null) return 0;

if (typeof valueA === 'string' && typeof valueB === 'string') {
return direction * valueA.localeCompare(valueB);
}

if (typeof valueA === 'number' && typeof valueB === 'number') {
return direction * (valueA - valueB);
}

if (typeof valueA === 'boolean' && typeof valueB === 'boolean') {
return direction * (Number(valueA) - Number(valueB));
}

return 0;
});
}

export function sortSpectra(
originSpectra: Spectrum[],
sortOptions: SortOptions,
) {
const { sortType } = sortOptions;
switch (sortType) {
case 'sortByPath':
return sortSpectraByPath(originSpectra, sortOptions);
case 'sortByReference':
return sortSpectraByReferences(originSpectra, sortOptions);
case 'sortByReferenceIndexes':
return sortSpectraByReferenceIndexes(originSpectra, sortOptions);

default:
return originSpectra;
}
}

function getNextSortType(currentSort: SortDirection): SortDirection {
if (currentSort === null) return 'asc';
if (currentSort === 'asc') return 'desc';
return null;
}

export function SortSpectraProvider(props: SpectraProviderProps) {
const { children } = props;
const [sortOptions, setSortOptions] = useState<SortOptions | null>(null);
const [activeSort, setActiveSort] = useState<string | null>(null);

const sort = useCallback(
(options: SortFuncOptions) => {
setActiveSort(options?.activeSort ?? null);
setSortOptions((prevOptions) => {
if (options.sortType !== 'sortByReferenceIndexes') {
let previousOptions = { ...prevOptions };
if (activeSort !== options.activeSort) {
previousOptions = { ...prevOptions, sortDirection: null };
}
const newOptions: SortOptions = {
...previousOptions,
...options,
sortDirection:
options.sortDirection ??
getNextSortType(
(previousOptions as BaseSortOptions)?.sortDirection ?? null,
),
};
return newOptions;
}

return {
...prevOptions,
...options,
};
});
},
[activeSort],
);

const state = useMemo(() => {
function reset() {
setSortOptions(null);
}
return {
activeSort,
sortOptions,
sort,
reset,
};
}, [activeSort, sort, sortOptions]);

return (
<SortSpectraContext.Provider value={state}>
{children}
</SortSpectraContext.Provider>
);
}
10 changes: 7 additions & 3 deletions src/component/elements/ReactTable/ReactTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type TableOptions<T extends object = any> = UseTableOptions<T> &
UseSortByOptions<T>;

interface SortEvent {
onSortEnd?: (data: any) => void;
onSortEnd?: (data: any, isTableSorted?: boolean) => void;
}

export interface BaseRowStyle {
Expand Down Expand Up @@ -186,6 +186,7 @@ function TableInner<T extends object>(
rows,
prepareRow,
rowSpanHeaders,
state, // Access the sort state here
} = useTable(
{
columns: memoColumns,
Expand All @@ -196,6 +197,8 @@ function TableInner<T extends object>(
useRowSpan,
) as TableInstanceWithHooks<T>;

const { sortBy } = state as any;

function clickHandler(event, row) {
setRowIndex(row.index);
onClick?.(event, row);
Expand All @@ -218,11 +221,12 @@ function TableInner<T extends object>(

useEffect(() => {
if (isSortedEventTriggered.current) {
const isTableSorted = sortBy.length > 0;
const data = rows.map((row) => row.original);
onSortEnd?.(data);
onSortEnd?.(data, isTableSorted);
isSortedEventTriggered.current = false;
}
}, [onSortEnd, rows]);
}, [onSortEnd, rows, sortBy?.length]);

function headerClickHandler() {
isSortedEventTriggered.current = true;
Expand Down
8 changes: 4 additions & 4 deletions src/component/hooks/useSpectraPerNucleus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ import nucleusToString from '../utility/nucleusToString.js';

export default function useSpectraByActiveNucleus() {
const {
data,
data: spectra,
view: {
spectra: { activeTab },
},
} = useChartData();

return useMemo<Spectrum[]>(() => {
if (data) {
if (spectra) {
return (
data.filter(
spectra.filter(
(spectrum) => activeTab === nucleusToString(spectrum.info.nucleus),
) || []
);
}
return [];
}, [activeTab, data]);
}, [activeTab, spectra]);
}
Loading
Loading