gramene-search 2.0.2 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ var $gXNCa$reacticonsai = require("react-icons/ai");
31
31
  var $gXNCa$gramenesearchvis = require("gramene-search-vis");
32
32
  var $gXNCa$numeral = require("numeral");
33
33
  var $gXNCa$firebaseauth = require("firebase/auth");
34
+ var $gXNCa$d3 = require("d3");
34
35
 
35
36
 
36
37
  function $parcel$defineInteropFlag(a) {
@@ -1420,12 +1421,6 @@ const $24971af0a229e0e3$var$grameneViews = {
1420
1421
  show: 'off',
1421
1422
  shouldScroll: false
1422
1423
  },
1423
- {
1424
- id: 'userLists',
1425
- name: 'User Gene Lists',
1426
- show: 'off',
1427
- shouldScroll: false
1428
- },
1429
1424
  {
1430
1425
  id: 'taxonomy',
1431
1426
  name: 'Taxonomic distribution',
@@ -1439,15 +1434,14 @@ const $24971af0a229e0e3$var$grameneViews = {
1439
1434
  shouldScroll: false
1440
1435
  },
1441
1436
  {
1442
- id: 'expression',
1443
- name: 'Gene expression',
1437
+ id: 'exprViz',
1438
+ name: 'Expression visualization',
1444
1439
  show: 'off',
1445
- shouldScroll: false,
1446
- desiredSamples: {}
1440
+ shouldScroll: false
1447
1441
  },
1448
1442
  {
1449
- id: 'attribs',
1450
- name: 'Gene attributes',
1443
+ id: 'userLists',
1444
+ name: 'User Gene Lists',
1451
1445
  show: 'off',
1452
1446
  shouldScroll: false
1453
1447
  },
@@ -1456,6 +1450,19 @@ const $24971af0a229e0e3$var$grameneViews = {
1456
1450
  name: 'Data exporter',
1457
1451
  show: 'off',
1458
1452
  shouldScroll: false
1453
+ },
1454
+ {
1455
+ id: 'expression',
1456
+ name: 'Gene expression',
1457
+ show: 'off',
1458
+ shouldScroll: false,
1459
+ desiredSamples: {}
1460
+ },
1461
+ {
1462
+ id: 'attribs',
1463
+ name: 'Gene attributes',
1464
+ show: 'off',
1465
+ shouldScroll: false
1459
1466
  }
1460
1467
  ],
1461
1468
  touched: {}
@@ -3831,6 +3838,574 @@ const $c921a0d2b34aadb6$var$ontologies = {
3831
3838
  var $c921a0d2b34aadb6$export$2e2bcd8739ae039 = $c921a0d2b34aadb6$var$ontologies;
3832
3839
 
3833
3840
 
3841
+
3842
+ // State shape:
3843
+ // {
3844
+ // pivot: { status, signature, data: { [taxon_id]: <gene count> }, error, requestId },
3845
+ // activeTaxon: <taxon_id|null>,
3846
+ // byTaxon: {
3847
+ // [taxon_id]: {
3848
+ // selectedFields: [<solr field name>...],
3849
+ // fieldsModalOpen: <bool>,
3850
+ // fetch: { status, offset, total, signature, requestId, error },
3851
+ // rows: [<doc>...]
3852
+ // }
3853
+ // }
3854
+ // }
3855
+ // Per-taxon study lists are derived in the view layer from expressionStudies.
3856
+ const $4f15cd8a7d970b18$var$PAGE_SIZE = 200;
3857
+ let $4f15cd8a7d970b18$var$pivotPendingId = 0;
3858
+ const $4f15cd8a7d970b18$var$fetchPending = {}; // per-taxon request id
3859
+ const $4f15cd8a7d970b18$var$attrsPending = {}; // per-taxon request id for available attrs
3860
+ const $4f15cd8a7d970b18$var$existencePending = {}; // per-taxon request id for field-existence check
3861
+ function $4f15cd8a7d970b18$var$pivotSignature(store) {
3862
+ const q = store.selectGrameneFiltersQueryString();
3863
+ const g = store.selectGrameneGenomes();
3864
+ const m = store.selectGrameneMaps() || {};
3865
+ const taxa = Object.keys(g.active || {}).filter((tid)=>m[tid] && !m[tid].hidden);
3866
+ return `${q}|${taxa.sort().join(',')}`;
3867
+ }
3868
+ function $4f15cd8a7d970b18$var$fetchSignature(store, taxon) {
3869
+ const q = store.selectGrameneFiltersQueryString();
3870
+ const ev = store.selectExprViz();
3871
+ const sel = ev.byTaxon[taxon] && ev.byTaxon[taxon].selectedFields || [];
3872
+ return `${q}|${taxon}|${sel.slice().sort().join(',')}`;
3873
+ }
3874
+ const $4f15cd8a7d970b18$var$ALWAYS_FL = [
3875
+ 'id',
3876
+ 'name',
3877
+ 'system_name',
3878
+ 'taxon_id'
3879
+ ];
3880
+ const $4f15cd8a7d970b18$var$exprViz = {
3881
+ name: 'exprViz',
3882
+ getReducer: ()=>{
3883
+ const initialState = {
3884
+ pivot: {
3885
+ status: 'idle',
3886
+ signature: null,
3887
+ data: {},
3888
+ error: null,
3889
+ requestId: 0
3890
+ },
3891
+ activeTaxon: null,
3892
+ byTaxon: {}
3893
+ };
3894
+ function ensureTaxon(state, taxon) {
3895
+ if (state.byTaxon[taxon]) return state.byTaxon[taxon];
3896
+ return {
3897
+ selectedFields: [],
3898
+ fieldsModalOpen: false,
3899
+ fetch: {
3900
+ status: 'idle',
3901
+ offset: 0,
3902
+ total: 0,
3903
+ signature: null,
3904
+ requestId: 0,
3905
+ error: null
3906
+ },
3907
+ rows: [],
3908
+ availableAttrs: null,
3909
+ attrsSignature: null,
3910
+ attrsRequestId: 0,
3911
+ fieldExistence: null,
3912
+ existenceStatus: 'idle',
3913
+ existenceSignature: null,
3914
+ existenceRequestId: 0
3915
+ };
3916
+ }
3917
+ return (state = initialState, { type: type, payload: payload })=>{
3918
+ switch(type){
3919
+ case 'EXPRVIZ_PIVOT_STARTED':
3920
+ return {
3921
+ ...state,
3922
+ pivot: {
3923
+ status: 'loading',
3924
+ signature: payload.signature,
3925
+ data: state.pivot.data,
3926
+ error: null,
3927
+ requestId: payload.requestId
3928
+ }
3929
+ };
3930
+ case 'EXPRVIZ_PIVOT_SUCCEEDED':
3931
+ if (payload.requestId !== state.pivot.requestId) return state;
3932
+ return {
3933
+ ...state,
3934
+ pivot: {
3935
+ status: 'ready',
3936
+ signature: state.pivot.signature,
3937
+ data: payload.data,
3938
+ error: null,
3939
+ requestId: payload.requestId
3940
+ }
3941
+ };
3942
+ case 'EXPRVIZ_PIVOT_FAILED':
3943
+ if (payload.requestId !== state.pivot.requestId) return state;
3944
+ return {
3945
+ ...state,
3946
+ pivot: {
3947
+ ...state.pivot,
3948
+ status: 'error',
3949
+ error: payload.error
3950
+ }
3951
+ };
3952
+ case 'EXPRVIZ_ACTIVE_TAXON_SET':
3953
+ return {
3954
+ ...state,
3955
+ activeTaxon: payload
3956
+ };
3957
+ case 'EXPRVIZ_FIELDS_MODAL_TOGGLED':
3958
+ {
3959
+ const t = ensureTaxon(state, payload.taxon);
3960
+ return {
3961
+ ...state,
3962
+ byTaxon: {
3963
+ ...state.byTaxon,
3964
+ [payload.taxon]: {
3965
+ ...t,
3966
+ fieldsModalOpen: payload.open
3967
+ }
3968
+ }
3969
+ };
3970
+ }
3971
+ case 'EXPRVIZ_FIELDS_SET':
3972
+ {
3973
+ const t = ensureTaxon(state, payload.taxon);
3974
+ return {
3975
+ ...state,
3976
+ byTaxon: {
3977
+ ...state.byTaxon,
3978
+ [payload.taxon]: {
3979
+ ...t,
3980
+ selectedFields: payload.fields,
3981
+ rows: [],
3982
+ fetch: {
3983
+ status: 'idle',
3984
+ offset: 0,
3985
+ total: 0,
3986
+ signature: null,
3987
+ requestId: 0,
3988
+ error: null
3989
+ }
3990
+ }
3991
+ }
3992
+ };
3993
+ }
3994
+ case 'EXPRVIZ_FIELDS_REORDERED':
3995
+ {
3996
+ const t = state.byTaxon[payload.taxon];
3997
+ if (!t) return state;
3998
+ // Same set of fields — preserve rows; only the column/axis order changes.
3999
+ const setA = new Set(t.selectedFields);
4000
+ const setB = new Set(payload.fields);
4001
+ if (setA.size !== setB.size || ![
4002
+ ...setA
4003
+ ].every((f)=>setB.has(f))) return state;
4004
+ return {
4005
+ ...state,
4006
+ byTaxon: {
4007
+ ...state.byTaxon,
4008
+ [payload.taxon]: {
4009
+ ...t,
4010
+ selectedFields: payload.fields
4011
+ }
4012
+ }
4013
+ };
4014
+ }
4015
+ case 'EXPRVIZ_FETCH_STARTED':
4016
+ {
4017
+ const t = ensureTaxon(state, payload.taxon);
4018
+ return {
4019
+ ...state,
4020
+ byTaxon: {
4021
+ ...state.byTaxon,
4022
+ [payload.taxon]: {
4023
+ ...t,
4024
+ fetch: {
4025
+ ...t.fetch,
4026
+ status: 'loading',
4027
+ signature: payload.signature,
4028
+ requestId: payload.requestId,
4029
+ error: null
4030
+ }
4031
+ }
4032
+ }
4033
+ };
4034
+ }
4035
+ case 'EXPRVIZ_FETCH_BATCH':
4036
+ {
4037
+ const t = state.byTaxon[payload.taxon];
4038
+ if (!t || payload.requestId !== t.fetch.requestId) return state;
4039
+ const rows = t.rows.concat(payload.docs);
4040
+ return {
4041
+ ...state,
4042
+ byTaxon: {
4043
+ ...state.byTaxon,
4044
+ [payload.taxon]: {
4045
+ ...t,
4046
+ rows: rows,
4047
+ fetch: {
4048
+ ...t.fetch,
4049
+ offset: rows.length,
4050
+ total: payload.total,
4051
+ status: rows.length >= payload.total ? 'done' : 'loading'
4052
+ }
4053
+ }
4054
+ }
4055
+ };
4056
+ }
4057
+ case 'EXPRVIZ_FETCH_FAILED':
4058
+ {
4059
+ const t = state.byTaxon[payload.taxon];
4060
+ if (!t || payload.requestId !== t.fetch.requestId) return state;
4061
+ return {
4062
+ ...state,
4063
+ byTaxon: {
4064
+ ...state.byTaxon,
4065
+ [payload.taxon]: {
4066
+ ...t,
4067
+ fetch: {
4068
+ ...t.fetch,
4069
+ status: 'error',
4070
+ error: payload.error
4071
+ }
4072
+ }
4073
+ }
4074
+ };
4075
+ }
4076
+ case 'EXPRVIZ_ATTRS_STARTED':
4077
+ {
4078
+ const t = ensureTaxon(state, payload.taxon);
4079
+ return {
4080
+ ...state,
4081
+ byTaxon: {
4082
+ ...state.byTaxon,
4083
+ [payload.taxon]: {
4084
+ ...t,
4085
+ attrsRequestId: payload.requestId,
4086
+ attrsSignature: payload.signature
4087
+ }
4088
+ }
4089
+ };
4090
+ }
4091
+ case 'EXPRVIZ_ATTRS_SUCCEEDED':
4092
+ {
4093
+ const t = state.byTaxon[payload.taxon];
4094
+ if (!t || payload.requestId !== t.attrsRequestId) return state;
4095
+ return {
4096
+ ...state,
4097
+ byTaxon: {
4098
+ ...state.byTaxon,
4099
+ [payload.taxon]: {
4100
+ ...t,
4101
+ availableAttrs: payload.attrs
4102
+ }
4103
+ }
4104
+ };
4105
+ }
4106
+ case 'EXPRVIZ_EXISTENCE_STARTED':
4107
+ {
4108
+ const t = ensureTaxon(state, payload.taxon);
4109
+ return {
4110
+ ...state,
4111
+ byTaxon: {
4112
+ ...state.byTaxon,
4113
+ [payload.taxon]: {
4114
+ ...t,
4115
+ existenceStatus: 'loading',
4116
+ existenceRequestId: payload.requestId,
4117
+ existenceSignature: payload.signature
4118
+ }
4119
+ }
4120
+ };
4121
+ }
4122
+ case 'EXPRVIZ_EXISTENCE_SUCCEEDED':
4123
+ {
4124
+ const t = state.byTaxon[payload.taxon];
4125
+ if (!t || payload.requestId !== t.existenceRequestId) return state;
4126
+ return {
4127
+ ...state,
4128
+ byTaxon: {
4129
+ ...state.byTaxon,
4130
+ [payload.taxon]: {
4131
+ ...t,
4132
+ existenceStatus: 'ready',
4133
+ fieldExistence: payload.counts
4134
+ }
4135
+ }
4136
+ };
4137
+ }
4138
+ case 'GRAMENE_SEARCH_CLEARED':
4139
+ {
4140
+ // Search context changed — invalidate pivot and any loaded rows.
4141
+ // Selected fields are kept so the user can re-run the load.
4142
+ const newByTaxon = {};
4143
+ for (const tid of Object.keys(state.byTaxon))newByTaxon[tid] = {
4144
+ ...state.byTaxon[tid],
4145
+ rows: [],
4146
+ fetch: {
4147
+ status: 'idle',
4148
+ offset: 0,
4149
+ total: 0,
4150
+ signature: null,
4151
+ requestId: 0,
4152
+ error: null
4153
+ },
4154
+ availableAttrs: null,
4155
+ attrsSignature: null,
4156
+ fieldExistence: null,
4157
+ existenceStatus: 'idle',
4158
+ existenceSignature: null
4159
+ };
4160
+ return {
4161
+ ...state,
4162
+ pivot: {
4163
+ status: 'idle',
4164
+ signature: null,
4165
+ data: {},
4166
+ error: null,
4167
+ requestId: 0
4168
+ },
4169
+ byTaxon: newByTaxon
4170
+ };
4171
+ }
4172
+ default:
4173
+ return state;
4174
+ }
4175
+ };
4176
+ },
4177
+ doFetchExprVizPivot: ()=>({ dispatch: dispatch, store: store })=>{
4178
+ const requestId = ++$4f15cd8a7d970b18$var$pivotPendingId;
4179
+ const signature = $4f15cd8a7d970b18$var$pivotSignature(store);
4180
+ dispatch({
4181
+ type: 'EXPRVIZ_PIVOT_STARTED',
4182
+ payload: {
4183
+ requestId: requestId,
4184
+ signature: signature
4185
+ }
4186
+ });
4187
+ const api = store.selectGrameneAPI();
4188
+ const q = store.selectGrameneFiltersQueryString();
4189
+ const g = store.selectGrameneGenomes();
4190
+ const m = store.selectGrameneMaps() || {};
4191
+ const taxa = Object.keys(g.active || {}).filter((tid)=>m[tid] && !m[tid].hidden);
4192
+ const fq = taxa.length ? `&fq=taxon_id:(${taxa.join(' OR ')})` : '';
4193
+ const facetField = "{!facet.limit='300' facet.mincount='1' key='taxon_id'}taxon_id";
4194
+ const url = `${api}/search?q=${q}${fq}&fq=expressed_in_gxa_attr_ss:*&rows=0&facet=true&facet.field=${facetField}`;
4195
+ fetch(url).then((r)=>r.json()).then((json)=>{
4196
+ const arr = json && json.facet_counts && json.facet_counts.facet_fields && json.facet_counts.facet_fields.taxon_id || [];
4197
+ const data = {};
4198
+ for(let i = 0; i < arr.length; i += 2)data[arr[i]] = arr[i + 1];
4199
+ if (Object.keys(data).length === 0) console.warn('[exprViz] no taxon_ids found with expression', {
4200
+ url: url,
4201
+ json: json
4202
+ });
4203
+ dispatch({
4204
+ type: 'EXPRVIZ_PIVOT_SUCCEEDED',
4205
+ payload: {
4206
+ requestId: requestId,
4207
+ data: data
4208
+ }
4209
+ });
4210
+ }).catch((err)=>{
4211
+ dispatch({
4212
+ type: 'EXPRVIZ_PIVOT_FAILED',
4213
+ payload: {
4214
+ requestId: requestId,
4215
+ error: String(err)
4216
+ }
4217
+ });
4218
+ });
4219
+ },
4220
+ doSetExprVizActiveTaxon: (tid)=>({ dispatch: dispatch })=>dispatch({
4221
+ type: 'EXPRVIZ_ACTIVE_TAXON_SET',
4222
+ payload: tid
4223
+ }),
4224
+ doToggleExprVizFieldsModal: (taxon, open)=>({ dispatch: dispatch, store: store })=>{
4225
+ dispatch({
4226
+ type: 'EXPRVIZ_FIELDS_MODAL_TOGGLED',
4227
+ payload: {
4228
+ taxon: taxon,
4229
+ open: !!open
4230
+ }
4231
+ });
4232
+ if (open) store.doFetchExprVizAvailableAttrs(taxon);
4233
+ },
4234
+ doFetchExprVizAvailableAttrs: (taxon)=>({ dispatch: dispatch, store: store })=>{
4235
+ const q = store.selectGrameneFiltersQueryString();
4236
+ const signature = `${q}|${taxon}`;
4237
+ const ev = store.selectExprViz();
4238
+ const t = ev.byTaxon[taxon];
4239
+ if (t && t.availableAttrs && t.attrsSignature === signature) return;
4240
+ const requestId = $4f15cd8a7d970b18$var$attrsPending[taxon] = ($4f15cd8a7d970b18$var$attrsPending[taxon] || 0) + 1;
4241
+ dispatch({
4242
+ type: 'EXPRVIZ_ATTRS_STARTED',
4243
+ payload: {
4244
+ taxon: taxon,
4245
+ requestId: requestId,
4246
+ signature: signature
4247
+ }
4248
+ });
4249
+ const api = store.selectGrameneAPI();
4250
+ const facetField = "{!facet.limit='2000' facet.mincount='1' key='attrs'}expressed_in_gxa_attr_ss";
4251
+ const url = `${api}/search?q=${q}&fq=taxon_id:${taxon}&fq=expressed_in_gxa_attr_ss:*&rows=0&facet=true&facet.field=${facetField}`;
4252
+ fetch(url).then((r)=>r.json()).then((json)=>{
4253
+ if (requestId !== $4f15cd8a7d970b18$var$attrsPending[taxon]) return;
4254
+ const arr = json && json.facet_counts && json.facet_counts.facet_fields && json.facet_counts.facet_fields.attrs || [];
4255
+ const attrs = [];
4256
+ for(let i = 0; i < arr.length; i += 2)attrs.push(arr[i]);
4257
+ dispatch({
4258
+ type: 'EXPRVIZ_ATTRS_SUCCEEDED',
4259
+ payload: {
4260
+ taxon: taxon,
4261
+ requestId: requestId,
4262
+ attrs: attrs
4263
+ }
4264
+ });
4265
+ }).catch(()=>{});
4266
+ },
4267
+ doSetExprVizFields: (taxon, fields)=>({ dispatch: dispatch })=>dispatch({
4268
+ type: 'EXPRVIZ_FIELDS_SET',
4269
+ payload: {
4270
+ taxon: taxon,
4271
+ fields: fields
4272
+ }
4273
+ }),
4274
+ doReorderExprVizFields: (taxon, fields)=>({ dispatch: dispatch })=>dispatch({
4275
+ type: 'EXPRVIZ_FIELDS_REORDERED',
4276
+ payload: {
4277
+ taxon: taxon,
4278
+ fields: fields
4279
+ }
4280
+ }),
4281
+ doFetchExprVizData: (taxon)=>({ dispatch: dispatch, store: store })=>{
4282
+ const ev = store.selectExprViz();
4283
+ const t = ev.byTaxon[taxon];
4284
+ if (!t || t.selectedFields.length === 0) return;
4285
+ const requestId = $4f15cd8a7d970b18$var$fetchPending[taxon] = ($4f15cd8a7d970b18$var$fetchPending[taxon] || 0) + 1;
4286
+ const signature = $4f15cd8a7d970b18$var$fetchSignature(store, taxon);
4287
+ dispatch({
4288
+ type: 'EXPRVIZ_FETCH_STARTED',
4289
+ payload: {
4290
+ taxon: taxon,
4291
+ requestId: requestId,
4292
+ signature: signature
4293
+ }
4294
+ });
4295
+ const api = store.selectGrameneAPI();
4296
+ const q = store.selectGrameneFiltersQueryString();
4297
+ const fl = [
4298
+ ...new Set([
4299
+ ...$4f15cd8a7d970b18$var$ALWAYS_FL,
4300
+ ...t.selectedFields
4301
+ ])
4302
+ ].join(',');
4303
+ const fetchPage = (offset)=>{
4304
+ if (requestId !== $4f15cd8a7d970b18$var$fetchPending[taxon]) return; // superseded
4305
+ const url = `${api}/search?q=${q}&fq=taxon_id:${taxon}&fl=${fl}&rows=${$4f15cd8a7d970b18$var$PAGE_SIZE}&start=${offset}`;
4306
+ fetch(url).then((r)=>r.json()).then((json)=>{
4307
+ if (requestId !== $4f15cd8a7d970b18$var$fetchPending[taxon]) return;
4308
+ const docs = json.response && json.response.docs || [];
4309
+ const total = json.response && json.response.numFound || 0;
4310
+ dispatch({
4311
+ type: 'EXPRVIZ_FETCH_BATCH',
4312
+ payload: {
4313
+ taxon: taxon,
4314
+ requestId: requestId,
4315
+ docs: docs,
4316
+ total: total
4317
+ }
4318
+ });
4319
+ const next = offset + docs.length;
4320
+ if (docs.length > 0 && next < total) fetchPage(next);
4321
+ }).catch((err)=>{
4322
+ dispatch({
4323
+ type: 'EXPRVIZ_FETCH_FAILED',
4324
+ payload: {
4325
+ taxon: taxon,
4326
+ requestId: requestId,
4327
+ error: String(err)
4328
+ }
4329
+ });
4330
+ });
4331
+ };
4332
+ fetchPage(0);
4333
+ },
4334
+ // Per-candidate-field non-null counts under the current q + taxon. Used by
4335
+ // FieldsModal to hide fields that have no data in the current result set
4336
+ // before the user picks. Uses stats.field (the swagger whitelist drops
4337
+ // facet.query) batched across multiple GETs to stay under URL limits.
4338
+ doFetchExprVizFieldExistence: (taxon, fields)=>({ dispatch: dispatch, store: store })=>{
4339
+ if (!fields || fields.length === 0) return;
4340
+ const q = store.selectGrameneFiltersQueryString();
4341
+ const fieldsKey = fields.slice().sort().join(',');
4342
+ const signature = `${q}|${taxon}|${fieldsKey}`;
4343
+ const ev = store.selectExprViz();
4344
+ const t = ev.byTaxon[taxon];
4345
+ if (t && t.fieldExistence && t.existenceSignature === signature) return;
4346
+ const requestId = $4f15cd8a7d970b18$var$existencePending[taxon] = ($4f15cd8a7d970b18$var$existencePending[taxon] || 0) + 1;
4347
+ dispatch({
4348
+ type: 'EXPRVIZ_EXISTENCE_STARTED',
4349
+ payload: {
4350
+ taxon: taxon,
4351
+ requestId: requestId,
4352
+ signature: signature
4353
+ }
4354
+ });
4355
+ const api = store.selectGrameneAPI();
4356
+ const BATCH_SIZE = 40;
4357
+ const batches = [];
4358
+ for(let i = 0; i < fields.length; i += BATCH_SIZE)batches.push(fields.slice(i, i + BATCH_SIZE));
4359
+ Promise.all(batches.map((batch)=>{
4360
+ const params = new URLSearchParams();
4361
+ params.append('q', q);
4362
+ params.append('fq', `taxon_id:${taxon}`);
4363
+ params.append('rows', '0');
4364
+ params.append('stats', 'true');
4365
+ batch.forEach((f)=>params.append('stats.field', f));
4366
+ return fetch(`${api}/search?${params.toString()}`).then((r)=>r.json()).then((json)=>{
4367
+ const sf = json && json.stats && json.stats.stats_fields || {};
4368
+ const out = {};
4369
+ for (const f of Object.keys(sf)){
4370
+ const s = sf[f];
4371
+ out[f] = s && typeof s.count === 'number' ? s.count : 0;
4372
+ }
4373
+ return out;
4374
+ });
4375
+ })).then((results)=>{
4376
+ if (requestId !== $4f15cd8a7d970b18$var$existencePending[taxon]) return;
4377
+ const counts = Object.assign({}, ...results);
4378
+ dispatch({
4379
+ type: 'EXPRVIZ_EXISTENCE_SUCCEEDED',
4380
+ payload: {
4381
+ taxon: taxon,
4382
+ requestId: requestId,
4383
+ counts: counts
4384
+ }
4385
+ });
4386
+ }).catch(()=>{});
4387
+ },
4388
+ reactExprVizPivot: (0, $gXNCa$reduxbundler.createSelector)('selectExprViz', 'selectGrameneFiltersStatus', 'selectGrameneViews', 'selectGrameneFiltersQueryString', 'selectGrameneGenomes', 'selectGrameneMaps', (ev, filtersStatus, views, q, g, m)=>{
4389
+ if (!ev || filtersStatus === 'init') return;
4390
+ const onView = views && views.options && views.options.find((v)=>v.id === 'exprViz');
4391
+ if (!onView || onView.show !== 'on') return;
4392
+ const maps = m || {};
4393
+ const taxa = Object.keys(g && g.active || {}).filter((tid)=>maps[tid] && !maps[tid].hidden);
4394
+ const sig = `${q}|${taxa.sort().join(',')}`;
4395
+ if (ev.pivot.status === 'loading') return;
4396
+ if (ev.pivot.signature === sig && ev.pivot.status === 'ready') return;
4397
+ return {
4398
+ actionCreator: 'doFetchExprVizPivot'
4399
+ };
4400
+ }),
4401
+ selectExprViz: (state)=>state.exprViz,
4402
+ selectExprVizPivot: (state)=>state.exprViz.pivot,
4403
+ selectExprVizActiveTaxon: (state)=>state.exprViz.activeTaxon,
4404
+ selectExprVizByTaxon: (state)=>state.exprViz.byTaxon
4405
+ };
4406
+ var $4f15cd8a7d970b18$export$2e2bcd8739ae039 = $4f15cd8a7d970b18$var$exprViz;
4407
+
4408
+
3834
4409
  var $5df6c55c1bef3469$export$2e2bcd8739ae039 = [
3835
4410
  ...(0, $9d9aeaf9299e61a1$export$2e2bcd8739ae039),
3836
4411
  (0, $671312b287158a8a$export$2e2bcd8739ae039),
@@ -3839,7 +4414,8 @@ var $5df6c55c1bef3469$export$2e2bcd8739ae039 = [
3839
4414
  (0, $0d54502f6cafe273$export$2e2bcd8739ae039),
3840
4415
  (0, $0f839422d0d8c772$export$2e2bcd8739ae039),
3841
4416
  (0, $1508f5a42be6e7b5$export$2e2bcd8739ae039),
3842
- (0, $c921a0d2b34aadb6$export$2e2bcd8739ae039)
4417
+ (0, $c921a0d2b34aadb6$export$2e2bcd8739ae039),
4418
+ (0, $4f15cd8a7d970b18$export$2e2bcd8739ae039)
3843
4419
  ];
3844
4420
 
3845
4421
 
@@ -10203,6 +10779,1438 @@ var $37b3bb0145d266b0$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.conn
10203
10779
 
10204
10780
 
10205
10781
 
10782
+
10783
+ function $4d0c2e01f58b53b1$var$speciesTaxonId(tid) {
10784
+ const n = +tid;
10785
+ return n > 1000000 ? Math.floor(n / 1000) : n;
10786
+ }
10787
+ const $4d0c2e01f58b53b1$var$EXPERIMENT_KEYS = [
10788
+ {
10789
+ key: 'exp:study',
10790
+ label: 'Study'
10791
+ },
10792
+ {
10793
+ key: 'exp:type',
10794
+ label: 'Study type'
10795
+ },
10796
+ {
10797
+ key: 'exp:organism',
10798
+ label: 'Organism'
10799
+ }
10800
+ ];
10801
+ // One record per expression field (one sample group within one study).
10802
+ // `props` is a flat map of property-key → value used for filtering and display.
10803
+ function $4d0c2e01f58b53b1$var$buildFieldRecords(taxon, studyIds, expressionStudies, expressionSamples, fieldCatalog, grameneMaps) {
10804
+ const records = [];
10805
+ const factorTypes = new Set();
10806
+ const charTypes = new Set();
10807
+ if (!expressionSamples || !expressionStudies) return {
10808
+ records: records,
10809
+ factorTypes: [],
10810
+ charTypes: []
10811
+ };
10812
+ const studyById = {};
10813
+ const list = expressionStudies[taxon] || expressionStudies[$4d0c2e01f58b53b1$var$speciesTaxonId(taxon)] || [];
10814
+ list.forEach((s)=>{
10815
+ studyById[s._id] = s;
10816
+ });
10817
+ for (const studyId of studyIds){
10818
+ const study = studyById[studyId];
10819
+ if (!study) continue;
10820
+ const samples = expressionSamples[studyId];
10821
+ if (!samples) continue;
10822
+ const byGroup = {};
10823
+ for (const s of samples)if (!byGroup[s.group]) byGroup[s.group] = s;
10824
+ for (const group of Object.keys(byGroup)){
10825
+ const sample = byGroup[group];
10826
+ const fieldName = `${studyId.replace(/-/g, '_')}_${group}__expr`;
10827
+ if (fieldCatalog && fieldCatalog.fields && !fieldCatalog.fields[fieldName]) continue;
10828
+ const props = {};
10829
+ props['exp:study'] = study.description || studyId;
10830
+ props['exp:type'] = study.type || '(unknown)';
10831
+ const taxon_id = study.taxon_id;
10832
+ const taxName = grameneMaps && (grameneMaps[taxon_id] || grameneMaps[$4d0c2e01f58b53b1$var$speciesTaxonId(taxon_id)]);
10833
+ props['exp:organism'] = taxName && taxName.display_name || String(taxon_id);
10834
+ const usedFactorTypes = new Set();
10835
+ (sample.factor || []).forEach((f)=>{
10836
+ props[`fac:${f.type}`] = f.label;
10837
+ factorTypes.add(f.type);
10838
+ usedFactorTypes.add(f.type);
10839
+ });
10840
+ (sample.characteristic || []).forEach((c)=>{
10841
+ if (usedFactorTypes.has(c.type)) return; // factor takes precedence
10842
+ props[`char:${c.type}`] = c.label;
10843
+ charTypes.add(c.type);
10844
+ });
10845
+ records.push({
10846
+ fieldName: fieldName,
10847
+ studyId: studyId,
10848
+ group: group,
10849
+ props: props
10850
+ });
10851
+ }
10852
+ }
10853
+ return {
10854
+ records: records,
10855
+ factorTypes: Array.from(factorTypes).sort(),
10856
+ charTypes: Array.from(charTypes).sort()
10857
+ };
10858
+ }
10859
+ function $4d0c2e01f58b53b1$var$fieldMatches(field, selections, excludeKey) {
10860
+ for (const key of Object.keys(selections)){
10861
+ if (key === excludeKey) continue;
10862
+ const values = selections[key];
10863
+ if (!values || values.size === 0) continue;
10864
+ const v = field.props[key];
10865
+ if (v == null || !values.has(v)) return false;
10866
+ }
10867
+ return true;
10868
+ }
10869
+ function $4d0c2e01f58b53b1$var$valueCounts(records, key, selections) {
10870
+ const counts = new Map();
10871
+ for (const f of records){
10872
+ if (!$4d0c2e01f58b53b1$var$fieldMatches(f, selections, key)) continue;
10873
+ const v = f.props[key];
10874
+ if (v == null) continue;
10875
+ counts.set(v, (counts.get(v) || 0) + 1);
10876
+ }
10877
+ return counts;
10878
+ }
10879
+ function $4d0c2e01f58b53b1$var$labelForKey(key, tree) {
10880
+ for (const grp of tree){
10881
+ for (const t of grp.types)if (t.key === key) return t.label;
10882
+ }
10883
+ return key;
10884
+ }
10885
+ const $4d0c2e01f58b53b1$var$FieldsModalCmp = (props)=>{
10886
+ const { fieldCatalog: fieldCatalog, exprViz: exprViz, expressionStudies: expressionStudies, expressionSamples: expressionSamples, grameneMaps: grameneMaps, doToggleExprVizFieldsModal: doToggleExprVizFieldsModal, doSetExprVizFields: doSetExprVizFields, doFetchExprVizFieldExistence: doFetchExprVizFieldExistence } = props;
10887
+ const taxon = Object.keys(exprViz.byTaxon).find((t)=>exprViz.byTaxon[t].fieldsModalOpen);
10888
+ const open = !!taxon;
10889
+ const availableAttrs = taxon && exprViz.byTaxon[taxon] && exprViz.byTaxon[taxon].availableAttrs;
10890
+ const studyIds = (0, $gXNCa$react.useMemo)(()=>{
10891
+ if (!taxon || !expressionStudies) return [];
10892
+ const list = expressionStudies[taxon] || expressionStudies[$4d0c2e01f58b53b1$var$speciesTaxonId(taxon)] || [];
10893
+ let ids = list.map((s)=>s._id);
10894
+ if (availableAttrs) {
10895
+ const allow = new Set(availableAttrs);
10896
+ ids = ids.filter((id)=>allow.has(id));
10897
+ }
10898
+ return ids;
10899
+ }, [
10900
+ taxon,
10901
+ expressionStudies,
10902
+ availableAttrs
10903
+ ]);
10904
+ const allRecords = (0, $gXNCa$react.useMemo)(()=>$4d0c2e01f58b53b1$var$buildFieldRecords(taxon, studyIds, expressionStudies, expressionSamples, fieldCatalog, grameneMaps), [
10905
+ taxon,
10906
+ studyIds,
10907
+ expressionStudies,
10908
+ expressionSamples,
10909
+ fieldCatalog,
10910
+ grameneMaps
10911
+ ]);
10912
+ const candidateFields = (0, $gXNCa$react.useMemo)(()=>allRecords.records.map((r)=>r.fieldName), [
10913
+ allRecords
10914
+ ]);
10915
+ // Trigger the field-existence facet check whenever the modal is open and
10916
+ // the candidate field list is known. The bundle caches per (q, taxon, fields).
10917
+ (0, $gXNCa$react.useEffect)(()=>{
10918
+ if (open && taxon && candidateFields.length > 0) doFetchExprVizFieldExistence(taxon, candidateFields);
10919
+ }, [
10920
+ open,
10921
+ taxon,
10922
+ candidateFields,
10923
+ doFetchExprVizFieldExistence
10924
+ ]);
10925
+ const fieldExistence = taxon && exprViz.byTaxon[taxon] && exprViz.byTaxon[taxon].fieldExistence;
10926
+ const existenceStatus = taxon && exprViz.byTaxon[taxon] && exprViz.byTaxon[taxon].existenceStatus;
10927
+ // Once existence counts are loaded, drop fields with no data in the current
10928
+ // result set. Until then, render with the full candidate set so the modal
10929
+ // doesn't flash empty during the precheck.
10930
+ const { records: records, factorTypes: factorTypes, charTypes: charTypes } = (0, $gXNCa$react.useMemo)(()=>{
10931
+ if (!fieldExistence) return allRecords;
10932
+ const live = new Set(Object.keys(fieldExistence).filter((f)=>fieldExistence[f] > 0));
10933
+ const filtered = allRecords.records.filter((r)=>live.has(r.fieldName));
10934
+ const factorTypes = new Set();
10935
+ const charTypes = new Set();
10936
+ for (const r of filtered)for (const k of Object.keys(r.props)){
10937
+ if (k.startsWith('fac:')) factorTypes.add(k.slice(4));
10938
+ else if (k.startsWith('char:')) charTypes.add(k.slice(5));
10939
+ }
10940
+ return {
10941
+ records: filtered,
10942
+ factorTypes: Array.from(factorTypes).sort(),
10943
+ charTypes: Array.from(charTypes).sort()
10944
+ };
10945
+ }, [
10946
+ allRecords,
10947
+ fieldExistence
10948
+ ]);
10949
+ const propTree = (0, $gXNCa$react.useMemo)(()=>[
10950
+ {
10951
+ group: 'experiment',
10952
+ label: 'Experiment',
10953
+ types: $4d0c2e01f58b53b1$var$EXPERIMENT_KEYS
10954
+ },
10955
+ {
10956
+ group: 'factors',
10957
+ label: 'Factors',
10958
+ types: factorTypes.map((t)=>({
10959
+ key: `fac:${t}`,
10960
+ label: t
10961
+ }))
10962
+ },
10963
+ {
10964
+ group: 'characteristics',
10965
+ label: 'Characteristics',
10966
+ types: charTypes.map((t)=>({
10967
+ key: `char:${t}`,
10968
+ label: t
10969
+ }))
10970
+ }
10971
+ ], [
10972
+ factorTypes,
10973
+ charTypes
10974
+ ]);
10975
+ const [selections, setSelections] = (0, $gXNCa$react.useState)({});
10976
+ const [expandedKey, setExpandedKey] = (0, $gXNCa$react.useState)(null);
10977
+ const [collapsedGroups, setCollapsedGroups] = (0, $gXNCa$react.useState)({});
10978
+ const [valueSort, setValueSort] = (0, $gXNCa$react.useState)('count');
10979
+ const [orderedSelectedKeys, setOrderedSelectedKeys] = (0, $gXNCa$react.useState)([]);
10980
+ const [searchQuery, setSearchQuery] = (0, $gXNCa$react.useState)('');
10981
+ const searchLc = searchQuery.trim().toLowerCase();
10982
+ const isSearching = searchLc.length > 0;
10983
+ // Distinct values per property type, ignoring the type's own selection but
10984
+ // applying every other selected type. Counts shown next to a property type
10985
+ // reflect "how many distinct values exist among the fields currently
10986
+ // matching the rest of the filter."
10987
+ const valueSetByKey = (0, $gXNCa$react.useMemo)(()=>{
10988
+ const allKeys = new Set();
10989
+ for (const r of records)for (const k of Object.keys(r.props))allKeys.add(k);
10990
+ const m = {};
10991
+ for (const key of allKeys){
10992
+ const set = new Set();
10993
+ for (const r of records){
10994
+ if (!$4d0c2e01f58b53b1$var$fieldMatches(r, selections, key)) continue;
10995
+ const v = r.props[key];
10996
+ if (v != null) set.add(v);
10997
+ }
10998
+ m[key] = set;
10999
+ }
11000
+ return m;
11001
+ }, [
11002
+ records,
11003
+ selections
11004
+ ]);
11005
+ // Reset state when modal (re)opens for a taxon.
11006
+ (0, $gXNCa$react.useEffect)(()=>{
11007
+ if (open && taxon) {
11008
+ setSelections({});
11009
+ setExpandedKey(null);
11010
+ setCollapsedGroups({});
11011
+ setOrderedSelectedKeys([]);
11012
+ setSearchQuery('');
11013
+ }
11014
+ }, [
11015
+ open,
11016
+ taxon
11017
+ ]);
11018
+ // Keep orderedSelectedKeys in sync with selections (preserves the order in
11019
+ // which the user first selected each property type).
11020
+ (0, $gXNCa$react.useEffect)(()=>{
11021
+ setOrderedSelectedKeys((prev)=>{
11022
+ const active = Object.keys(selections);
11023
+ const activeSet = new Set(active);
11024
+ const kept = prev.filter((k)=>activeSet.has(k));
11025
+ const newOnes = active.filter((k)=>!prev.includes(k));
11026
+ return [
11027
+ ...kept,
11028
+ ...newOnes
11029
+ ];
11030
+ });
11031
+ }, [
11032
+ selections
11033
+ ]);
11034
+ const matchingFields = (0, $gXNCa$react.useMemo)(()=>records.filter((r)=>$4d0c2e01f58b53b1$var$fieldMatches(r, selections, null)), [
11035
+ records,
11036
+ selections
11037
+ ]);
11038
+ if (!open) return null;
11039
+ const toggleValue = (key, value)=>{
11040
+ setSelections((prev)=>{
11041
+ const next = {
11042
+ ...prev
11043
+ };
11044
+ const set = new Set(next[key] || []);
11045
+ if (set.has(value)) set.delete(value);
11046
+ else set.add(value);
11047
+ if (set.size === 0) delete next[key];
11048
+ else next[key] = set;
11049
+ return next;
11050
+ });
11051
+ };
11052
+ const clearTypeSelection = (key)=>{
11053
+ setSelections((prev)=>{
11054
+ if (!prev[key]) return prev;
11055
+ const next = {
11056
+ ...prev
11057
+ };
11058
+ delete next[key];
11059
+ return next;
11060
+ });
11061
+ };
11062
+ const toggleGroup = (g)=>setCollapsedGroups((prev)=>({
11063
+ ...prev,
11064
+ [g]: !prev[g]
11065
+ }));
11066
+ const renderValues = (key, typeLabelMatches)=>{
11067
+ const counts = $4d0c2e01f58b53b1$var$valueCounts(records, key, selections);
11068
+ let entries = Array.from(counts.entries());
11069
+ if (isSearching && !typeLabelMatches) entries = entries.filter(([v])=>String(v).toLowerCase().includes(searchLc));
11070
+ if (valueSort === 'name') entries.sort((a, b)=>String(a[0]).localeCompare(String(b[0])));
11071
+ else entries.sort((a, b)=>b[1] - a[1] || String(a[0]).localeCompare(String(b[0])));
11072
+ const sel = selections[key] || new Set();
11073
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11074
+ className: "exprviz-tree-values",
11075
+ children: [
11076
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11077
+ className: "exprviz-values-toolbar",
11078
+ children: [
11079
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.ToggleButtonGroup), {
11080
+ type: "radio",
11081
+ name: `exprviz-vsort-${key}`,
11082
+ size: "sm",
11083
+ value: valueSort,
11084
+ onChange: setValueSort,
11085
+ children: [
11086
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.ToggleButton), {
11087
+ id: `exprviz-vsort-name-${key}`,
11088
+ value: "name",
11089
+ variant: "outline-secondary",
11090
+ children: "Name"
11091
+ }),
11092
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.ToggleButton), {
11093
+ id: `exprviz-vsort-count-${key}`,
11094
+ value: "count",
11095
+ variant: "outline-secondary",
11096
+ children: "Count"
11097
+ })
11098
+ ]
11099
+ }),
11100
+ sel.size > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
11101
+ size: "sm",
11102
+ variant: "link",
11103
+ onClick: ()=>clearTypeSelection(key),
11104
+ children: "clear"
11105
+ })
11106
+ ]
11107
+ }),
11108
+ entries.map(([v, c])=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("label", {
11109
+ className: "exprviz-tree-value",
11110
+ children: [
11111
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("input", {
11112
+ type: "checkbox",
11113
+ checked: sel.has(v),
11114
+ onChange: ()=>toggleValue(key, v)
11115
+ }),
11116
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11117
+ className: "exprviz-tree-value-label",
11118
+ title: v,
11119
+ children: v
11120
+ }),
11121
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11122
+ className: "exprviz-tree-value-count",
11123
+ children: c
11124
+ })
11125
+ ]
11126
+ }, v)),
11127
+ entries.length === 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11128
+ className: "exprviz-tree-empty",
11129
+ children: "No values"
11130
+ })
11131
+ ]
11132
+ });
11133
+ };
11134
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Modal), {
11135
+ show: open,
11136
+ onHide: ()=>doToggleExprVizFieldsModal(taxon, false),
11137
+ size: "xl",
11138
+ scrollable: true,
11139
+ children: [
11140
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Modal).Header, {
11141
+ closeButton: true,
11142
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Modal).Title, {
11143
+ children: [
11144
+ "Filter expression fields (",
11145
+ matchingFields.length,
11146
+ " of ",
11147
+ records.length,
11148
+ " match)",
11149
+ existenceStatus === 'loading' && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11150
+ className: "exprviz-modal-loading",
11151
+ children: " \xb7 checking field availability\u2026"
11152
+ })
11153
+ ]
11154
+ })
11155
+ }),
11156
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Modal).Body, {
11157
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11158
+ className: "exprviz-fields-layout",
11159
+ children: [
11160
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11161
+ className: "exprviz-tree",
11162
+ children: [
11163
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("input", {
11164
+ type: "text",
11165
+ className: "exprviz-tree-search",
11166
+ placeholder: "Search property types and values\u2026",
11167
+ value: searchQuery,
11168
+ onChange: (e)=>setSearchQuery(e.target.value)
11169
+ }),
11170
+ propTree.map((grp)=>{
11171
+ const typeRows = grp.types.map((t)=>{
11172
+ const numValues = valueSetByKey[t.key] && valueSetByKey[t.key].size || 0;
11173
+ if (numValues === 0) return null;
11174
+ const typeLabelMatches = isSearching && t.label.toLowerCase().includes(searchLc);
11175
+ let matchingValueCount = numValues;
11176
+ if (isSearching && !typeLabelMatches) {
11177
+ const vset = valueSetByKey[t.key] || new Set();
11178
+ matchingValueCount = 0;
11179
+ for (const v of vset)if (String(v).toLowerCase().includes(searchLc)) matchingValueCount++;
11180
+ if (matchingValueCount === 0) return null;
11181
+ }
11182
+ const sel = selections[t.key];
11183
+ const selCount = sel ? sel.size : 0;
11184
+ const isExpanded = isSearching ? !typeLabelMatches : expandedKey === t.key;
11185
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11186
+ className: `exprviz-tree-type${selCount > 0 ? ' is-active' : ''}`,
11187
+ children: [
11188
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11189
+ className: "exprviz-tree-type-header",
11190
+ onClick: ()=>setExpandedKey(isExpanded ? null : t.key),
11191
+ children: [
11192
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11193
+ className: "exprviz-tree-caret",
11194
+ children: isExpanded ? "\u25BE" : "\u25B8"
11195
+ }),
11196
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11197
+ className: "exprviz-tree-type-label",
11198
+ children: t.label
11199
+ }),
11200
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11201
+ className: "exprviz-tree-type-count",
11202
+ children: selCount > 0 ? `${selCount}/${numValues}` : numValues
11203
+ })
11204
+ ]
11205
+ }),
11206
+ isExpanded && renderValues(t.key, typeLabelMatches)
11207
+ ]
11208
+ }, t.key);
11209
+ }).filter(Boolean);
11210
+ if (isSearching && typeRows.length === 0) return null;
11211
+ const groupCollapsed = !isSearching && collapsedGroups[grp.group];
11212
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11213
+ className: "exprviz-tree-group",
11214
+ children: [
11215
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11216
+ className: "exprviz-tree-group-header",
11217
+ onClick: ()=>toggleGroup(grp.group),
11218
+ children: [
11219
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11220
+ className: "exprviz-tree-caret",
11221
+ children: groupCollapsed ? "\u25B6" : "\u25BC"
11222
+ }),
11223
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11224
+ children: grp.label
11225
+ })
11226
+ ]
11227
+ }),
11228
+ !groupCollapsed && grp.types.length === 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11229
+ className: "exprviz-tree-empty",
11230
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11231
+ children: "(none)"
11232
+ })
11233
+ }),
11234
+ !groupCollapsed && typeRows
11235
+ ]
11236
+ }, grp.group);
11237
+ })
11238
+ ]
11239
+ }),
11240
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11241
+ className: "exprviz-fields-preview",
11242
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("table", {
11243
+ className: "exprviz-fields-table",
11244
+ children: [
11245
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("thead", {
11246
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tr", {
11247
+ children: [
11248
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("th", {
11249
+ children: "Field"
11250
+ }),
11251
+ orderedSelectedKeys.map((k)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("th", {
11252
+ children: $4d0c2e01f58b53b1$var$labelForKey(k, propTree)
11253
+ }, k))
11254
+ ]
11255
+ })
11256
+ }),
11257
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tbody", {
11258
+ children: [
11259
+ matchingFields.map((f)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("tr", {
11260
+ children: [
11261
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11262
+ title: f.fieldName,
11263
+ children: f.fieldName.replace(/__expr$/, '')
11264
+ }),
11265
+ orderedSelectedKeys.map((k)=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11266
+ children: f.props[k] || ''
11267
+ }, k))
11268
+ ]
11269
+ }, f.fieldName)),
11270
+ matchingFields.length === 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("tr", {
11271
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("td", {
11272
+ colSpan: 1 + orderedSelectedKeys.length,
11273
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11274
+ children: "No matching fields"
11275
+ })
11276
+ })
11277
+ })
11278
+ ]
11279
+ })
11280
+ ]
11281
+ })
11282
+ })
11283
+ ]
11284
+ })
11285
+ }),
11286
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Modal).Footer, {
11287
+ children: [
11288
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
11289
+ variant: "secondary",
11290
+ onClick: ()=>doToggleExprVizFieldsModal(taxon, false),
11291
+ children: "Cancel"
11292
+ }),
11293
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Button), {
11294
+ variant: "primary",
11295
+ disabled: matchingFields.length === 0,
11296
+ onClick: ()=>{
11297
+ doSetExprVizFields(taxon, matchingFields.map((f)=>f.fieldName));
11298
+ doToggleExprVizFieldsModal(taxon, false);
11299
+ },
11300
+ children: [
11301
+ "Apply (",
11302
+ matchingFields.length,
11303
+ " fields)"
11304
+ ]
11305
+ })
11306
+ ]
11307
+ })
11308
+ ]
11309
+ });
11310
+ };
11311
+ var $4d0c2e01f58b53b1$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectFieldCatalog', 'selectExprViz', 'selectExpressionStudies', 'selectExpressionSamples', 'selectGrameneMaps', 'doToggleExprVizFieldsModal', 'doSetExprVizFields', 'doFetchExprVizFieldExistence', $4d0c2e01f58b53b1$var$FieldsModalCmp);
11312
+
11313
+
11314
+
11315
+
11316
+
11317
+
11318
+
11319
+
11320
+ const $4ab64e76c1caef59$var$baseColDefs = [
11321
+ {
11322
+ field: 'id',
11323
+ headerName: 'Gene ID',
11324
+ pinned: 'left',
11325
+ width: 160,
11326
+ suppressMovable: true
11327
+ },
11328
+ {
11329
+ field: 'name',
11330
+ headerName: 'Name',
11331
+ width: 140,
11332
+ suppressMovable: true
11333
+ }
11334
+ ];
11335
+ function $4ab64e76c1caef59$var$arraysEqual(a, b) {
11336
+ if (a.length !== b.length) return false;
11337
+ for(let i = 0; i < a.length; i++)if (a[i] !== b[i]) return false;
11338
+ return true;
11339
+ }
11340
+ function $4ab64e76c1caef59$var$buildFieldInfo(fields, studies, expressionSamples) {
11341
+ const info = {};
11342
+ if (!fields || !studies || !expressionSamples) return info;
11343
+ const wanted = new Set(fields);
11344
+ for (const study of studies){
11345
+ const studyId = study._id;
11346
+ const samples = expressionSamples[studyId];
11347
+ if (!samples) continue;
11348
+ const byGroup = {};
11349
+ for (const s of samples)if (!byGroup[s.group]) byGroup[s.group] = s;
11350
+ for (const group of Object.keys(byGroup)){
11351
+ const fieldName = `${studyId.replace(/-/g, '_')}_${group}__expr`;
11352
+ if (!wanted.has(fieldName)) continue;
11353
+ const sample = byGroup[group];
11354
+ const factors = {};
11355
+ (sample.factor || []).forEach((f)=>{
11356
+ factors[f.type] = f.label;
11357
+ });
11358
+ const characteristics = {};
11359
+ (sample.characteristic || []).forEach((c)=>{
11360
+ if (factors[c.type] != null) return;
11361
+ characteristics[c.type] = c.label;
11362
+ });
11363
+ info[fieldName] = {
11364
+ studyId: studyId,
11365
+ studyDescription: study.description || studyId,
11366
+ group: group,
11367
+ replicates: samples.filter((s)=>s.group === group).length,
11368
+ factors: factors,
11369
+ characteristics: characteristics
11370
+ };
11371
+ }
11372
+ }
11373
+ return info;
11374
+ }
11375
+ // Custom header: re-implements sort click + menu button so ag-grid's column
11376
+ // drag/menu/filter still work, with an info icon as the popover trigger.
11377
+ const $4ab64e76c1caef59$var$HeaderWithInfo = (props)=>{
11378
+ const { displayName: displayName, enableSorting: enableSorting, enableMenu: enableMenu, showColumnMenu: showColumnMenu, progressSort: progressSort, column: column, info: info } = props;
11379
+ const [sort, setSort] = (0, $gXNCa$react.useState)(column && column.getSort ? column.getSort() : null);
11380
+ const menuRef = (0, $gXNCa$react.useRef)(null);
11381
+ (0, $gXNCa$react.useEffect)(()=>{
11382
+ if (!column || !column.addEventListener) return;
11383
+ const handler = ()=>setSort(column.getSort());
11384
+ column.addEventListener('sortChanged', handler);
11385
+ return ()=>column.removeEventListener('sortChanged', handler);
11386
+ }, [
11387
+ column
11388
+ ]);
11389
+ const onSortClick = (event)=>{
11390
+ if (enableSorting && progressSort) progressSort(event.shiftKey);
11391
+ };
11392
+ const onMenuClick = (event)=>{
11393
+ event.stopPropagation();
11394
+ if (showColumnMenu && menuRef.current) showColumnMenu(menuRef.current);
11395
+ };
11396
+ const factorEntries = info ? Object.entries(info.factors || {}) : [];
11397
+ const charEntries = info ? Object.entries(info.characteristics || {}) : [];
11398
+ const popover = info ? /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Popover), {
11399
+ id: `exprviz-header-${info.studyId}-${info.group}`,
11400
+ className: "exprviz-header-popover",
11401
+ children: [
11402
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Popover).Header, {
11403
+ as: "h6",
11404
+ children: info.studyDescription
11405
+ }),
11406
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Popover).Body, {
11407
+ children: [
11408
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11409
+ children: [
11410
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11411
+ children: "Study:"
11412
+ }),
11413
+ " ",
11414
+ info.studyId
11415
+ ]
11416
+ }),
11417
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11418
+ children: [
11419
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11420
+ children: "Group:"
11421
+ }),
11422
+ " ",
11423
+ info.group,
11424
+ " (",
11425
+ info.replicates,
11426
+ " ",
11427
+ info.replicates === 1 ? 'rep' : 'reps',
11428
+ ")"
11429
+ ]
11430
+ }),
11431
+ factorEntries.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11432
+ className: "exprviz-header-section",
11433
+ children: [
11434
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11435
+ children: "Factors"
11436
+ }),
11437
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("ul", {
11438
+ children: factorEntries.map(([t, v])=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
11439
+ children: [
11440
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
11441
+ children: [
11442
+ t,
11443
+ ":"
11444
+ ]
11445
+ }),
11446
+ " ",
11447
+ v
11448
+ ]
11449
+ }, t))
11450
+ })
11451
+ ]
11452
+ }),
11453
+ charEntries.length > 0 && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11454
+ className: "exprviz-header-section",
11455
+ children: [
11456
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("strong", {
11457
+ children: "Characteristics"
11458
+ }),
11459
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("ul", {
11460
+ children: charEntries.map(([t, v])=>/*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("li", {
11461
+ children: [
11462
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
11463
+ children: [
11464
+ t,
11465
+ ":"
11466
+ ]
11467
+ }),
11468
+ " ",
11469
+ v
11470
+ ]
11471
+ }, t))
11472
+ })
11473
+ ]
11474
+ })
11475
+ ]
11476
+ })
11477
+ ]
11478
+ }) : null;
11479
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11480
+ className: "exprviz-header",
11481
+ children: [
11482
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
11483
+ className: "exprviz-header-text",
11484
+ onClick: onSortClick,
11485
+ style: {
11486
+ cursor: enableSorting ? 'pointer' : 'default'
11487
+ },
11488
+ children: [
11489
+ displayName,
11490
+ sort === 'asc' && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11491
+ className: "exprviz-header-sort",
11492
+ children: " \u25B2"
11493
+ }),
11494
+ sort === 'desc' && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11495
+ className: "exprviz-header-sort",
11496
+ children: " \u25BC"
11497
+ })
11498
+ ]
11499
+ }),
11500
+ info && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.OverlayTrigger), {
11501
+ trigger: [
11502
+ 'hover',
11503
+ 'focus'
11504
+ ],
11505
+ placement: "bottom",
11506
+ overlay: popover,
11507
+ delay: {
11508
+ show: 200,
11509
+ hide: 100
11510
+ },
11511
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11512
+ className: "exprviz-header-info",
11513
+ onClick: (e)=>e.stopPropagation(),
11514
+ onMouseDown: (e)=>e.stopPropagation(),
11515
+ "aria-label": "More info",
11516
+ children: "\u24D8"
11517
+ })
11518
+ }),
11519
+ enableMenu && /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("span", {
11520
+ ref: menuRef,
11521
+ className: "exprviz-header-menu ag-icon ag-icon-menu",
11522
+ onClick: onMenuClick
11523
+ })
11524
+ ]
11525
+ });
11526
+ };
11527
+ const $4ab64e76c1caef59$var$ExprTable = ({ rows: rows, fields: fields, onReorder: onReorder, studies: studies, expressionSamples: expressionSamples, onHoverRow: onHoverRow })=>{
11528
+ const fieldInfo = (0, $gXNCa$react.useMemo)(()=>$4ab64e76c1caef59$var$buildFieldInfo(fields, studies, expressionSamples), [
11529
+ fields,
11530
+ studies,
11531
+ expressionSamples
11532
+ ]);
11533
+ const columnDefs = (0, $gXNCa$react.useMemo)(()=>{
11534
+ const expressionCols = (fields || []).map((f)=>({
11535
+ field: f,
11536
+ headerName: f.replace(/__expr$/, ''),
11537
+ width: 160,
11538
+ suppressMovable: false,
11539
+ headerComponent: $4ab64e76c1caef59$var$HeaderWithInfo,
11540
+ headerComponentParams: {
11541
+ info: fieldInfo[f]
11542
+ },
11543
+ valueFormatter: (p)=>{
11544
+ const v = p.value;
11545
+ if (v == null) return '';
11546
+ if (Array.isArray(v)) return v.join(', ');
11547
+ if (typeof v === 'object') return JSON.stringify(v);
11548
+ return String(v);
11549
+ }
11550
+ }));
11551
+ return [
11552
+ ...$4ab64e76c1caef59$var$baseColDefs,
11553
+ ...expressionCols
11554
+ ];
11555
+ }, [
11556
+ fields,
11557
+ fieldInfo
11558
+ ]);
11559
+ const onColumnMoved = (e)=>{
11560
+ if (!onReorder || !e.finished) return;
11561
+ const allCols = e.api.getAllGridColumns ? e.api.getAllGridColumns() : e.columnApi && e.columnApi.getAllGridColumns && e.columnApi.getAllGridColumns();
11562
+ if (!allCols) return;
11563
+ const next = allCols.map((c)=>c.getColId()).filter((id)=>fields.includes(id));
11564
+ if (!$4ab64e76c1caef59$var$arraysEqual(next, fields)) onReorder(next);
11565
+ };
11566
+ if (!rows || rows.length === 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11567
+ className: "exprviz-table-empty",
11568
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11569
+ children: "No data loaded."
11570
+ })
11571
+ });
11572
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11573
+ className: "ag-theme-quartz exprviz-aggrid",
11574
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$aggridreact.AgGridReact), {
11575
+ rowData: rows,
11576
+ columnDefs: columnDefs,
11577
+ defaultColDef: {
11578
+ resizable: true,
11579
+ sortable: true,
11580
+ filter: true
11581
+ },
11582
+ animateRows: false,
11583
+ suppressFieldDotNotation: true,
11584
+ suppressDragLeaveHidesColumns: true,
11585
+ onColumnMoved: onColumnMoved,
11586
+ onCellMouseOver: (e)=>onHoverRow && onHoverRow(e.data && e.data.id),
11587
+ onCellMouseOut: ()=>onHoverRow && onHoverRow(null)
11588
+ })
11589
+ });
11590
+ };
11591
+ var $4ab64e76c1caef59$export$2e2bcd8739ae039 = $4ab64e76c1caef59$var$ExprTable;
11592
+
11593
+
11594
+
11595
+
11596
+
11597
+ // Parallel-coordinates plot with per-axis brushing and drag-to-reorder axes.
11598
+ // - Axis labels are draggable horizontally; on drop, onReorder(newOrder) fires.
11599
+ // - Brushes on axes intersect (AND): a row is "in" only when every active brush contains it.
11600
+ // - Numeric fields use a linear or symlog scale; non-numeric values are skipped.
11601
+ const $caf32827df861c4e$var$MARGIN = {
11602
+ top: 100,
11603
+ right: 24,
11604
+ bottom: 24,
11605
+ left: 32
11606
+ };
11607
+ const $caf32827df861c4e$var$LABEL_ROTATION = -40;
11608
+ const $caf32827df861c4e$var$BRUSH_WIDTH = 16;
11609
+ function $caf32827df861c4e$var$isNumeric(v) {
11610
+ if (v == null) return false;
11611
+ if (Array.isArray(v)) return false;
11612
+ const n = +v;
11613
+ return Number.isFinite(n);
11614
+ }
11615
+ function $caf32827df861c4e$var$arraysEqual(a, b) {
11616
+ if (a.length !== b.length) return false;
11617
+ for(let i = 0; i < a.length; i++)if (a[i] !== b[i]) return false;
11618
+ return true;
11619
+ }
11620
+ // Powers-of-10 tick values spanning [lo, hi]; includes 0 if the range crosses zero.
11621
+ // Clamped to |v| >= 0.1 to keep low-magnitude tick labels from overlapping near 0.
11622
+ const $caf32827df861c4e$var$MIN_LOG_TICK = 0.1;
11623
+ function $caf32827df861c4e$var$logTickValues([lo, hi]) {
11624
+ const ticks = new Set();
11625
+ if (lo <= 0 && hi >= 0) ticks.add(0);
11626
+ if (hi >= $caf32827df861c4e$var$MIN_LOG_TICK) {
11627
+ const start = Math.max(-1, Math.floor(Math.log10(lo > 0 ? lo : $caf32827df861c4e$var$MIN_LOG_TICK)));
11628
+ const end = Math.ceil(Math.log10(hi));
11629
+ for(let p = start; p <= end; p++){
11630
+ const v = Math.pow(10, p);
11631
+ if (v >= $caf32827df861c4e$var$MIN_LOG_TICK && v <= hi) ticks.add(v);
11632
+ }
11633
+ }
11634
+ if (lo <= -$caf32827df861c4e$var$MIN_LOG_TICK) {
11635
+ const start = Math.max(-1, Math.floor(Math.log10(hi < 0 ? -hi : $caf32827df861c4e$var$MIN_LOG_TICK)));
11636
+ const end = Math.ceil(Math.log10(-lo));
11637
+ for(let p = start; p <= end; p++){
11638
+ const v = -Math.pow(10, p);
11639
+ if (-v >= $caf32827df861c4e$var$MIN_LOG_TICK && v >= lo) ticks.add(v);
11640
+ }
11641
+ }
11642
+ return Array.from(ticks).sort((a, b)=>a - b);
11643
+ }
11644
+ function $caf32827df861c4e$var$logTickFormat(v) {
11645
+ if (v === 0) return '0';
11646
+ const a = Math.abs(v);
11647
+ if (a >= 0.01 && a < 10000) return $gXNCa$d3.format('~g')(v);
11648
+ return $gXNCa$d3.format('.0e')(v);
11649
+ }
11650
+ const $caf32827df861c4e$var$ParallelCoordsPlot = ({ rows: rows, fields: fields, scale: scale = 'linear', onBrushChange: onBrushChange, onReorder: onReorder, clearVersion: clearVersion = 0, hoveredId: hoveredId = null })=>{
11651
+ const svgRef = (0, $gXNCa$react.useRef)(null);
11652
+ const containerRef = (0, $gXNCa$react.useRef)(null);
11653
+ // selections in data domain: { [field]: [lo, hi] }
11654
+ const selectionsRef = (0, $gXNCa$react.useRef)({});
11655
+ const lastClearRef = (0, $gXNCa$react.useRef)(0);
11656
+ (0, $gXNCa$react.useEffect)(()=>{
11657
+ if (clearVersion !== lastClearRef.current) {
11658
+ selectionsRef.current = {};
11659
+ lastClearRef.current = clearVersion;
11660
+ if (onBrushChange) onBrushChange({});
11661
+ }
11662
+ Object.keys(selectionsRef.current).forEach((f)=>{
11663
+ if (!fields || !fields.includes(f)) delete selectionsRef.current[f];
11664
+ });
11665
+ const svg = $gXNCa$d3.select(svgRef.current);
11666
+ svg.selectAll('*').remove();
11667
+ if (!fields || fields.length === 0 || !rows || rows.length === 0) return;
11668
+ const el = containerRef.current;
11669
+ const width = el && el.clientWidth || 600;
11670
+ const height = el && el.clientHeight || 300;
11671
+ const innerW = width - $caf32827df861c4e$var$MARGIN.left - $caf32827df861c4e$var$MARGIN.right;
11672
+ const innerH = height - $caf32827df861c4e$var$MARGIN.top - $caf32827df861c4e$var$MARGIN.bottom;
11673
+ svg.attr('viewBox', `0 0 ${width} ${height}`);
11674
+ const g = svg.append('g').attr('transform', `translate(${$caf32827df861c4e$var$MARGIN.left},${$caf32827df861c4e$var$MARGIN.top})`);
11675
+ // Mutable order during drag — starts as a copy of fields.
11676
+ let order = fields.slice();
11677
+ const x = $gXNCa$d3.scalePoint().range([
11678
+ 0,
11679
+ innerW
11680
+ ]).padding(0.5).domain(order);
11681
+ const yByField = {};
11682
+ let globalExt = null;
11683
+ if (scale === 'log') {
11684
+ const all = [];
11685
+ fields.forEach((f)=>{
11686
+ rows.forEach((r)=>{
11687
+ const v = r[f];
11688
+ if ($caf32827df861c4e$var$isNumeric(v)) all.push(+v);
11689
+ });
11690
+ });
11691
+ globalExt = all.length ? $gXNCa$d3.extent(all) : [
11692
+ 0,
11693
+ 1
11694
+ ];
11695
+ }
11696
+ fields.forEach((f)=>{
11697
+ if (scale === 'log') yByField[f] = $gXNCa$d3.scaleSymlog().domain(globalExt).range([
11698
+ innerH,
11699
+ 0
11700
+ ]).nice();
11701
+ else {
11702
+ const vals = rows.map((r)=>r[f]).filter($caf32827df861c4e$var$isNumeric).map(Number);
11703
+ const ext = vals.length ? $gXNCa$d3.extent(vals) : [
11704
+ 0,
11705
+ 1
11706
+ ];
11707
+ yByField[f] = $gXNCa$d3.scaleLinear().domain(ext).range([
11708
+ innerH,
11709
+ 0
11710
+ ]).nice();
11711
+ }
11712
+ });
11713
+ function pathForRow(row, posOf) {
11714
+ const pts = order.map((f)=>{
11715
+ const v = row[f];
11716
+ if (!$caf32827df861c4e$var$isNumeric(v)) return null;
11717
+ return [
11718
+ posOf(f),
11719
+ yByField[f](Number(v))
11720
+ ];
11721
+ });
11722
+ return line(pts);
11723
+ }
11724
+ const line = $gXNCa$d3.line().defined((d)=>d != null && Number.isFinite(d[1])).x((d)=>d[0]).y((d)=>d[1]);
11725
+ const linesG = g.append('g').attr('class', 'exprviz-pc-lines');
11726
+ const paths = linesG.selectAll('path').data(rows).enter().append('path').attr('fill', 'none').attr('stroke', 'steelblue').attr('stroke-width', 1).attr('data-id', (d)=>d && d.id != null ? String(d.id) : null).attr('d', (row)=>pathForRow(row, (f)=>x(f)));
11727
+ function isBrushedIn(row) {
11728
+ for (const f of order){
11729
+ const sel = selectionsRef.current[f];
11730
+ if (!sel) continue;
11731
+ const v = row[f];
11732
+ if (!$caf32827df861c4e$var$isNumeric(v)) return false;
11733
+ const n = Number(v);
11734
+ const [lo, hi] = sel;
11735
+ if (n < lo || n > hi) return false;
11736
+ }
11737
+ return true;
11738
+ }
11739
+ function applyBrushStyles() {
11740
+ const anyActive = Object.keys(selectionsRef.current).length > 0;
11741
+ paths.classed('exprviz-pc-line-in', (d)=>!anyActive || isBrushedIn(d)).classed('exprviz-pc-line-out', (d)=>anyActive && !isBrushedIn(d));
11742
+ }
11743
+ applyBrushStyles();
11744
+ // axis groups, keyed by field name so D3 can match them across reorders
11745
+ const axisG = g.selectAll('.exprviz-pc-axis').data(order, (d)=>d).enter().append('g').attr('class', 'exprviz-pc-axis').attr('transform', (d)=>`translate(${x(d)},0)`);
11746
+ axisG.each(function(f) {
11747
+ const ax = $gXNCa$d3.select(this);
11748
+ const axisGen = $gXNCa$d3.axisLeft(yByField[f]);
11749
+ if (scale === 'log') axisGen.tickValues($caf32827df861c4e$var$logTickValues(yByField[f].domain())).tickFormat($caf32827df861c4e$var$logTickFormat);
11750
+ else axisGen.ticks(5);
11751
+ ax.call(axisGen);
11752
+ const label = ax.append('text').attr('class', 'exprviz-pc-axis-label').attr('x', 4).attr('y', -4).attr('text-anchor', 'start').attr('transform', `rotate(${$caf32827df861c4e$var$LABEL_ROTATION}, 0, -4)`).attr('fill', '#333').style('font-size', '10px').style('cursor', 'grab').text(f.replace(/__expr$/, ''));
11753
+ // hit area for grabbing — sits along the rotated label
11754
+ ax.append('rect').attr('class', 'exprviz-pc-axis-handle').attr('x', 0).attr('y', -11).attr('width', 140).attr('height', 14).attr('transform', `rotate(${$caf32827df861c4e$var$LABEL_ROTATION}, 0, -4)`).attr('fill', 'transparent').style('cursor', 'grab');
11755
+ const brush = $gXNCa$d3.brushY().extent([
11756
+ [
11757
+ -$caf32827df861c4e$var$BRUSH_WIDTH / 2,
11758
+ 0
11759
+ ],
11760
+ [
11761
+ $caf32827df861c4e$var$BRUSH_WIDTH / 2,
11762
+ innerH
11763
+ ]
11764
+ ]).on('brush end', (event)=>{
11765
+ const s = event.selection;
11766
+ if (!s) delete selectionsRef.current[f];
11767
+ else {
11768
+ const y = yByField[f];
11769
+ const a = y.invert(s[0]);
11770
+ const b = y.invert(s[1]);
11771
+ selectionsRef.current[f] = [
11772
+ Math.min(a, b),
11773
+ Math.max(a, b)
11774
+ ];
11775
+ }
11776
+ applyBrushStyles();
11777
+ // event.sourceEvent is null when brush.move is called programmatically
11778
+ // (e.g. when this effect re-runs and we restore prior selections).
11779
+ // Skipping that case avoids a re-render loop with the parent.
11780
+ if (event.type === 'end' && event.sourceEvent && onBrushChange) onBrushChange({
11781
+ ...selectionsRef.current
11782
+ });
11783
+ });
11784
+ const brushG = ax.append('g').attr('class', 'exprviz-pc-brush').call(brush);
11785
+ const prior = selectionsRef.current[f];
11786
+ if (prior) {
11787
+ const y = yByField[f];
11788
+ const py0 = y(prior[1]);
11789
+ const py1 = y(prior[0]);
11790
+ if (Number.isFinite(py0) && Number.isFinite(py1)) brushG.call(brush.move, [
11791
+ py0,
11792
+ py1
11793
+ ]);
11794
+ }
11795
+ });
11796
+ // Drag-to-reorder: while dragging, only the dragged axis moves and the
11797
+ // line segments connecting to it are recomputed. Other axes stay put.
11798
+ // The new order is computed once at drag end and emitted via onReorder.
11799
+ const drag = $gXNCa$d3.drag().container(function() {
11800
+ return g.node();
11801
+ }).subject(function(event, d) {
11802
+ return {
11803
+ x: x(d),
11804
+ y: 0
11805
+ };
11806
+ }).on('start', function(event, d) {
11807
+ const axNode = this.parentNode;
11808
+ $gXNCa$d3.select(axNode).raise().classed('exprviz-pc-axis-dragging', true);
11809
+ $gXNCa$d3.select(axNode).select('.exprviz-pc-axis-label').style('cursor', 'grabbing');
11810
+ linesG.classed('exprviz-pc-lines-dragging', true);
11811
+ }).on('drag', function(event, d) {
11812
+ const axNode = this.parentNode;
11813
+ const newX = Math.max(0, Math.min(innerW, event.x));
11814
+ $gXNCa$d3.select(axNode).attr('transform', `translate(${newX},0)`);
11815
+ paths.attr('d', (row)=>pathForRow(row, (f)=>f === d ? newX : x(f)));
11816
+ }).on('end', function(event, d) {
11817
+ const axNode = this.parentNode;
11818
+ const newX = Math.max(0, Math.min(innerW, event.x));
11819
+ $gXNCa$d3.select(axNode).classed('exprviz-pc-axis-dragging', false);
11820
+ $gXNCa$d3.select(axNode).select('.exprviz-pc-axis-label').style('cursor', 'grab');
11821
+ linesG.classed('exprviz-pc-lines-dragging', false);
11822
+ const newOrder = order.slice().sort((a, b)=>{
11823
+ const xa = a === d ? newX : x(a);
11824
+ const xb = b === d ? newX : x(b);
11825
+ return xa - xb;
11826
+ });
11827
+ if (onReorder && !$caf32827df861c4e$var$arraysEqual(newOrder, fields)) {
11828
+ // Snap the dragged axis to its target slot for the brief moment
11829
+ // before the parent re-renders with the new order.
11830
+ x.domain(newOrder);
11831
+ $gXNCa$d3.select(axNode).attr('transform', `translate(${x(d)},0)`);
11832
+ paths.attr('d', (row)=>pathForRow(row, (f)=>x(f)));
11833
+ onReorder(newOrder);
11834
+ } else {
11835
+ // No order change — restore the dragged axis to its original slot.
11836
+ $gXNCa$d3.select(axNode).attr('transform', `translate(${x(d)},0)`);
11837
+ paths.attr('d', (row)=>pathForRow(row, (f)=>x(f)));
11838
+ }
11839
+ });
11840
+ axisG.selectAll('.exprviz-pc-axis-label, .exprviz-pc-axis-handle').call(drag);
11841
+ }, [
11842
+ rows,
11843
+ fields,
11844
+ scale,
11845
+ onBrushChange,
11846
+ onReorder,
11847
+ clearVersion
11848
+ ]);
11849
+ // Highlight the polyline matching the hovered row id without rebuilding the
11850
+ // SVG. Raises the highlighted path so it draws above its neighbors.
11851
+ (0, $gXNCa$react.useEffect)(()=>{
11852
+ const svg = $gXNCa$d3.select(svgRef.current);
11853
+ if (svg.empty()) return;
11854
+ const paths = svg.selectAll('.exprviz-pc-lines path');
11855
+ paths.classed('exprviz-pc-line-hover', false);
11856
+ if (hoveredId == null) return;
11857
+ const target = paths.filter(function() {
11858
+ return this.getAttribute('data-id') === String(hoveredId);
11859
+ });
11860
+ target.classed('exprviz-pc-line-hover', true).raise();
11861
+ }, [
11862
+ hoveredId,
11863
+ rows,
11864
+ fields,
11865
+ scale
11866
+ ]);
11867
+ if (!fields || fields.length === 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11868
+ className: "exprviz-plot-empty",
11869
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11870
+ children: "Select fields to plot."
11871
+ })
11872
+ });
11873
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11874
+ ref: containerRef,
11875
+ className: "exprviz-pc-container",
11876
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("svg", {
11877
+ ref: svgRef,
11878
+ width: "100%",
11879
+ height: "100%",
11880
+ preserveAspectRatio: "none"
11881
+ })
11882
+ });
11883
+ };
11884
+ var $caf32827df861c4e$export$2e2bcd8739ae039 = $caf32827df861c4e$var$ParallelCoordsPlot;
11885
+
11886
+
11887
+
11888
+ function $1fd2507769d5bd00$var$speciesTaxonId(tid) {
11889
+ const n = +tid;
11890
+ return n > 1000000 ? Math.floor(n / 1000) : n;
11891
+ }
11892
+ function $1fd2507769d5bd00$var$genomeName(grameneMaps, tid) {
11893
+ if (!grameneMaps) return tid;
11894
+ const direct = grameneMaps[tid];
11895
+ if (direct && direct.display_name) return direct.display_name;
11896
+ const sp = grameneMaps[$1fd2507769d5bd00$var$speciesTaxonId(tid)];
11897
+ if (sp && sp.display_name) return sp.display_name;
11898
+ return tid;
11899
+ }
11900
+ const $1fd2507769d5bd00$var$ExprVizViewCmp = (props)=>{
11901
+ const { exprVizPivot: pivot, exprViz: exprViz, exprVizActiveTaxon: activeTaxon, grameneMaps: grameneMaps, expressionStudies: expressionStudies, expressionSamples: expressionSamples, doSetExprVizActiveTaxon: doSetExprVizActiveTaxon, doToggleExprVizFieldsModal: doToggleExprVizFieldsModal, doFetchExprVizData: doFetchExprVizData } = props;
11902
+ const studiesFor = (tid)=>{
11903
+ if (!expressionStudies) return [];
11904
+ return expressionStudies[tid] || expressionStudies[$1fd2507769d5bd00$var$speciesTaxonId(tid)] || [];
11905
+ };
11906
+ const taxa = (0, $gXNCa$react.useMemo)(()=>{
11907
+ const ids = Object.keys(pivot.data || {});
11908
+ if (!grameneMaps) return ids;
11909
+ return ids.sort((a, b)=>{
11910
+ const ma = grameneMaps[a] || grameneMaps[$1fd2507769d5bd00$var$speciesTaxonId(a)];
11911
+ const mb = grameneMaps[b] || grameneMaps[$1fd2507769d5bd00$var$speciesTaxonId(b)];
11912
+ return (ma && ma.left_index || 0) - (mb && mb.left_index || 0);
11913
+ });
11914
+ }, [
11915
+ pivot.data,
11916
+ grameneMaps
11917
+ ]);
11918
+ (0, $gXNCa$react.useEffect)(()=>{
11919
+ if (taxa.length === 0) return;
11920
+ if (!activeTaxon || !taxa.includes(String(activeTaxon))) doSetExprVizActiveTaxon(taxa[0]);
11921
+ }, [
11922
+ taxa,
11923
+ activeTaxon,
11924
+ doSetExprVizActiveTaxon
11925
+ ]);
11926
+ if (pivot.status === 'loading') return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11927
+ className: "exprviz-view",
11928
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11929
+ children: "Loading studies\u2026"
11930
+ })
11931
+ });
11932
+ if (pivot.status === 'error') return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11933
+ className: "exprviz-view",
11934
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("em", {
11935
+ children: [
11936
+ "Error: ",
11937
+ pivot.error
11938
+ ]
11939
+ })
11940
+ });
11941
+ if (taxa.length === 0) return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
11942
+ className: "exprviz-view",
11943
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("em", {
11944
+ children: "No expression studies for current results."
11945
+ })
11946
+ });
11947
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
11948
+ className: "exprviz-view",
11949
+ children: [
11950
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tabs), {
11951
+ activeKey: activeTaxon || taxa[0],
11952
+ onSelect: (k)=>doSetExprVizActiveTaxon(k),
11953
+ className: "exprviz-tabs",
11954
+ children: taxa.map((tid)=>{
11955
+ const studies = studiesFor(tid);
11956
+ const taxName = $1fd2507769d5bd00$var$genomeName(grameneMaps, tid);
11957
+ const geneCount = pivot.data[tid] || 0;
11958
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Tab), {
11959
+ eventKey: tid,
11960
+ title: `${taxName} (${studies.length} studies \xb7 ${geneCount} genes)`,
11961
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)($1fd2507769d5bd00$var$TaxonPanel, {
11962
+ taxon: tid,
11963
+ studies: studies,
11964
+ expressionSamples: expressionSamples,
11965
+ tabState: exprViz.byTaxon[tid],
11966
+ onOpenFields: ()=>doToggleExprVizFieldsModal(tid, true),
11967
+ onLoad: ()=>doFetchExprVizData(tid),
11968
+ onReorder: (next)=>props.doReorderExprVizFields(tid, next),
11969
+ onAddRangeQuery: props.doAddGrameneRangeQuery
11970
+ })
11971
+ }, tid);
11972
+ })
11973
+ }),
11974
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $4d0c2e01f58b53b1$export$2e2bcd8739ae039), {})
11975
+ ]
11976
+ });
11977
+ };
11978
+ function $1fd2507769d5bd00$var$rowMatchesSelections(row, selections) {
11979
+ for (const f of Object.keys(selections)){
11980
+ const v = row[f];
11981
+ if (v == null || Array.isArray(v)) return false;
11982
+ const n = +v;
11983
+ if (!Number.isFinite(n)) return false;
11984
+ const [lo, hi] = selections[f];
11985
+ if (n < lo || n > hi) return false;
11986
+ }
11987
+ return true;
11988
+ }
11989
+ function $1fd2507769d5bd00$var$fmt(n) {
11990
+ if (!Number.isFinite(n)) return String(n);
11991
+ const a = Math.abs(n);
11992
+ if (a !== 0 && (a < 0.001 || a >= 1e6)) return n.toExponential(3);
11993
+ return Number(n.toFixed(4)).toString();
11994
+ }
11995
+ function $1fd2507769d5bd00$var$tsvCell(v) {
11996
+ if (v == null) return '';
11997
+ if (Array.isArray(v)) return v.join(',');
11998
+ const s = typeof v === 'object' ? JSON.stringify(v) : String(v);
11999
+ return s.replace(/[\t\r\n]+/g, ' ');
12000
+ }
12001
+ function $1fd2507769d5bd00$var$downloadTsv(filename, rows, fields) {
12002
+ const cols = [
12003
+ 'id',
12004
+ 'name',
12005
+ ...fields
12006
+ ];
12007
+ const headerLabels = [
12008
+ 'id',
12009
+ 'name',
12010
+ ...fields.map((f)=>f.replace(/__expr$/, ''))
12011
+ ];
12012
+ const lines = [
12013
+ headerLabels.join('\t')
12014
+ ];
12015
+ for (const r of rows)lines.push(cols.map((c)=>$1fd2507769d5bd00$var$tsvCell(r[c])).join('\t'));
12016
+ const blob = new Blob([
12017
+ lines.join('\n') + '\n'
12018
+ ], {
12019
+ type: 'text/tab-separated-values'
12020
+ });
12021
+ const url = URL.createObjectURL(blob);
12022
+ const a = document.createElement('a');
12023
+ a.href = url;
12024
+ a.download = filename;
12025
+ document.body.appendChild(a);
12026
+ a.click();
12027
+ document.body.removeChild(a);
12028
+ URL.revokeObjectURL(url);
12029
+ }
12030
+ const $1fd2507769d5bd00$var$TaxonPanel = ({ taxon: taxon, studies: studies, expressionSamples: expressionSamples, tabState: tabState, onOpenFields: onOpenFields, onLoad: onLoad, onReorder: onReorder, onAddRangeQuery: onAddRangeQuery })=>{
12031
+ const selected = tabState && tabState.selectedFields || [];
12032
+ const rows = tabState && tabState.rows || [];
12033
+ const fetchInfo = tabState && tabState.fetch || {
12034
+ status: 'idle',
12035
+ total: 0
12036
+ };
12037
+ const [scale, setScale] = (0, $gXNCa$react.useState)('linear');
12038
+ const [selections, setSelections] = (0, $gXNCa$react.useState)({});
12039
+ const [clearVersion, setClearVersion] = (0, $gXNCa$react.useState)(0);
12040
+ const [hoveredId, setHoveredId] = (0, $gXNCa$react.useState)(null);
12041
+ const hasBrush = Object.keys(selections).length > 0;
12042
+ const filteredRows = (0, $gXNCa$react.useMemo)(()=>{
12043
+ if (!hasBrush) return rows;
12044
+ return rows.filter((r)=>$1fd2507769d5bd00$var$rowMatchesSelections(r, selections));
12045
+ }, [
12046
+ rows,
12047
+ selections,
12048
+ hasBrush
12049
+ ]);
12050
+ // Drop fields with no numeric data in the loaded rows so empty axes/columns
12051
+ // don't clutter the visualization. Selected-but-empty fields stay in the
12052
+ // underlying selection so a future load can repopulate them.
12053
+ const visibleFields = (0, $gXNCa$react.useMemo)(()=>{
12054
+ if (rows.length === 0 || selected.length === 0) return selected;
12055
+ return selected.filter((f)=>rows.some((r)=>{
12056
+ const v = r[f];
12057
+ return v != null && !Array.isArray(v) && Number.isFinite(+v);
12058
+ }));
12059
+ }, [
12060
+ rows,
12061
+ selected
12062
+ ]);
12063
+ const handleReorder = onReorder ? (newVisibleOrder)=>{
12064
+ const visibleSet = new Set(newVisibleOrder);
12065
+ const hidden = selected.filter((f)=>!visibleSet.has(f));
12066
+ onReorder([
12067
+ ...newVisibleOrder,
12068
+ ...hidden
12069
+ ]);
12070
+ } : undefined;
12071
+ (0, $gXNCa$react.useEffect)(()=>{
12072
+ if (rows.length === 0 && hasBrush) {
12073
+ setSelections({});
12074
+ setClearVersion((v)=>v + 1);
12075
+ }
12076
+ }, [
12077
+ rows.length,
12078
+ hasBrush
12079
+ ]);
12080
+ return /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12081
+ className: "exprviz-tab-panel",
12082
+ children: [
12083
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12084
+ className: "exprviz-toolbar",
12085
+ children: [
12086
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.Button), {
12087
+ size: "sm",
12088
+ onClick: onOpenFields,
12089
+ children: [
12090
+ "Select fields (",
12091
+ selected.length,
12092
+ " selected, ",
12093
+ studies.length,
12094
+ " studies)"
12095
+ ]
12096
+ }),
12097
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
12098
+ size: "sm",
12099
+ variant: "primary",
12100
+ disabled: selected.length === 0 || fetchInfo.status === 'loading',
12101
+ onClick: onLoad,
12102
+ children: fetchInfo.status === 'loading' ? "Loading\u2026" : 'Load data'
12103
+ }),
12104
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)((0, $gXNCa$reactbootstrap.ToggleButtonGroup), {
12105
+ type: "radio",
12106
+ name: `exprviz-scale-${taxon}`,
12107
+ size: "sm",
12108
+ value: scale,
12109
+ onChange: setScale,
12110
+ children: [
12111
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.ToggleButton), {
12112
+ id: `exprviz-scale-${taxon}-lin`,
12113
+ value: "linear",
12114
+ variant: "outline-secondary",
12115
+ children: "Linear"
12116
+ }),
12117
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.ToggleButton), {
12118
+ id: `exprviz-scale-${taxon}-log`,
12119
+ value: "log",
12120
+ variant: "outline-secondary",
12121
+ children: "Log"
12122
+ })
12123
+ ]
12124
+ }),
12125
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
12126
+ size: "sm",
12127
+ variant: "outline-secondary",
12128
+ disabled: !hasBrush,
12129
+ onClick: ()=>{
12130
+ setClearVersion((v)=>v + 1);
12131
+ setSelections({});
12132
+ },
12133
+ children: "Clear brushes"
12134
+ }),
12135
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
12136
+ size: "sm",
12137
+ variant: "outline-secondary",
12138
+ disabled: filteredRows.length === 0 || visibleFields.length === 0,
12139
+ onClick: ()=>$1fd2507769d5bd00$var$downloadTsv(`expression_${taxon}.tsv`, filteredRows, visibleFields),
12140
+ title: "Download the visible rows and columns as tab-delimited text",
12141
+ children: "Download TSV"
12142
+ }),
12143
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $gXNCa$reactbootstrap.Button), {
12144
+ size: "sm",
12145
+ variant: "success",
12146
+ disabled: !hasBrush || !onAddRangeQuery,
12147
+ onClick: ()=>{
12148
+ const terms = Object.keys(selections).map((field)=>{
12149
+ const [lo, hi] = selections[field];
12150
+ return {
12151
+ category: 'Expression',
12152
+ name: `${field}: ${$1fd2507769d5bd00$var$fmt(lo)}\u{2013}${$1fd2507769d5bd00$var$fmt(hi)}`,
12153
+ fq_field: field,
12154
+ fq_value: `[${lo} TO ${hi}]`
12155
+ };
12156
+ });
12157
+ onAddRangeQuery(terms);
12158
+ },
12159
+ title: "Add brush ranges as an AND-conjunction filter on the search",
12160
+ children: "Apply as filter"
12161
+ }),
12162
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("span", {
12163
+ className: "exprviz-status",
12164
+ children: [
12165
+ hasBrush ? `${filteredRows.length} of ${rows.length}` : rows.length,
12166
+ fetchInfo.total ? ` / ${fetchInfo.total}` : '',
12167
+ " genes",
12168
+ hasBrush ? ' (brushed)' : ' loaded'
12169
+ ]
12170
+ })
12171
+ ]
12172
+ }),
12173
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsxs)("div", {
12174
+ className: "exprviz-body",
12175
+ children: [
12176
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12177
+ className: "exprviz-plot",
12178
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $caf32827df861c4e$export$2e2bcd8739ae039), {
12179
+ rows: rows,
12180
+ fields: visibleFields,
12181
+ scale: scale,
12182
+ onBrushChange: setSelections,
12183
+ onReorder: handleReorder,
12184
+ clearVersion: clearVersion,
12185
+ hoveredId: hoveredId
12186
+ })
12187
+ }),
12188
+ /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)("div", {
12189
+ className: "exprviz-table",
12190
+ children: /*#__PURE__*/ (0, $gXNCa$reactjsxruntime.jsx)((0, $4ab64e76c1caef59$export$2e2bcd8739ae039), {
12191
+ rows: filteredRows,
12192
+ fields: visibleFields,
12193
+ onReorder: handleReorder,
12194
+ studies: studies,
12195
+ expressionSamples: expressionSamples,
12196
+ onHoverRow: setHoveredId
12197
+ })
12198
+ })
12199
+ ]
12200
+ })
12201
+ ]
12202
+ });
12203
+ };
12204
+ var $1fd2507769d5bd00$export$2e2bcd8739ae039 = (0, $gXNCa$reduxbundlerreact.connect)('selectExprViz', 'selectExprVizPivot', 'selectExprVizActiveTaxon', 'selectGrameneMaps', 'selectExpressionStudies', 'selectExpressionSamples', 'doSetExprVizActiveTaxon', 'doToggleExprVizFieldsModal', 'doFetchExprVizData', 'doReorderExprVizFields', 'doAddGrameneRangeQuery', $1fd2507769d5bd00$var$ExprVizViewCmp);
12205
+
12206
+
12207
+
12208
+
12209
+
12210
+
12211
+
12212
+
12213
+
10206
12214
  const $c5d403787de8b05f$var$provider = new (0, $gXNCa$firebaseauth.GoogleAuthProvider)();
10207
12215
  const $c5d403787de8b05f$var$Auth = (props)=>{
10208
12216
  const firebaseConfig = props.configuration && props.configuration.firebaseConfig;
@@ -10313,6 +12321,7 @@ const $693dd8c7a5607c3a$var$inventory = {
10313
12321
  taxonomy: (0, $a67cad486021eb32$export$2e2bcd8739ae039),
10314
12322
  attribs: (0, $67bf5a43401bffdc$export$2e2bcd8739ae039),
10315
12323
  expression: (0, $261baeb81c4d4d8a$export$2e2bcd8739ae039),
12324
+ exprViz: (0, $1fd2507769d5bd00$export$2e2bcd8739ae039),
10316
12325
  userLists: (0, $0f50f369018a42ef$export$2e2bcd8739ae039),
10317
12326
  export: (0, $37b3bb0145d266b0$export$2e2bcd8739ae039)
10318
12327
  };