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
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { type FunctionComponent } from 'preact';
import { type FunctionComponent, type JSX } from 'preact';
import { useMemo } from 'preact/hooks';
import z from 'zod';

import { type MutationOverTimeDataMap } from './MutationOverTimeData';
import { MutationsOverTimeGridTooltip } from './mutations-over-time-grid-tooltip';
import { getProportion, type MutationOverTimeMutationValue } from '../../query/queryMutationsOverTime';
import { type SequenceType } from '../../types';
import { type Deletion, type Substitution } from '../../utils/mutations';
import { type ColorScale, getColorWithinScale, getTextColorForScale } from './color-scale-selector';
import PortalTooltip from './portal-tooltip';
import { type TooltipPosition } from './tooltip';
import { getProportion, type ProportionValue } from '../../query/queryMutationsOverTime';
import { type Temporal } from '../../utils/temporalClass';
import { AnnotatedMutation } from '../components/annotated-mutation';
import { type ColorScale, getColorWithinScale, getTextColorForScale } from '../components/color-scale-selector';
import PortalTooltip from '../components/portal-tooltip';
import { type TooltipPosition } from '../components/tooltip';
import { type TemporalDataMap } from '../mutationsOverTime/MutationOverTimeData';
import { formatProportion } from '../shared/table/formatProportion';
import { type PageSizes, Pagination } from '../shared/tanstackTable/pagination';
import { usePageSizeContext } from '../shared/tanstackTable/pagination-context';
Expand All @@ -31,55 +27,56 @@ export const customColumnSchema = z.object({
});
export type CustomColumn = z.infer<typeof customColumnSchema>;

