Skip to content
Open
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
29 changes: 29 additions & 0 deletions src/lib/data/byod.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { upsertSampleEntry } from '$src/lib/data/byod';
import type { Sample } from '$src/lib/data/objects/sample';
import { describe, expect, it } from 'vitest';

describe('upsertSampleEntry', () => {
const makeSample = (name: string, marker: string) => ({
name,
sample: { marker } as unknown as Sample
});

it('replaces existing sample entries by name', () => {
const remote = makeSample('demo', 'remote');
const local = makeSample('demo', 'local');

const updated = upsertSampleEntry([remote], local);
expect(updated).toHaveLength(1);
expect(updated[0]).not.toBe(remote);
expect(updated[0].sample).toBe(local.sample);
});

it('appends when no matching name exists', () => {
const first = makeSample('a', 'first');
const second = makeSample('b', 'second');

const result = upsertSampleEntry([first], second);
expect(result).toHaveLength(2);
expect(result[1]).toBe(second);
});
});
33 changes: 22 additions & 11 deletions src/lib/data/byod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ export async function byod() {
return processHandle(handle, true);
}

type NamedSample = { name: string; sample: Sample };

export function upsertSampleEntry(existing: NamedSample[], entry: NamedSample): NamedSample[] {
const idx = existing.findIndex((x) => x.name === entry.name);
if (idx === -1) return [...existing, entry];
const next = existing.slice();
next[idx] = entry;
return next;
}

