11/**
2- * Reactive data stores for the microscape amplicon pipeline visualization.
3- * Uses Svelte 5 runes ($state) with setter functions for cross-module access.
2+ * Reactive data stores for microscape visualization.
3+ *
4+ * Svelte 5 module-level $state can't be exported AND reassigned.
5+ * Solution: wrap all state in a single exported object whose
6+ * properties are mutated (not reassigned).
47 */
58
6- // ── Core data ───────────────────────────────────────────────────────────────
9+ export const store = $state ( {
10+ // Core data
11+ samples : [ ] ,
12+ asvs : [ ] ,
13+ counts : { data : [ ] , samples : [ ] , asvs : [ ] } ,
14+ network : { edges : [ ] } ,
15+ taxonomy : { } ,
716
8- export let samples = $state ( [ ] ) ;
9- export let asvs = $state ( [ ] ) ;
10- export let counts = $state ( [ ] ) ;
11- export let network = $state ( [ ] ) ;
12- export let taxonomy = $state ( { } ) ;
17+ // Selection
18+ selectedSample : null ,
19+ selectedAsv : null ,
1320
14- // ── Selection state ─────────────────────────────────────────────────────────
21+ // UI
22+ loading : true ,
23+ error : null ,
24+ } ) ;
1525
16- export let selectedSample = $state ( null ) ;
17- export let selectedAsv = $state ( null ) ;
18-
19- /** Setter functions for cross-module mutation */
20- export function setSelectedSample ( v ) { selectedSample = v ; }
21- export function setSelectedAsv ( v ) { selectedAsv = v ; }
22-
23- // ── UI state ────────────────────────────────────────────────────────────────
26+ /** Group colors as RGBA for regl-scatterplot */
27+ export const GROUP_COLORS = {
28+ prokaryote : [ 0.3 , 0.5 , 1.0 , 0.8 ] ,
29+ eukaryote : [ 1.0 , 0.3 , 0.3 , 0.8 ] ,
30+ chloroplast : [ 0.2 , 0.85 , 0.4 , 0.8 ] ,
31+ mitochondria : [ 0.2 , 0.9 , 0.9 , 0.8 ] ,
32+ unknown : [ 0.6 , 0.6 , 0.6 , 0.5 ] ,
33+ } ;
2434
25- export let loading = $state ( true ) ;
26- export let error = $state ( null ) ;
35+ /** Group colors as hex for UI */
36+ export const GROUP_HEX = {
37+ prokaryote : '#4d80ff' ,
38+ eukaryote : '#ff4d4d' ,
39+ chloroplast : '#33d966' ,
40+ mitochondria : '#33e6e6' ,
41+ unknown : '#999999' ,
42+ } ;
2743
2844// ── Data loading ────────────────────────────────────────────────────────────
2945
3046async function fetchJson ( url ) {
47+ // Try .gz first
48+ try {
49+ const gzRes = await fetch ( url + '.gz' ) ;
50+ if ( gzRes . ok ) {
51+ const buf = await gzRes . arrayBuffer ( ) ;
52+ const bytes = new Uint8Array ( buf ) ;
53+ if ( bytes [ 0 ] === 0x1f && bytes [ 1 ] === 0x8b ) {
54+ const ds = new DecompressionStream ( 'gzip' ) ;
55+ const reader = new Blob ( [ buf ] ) . stream ( ) . pipeThrough ( ds ) . getReader ( ) ;
56+ const chunks = [ ] ;
57+ while ( true ) {
58+ const { done, value } = await reader . read ( ) ;
59+ if ( done ) break ;
60+ chunks . push ( value ) ;
61+ }
62+ const combined = new Uint8Array ( chunks . reduce ( ( a , c ) => a + c . length , 0 ) ) ;
63+ let offset = 0 ;
64+ for ( const c of chunks ) { combined . set ( c , offset ) ; offset += c . length ; }
65+ return JSON . parse ( new TextDecoder ( ) . decode ( combined ) ) ;
66+ }
67+ return JSON . parse ( new TextDecoder ( ) . decode ( buf ) ) ;
68+ }
69+ } catch ( _ ) { /* fall through */ }
70+
3171 const res = await fetch ( url ) ;
32- if ( ! res . ok ) throw new Error ( `Failed to fetch ${ url } : ${ res . status } ` ) ;
72+ if ( ! res . ok ) throw new Error ( `${ res . status } ${ url } ` ) ;
3373 return res . json ( ) ;
3474}
3575
3676export async function loadData ( ) {
37- loading = true ;
38- error = null ;
39-
40- const files = [
41- { key : 'samples' , url : '/data/samples.json' } ,
42- { key : 'asvs' , url : '/data/asvs.json' } ,
43- { key : 'counts' , url : '/data/counts.json' } ,
44- { key : 'network' , url : '/data/network.json' } ,
45- { key : 'taxonomy' , url : '/data/taxonomy.json' } ,
46- ] ;
47-
48- const results = { } ;
49-
50- await Promise . all (
51- files . map ( async ( { key, url } ) => {
52- try {
53- // Try gzipped first
54- let gzUrl = url + '.gz' ;
55- let res = await fetch ( gzUrl ) ;
56- if ( res . ok ) {
57- // Check if it's actually gzipped
58- const buf = await res . arrayBuffer ( ) ;
59- const bytes = new Uint8Array ( buf ) ;
60- if ( bytes [ 0 ] === 0x1f && bytes [ 1 ] === 0x8b ) {
61- // Decompress
62- const ds = new DecompressionStream ( 'gzip' ) ;
63- const reader = new Blob ( [ buf ] ) . stream ( ) . pipeThrough ( ds ) . getReader ( ) ;
64- const chunks = [ ] ;
65- while ( true ) {
66- const { done, value } = await reader . read ( ) ;
67- if ( done ) break ;
68- chunks . push ( value ) ;
69- }
70- const text = new TextDecoder ( ) . decode (
71- new Uint8Array ( chunks . reduce ( ( acc , c ) => [ ...acc , ...c ] , [ ] ) )
72- ) ;
73- results [ key ] = JSON . parse ( text ) ;
74- } else {
75- // Not actually gzipped, parse as text
76- results [ key ] = JSON . parse ( new TextDecoder ( ) . decode ( buf ) ) ;
77- }
78- } else {
79- // Fall back to plain JSON
80- results [ key ] = await fetchJson ( url ) ;
81- }
82- } catch ( e ) {
83- console . warn ( `[microscape-viz] Could not load ${ url } :` , e . message ) ;
84- results [ key ] = key === 'taxonomy' ? { } : [ ] ;
85- }
86- } )
87- ) ;
88-
89- samples = results . samples || [ ] ;
90- asvs = results . asvs || [ ] ;
91- counts = results . counts || [ ] ;
92- network = results . network || [ ] ;
93- taxonomy = results . taxonomy || { } ;
77+ store . loading = true ;
78+ store . error = null ;
79+
80+ try {
81+ const [ samples , asvs , counts , network , taxonomy ] = await Promise . all ( [
82+ fetchJson ( '/data/samples.json' ) . catch ( ( ) => [ ] ) ,
83+ fetchJson ( '/data/asvs.json' ) . catch ( ( ) => [ ] ) ,
84+ fetchJson ( '/data/counts.json' ) . catch ( ( ) => ( { data : [ ] , samples : [ ] , asvs : [ ] } ) ) ,
85+ fetchJson ( '/data/network.json' ) . catch ( ( ) => ( { edges : [ ] } ) ) ,
86+ fetchJson ( '/data/taxonomy.json' ) . catch ( ( ) => ( { } ) ) ,
87+ ] ) ;
88+
89+ store . samples = samples ;
90+ store . asvs = asvs ;
91+ store . counts = counts ;
92+ store . network = network ;
93+ store . taxonomy = taxonomy ;
94+ } catch ( e ) {
95+ store . error = e . message ;
96+ console . error ( '[microscape] Data load failed:' , e ) ;
97+ }
9498
95- loading = false ;
99+ store . loading = false ;
96100}
97101
98- // ── Derived helpers ─────────────────────────────────────────────────────────
102+ // ── Helpers ──────── ─────────────────────────────────────────────────────────
99103
100104export function countsBySample ( ) {
101105 const map = new Map ( ) ;
102- if ( ! counts || ! counts . data ) return map ;
103- for ( const [ si , ai , count , prop ] of counts . data ) {
106+ const data = store . counts ?. data ;
107+ if ( ! data ) return map ;
108+ for ( const row of data ) {
109+ const [ si , ai , count , prop ] = row ;
104110 if ( ! map . has ( si ) ) map . set ( si , [ ] ) ;
105111 map . get ( si ) . push ( { asv_idx : ai , count, proportion : prop } ) ;
106112 }
@@ -109,28 +115,12 @@ export function countsBySample() {
109115
110116export function countsByAsv ( ) {
111117 const map = new Map ( ) ;
112- if ( ! counts || ! counts . data ) return map ;
113- for ( const [ si , ai , count , prop ] of counts . data ) {
118+ const data = store . counts ?. data ;
119+ if ( ! data ) return map ;
120+ for ( const row of data ) {
121+ const [ si , ai , count , prop ] = row ;
114122 if ( ! map . has ( ai ) ) map . set ( ai , [ ] ) ;
115123 map . get ( ai ) . push ( { sample_idx : si , count, proportion : prop } ) ;
116124 }
117125 return map ;
118126}
119-
120- /** Group colors as RGBA arrays for regl-scatterplot */
121- export const GROUP_COLORS = {
122- prokaryote : [ 0.3 , 0.5 , 1.0 , 0.8 ] ,
123- eukaryote : [ 1.0 , 0.3 , 0.3 , 0.8 ] ,
124- chloroplast : [ 0.2 , 0.85 , 0.4 , 0.8 ] ,
125- mitochondria : [ 0.2 , 0.9 , 0.9 , 0.8 ] ,
126- unknown : [ 0.6 , 0.6 , 0.6 , 0.5 ] ,
127- } ;
128-
129- /** Group colors as hex for UI elements */
130- export const GROUP_HEX = {
131- prokaryote : '#4d80ff' ,
132- eukaryote : '#ff4d4d' ,
133- chloroplast : '#33d966' ,
134- mitochondria : '#33e6e6' ,
135- unknown : '#999999' ,
136- } ;
0 commit comments