diff --git a/components/src/preact/mutationsOverTime/mutations-over-time-grid.tsx b/components/src/preact/components/features-over-time-grid.tsx similarity index 73% rename from components/src/preact/mutationsOverTime/mutations-over-time-grid.tsx rename to components/src/preact/components/features-over-time-grid.tsx index 3daaf613a..2723958a4 100644 --- a/components/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +++ b/components/src/preact/components/features-over-time-grid.tsx @@ -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'; @@ -31,55 +27,56 @@ export const customColumnSchema = z.object({ }); export type CustomColumn = z.infer; -export interface MutationsOverTimeGridProps { - data: MutationOverTimeDataMap; +export interface FeatureRenderer { + asString(value: D): string; + renderRowLabel(value: D): JSX.Element; + renderTooltip(value: D, temporal: Temporal, proportionValue: ProportionValue | undefined): JSX.Element; +} + +export interface FeaturesOverTimeGridProps { + rowLabelHeader: string; + data: TemporalDataMap; colorScale: ColorScale; - sequenceType: SequenceType; pageSizes: PageSizes; customColumns?: CustomColumn[]; + featureRenderer: FeatureRenderer; tooltipPortalTarget: HTMLElement | null; } -type RowType = { - mutation: Substitution | Deletion; - values: (MutationOverTimeMutationValue | undefined)[]; +type RowType = { + feature: F; + values: (ProportionValue | undefined)[]; customValues: (string | number | undefined)[]; }; const EMPTY_COLUMNS: CustomColumn[] = []; -const MutationsOverTimeGrid: FunctionComponent = ({ +function FeaturesOverTimeGrid({ + rowLabelHeader, data, colorScale, - sequenceType, pageSizes, customColumns = EMPTY_COLUMNS, + featureRenderer, tooltipPortalTarget, -}) => { +}: FeaturesOverTimeGridProps) { 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 => { + 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(); + const columnHelper = createColumnHelper>(); const dates = data.getSecondAxisKeys(); - const mutationHeader = columnHelper.accessor((row) => row.mutation, { - id: 'mutation', - header: () => Mutation, - cell: ({ getValue }) => { - const value = getValue(); - return ( -
- -
- ); - }, + const featureHeader = columnHelper.accessor((row) => row.feature, { + id: 'feature', + header: () => {rowLabelHeader}, + cell: ({ getValue }) => featureRenderer.renderRowLabel(getValue()), }); const customColumnHeaders = customColumns.map((customCol, index) => { @@ -108,12 +105,13 @@ const MutationsOverTimeGrid: FunctionComponent = ({ const numberOfRows = table.getRowModel().rows.length; const numberOfColumns = table.getAllColumns().length; + const tooltip = featureRenderer.renderTooltip(row.original.feature, date, value); + return (
= ({ }); }); - 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({ @@ -183,7 +181,7 @@ const MutationsOverTimeGrid: FunctionComponent = ({
); -}; +} function styleGridHeader(columnIndex: number, numDateColumns: number) { if (columnIndex === 0) { @@ -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 (
- } - position={tooltipPosition} - portalTarget={tooltipPortalTarget} - > +
= Map2d; + export type MutationOverTimeDataMap = Map2d< Substitution | Deletion, T, - MutationOverTimeMutationValue + ProportionValue >; export class BaseMutationOverTimeDataMap extends Map2dBase< Substitution | Deletion, T, - MutationOverTimeMutationValue + ProportionValue > { - constructor(initialContent?: Map2DContents) { + constructor(initialContent?: Map2DContents) { super(serializeSubstitutionOrDeletion, serializeTemporal, initialContent); } } diff --git a/components/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts b/components/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts index 35270798e..d82ab51cb 100644 --- a/components/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +++ b/components/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts @@ -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'; @@ -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 | DeletionEntry)[], diff --git a/components/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.tsx b/components/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.tsx index 9af4e33cc..dc8b69ac9 100644 --- a/components/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.tsx +++ b/components/src/preact/mutationsOverTime/mutations-over-time-grid-tooltip.tsx @@ -1,9 +1,6 @@ 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'; @@ -11,7 +8,7 @@ import { formatProportion } from '../shared/table/formatProportion'; export type MutationsOverTimeGridTooltipProps = { mutation: Substitution | Deletion; date: Temporal; - value: MutationOverTimeMutationValue; + value: ProportionValue; }; export const MutationsOverTimeGridTooltip: FunctionComponent = ({ @@ -66,7 +63,7 @@ export const MutationsOverTimeGridTooltip: FunctionComponent; + value: NonNullable; mutationCode: string; mutationPosition: number; }> = ({ value, mutationCode, mutationPosition }) => { diff --git a/components/src/preact/mutationsOverTime/mutations-over-time.tsx b/components/src/preact/mutationsOverTime/mutations-over-time.tsx index 996257588..4ad385276 100644 --- a/components/src/preact/mutationsOverTime/mutations-over-time.tsx +++ b/components/src/preact/mutationsOverTime/mutations-over-time.tsx @@ -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, @@ -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'; @@ -173,6 +175,18 @@ const MutationsOverTimeTabs: FunctionComponent = ({ annotationProvider, ]); + const mutationRenderer: FeatureRenderer = { + asString: (value: Substitution | Deletion) => value.code, + renderRowLabel: (value: Substitution | Deletion) => ( +
+ +
+ ), + renderTooltip: (value: Substitution | Deletion, temporal: Temporal, proportionValue: ProportionValue) => ( + + ), + }; + const getTab = (view: MutationsOverTimeView) => { switch (view) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- for extensibility @@ -180,12 +194,13 @@ const MutationsOverTimeTabs: FunctionComponent = ({ return { title: 'Grid', content: ( - ), diff --git a/components/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx b/components/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx index 7b66578a4..340492a90 100644 --- a/components/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +++ b/components/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx @@ -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'; @@ -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'; @@ -166,7 +171,8 @@ const MutationsOverTimeTabs: FunctionComponent = ({ mutationOverTimeDataPerLocation.map(({ location, data }) => ({ title: location, content: ( - = ({ })} colorScale={colorScale} pageSizes={originalComponentProps.pageSizes} - sequenceType={originalComponentProps.sequenceType} + featureRenderer={{ + asString: (value: Substitution | Deletion) => value.code, + renderRowLabel: (value: Substitution | Deletion) => ( +
+ +
+ ), + renderTooltip: ( + value: Substitution | Deletion, + temporal: Temporal, + proportionValue: ProportionValue, + ) => ( + + ), + }} tooltipPortalTarget={tooltipPortalTargetRef.current} /> ), diff --git a/components/src/query/queryMutationsOverTime.ts b/components/src/query/queryMutationsOverTime.ts index 34ac18e11..077b18a8b 100644 --- a/components/src/query/queryMutationsOverTime.ts +++ b/components/src/query/queryMutationsOverTime.ts @@ -14,7 +14,7 @@ import { type Map2DContents } from '../utils/map2d'; import { type Deletion, type Substitution, DeletionClass, SubstitutionClass } from '../utils/mutations'; import { type Temporal } from '../utils/temporalClass'; -export type MutationOverTimeMutationValue = +export type ProportionValue = | { type: 'value'; proportion: number; @@ -37,7 +37,7 @@ export type MutationOverTimeMutationValue = } | null; -export function getProportion(value: MutationOverTimeMutationValue) { +export function getProportion(value: ProportionValue) { switch (value?.type) { case 'value': case 'wastewaterValue': @@ -211,14 +211,14 @@ export async function queryMutationsOverTimeData( } }); - const mutationOverTimeData: Map2DContents = { + const mutationOverTimeData: Map2DContents = { keysFirstAxis: new Map(responseMutations.map((mutation) => [mutation.code, mutation])), keysSecondAxis: new Map(requestedDateRanges.map((date) => [date.dateString, date])), data: new Map( responseMutations.map((mutation, i) => [ mutation.code, new Map( - requestedDateRanges.map((date, j): [string, MutationOverTimeMutationValue] => { + requestedDateRanges.map((date, j): [string, ProportionValue] => { if (totalCounts[j] === 0) { return [date.dateString, null]; } diff --git a/components/src/utilEntrypoint.ts b/components/src/utilEntrypoint.ts index 59ae84cea..dad129484 100644 --- a/components/src/utilEntrypoint.ts +++ b/components/src/utilEntrypoint.ts @@ -51,4 +51,4 @@ export { } from './preact/numberRangeFilter/NumberRangeFilterChangedEvent'; export { type MeanProportionInterval } from './preact/mutationsOverTime/mutations-over-time'; -export { type CustomColumn } from './preact/mutationsOverTime/mutations-over-time-grid'; +export { type CustomColumn } from './preact/components/features-over-time-grid';