async function processCSV(name: string, text: string) {
const res = (await fromCSV(text))?.data;
if (!res) {
Expand Down Expand Up @@ -71,7 +81,7 @@ async function processCSV(name: string, text: string) {
export async function processHandle(
handle: Promise<FileSystemDirectoryHandle | FileSystemFileHandle>,
setSample = true
) {
): Promise<'folder' | 'file' | undefined> {
const h = await handle;
if (h instanceof FileSystemFileHandle) {
const file = await h.getFile();
Expand All @@ -82,12 +92,12 @@ export async function processHandle(
proc = JSON.parse(text);
} catch (e) {
await processCSV(file.name, text);
return;
return 'file';
}

if (!proc) {
alert('Invalid JSON file');
return;
return 'file';
}

const map = get(sMapp);
Expand All @@ -101,22 +111,22 @@ export async function processHandle(
const roidata = proc;
console.log('Got roi feature data');
map.persistentLayers.rois.loadFeatures(roidata);
return;
return 'file';
}

if (valAnnFeatData(proc)) {
const annfeatdata = proc;
console.log('Got annotation feature data');
map.persistentLayers.annotations.loadFeatures(annfeatdata);
return;
return 'file';
}

alert('Validation error: ' + JSON.stringify(valROIData.errors));
return;
return 'file';
}

alert('Unknown file type.');
return;
return 'file';
}

return processFolder(h, setSample);
Expand All @@ -128,7 +138,7 @@ async function processFolder(handle: FileSystemDirectoryHandle, setSample = true
sp = (await readFile<SampleParams>(handle, 'sample.json', 'plain')) as SampleParams;
} catch (e) {
alert('Got folder but cannot find sample.json in the specified directory');
return;
return undefined;
}

const existing = get(samples);
Expand All @@ -139,10 +149,10 @@ async function processFolder(handle: FileSystemDirectoryHandle, setSample = true
existing.find((x) => x.name === sample.name) &&
!confirm(`Sample ${sample.name} already exists. Overwrite?`)
) {
return;
return undefined;
}
existing.push({ name: sample.name, sample });
samples.set(existing);
const updated = upsertSampleEntry(existing, { name: sample.name, sample });
samples.set(updated);
if (setSample) {
const curr = get(mapIdSample);
for (const id of Object.keys(curr)) {
Expand All @@ -151,4 +161,5 @@ async function processFolder(handle: FileSystemDirectoryHandle, setSample = true
mapIdSample.set(curr);
console.log('Set sample to', sample.name);
}
return 'folder';
}
26 changes: 26 additions & 0 deletions src/lib/data/objects/sample.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,30 @@ export class Sample extends Deferrable {
}
return featureList;
}

hasFeature(fn: FeatureAndGroup) {
const dataset = this.features[fn.group];
if (!dataset) return false;
const names = (dataset as { featNames?: string[] }).featNames;
if (!Array.isArray(names)) return false;
return names.includes(fn.feature);
}

async firstFeature(): Promise<FeatureAndGroup | undefined> {
const defaults = this.overlayParams?.defaults;
if (defaults) {
for (const entry of defaults) {
if (this.hasFeature(entry)) {
return entry;
}
}
}
const list = await this.genFeatureList();
for (const { group, features } of list) {
if (features?.length) {
return { group, feature: features[0] };
}
}
return undefined;
}
}
15 changes: 15 additions & 0 deletions src/lib/data/objects/test/sample.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,21 @@ describe('Sample', () => {
expect(sample.metadataMd).toEqual(sampleParams.metadataMd);
});

it('reports feature availability and returns the first available feature', async () => {
const sample = new Sample(sampleParams);
await sample.hydrate();
plainCsvInstances[0].featNames = ['geneA', 'geneB'];
chunkedCsvInstances[0].featNames = ['proteinX'];
chunkedCsvInstances[0].names = { proteinX: 0 };

expect(sample.hasFeature({ group: 'plain', feature: 'geneA' })).toBe(true);
expect(sample.hasFeature({ group: 'plain', feature: 'missing' })).toBe(false);
expect(sample.hasFeature({ group: 'chunked', feature: 'proteinX' })).toBe(true);

const first = await sample.firstFeature();
expect(first).toEqual({ group: 'plain', feature: 'geneA' });
});

it('hydrates image and features once', async () => {
const handle = { kind: 'dir' } as unknown as FileSystemDirectoryHandle;
const sample = new Sample(sampleParams, handle);
Expand Down
33 changes: 33 additions & 0 deletions src/lib/sidebar/recent.browser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { HoverSelect } from '$lib/sidebar/searchBox';
import { hoverSelect, sSample } from '$lib/store';
import Recent from '$src/lib/sidebar/recent.svelte';
import { tick } from 'svelte';
import { afterEach, describe, expect, test } from 'vitest';
import { render } from 'vitest-browser-svelte';

const makeFeature = (name: string) => ({ group: 'grp', feature: name });

afterEach(() => {
hoverSelect.set(new HoverSelect());
sSample.set(undefined as any);
});

describe('Recent sidebar', () => {
test('clears queue when sample changes', async () => {
const { getByText, container, unmount } = render(Recent);

sSample.set({ name: 'sample-a' } as any);
hoverSelect.set(new HoverSelect({ selected: makeFeature('geneA') }));
await tick();

expect(getByText('geneA')).toBeTruthy();

sSample.set({ name: 'sample-b' } as any);
await tick();

expect(container.textContent).not.toContain('geneA');
expect(container.textContent).toContain('No recent features (yet).');

unmount();
});
});
24 changes: 20 additions & 4 deletions src/lib/sidebar/recent.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,33 @@
<script lang="ts">
import { hoverSelect, setHoverSelect } from '$lib/store';
import { hoverSelect, setHoverSelect, sSample } from '$lib/store';
import { isEqual } from 'lodash-es';
import type { FeatureAndGroup } from '../data/objects/feature';
import HoverableFeature from './hoverableFeature.svelte';

export let maxLength = 6;

let queue = [] as FeatureAndGroup[];
let lastSampleName: string | undefined;
let skipNextSelection = false;

$: currentSampleName = $sSample?.name;
$: {
if (currentSampleName !== lastSampleName) {
const hadItems = queue.length > 0;
queue = [];
lastSampleName = currentSampleName;
skipNextSelection = hadItems;
}
}

$: if ($hoverSelect.selected && !queue.find((x) => isEqual($hoverSelect.selected, x))) {
queue.push($hoverSelect.selected);
if (queue.length > maxLength) queue.shift();
queue = queue;
if (skipNextSelection) {
skipNextSelection = false;
} else {
queue.push($hoverSelect.selected);
if (queue.length > maxLength) queue.shift();
queue = queue;
}
}
</script>

Expand Down
Loading
Loading