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
12 changes: 10 additions & 2 deletions components/src/preact/components/features-over-time-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export type CustomColumn = z.infer<typeof customColumnSchema>;
export interface FeatureRenderer<D> {
asString(value: D): string;
renderRowLabel(value: D): JSX.Element;
renderTooltip(value: D, temporal: Temporal, proportionValue: ProportionValue | undefined): JSX.Element;
renderTooltip(value: D, temporal: Temporal, proportionValue: ProportionValue): JSX.Element;
}

export interface FeaturesOverTimeGridProps<F> {
Expand Down Expand Up @@ -99,12 +99,20 @@ function FeaturesOverTimeGrid<F>({
</div>
),
cell: ({ getValue, row, column, table }) => {
const value = getValue();
const valueRaw = getValue();
const value = valueRaw ?? null;
const rowIndex = row.index;
const columnIndex = column.getIndex();
const numberOfRows = table.getRowModel().rows.length;
const numberOfColumns = table.getAllColumns().length;

if (valueRaw === undefined) {
// eslint-disable-next-line no-console -- We want to warn that something might be wrong.
console.error(
`Found undefined value for ${row.original.feature} - ${date.dateString}. This shouldn't happen.`,
);
}

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

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,4 +156,59 @@ describe('groupMutationDataByLocation', () => {

expect(location1Data.getFirstAxisKeys()).to.deep.equal([mutation1, mutation2, mutation3]);
});

test('should backfill missing mutation-date combinations with explicit null', () => {
const input: WastewaterData = [
{
location: location1,
date: temporalCache.getYearMonthDay('2025-01-01'),
nucleotideMutationFrequency: [
{ mutation: mutation1, proportion: 0.1 },
// mutation2 missing for this date
],
aminoAcidMutationFrequency: [],
},
{
location: location1,
date: temporalCache.getYearMonthDay('2025-01-02'),
nucleotideMutationFrequency: [
// mutation1 missing for this date
{ mutation: mutation2, proportion: 0.2 },
],
aminoAcidMutationFrequency: [],
},
];

const result = groupMutationDataByLocation(input, 'nucleotide');

expect(result).to.have.length(1);
const location1Data = result[0].data;

// Both mutations should appear in all dates
expect(location1Data.getFirstAxisKeys()).to.deep.equal([mutation1, mutation2]);
expect(location1Data.getSecondAxisKeys()).to.deep.equal([
temporalCache.getYearMonthDay('2025-01-01'),
temporalCache.getYearMonthDay('2025-01-02'),
]);

// Verify backfilled nulls (not undefined)
expect(location1Data.get(mutation1, temporalCache.getYearMonthDay('2025-01-02'))).to.equal(null);
expect(location1Data.get(mutation2, temporalCache.getYearMonthDay('2025-01-01'))).to.equal(null);

// Verify actual values still present
expect(location1Data.get(mutation1, temporalCache.getYearMonthDay('2025-01-01'))).to.deep.equal({
type: 'wastewaterValue',
proportion: 0.1,
});
expect(location1Data.get(mutation2, temporalCache.getYearMonthDay('2025-01-02'))).to.deep.equal({
type: 'wastewaterValue',
proportion: 0.2,
});

// Verify the complete grid with getAsArray
expect(location1Data.getAsArray()).to.deep.equal([
[{ type: 'wastewaterValue', proportion: 0.1 }, null],
[null, { type: 'wastewaterValue', proportion: 0.2 }],
]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,26 @@ export async function computeWastewaterMutationsOverTimeDataPerLocation(

export function groupMutationDataByLocation(data: WastewaterData, sequenceType: 'nucleotide' | 'amino acid') {
const locationMap = new Map<string, MutationOverTimeDataMap<TemporalClass>>();
// Track all unique mutations and dates per location for backfilling
const locationMutations = new Map<string, Set<string>>();
const locationDates = new Map<string, Set<string>>();

// First pass: populate sparse data and track all keys
for (const row of data) {
if (!locationMap.has(row.location)) {
locationMap.set(row.location, new BaseMutationOverTimeDataMap<TemporalClass>());
locationMutations.set(row.location, new Set());
locationDates.set(row.location, new Set());
}
const map = locationMap.get(row.location)!;
const mutations = locationMutations.get(row.location)!;
const dates = locationDates.get(row.location)!;

const mutationFrequencies =
sequenceType === 'nucleotide' ? row.nucleotideMutationFrequency : row.aminoAcidMutationFrequency;
for (const mutation of mutationFrequencies) {
dates.add(row.date.dateString);
mutations.add(mutation.mutation.code);
map.set(
mutation.mutation,
row.date,
Expand All @@ -38,6 +49,21 @@ export function groupMutationDataByLocation(data: WastewaterData, sequenceType:
}
}

// Second pass: backfill missing cells with explicit null
for (const [location, map] of locationMap.entries()) {
const allMutations = Array.from(locationMutations.get(location)!);
const allDates = Array.from(locationDates.get(location)!).map((dateStr) => map.keysSecondAxis.get(dateStr)!);

for (const mutationCode of allMutations) {
const mutation = map.keysFirstAxis.get(mutationCode)!;
for (const date of allDates) {
if (map.get(mutation, date) === undefined) {
map.set(mutation, date, null);
}
}
}
}

return [...locationMap.entries()].map(([location, data]) => ({
location,
data: new SortedMap2d(
Expand Down