export interface MutationsOverTimeGridProps {
data: MutationOverTimeDataMap;
export interface FeatureRenderer<D> {
asString(value: D): string;
renderRowLabel(value: D): JSX.Element;
renderTooltip(value: D, temporal: Temporal, proportionValue: ProportionValue | undefined): JSX.Element;
}

export interface FeaturesOverTimeGridProps<F> {
rowLabelHeader: string;
data: TemporalDataMap<F>;
colorScale: ColorScale;
sequenceType: SequenceType;
pageSizes: PageSizes;
customColumns?: CustomColumn[];
featureRenderer: FeatureRenderer<F>;
tooltipPortalTarget: HTMLElement | null;
}

type RowType = {
mutation: Substitution | Deletion;
values: (MutationOverTimeMutationValue | undefined)[];
type RowType<F> = {
feature: F;
values: (ProportionValue | undefined)[];
customValues: (string | number | undefined)[];
};

const EMPTY_COLUMNS: CustomColumn[] = [];

const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
function FeaturesOverTimeGrid<F>({
rowLabelHeader,
data,
colorScale,
sequenceType,
pageSizes,
customColumns = EMPTY_COLUMNS,
featureRenderer,
tooltipPortalTarget,
}) => {
}: FeaturesOverTimeGridProps<F>) {
const tableData = useMemo(() => {
const allMutations = data.getFirstAxisKeys();
return data.getAsArray().map((row, index): RowType => {
const mutation = allMutations[index];
const customValues = customColumns.map((col) => col.values[mutation.code]);
return { mutation, values: [...row], customValues };
const firstAxisKeys = data.getFirstAxisKeys();
return data.getAsArray().map((row, index): RowType<F> => {
const firstAxisKey = firstAxisKeys[index];
const customValues = customColumns.map((col) => col.values[featureRenderer.asString(firstAxisKey)]);
return { feature: firstAxisKey, values: [...row], customValues };
});
}, [data, customColumns]);
}, [data, customColumns, featureRenderer]);

const columns = useMemo(() => {
const columnHelper = createColumnHelper<RowType>();
const columnHelper = createColumnHelper<RowType<F>>();
const dates = data.getSecondAxisKeys();

const mutationHeader = columnHelper.accessor((row) => row.mutation, {
id: 'mutation',
header: () => <span>Mutation</span>,
cell: ({ getValue }) => {
const value = getValue();
return (
<div className={'text-center'}>
<AnnotatedMutation mutation={value} sequenceType={sequenceType} />
</div>
);
},
const featureHeader = columnHelper.accessor((row) => row.feature, {
id: 'feature',
header: () => <span>{rowLabelHeader}</span>,
cell: ({ getValue }) => featureRenderer.renderRowLabel(getValue()),
});

const customColumnHeaders = customColumns.map((customCol, index) => {
Expand Down Expand Up @@ -108,12 +105,13 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
const numberOfRows = table.getRowModel().rows.length;
const numberOfColumns = table.getAllColumns().length;

const tooltip = featureRenderer.renderTooltip(row.original.feature, date, value);

return (
<div className={'text-center'}>
<ProportionCell
value={value ?? null}
date={date}
mutation={row.original.mutation}
tooltip={tooltip}
tooltipPosition={getTooltipPosition(
rowIndex -
table.getState().pagination.pageIndex * table.getState().pagination.pageSize,
Expand All @@ -130,8 +128,8 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
});
});

return [mutationHeader, ...customColumnHeaders, ...dateHeaders];
}, [colorScale, data, sequenceType, customColumns, tooltipPortalTarget]);
return [featureHeader, ...customColumnHeaders, ...dateHeaders];
}, [colorScale, data, customColumns, tooltipPortalTarget, featureRenderer, rowLabelHeader]);

const { pageSize } = usePageSizeContext();
const table = usePreactTable({
Expand Down Expand Up @@ -183,7 +181,7 @@ const MutationsOverTimeGrid: FunctionComponent<MutationsOverTimeGridProps> = ({
</div>
</div>
);
};
}

function styleGridHeader(columnIndex: number, numDateColumns: number) {
if (columnIndex === 0) {
Expand All @@ -204,22 +202,17 @@ function getTooltipPosition(rowIndex: number, rows: number, columnIndex: number,
}

const ProportionCell: FunctionComponent<{
value: MutationOverTimeMutationValue;
date: Temporal;
mutation: Substitution | Deletion;
value: ProportionValue;
tooltip: JSX.Element;
tooltipPosition: TooltipPosition;
colorScale: ColorScale;
tooltipPortalTarget: HTMLElement | null;
}> = ({ value, mutation, date, tooltipPosition, colorScale, tooltipPortalTarget }) => {
}> = ({ value, tooltip, tooltipPosition, colorScale, tooltipPortalTarget }) => {
const proportion = getProportion(value);

return (
<div className={'py-1 w-full h-full'}>
<PortalTooltip
content={<MutationsOverTimeGridTooltip mutation={mutation} date={date} value={value} />}
position={tooltipPosition}
portalTarget={tooltipPortalTarget}
>
<PortalTooltip content={tooltip} position={tooltipPosition} portalTarget={tooltipPortalTarget}>
<div
style={{
backgroundColor: getColorWithinScale(proportion, colorScale),
Expand All @@ -240,4 +233,4 @@ const ProportionCell: FunctionComponent<{
);
};

export default MutationsOverTimeGrid;
export default FeaturesOverTimeGrid;
10 changes: 6 additions & 4 deletions components/src/preact/mutationsOverTime/MutationOverTimeData.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import {
type MutationOverTimeMutationValue,
type ProportionValue,
serializeSubstitutionOrDeletion,
serializeTemporal,
} from '../../query/queryMutationsOverTime';
import { type Map2d, Map2dBase, type Map2DContents } from '../../utils/map2d';
import type { Deletion, Substitution } from '../../utils/mutations';
import type { Temporal, TemporalClass } from '../../utils/temporalClass';

export type TemporalDataMap<D, T extends Temporal | TemporalClass = Temporal> = Map2d<D, T, ProportionValue>;

export type MutationOverTimeDataMap<T extends Temporal | TemporalClass = Temporal> = Map2d<
Substitution | Deletion,
T,
MutationOverTimeMutationValue
ProportionValue
>;

export class BaseMutationOverTimeDataMap<T extends Temporal | TemporalClass = Temporal> extends Map2dBase<
Substitution | Deletion,
T,
MutationOverTimeMutationValue
ProportionValue
> {
constructor(initialContent?: Map2DContents<Substitution | Deletion, T, MutationOverTimeMutationValue>) {
constructor(initialContent?: Map2DContents<Substitution | Deletion, T, ProportionValue>) {
super(serializeSubstitutionOrDeletion, serializeTemporal, initialContent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest';

import { BaseMutationOverTimeDataMap } from './MutationOverTimeData';
import { getFilteredMutationOverTimeData, type MutationFilter } from './getFilteredMutationsOverTimeData';
import { type MutationOverTimeMutationValue } from '../../query/queryMutationsOverTime';
import { type ProportionValue } from '../../query/queryMutationsOverTime';
import { type DeletionEntry, type SubstitutionEntry } from '../../types';
import { type Deletion, type Substitution } from '../../utils/mutations';
import { type TemporalClass } from '../../utils/temporalClass';
Expand Down Expand Up @@ -392,13 +392,13 @@ describe('getFilteredMutationOverTimeData', () => {
count: 1,
proportion: inFilter,
totalCount: 10,
} satisfies MutationOverTimeMutationValue;
} satisfies ProportionValue;
const emptyMutationOverTimeValue = {
type: 'value',
count: 0,
proportion: NaN,
totalCount: 0,
} satisfies MutationOverTimeMutationValue;
} satisfies ProportionValue;

function prepareMutationOverTimeData(
mutationEntries: (SubstitutionEntry<Substitution> | DeletionEntry<Deletion>)[],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import type { FunctionComponent } from 'preact';

import {
type MutationOverTimeMutationValue,
MUTATIONS_OVER_TIME_MIN_PROPORTION,
} from '../../query/queryMutationsOverTime';
import { type ProportionValue, MUTATIONS_OVER_TIME_MIN_PROPORTION } from '../../query/queryMutationsOverTime';
import type { Deletion, Substitution } from '../../utils/mutations';
import { type Temporal, type TemporalClass, toTemporalClass, YearMonthDayClass } from '../../utils/temporalClass';
import { formatProportion } from '../shared/table/formatProportion';

export type MutationsOverTimeGridTooltipProps = {
mutation: Substitution | Deletion;
date: Temporal;
value: MutationOverTimeMutationValue;
value: ProportionValue;
};

export const MutationsOverTimeGridTooltip: FunctionComponent<MutationsOverTimeGridTooltipProps> = ({
Expand Down Expand Up @@ -66,7 +63,7 @@ export const MutationsOverTimeGridTooltip: FunctionComponent<MutationsOverTimeGr
};

const TooltipValueCountsDescription: FunctionComponent<{
value: NonNullable<MutationOverTimeMutationValue>;
value: NonNullable<ProportionValue>;
mutationCode: string;
mutationPosition: number;
}> = ({ value, mutationCode, mutationPosition }) => {
Expand Down
25 changes: 20 additions & 5 deletions components/src/preact/mutationsOverTime/mutations-over-time.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
getFilteredMutationOverTimeData,
type MutationFilter,
} from './getFilteredMutationsOverTimeData';
import MutationsOverTimeGrid, { customColumnSchema } from './mutations-over-time-grid';
import { getProportion, queryMutationsOverTimeData } from '../../query/queryMutationsOverTime';
import { MutationsOverTimeGridTooltip } from './mutations-over-time-grid-tooltip';
import { type ProportionValue, getProportion, queryMutationsOverTimeData } from '../../query/queryMutationsOverTime';
import {
lapisFilterSchema,
sequenceTypeSchema,
Expand All @@ -18,14 +18,16 @@ import {
views,
} from '../../types';
import { type Deletion, type Substitution } from '../../utils/mutations';
import { toTemporalClass } from '../../utils/temporalClass';
import { type Temporal, toTemporalClass } from '../../utils/temporalClass';
import { useDispatchFinishedLoadingEvent } from '../../utils/useDispatchFinishedLoadingEvent';
import { useLapisUrl } from '../LapisUrlContext';
import { useMutationAnnotationsProvider } from '../MutationAnnotationsContext';
import { AnnotatedMutation } from '../components/annotated-mutation';
import { type ColorScale } from '../components/color-scale-selector';
import { ColorScaleSelectorDropdown } from '../components/color-scale-selector-dropdown';
import { CsvDownloadButton } from '../components/csv-download-button';
import { ErrorBoundary } from '../components/error-boundary';
import FeaturesOverTimeGrid, { type FeatureRenderer, customColumnSchema } from '../components/features-over-time-grid';
import { Fullscreen } from '../components/fullscreen';
import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
import { LoadingDisplay } from '../components/loading-display';
Expand Down Expand Up @@ -173,19 +175,32 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
annotationProvider,
]);

const mutationRenderer: FeatureRenderer<Substitution | Deletion> = {
asString: (value: Substitution | Deletion) => value.code,
renderRowLabel: (value: Substitution | Deletion) => (
<div className={'text-center'}>
<AnnotatedMutation mutation={value} sequenceType={originalComponentProps.sequenceType} />
</div>
),
renderTooltip: (value: Substitution | Deletion, temporal: Temporal, proportionValue: ProportionValue) => (
<MutationsOverTimeGridTooltip mutation={value} date={temporal} value={proportionValue} />
),
};

const getTab = (view: MutationsOverTimeView) => {
switch (view) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- for extensibility
case 'grid':
return {
title: 'Grid',
content: (
<MutationsOverTimeGrid
<FeaturesOverTimeGrid
rowLabelHeader='Mutation'
data={filteredData}
colorScale={colorScale}
sequenceType={originalComponentProps.sequenceType}
pageSizes={originalComponentProps.pageSizes}
customColumns={originalComponentProps.customColumns}
featureRenderer={mutationRenderer}
tooltipPortalTarget={tooltipPortalTarget}
/>
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@ import { type Dispatch, type StateUpdater, useMemo, useState, useRef } from 'pre
import z from 'zod';

import { computeWastewaterMutationsOverTimeDataPerLocation } from './computeWastewaterMutationsOverTimeDataPerLocation';
import { type ProportionValue } from '../../../query/queryMutationsOverTime';
import { lapisFilterSchema, type SequenceType, sequenceTypeSchema } from '../../../types';
import { Map2dView } from '../../../utils/map2d';
import { type Deletion, type Substitution } from '../../../utils/mutations';
import { type Temporal } from '../../../utils/temporalClass';
import { useDispatchFinishedLoadingEvent } from '../../../utils/useDispatchFinishedLoadingEvent';
import { useLapisUrl } from '../../LapisUrlContext';
import { useMutationAnnotationsProvider } from '../../MutationAnnotationsContext';
import { AnnotatedMutation } from '../../components/annotated-mutation';
import { type ColorScale } from '../../components/color-scale-selector';
import { ColorScaleSelectorDropdown } from '../../components/color-scale-selector-dropdown';
import { ErrorBoundary } from '../../components/error-boundary';
import FeaturesOverTimeGrid from '../../components/features-over-time-grid';
import { Fullscreen } from '../../components/fullscreen';
import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../../components/info';
import { LoadingDisplay } from '../../components/loading-display';
Expand All @@ -24,7 +29,7 @@ import {
type MutationFilter,
mutationOrAnnotationDoNotMatchFilter,
} from '../../mutationsOverTime/getFilteredMutationsOverTimeData';
import MutationsOverTimeGrid from '../../mutationsOverTime/mutations-over-time-grid';
import { MutationsOverTimeGridTooltip } from '../../mutationsOverTime/mutations-over-time-grid-tooltip';
import { pageSizesSchema } from '../../shared/tanstackTable/pagination';
import { PageSizeContextProvider } from '../../shared/tanstackTable/pagination-context';
import { useQuery } from '../../useQuery';
Expand Down Expand Up @@ -166,7 +171,8 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
mutationOverTimeDataPerLocation.map(({ location, data }) => ({
title: location,
content: (
<MutationsOverTimeGrid
<FeaturesOverTimeGrid
rowLabelHeader='Mutation'
data={getFilteredMutationOverTimeData({
data,
displayedSegments,
Expand All @@ -176,7 +182,28 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
})}
colorScale={colorScale}
pageSizes={originalComponentProps.pageSizes}
sequenceType={originalComponentProps.sequenceType}
featureRenderer={{
asString: (value: Substitution | Deletion) => value.code,
renderRowLabel: (value: Substitution | Deletion) => (
<div className={'text-center'}>
<AnnotatedMutation
mutation={value}
sequenceType={originalComponentProps.sequenceType}
/>
</div>
),
renderTooltip: (
value: Substitution | Deletion,
temporal: Temporal,
proportionValue: ProportionValue,
) => (
<MutationsOverTimeGridTooltip
mutation={value}
date={temporal}
value={proportionValue}
/>
),
}}
tooltipPortalTarget={tooltipPortalTargetRef.current}
/>
),
Expand Down
Loading
Loading