Skip to content
Snippets Groups Projects
Commit d7527586 authored by Adrian Orłów's avatar Adrian Orłów :fire:
Browse files

Merge branch 'MIN-315-summary-overlays-should-list-named-elements' into 'development'

feat: Add summary overlays with named elements (MIN-315)

Closes MIN-315

See merge request !181
parents d1abccf7 4f8bc229
No related branches found
No related tags found
2 merge requests!223reset the pin numbers before search results are fetch (so the results will be...,!181feat: Add summary overlays with named elements (MIN-315)
Pipeline #88995 passed
Showing
with 329 additions and 15 deletions
...@@ -82,7 +82,10 @@ export const BioEntityDrawer = (): React.ReactNode => { ...@@ -82,7 +82,10 @@ export const BioEntityDrawer = (): React.ReactNode => {
</CollapsibleSection> </CollapsibleSection>
</> </>
)} )}
<OverlayData /> <OverlayData
isShowGroupedOverlays={Boolean(relatedSubmap)}
isShowOverlayBioEntityName={Boolean(relatedSubmap)}
/>
</div> </div>
</div> </div>
); );
......
import { ZERO } from '@/constants/common';
import { overlayFixture } from '@/models/fixtures/overlaysFixture';
import { MapOverlay } from '@/types/models';
import { render, screen } from '@testing-library/react';
import { OverlayDataAxis } from '../OverlayData.types';
import { GroupedOverlayAxes } from './GroupedOverlayAxes.components';
const BASE_AXIS: OverlayDataAxis = {
id: 123,
title: 'axis title',
value: 2137,
color: '#FFFFFF',
geneVariants: undefined,
overlayId: overlayFixture.idObject,
};
const renderComponent = (axes: OverlayDataAxis[], overlay: MapOverlay): void => {
render(<GroupedOverlayAxes axes={axes} overlay={overlay} />);
};
describe('GroupedOverlayAxes', () => {
describe('when axes array is empty', () => {
beforeEach(() => {
renderComponent([], overlayFixture);
});
it('should not render title', () => {
expect(screen.queryAllByText(overlayFixture.name).length).toEqual(ZERO);
});
});
describe('when axes array is present', () => {
const AXES = [
{
...BASE_AXIS,
title: 'axis 1',
},
{
...BASE_AXIS,
title: 'axis 2',
},
];
beforeEach(() => {
renderComponent(AXES, overlayFixture);
});
it('should render title', () => {
expect(screen.getByText(overlayFixture.name)).toBeInTheDocument();
});
it.each(AXES)('should render overlay axis', ({ title }) => {
expect(screen.getByText(title)).toBeInTheDocument();
});
});
});
import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { MapOverlay } from '@/types/models';
import { useMemo } from 'react';
import { OverlayAxis } from '../OverlayAxis';
import { OverlayDataAxis } from '../OverlayData.types';
import { getAxesSortedByValue } from '../utils/getAxesSortedByValue';
interface Props {
overlay: MapOverlay;
axes: OverlayDataAxis[];
}
export const GroupedOverlayAxes = ({ overlay, axes }: Props): JSX.Element | null => {
const { idObject, name } = overlay;
const overlayAxes = axes.filter(axis => axis.overlayId === idObject);
const sortedAxes = useMemo(() => getAxesSortedByValue(overlayAxes), [overlayAxes]);
if (overlayAxes.length === SIZE_OF_EMPTY_ARRAY) {
return null;
}
return (
<div className="flex flex-col gap-2 rounded-lg border border-divide p-4">
<div className="font-bold" data-testid="grouped-overlay-title">
{name}
</div>
<div className="my-1 h-[1px] w-full bg-divide" />
{sortedAxes.map(axis => (
<OverlayAxis key={axis.title} axis={axis} />
))}
</div>
);
};
export { GroupedOverlayAxes } from './GroupedOverlayAxes.components';
...@@ -15,6 +15,7 @@ const BASE_AXIS: OverlayDataAxis = { ...@@ -15,6 +15,7 @@ const BASE_AXIS: OverlayDataAxis = {
value: 2137, value: 2137,
color: '#FFFFFF', color: '#FFFFFF',
geneVariants: undefined, geneVariants: undefined,
overlayId: 1,
}; };
describe('OverlayAxis - component', () => { describe('OverlayAxis - component', () => {
......
...@@ -15,15 +15,18 @@ import { StoreType } from '@/redux/store'; ...@@ -15,15 +15,18 @@ import { StoreType } from '@/redux/store';
import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener'; import { InitialStoreState } from '@/utils/testing/getReduxStoreActionsListener';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore'; import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { OverlayData } from './OverlayData.component'; import { OverlayData, Props } from './OverlayData.component';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => { const renderComponent = (
initialStoreState: InitialStoreState = {},
props: Props = {},
): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState); const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return ( return (
render( render(
<Wrapper> <Wrapper>
<OverlayData /> <OverlayData {...props} />
</Wrapper>, </Wrapper>,
), ),
{ {
...@@ -113,4 +116,157 @@ describe('OverlayData - component', () => { ...@@ -113,4 +116,157 @@ describe('OverlayData - component', () => {
expect(screen.getByText('axis name')).toBeInTheDocument(); expect(screen.getByText('axis name')).toBeInTheDocument();
}); });
}); });
describe('when axes list is present and isShowGroupedOverlays=true', () => {
beforeEach(() => {
const OVERLAY_ID = overlayFixture.idObject;
const BIO_ENTITY = MOCKED_OVERLAY_BIO_ENTITY_RENDER[0];
renderComponent(
{
...INITIAL_STORE_STATE_MOCK,
map: {
...initialMapStateFixture,
data: { ...initialMapStateFixture.data, modelId: CORE_PD_MODEL_MOCK.idObject },
},
overlays: {
...INITIAL_STORE_STATE_MOCK.overlays,
data: [{ ...overlayFixture, name: 'overlay name' }],
},
bioEntity: {
data: [
{
searchQueryElement: '',
loading: 'pending',
error: { name: '', message: '' },
data: [
{
...bioEntitiesContentFixture[0],
bioEntity: {
...bioEntitiesContentFixture[0].bioEntity,
id: BIO_ENTITY.id,
},
},
],
},
],
loading: 'pending',
error: { name: '', message: '' },
},
overlayBioEntity: {
...OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK,
overlaysId: [OVERLAY_ID],
data: {
[OVERLAY_ID]: {
[CORE_PD_MODEL_MOCK.idObject]: [
{
...BIO_ENTITY,
geneVariants: GENE_VARIANTS_MOCK,
modelId: CORE_PD_MODEL_MOCK.idObject,
overlayId: OVERLAY_ID,
},
],
},
},
},
models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] },
drawer: {
...INITIAL_STORE_STATE_MOCK.drawer,
bioEntityDrawerState: {
...INITIAL_STORE_STATE_MOCK.drawer.bioEntityDrawerState,
bioentityId: BIO_ENTITY.id,
},
},
},
{
isShowGroupedOverlays: true,
},
);
});
it('should render title', () => {
expect(screen.getByText('Overlay data:')).toBeInTheDocument();
});
it('should render names of two overlays', () => {
expect(screen.queryAllByText('overlay name').length).toEqual(2);
});
});
describe('when axes list is present and isShowOverlayBioEntityName=true', () => {
beforeEach(() => {
const OVERLAY_ID = overlayFixture.idObject;
const BIO_ENTITY = MOCKED_OVERLAY_BIO_ENTITY_RENDER[0];
renderComponent(
{
...INITIAL_STORE_STATE_MOCK,
map: {
...initialMapStateFixture,
data: { ...initialMapStateFixture.data, modelId: CORE_PD_MODEL_MOCK.idObject },
},
overlays: {
...INITIAL_STORE_STATE_MOCK.overlays,
data: [{ ...overlayFixture, name: 'overlay name' }],
},
bioEntity: {
data: [
{
searchQueryElement: '',
loading: 'pending',
error: { name: '', message: '' },
data: [
{
...bioEntitiesContentFixture[0],
bioEntity: {
...bioEntitiesContentFixture[0].bioEntity,
id: BIO_ENTITY.id,
},
},
],
},
],
loading: 'pending',
error: { name: '', message: '' },
},
overlayBioEntity: {
...OVERLAY_BIO_ENTITY_INITIAL_STATE_MOCK,
overlaysId: [OVERLAY_ID],
data: {
[OVERLAY_ID]: {
[CORE_PD_MODEL_MOCK.idObject]: [
{
...BIO_ENTITY,
geneVariants: GENE_VARIANTS_MOCK,
modelId: CORE_PD_MODEL_MOCK.idObject,
overlayId: OVERLAY_ID,
name: 'element name',
},
],
},
},
},
models: { ...MODELS_INITIAL_STATE_MOCK, data: [CORE_PD_MODEL_MOCK] },
drawer: {
...INITIAL_STORE_STATE_MOCK.drawer,
bioEntityDrawerState: {
...INITIAL_STORE_STATE_MOCK.drawer.bioEntityDrawerState,
bioentityId: BIO_ENTITY.id,
},
},
},
{
isShowOverlayBioEntityName: true,
},
);
});
it('should render title', () => {
expect(screen.getByText('Overlay data:')).toBeInTheDocument();
});
it('should render element name', () => {
expect(screen.getByText('element name')).toBeInTheDocument();
});
});
}); });
import { ZERO } from '@/constants/common'; import { ZERO } from '@/constants/common';
import { overlaysOpenedSelector } from '@/redux/overlayBioEntity/overlayBioEntity.selector';
import { useMemo } from 'react';
import { useSelector } from 'react-redux';
import { GroupedOverlayAxes } from './GroupedOverlayAxes';
import { OverlayAxis } from './OverlayAxis'; import { OverlayAxis } from './OverlayAxis';
import { getAxesSortedByValue } from './utils/getAxesSortedByValue';
import { getUniqueAxes } from './utils/getUniqueAxes';
import { useOverlaysAxes } from './utils/useOverlaysAxes'; import { useOverlaysAxes } from './utils/useOverlaysAxes';
export const OverlayData = (): JSX.Element | null => { export interface Props {
const axes = useOverlaysAxes(); isShowGroupedOverlays?: boolean;
isShowOverlayBioEntityName?: boolean;
}
if (axes.length === ZERO) { export const OverlayData = ({
isShowGroupedOverlays = false,
isShowOverlayBioEntityName = false,
}: Props = {}): JSX.Element | null => {
const axes = useOverlaysAxes({ isShowOverlayBioEntityName });
const uniqueAxes = getUniqueAxes(axes);
const openedOverlays = useSelector(overlaysOpenedSelector);
const sortedAxes = useMemo(() => getAxesSortedByValue(uniqueAxes), [uniqueAxes]);
if (uniqueAxes.length === ZERO) {
return null; return null;
} }
const overlaysAxesContent = (
<div className="flex flex-col gap-2 rounded-lg border border-divide p-4">
{sortedAxes.map(axis => (
<OverlayAxis key={axis.title} axis={axis} />
))}
</div>
);
const groupedOverlayAxesContent = (
<>
{openedOverlays.map(overlay => (
<GroupedOverlayAxes key={overlay.idObject} overlay={overlay} axes={uniqueAxes} />
))}
</>
);
return ( return (
<div> <div className="flex flex-col gap-2">
<h3 className="mb-2 font-semibold">Overlay data:</h3> <h3 className="mb-2 font-semibold">Overlay data:</h3>
<div className="flex flex-col gap-2 rounded-lg border border-divide p-4"> {isShowGroupedOverlays ? groupedOverlayAxesContent : overlaysAxesContent}
{axes.map(axis => (
<OverlayAxis key={axis.title} axis={axis} />
))}
</div>
</div> </div>
); );
}; };
...@@ -6,4 +6,5 @@ export interface OverlayDataAxis { ...@@ -6,4 +6,5 @@ export interface OverlayDataAxis {
value?: number; value?: number;
color: string; color: string;
geneVariants?: GeneVariant[] | null; geneVariants?: GeneVariant[] | null;
overlayId: number;
} }
import { ZERO } from '@/constants/common';
import { OverlayDataAxis } from '../OverlayData.types';
export const getAxesSortedByValue = (axes: OverlayDataAxis[]): OverlayDataAxis[] =>
axes.sort((a, b) => (b?.value || ZERO) - (a?.value || ZERO));
import { OverlayDataAxis } from '../OverlayData.types';
const UNIQUE_KEYS: (keyof Pick<OverlayDataAxis, 'title' | 'value'>)[] = ['title', 'value'];
const SEPARATOR = ';';
export const getUniqueAxes = (nonUniqueAxes: OverlayDataAxis[]): OverlayDataAxis[] => {
const getUniqueAxesKey = (axis: OverlayDataAxis): string =>
UNIQUE_KEYS.map(key => axis[key]).join(SEPARATOR);
const uniqueAxesObj = nonUniqueAxes.reduce(
(obj, axis) => ({
...obj,
[getUniqueAxesKey(axis)]: axis,
}),
{} as Record<string, OverlayDataAxis>,
);
return Object.values(uniqueAxesObj);
};
...@@ -8,7 +8,13 @@ import { ...@@ -8,7 +8,13 @@ import {
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { OverlayDataAxis } from '../OverlayData.types'; import { OverlayDataAxis } from '../OverlayData.types';
export const useOverlaysAxes = (): OverlayDataAxis[] => { interface Options {
isShowOverlayBioEntityName?: boolean;
}
export const useOverlaysAxes = ({
isShowOverlayBioEntityName = false,
}: Options = {}): OverlayDataAxis[] => {
const openedOverlays = useSelector(overlaysOpenedSelector); const openedOverlays = useSelector(overlaysOpenedSelector);
const currentBioEntityOverlaysForCurrentBioEntity = useAppSelector( const currentBioEntityOverlaysForCurrentBioEntity = useAppSelector(
overlaysBioEntityForCurrentBioEntityAndCurrentModelSelector, overlaysBioEntityForCurrentBioEntityAndCurrentModelSelector,
...@@ -23,10 +29,11 @@ export const useOverlaysAxes = (): OverlayDataAxis[] => { ...@@ -23,10 +29,11 @@ export const useOverlaysAxes = (): OverlayDataAxis[] => {
return { return {
id: overlayBioEntity.id, id: overlayBioEntity.id,
title: overlay?.name || '', title: (isShowOverlayBioEntityName ? overlayBioEntity?.name : '') || overlay?.name || '',
value: overlayBioEntity.value || undefined, value: overlayBioEntity.value || undefined,
color: getOverlayBioEntityColorByAvailableProperties(overlayBioEntity), color: getOverlayBioEntityColorByAvailableProperties(overlayBioEntity),
geneVariants: overlayBioEntity?.geneVariants, geneVariants: overlayBioEntity?.geneVariants,
overlayId: overlayBioEntity.overlayId,
}; };
}); });
}; };
...@@ -34,6 +34,7 @@ export const parseOverlayBioEntityToOlRenderingFormat = ( ...@@ -34,6 +34,7 @@ export const parseOverlayBioEntityToOlRenderingFormat = (
value: entity.right.value, value: entity.right.value,
overlayId, overlayId,
color: entity.right.color, color: entity.right.color,
name: entity.right.name,
}); });
return acc; return acc;
} }
...@@ -53,6 +54,7 @@ export const parseOverlayBioEntityToOlRenderingFormat = ( ...@@ -53,6 +54,7 @@ export const parseOverlayBioEntityToOlRenderingFormat = (
overlayId, overlayId,
color: entity.right.color, color: entity.right.color,
geneVariants: entity.right.geneVariants, geneVariants: entity.right.geneVariants,
name: entity.right.name,
}); });
} }
......
...@@ -21,6 +21,7 @@ export type OverlayBioEntityRender = { ...@@ -21,6 +21,7 @@ export type OverlayBioEntityRender = {
hexColor?: string; hexColor?: string;
type: OverlayBioEntityRenderType; type: OverlayBioEntityRenderType;
geneVariants?: GeneVariant[] | null; geneVariants?: GeneVariant[] | null;
name?: string;
}; };
export interface OverlayReactionCoords { export interface OverlayReactionCoords {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment