visualifyjs 2.5.3-2.dev

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. package/.github/workflows/static.yml.bak +51 -0
  2. package/LICENSE +674 -0
  3. package/README.md +59 -0
  4. package/config-overrides.js +31 -0
  5. package/dist/visualify.js +188 -0
  6. package/docs/.nojekyll +0 -0
  7. package/docs/docs/CLI.md +34 -0
  8. package/docs/docs/README.md +65 -0
  9. package/docs/docs/Rechart/bar.md +190 -0
  10. package/docs/docs/Rechart/funnel.md +241 -0
  11. package/docs/docs/Rechart/geo.md +0 -0
  12. package/docs/docs/Rechart/line.md +355 -0
  13. package/docs/docs/Rechart/liquidfill.md +0 -0
  14. package/docs/docs/Rechart/pie.md +225 -0
  15. package/docs/docs/Rechart/polar.md +0 -0
  16. package/docs/docs/Rechart/radar.md +253 -0
  17. package/docs/docs/Rechart/sankey.md +0 -0
  18. package/docs/docs/Rechart/scatter.md +298 -0
  19. package/docs/docs/Rechart/sunburst.md +0 -0
  20. package/docs/docs/Rechart/tree.md +0 -0
  21. package/docs/docs/Rechart/wordcloud.md +0 -0
  22. package/docs/docs/_404.md +52 -0
  23. package/docs/docs/_coverpage.md +11 -0
  24. package/docs/docs/_sidebar.md +44 -0
  25. package/docs/docs/components/dotBio.md +34 -0
  26. package/docs/docs/components/echart.md +82 -0
  27. package/docs/docs/components/html.md +34 -0
  28. package/docs/docs/components/macaron.md +145 -0
  29. package/docs/docs/components/markdown.md +0 -0
  30. package/docs/docs/components/more.md +142 -0
  31. package/docs/docs/components/plotly.md +62 -0
  32. package/docs/docs/components/scatterL.md +70 -0
  33. package/docs/docs/components/visium.md +57 -0
  34. package/docs/docs/configuration.md +121 -0
  35. package/docs/docs/deploy.md +31 -0
  36. package/docs/docs/log.md +9 -0
  37. package/docs/docs/more-pages.md +23 -0
  38. package/docs/docs/quickstart.md +124 -0
  39. package/docs/docs/rechart-attributes.md +74 -0
  40. package/docs/docs/rechart-basic-usage.md +162 -0
  41. package/docs/docs/static/_images/deploy-github-pages.png +0 -0
  42. package/docs/docs/static/logo/favicon.ico +0 -0
  43. package/docs/docs/static/logo/logo_128x128.png +0 -0
  44. package/docs/docs/static/logo/logo_192x192.png +0 -0
  45. package/docs/docs/static/logo/logo_256x256.png +0 -0
  46. package/docs/docs/static/logo/logo_512x512.png +0 -0
  47. package/docs/docs/static/logo/logo_64x64.png +0 -0
  48. package/docs/docs/theme.md +5 -0
  49. package/docs/index.html +71 -0
  50. package/docs/manifest.json +24 -0
  51. package/docs/static/css/fluff-stuff.css +170 -0
  52. package/docs/static/css/font-awesome.min.css +4 -0
  53. package/docs/static/css/visualify.css +25 -0
  54. package/docs/static/fonts/fontawesome-webfont.woff2 +0 -0
  55. package/docs/static/images/star.png +0 -0
  56. package/docs/static/js/configuration.js +448 -0
  57. package/docs/static/js/fluff.js +1 -0
  58. package/docs/static/js/visualify.js +188 -0
  59. package/docs/static/logo/favicon.ico +0 -0
  60. package/docs/static/logo/logo_128x128.png +0 -0
  61. package/docs/static/logo/logo_192x192.png +0 -0
  62. package/docs/static/logo/logo_256x256.png +0 -0
  63. package/docs/static/logo/logo_512x512.png +0 -0
  64. package/docs/static/logo/logo_64x64.png +0 -0
  65. package/package.json +84 -0
  66. package/rollup.config.mjs +76 -0
  67. package/src/_css/404.css +116 -0
  68. package/src/_css/App.css +38 -0
  69. package/src/_css/autoSuggestion.css +27 -0
  70. package/src/_css/circular-progress.css +33 -0
  71. package/src/_css/index.css +37 -0
  72. package/src/_css/modern.css +25 -0
  73. package/src/_media/404.png +0 -0
  74. package/src/_media/corner.svg +8 -0
  75. package/src/_media/download.svg +3 -0
  76. package/src/_media/icon.svg +1 -0
  77. package/src/_media/logo.svg +14 -0
  78. package/src/_test/App.test.js +15 -0
  79. package/src/_utils/reportWebVitals.js +13 -0
  80. package/src/core/appContext.js +27 -0
  81. package/src/core/components/Scatter.js +188 -0
  82. package/src/core/components/ScatterBio.js +572 -0
  83. package/src/core/components/VisiumPlot.js +165 -0
  84. package/src/core/components/browser.js +42 -0
  85. package/src/core/components/dotplot.js +413 -0
  86. package/src/core/components/html.js +29 -0
  87. package/src/core/components/list.js +178 -0
  88. package/src/core/components/macaron.js +201 -0
  89. package/src/core/components/markdown.js +56 -0
  90. package/src/core/components/parser.scatterBio.js +587 -0
  91. package/src/core/components/ratio.js +80 -0
  92. package/src/core/components/scatterL.js +173 -0
  93. package/src/core/components/searchbar.js +131 -0
  94. package/src/core/components/selection.js +193 -0
  95. package/src/core/components/timeline.js +281 -0
  96. package/src/core/components/visium.js +97 -0
  97. package/src/core/fetch/condfetch.js +82 -0
  98. package/src/core/fetch/fetch.js +92 -0
  99. package/src/core/fetch/json.js +29 -0
  100. package/src/core/fetch/vfetch.js +42 -0
  101. package/src/core/liveEditor.js +44 -0
  102. package/src/core/modules/codeEditorWithPreview.js +104 -0
  103. package/src/core/modules/echarts/common.js +20 -0
  104. package/src/core/modules/echarts/presetHandler.js +41 -0
  105. package/src/core/modules/echarts/presets/esodev.chromium.js +172 -0
  106. package/src/core/modules/echarts/presets/esodev.codex.js +130 -0
  107. package/src/core/modules/echarts/presets/esodev.visium.js +123 -0
  108. package/src/core/modules/echarts/presets/mmtrbc.js +186 -0
  109. package/src/core/modules/echarts.js +71 -0
  110. package/src/core/modules/echartsUtils.js +43 -0
  111. package/src/core/modules/echartswitcher.js +152 -0
  112. package/src/core/modules/replotly/presetHandler.js +24 -0
  113. package/src/core/modules/replotly/presets/minimum.js +18 -0
  114. package/src/core/modules/replotly/presets/mmtrbc.dot.js +114 -0
  115. package/src/core/modules/replotly/presets/mmtrbc.violin.js +100 -0
  116. package/src/core/modules/replotly.js +71 -0
  117. package/src/core/pages/404.js +50 -0
  118. package/src/core/pages/error.js +27 -0
  119. package/src/core/pages/jsonPage.js +62 -0
  120. package/src/core/pages/loading.js +44 -0
  121. package/src/core/parser/echart.data.js +183 -0
  122. package/src/core/parser/echart.features.js +125 -0
  123. package/src/core/parser/echart.general.js +147 -0
  124. package/src/core/parser/echart.hilbert.js +57 -0
  125. package/src/core/parser/echart.parser.js +210 -0
  126. package/src/core/parser/echart.series.js +67 -0
  127. package/src/core/parser/echart.types.js +76 -0
  128. package/src/core/parser/plotly.config.js +10 -0
  129. package/src/core/parser/plotly.data.js +132 -0
  130. package/src/core/parser/plotly.layout.js +10 -0
  131. package/src/core/parser/plotly.violin.js +18 -0
  132. package/src/core/recharts.js +62 -0
  133. package/src/core/router/alias.js +49 -0
  134. package/src/core/router/jsonRouter.js +31 -0
  135. package/src/core/themes/modern.js +32 -0
  136. package/src/core/themes/themeSelector.js +33 -0
  137. package/src/core/visualify.js +47 -0
  138. package/src/core/widgets/circularProgress.js +24 -0
  139. package/src/core/widgets/controller.js +83 -0
  140. package/src/core/widgets/errorBoundary.js +36 -0
  141. package/src/core/widgets/footer.js +177 -0
  142. package/src/core/widgets/header.js +234 -0
  143. package/src/core/widgets/layout/Grid.js +31 -0
  144. package/src/core/widgets/layout.js +36 -0
  145. package/src/core/widgets/mapping.js +42 -0
  146. package/src/index.js +62 -0
  147. package/src/setupTests.js +5 -0
@@ -0,0 +1,587 @@
1
+ /*
2
+ * @Author : Lihao leolihao@arizona.edu
3
+ * @Date : 2023-11-12 17:35:02
4
+ * @FilePath : /visualify.js/src/core/components/parser.scatterBio.js
5
+ * @Description :
6
+ * Copyright (c) 2023 by Lihao (leolihao@arizona.edu), All Rights Reserved.
7
+ */
8
+ import simplefetch from '../fetch/fetch';
9
+
10
+ var cached_color = {};
11
+ export function rcolor(seed = 0) {
12
+ const maxHexValue = 16777215;
13
+ const randomSeed = seed || Math.random();
14
+ const randomColor = Math.floor(randomSeed * maxHexValue).toString(16);
15
+ const color = '#' + randomColor.padStart(6, '0');
16
+
17
+ return color;
18
+ }
19
+
20
+ export function isEmpty(obj) {
21
+ return Object.keys(obj).length === 0 && obj.constructor === Object;
22
+ }
23
+
24
+ export const parseConfig = (props) => {
25
+ const { echart } = props;
26
+ var option = {
27
+ series: [
28
+ {
29
+ data: null, // Use the fetched data here
30
+ type: 'scatter',
31
+ areaStyle: {},
32
+ },
33
+ ],
34
+ toolbox: {
35
+ feature: {
36
+ saveAsImage: {
37
+ icon: `path://M30 20.75c-0.69 0.001-1.249 0.56-1.25 1.25v6.75h-25.5v-6.75c0-0.69-0.56-1.25-1.25-1.25s-1.25 0.56-1.25 1.25v0 8c0 0.69 0.56 1.25 1.25 1.25h28c0.69-0.001 1.249-0.56 1.25-1.25v-8c-0.001-0.69-0.56-1.249-1.25-1.25h-0zM15.116 24.885c0.012 0.012 0.029 0.016 0.041 0.027 0.103 0.099 0.223 0.18 0.356 0.239l0.008 0.003 0.001 0c0.141 0.060 0.306 0.095 0.478 0.095 0.345 0 0.657-0.139 0.883-0.365l5.001-5c0.226-0.226 0.366-0.539 0.366-0.884 0-0.691-0.56-1.251-1.251-1.251-0.345 0-0.658 0.14-0.884 0.366l-2.865 2.867v-18.982c0-0.69-0.56-1.25-1.25-1.25s-1.25 0.56-1.25 1.25v0 18.981l-2.866-2.866c-0.226-0.226-0.539-0.366-0.884-0.366-0.691 0-1.251 0.56-1.251 1.251 0 0.346 0.14 0.658 0.367 0.885v0z`,
38
+ },
39
+ },
40
+ },
41
+ dataZoom: [
42
+ {
43
+ type: 'inside',
44
+ xAxisIndex: 0,
45
+ filterMode: 'filter',
46
+ },
47
+ {
48
+ type: 'inside',
49
+ yAxisIndex: 0,
50
+ filterMode: 'filter',
51
+ orient: 'vertical',
52
+ },
53
+ ],
54
+ tooltip: {
55
+ trigger: 'item',
56
+ axisPointer: {
57
+ type: 'cross',
58
+ },
59
+ formatter: (params) => {
60
+ let express =
61
+ Math.round(params.data.Expression * 10000) / 10000;
62
+ return `
63
+ <div style="text-align: center;">
64
+ ${params.data.Cell_ID}
65
+ <br/> Type: <strong>${params.data.Cell_Type} </strong>
66
+ <br/> Stage: <strong>${params.data.Stage}</strong>
67
+ <br/> #UMI: <strong>${params.data.UMI}</strong>
68
+ <br/> #Gene: <strong>${params.data.Gene}</strong>
69
+ <br/> MT%: <strong>${params.data.MT}</strong>
70
+ ${
71
+ express != null
72
+ ? '<br/> Expression: <strong>' +
73
+ express +
74
+ '</strong>'
75
+ : ''
76
+ }
77
+ </div>
78
+ `;
79
+ },
80
+ },
81
+ title: {
82
+ textStyle: {
83
+ fontSize: 20,
84
+ },
85
+ left: 'center',
86
+ top: 0,
87
+ },
88
+ animation: false,
89
+ legend: {
90
+ textStyle: {
91
+ fontSize: 18,
92
+ },
93
+ orient: 'horizontal',
94
+ right: 'center', // "top" | "bottom" | "center"
95
+ itemWidth: 20,
96
+ width: 600,
97
+ top: 20,
98
+ },
99
+
100
+ xAxis: {
101
+ name: 'UMAP 1',
102
+ type: 'value',
103
+ nameLocation: 'middle',
104
+ nameGap: 10,
105
+ nameTextStyle: {
106
+ fontSize: 18, // Set font size
107
+ fontWeight: 'bold', // Set font weight to bold
108
+ },
109
+ axisLine: { show: false },
110
+ axisTick: { show: false },
111
+ splitLine: { show: false },
112
+ axisLabel: { show: false },
113
+ //min: -20,
114
+ //max: 20,
115
+ },
116
+ yAxis: {
117
+ name: 'UMAP 2',
118
+ type: 'value',
119
+ nameLocation: 'middle',
120
+ nameGap: 10,
121
+ nameTextStyle: {
122
+ fontSize: 18, // Set font size
123
+ fontWeight: 'bold', // Set font weight to bold
124
+ },
125
+ //min: -20,
126
+ //max: 20,
127
+ axisLine: { show: false },
128
+ axisTick: { show: false },
129
+ splitLine: { show: false },
130
+ axisLabel: { show: false },
131
+ },
132
+ grid: {
133
+ top: '20%',
134
+ bottom: '12%',
135
+ left: '10%',
136
+ right: '10%',
137
+ },
138
+ ...echart,
139
+ };
140
+
141
+ return option;
142
+ };
143
+
144
+ export const handleSimplyLoad = (simpleload) => {
145
+ console.log(`handleSimplyLoad: `, simpleload);
146
+ // fetch data direcly from config
147
+ // fetch data from simply api
148
+ };
149
+
150
+ export const handleAPI = async (config, sharedData, bbox = false) => {
151
+ let fetched_dat = {};
152
+ let dependencies = {};
153
+
154
+ // Get corresponding api and its attributes
155
+ for (const [item, attr] of Object.entries(config.api)) {
156
+ const { href, val, dep = null } = attr;
157
+ let id = sharedData[val];
158
+
159
+ if (typeof id === 'object' && config.mapping && config.mapping.api) {
160
+ id = [...new Set(id.map((i) => config.mapping.api[i] ?? i))];
161
+ id = id[0] ? id : null;
162
+ } else if (config.mapping && config.mapping.api)
163
+ id = config.mapping.api[id] ?? id;
164
+
165
+ //console.log(`item: ${item}, href: ${href}, val: ${val}, id: ${id}`);
166
+ dependencies[item] = id;
167
+
168
+ if (id && typeof id === 'object') {
169
+ //console.log(`id for ${item} : `, id);
170
+ const promises = id.map(async (i) => {
171
+ //console.log(`request for ${item} : `, i);
172
+ return await simplefetch(href, {
173
+ id: i,
174
+ type: item,
175
+ debug: false,
176
+ bbox: bbox,
177
+ });
178
+ });
179
+ //console.log(`promises for ${item} : `, promises);
180
+
181
+ const resp = await Promise.all(promises);
182
+ const result = resp.reduce((acc, cur) => {
183
+ return {
184
+ ...acc,
185
+ ...cur,
186
+ };
187
+ }, {});
188
+ //console.log(`result for ${item} : `, result);
189
+ fetched_dat[item] = result;
190
+ } else if (id) {
191
+ //console.log(`dep2 id for ${item} : `, id, dependencies);
192
+
193
+ const result = await simplefetch(href, {
194
+ id: dependencies[dep] + '/' + id,
195
+ type: item,
196
+ debug: sharedData.debug,
197
+ bbox: bbox,
198
+ });
199
+ fetched_dat[item] = result;
200
+ dependencies[item] = id;
201
+ }
202
+ }
203
+
204
+ if (isEmpty(fetched_dat)) {
205
+ throw new Error(
206
+ config.startup_msg ?? 'Please select metadata to load data',
207
+ );
208
+ } else {
209
+ fetched_dat.fetched_ID = dependencies;
210
+ return fetched_dat;
211
+ }
212
+
213
+ //wait for user to provide values for api
214
+ };
215
+
216
+ // Function to output the axis values
217
+ export const outputAxisValues = (myChart) => {
218
+ if (!myChart) {
219
+ return;
220
+ }
221
+
222
+ const yAxis = myChart.getModel().getComponent('yAxis').axis;
223
+ const yAxisMin = yAxis.scale.getExtent()[0];
224
+ const yAxisMax = yAxis.scale.getExtent()[1];
225
+
226
+ const xAxis = myChart.getModel().getComponent('xAxis').axis;
227
+ const XAxisMin = Math.round(xAxis.scale.getExtent()[0]);
228
+ const XAxisMax = Math.round(xAxis.scale.getExtent()[1]);
229
+
230
+ //console.log(
231
+ // `${msgPrefix} X Axis Display Range: [${XAxisMin}, ${XAxisMax}]`,
232
+ // `${msgPrefix} Y Axis Display Range: [${yAxisMin}, ${yAxisMax}]`,
233
+ //);
234
+
235
+ return {
236
+ xMin: XAxisMin,
237
+ yMin: yAxisMin,
238
+ xMax: XAxisMax,
239
+ yMax: yAxisMax,
240
+ };
241
+ };
242
+
243
+ export const validateConfig = (config) => {
244
+ if (!config) {
245
+ throw new Error('config is required, minial config is {}');
246
+ }
247
+ // api or simpleload is required
248
+ if (!config.api && !config.simpleload) {
249
+ throw new Error('config.api or config.simpleload is required');
250
+ }
251
+ };
252
+
253
+ export const parseData = (fetched, config, sharedData) => {
254
+ if (!fetched) {
255
+ throw new Error('fetched data is not valid: ' + fetched);
256
+ }
257
+
258
+ //console.log(`fetched: `, fetched);
259
+
260
+ const metadata = fetched.metadata;
261
+ const genes = fetched.gene;
262
+
263
+ const {
264
+ colors: givenColors = [],
265
+ exclusion = [],
266
+ dotsize = 'auto',
267
+ } = config;
268
+ //console.log(`givenColors: `, givenColors);
269
+ let colourby = config.colourby;
270
+
271
+ if (colourby && sharedData[config.colourby]) {
272
+ colourby = sharedData[config.colourby].replace(' ', '_');
273
+ }
274
+ //console.log(`colourby: `, colourby);
275
+
276
+ // Exclude items if given by config
277
+ const category = [...new Set(metadata[colourby])].filter(
278
+ (_category) => !exclusion.includes(_category),
279
+ );
280
+ //console.log(`category: `, category);
281
+
282
+ const seriesAttr = processSeriesAttr(givenColors, category);
283
+ //console.log(`seriescolor: `, seriesAttr);
284
+
285
+ //console.log(fetched);
286
+ try {
287
+ return {
288
+ series: processData(
289
+ metadata,
290
+ genes,
291
+ colourby,
292
+ seriesAttr,
293
+ dotsize,
294
+ config?.mapping?.axis,
295
+ ),
296
+ legend: handleLegend(category),
297
+ visualMap: handleVisualMap(genes, config.visualmap),
298
+ title: fetched?.fetched_ID?.gene ?? '',
299
+ };
300
+ } catch (error) {
301
+ console.log(error);
302
+ throw new Error('Failed to parse data');
303
+ }
304
+ };
305
+
306
+ const processSymbolSize = (dotsize, totalcount) => {
307
+ if (typeof dotsize === 'number') {
308
+ return dotsize;
309
+ } else if (dotsize === 'auto' || typeof dotsize === 'object') {
310
+ const { dotFactor = 500, min = 2, max = 10 } = dotsize;
311
+ const sizeFactor = Math.max(1 - totalcount / dotFactor, 0.2);
312
+ return (max - min) * sizeFactor + min;
313
+ } else {
314
+ return 3;
315
+ }
316
+ };
317
+
318
+ const processSeriesAttr = (givenColors, category) => {
319
+ var __colors = givenColors;
320
+ let visualcolor = [];
321
+ let seriescolor = {};
322
+
323
+ if (!isEmpty(cached_color)) {
324
+ //console.log(`cached_color: `, cached_color);
325
+ seriescolor = cached_color;
326
+ }
327
+
328
+ if (Array.isArray(__colors) && __colors.length > 0) {
329
+ __colors.forEach((item) => {
330
+ if (item.color) visualcolor.push(item.color);
331
+ else visualcolor.push(rcolor());
332
+ });
333
+ category.forEach((celltype, index) => {
334
+ const cell = __colors.find((item) => item.name === celltype);
335
+ const _color = cell?.color ?? visualcolor[index];
336
+ const _symbol =
337
+ __colors.find((item) => item.name === celltype)?.symbol ??
338
+ 'circle';
339
+ seriescolor[celltype] = {
340
+ color: _color,
341
+ symbol: _symbol,
342
+ };
343
+ });
344
+ } else {
345
+ let no_zcolor = false;
346
+ if (visualcolor.length === 0) {
347
+ no_zcolor = true;
348
+ }
349
+ category.forEach((celltype, index) => {
350
+ if (no_zcolor) {
351
+ visualcolor.push(rcolor());
352
+ }
353
+ if (!seriescolor[celltype]) {
354
+ seriescolor[celltype] = {
355
+ color: rcolor(),
356
+ symbol: 'circle',
357
+ };
358
+ }
359
+ });
360
+ }
361
+
362
+ cached_color = seriescolor;
363
+ return seriescolor;
364
+ };
365
+
366
+ const processData = (
367
+ metadata,
368
+ genes,
369
+ colourby,
370
+ seriesAttr,
371
+ dotsize,
372
+ mapping,
373
+ ) => {
374
+ const { x = 'x', y = 'y', extra = [] } = mapping;
375
+ // get the key from metadata
376
+ const keys = Object.keys(metadata);
377
+ //console.log(`keys: `, keys);
378
+ // if colourby is not given, randomly pick one from keys
379
+ colourby = colourby ?? keys[Math.floor(Math.random() * keys.length)];
380
+ //console.log(`colourby: `, colourby);
381
+
382
+ // Aggregate data based on the "colourby" attribute
383
+ const aggregatedData = {};
384
+
385
+ if (metadata[colourby] === undefined) return Object.values(aggregatedData);
386
+
387
+ metadata[colourby].forEach((item, index) => {
388
+ if (!aggregatedData[item]) {
389
+ aggregatedData[item] = {
390
+ name: item,
391
+ type: 'scatter',
392
+ data: [],
393
+ itemStyle: {
394
+ color: seriesAttr[item].color,
395
+ },
396
+ symbol: seriesAttr[item].symbol,
397
+ symbolSize: processSymbolSize(
398
+ dotsize,
399
+ metadata[colourby].length,
400
+ ),
401
+ };
402
+ }
403
+
404
+ // console.log(metadata['Cell_ID'][index]);
405
+ // Todo: Leave for gene expression later
406
+ const extraProperties = {};
407
+ for (const property in mapping.extra) {
408
+ if (metadata[extra[property]]) {
409
+ extraProperties[property] = metadata[extra[property]][index];
410
+ }
411
+ }
412
+ // Todo: Leave for z-index of visualmap later
413
+
414
+ let Expression = genes ? genes[metadata['Cell_ID'][index]] : null;
415
+ aggregatedData[item].data.push({
416
+ value: [metadata[x][index], metadata[y][index], Expression ?? 0],
417
+ ...extraProperties,
418
+ Expression: Expression,
419
+ });
420
+ });
421
+
422
+ //console.log(`aggregatedData: `, aggregatedData);
423
+ // Convert aggregated data to an array of series
424
+ return Object.values(aggregatedData);
425
+ };
426
+
427
+ export async function onDataZoom(
428
+ props,
429
+ sharedData,
430
+ fetched_dat,
431
+ myChart,
432
+ option,
433
+ ) {
434
+ const bbox = outputAxisValues(myChart);
435
+
436
+ fetched_dat = await handleAPI(props.config, sharedData, bbox);
437
+
438
+ if (isEmpty(fetched_dat)) return;
439
+
440
+ var {
441
+ series: newseries,
442
+ legend,
443
+ visualMap,
444
+ title,
445
+ } = parseData(fetched_dat, props.config, sharedData);
446
+
447
+ const MAX_POINTS = 8000;
448
+
449
+ if (props.config.merge) {
450
+ // Ensure that both option.series and newseries are arrays
451
+ option.series = Array.isArray(option.series) ? option.series : [];
452
+ newseries = Array.isArray(newseries) ? newseries : [];
453
+
454
+ // Merge the existing series and new series while ensuring uniqueness based on the series name
455
+ const mergedSeries = [...option.series, ...newseries].reduce(
456
+ (uniqueSeries, currentSeries) => {
457
+ const existingIndex = uniqueSeries.findIndex(
458
+ (series) => series.name === currentSeries.name,
459
+ );
460
+
461
+ if (existingIndex === -1) {
462
+ // Series with this name doesn't exist yet, add it to the uniqueSeries array
463
+ uniqueSeries.push(currentSeries);
464
+ } else {
465
+ // Series with this name already exists, update its data
466
+ uniqueSeries[existingIndex].data = currentSeries.data;
467
+ }
468
+
469
+ return uniqueSeries;
470
+ },
471
+ [],
472
+ );
473
+
474
+ // Calculate the total number of points in the merged series
475
+ const totalPoints = mergedSeries.reduce(
476
+ (count, series) =>
477
+ (count += Array.isArray(series.data) ? series.data.length : 0),
478
+ 0,
479
+ );
480
+
481
+ // Check if the total number of points exceeds the MAX_POINTS limit
482
+ if (totalPoints > MAX_POINTS) {
483
+ // Calculate the number of excess points
484
+ let excess = totalPoints - MAX_POINTS;
485
+
486
+ // Iterate over the last option.series and reduce their data points
487
+ for (const series of option.series.slice().reverse()) {
488
+ if (excess > 0 && Array.isArray(series.data)) {
489
+ const removedPoints = series.data.splice(-excess);
490
+ excess -= removedPoints.length;
491
+ }
492
+ }
493
+ }
494
+
495
+ // Set the option's series to the merged series
496
+ option.series = mergedSeries;
497
+ } else {
498
+ option.series = newseries;
499
+ }
500
+
501
+ option.legend = {
502
+ ...option.legend,
503
+ ...legend,
504
+ };
505
+
506
+ option.visualMap = visualMap;
507
+ option.title = {
508
+ ...option.title,
509
+ text: title,
510
+ };
511
+ myChart.setOption(option);
512
+ return bbox;
513
+ }
514
+
515
+ const handleLegend = (category) => {
516
+ let legend = {};
517
+ let legendType = category.length > 10 ? 'scroll' : 'plain';
518
+
519
+ const sortedItems = category.slice().sort((a, b) => {
520
+ // Extract the character part from the strings
521
+ const charPartA = a.match(/[^0-9]+/)[0];
522
+ const charPartB = b.match(/[^0-9]+/)[0];
523
+
524
+ // Compare the character parts alphabetically
525
+ const charComparison = charPartA.localeCompare(charPartB);
526
+
527
+ if (charComparison !== 0) {
528
+ return charComparison;
529
+ }
530
+
531
+ // If the character parts are the same, extract and compare the numeric part
532
+ const numPartA = parseInt(a.match(/\d+/)[0], 10);
533
+ const numPartB = parseInt(b.match(/\d+/)[0], 10);
534
+
535
+ return numPartA - numPartB;
536
+ });
537
+
538
+ legend.orient = 'horizontal';
539
+ legend.top = 25;
540
+ legend.right = 'center';
541
+ legend.type = legendType;
542
+ legend.data = sortedItems;
543
+ return legend;
544
+ };
545
+
546
+ const handleVisualMap = (genes, visualmap = {}) => {
547
+ if (!genes) return [];
548
+ // Initialize max and min variables with the first value in the object
549
+
550
+ console.log(`visualmap in handleVisualMap: `, visualmap);
551
+
552
+
553
+ let maxValue = Number.NEGATIVE_INFINITY;
554
+ let minValue = Number.POSITIVE_INFINITY;
555
+ let visualMap = {};
556
+ // Iterate through the values of the object
557
+ for (const value of Object.values(genes)) {
558
+ // Compare the current value to the max and min values
559
+ if (value > maxValue) {
560
+ maxValue = value;
561
+ }
562
+ if (value < minValue) {
563
+ minValue = value;
564
+ }
565
+ }
566
+ //console.log(`handlesVisualMap: `, maxValue, minValue);
567
+ visualMap.min = minValue;
568
+ visualMap.max = maxValue;
569
+ visualMap.dimension = 2;
570
+ visualMap.orient = 'vertical';
571
+ visualMap.top = 'center';
572
+ visualMap.left = 0;
573
+ visualMap.text = ['log2\n(tpm+1)', ''];
574
+ visualMap.textGap = 10;
575
+ visualMap.calculable = true;
576
+ visualMap.inRange = {
577
+ color: ['#808080', '#FFA500', '#FF0000'],
578
+ };
579
+ visualMap.textStyle = {
580
+ writingMode: 'vertical-lr',
581
+ };
582
+
583
+ visualMap = { ...visualMap, ...visualmap };
584
+
585
+ //console.log(`handlesVisualMap: `, visualMap);
586
+ return visualMap;
587
+ };
@@ -0,0 +1,80 @@
1
+ /*
2
+ * @Author : Lihao leolihao@arizona.edu
3
+ * @Date : 2023-12-30 15:11:35
4
+ * @FilePath : /visualifyjs/src/core/components/ratio.js
5
+ * @Description :
6
+ * Copyright (c) 2023 by Lihao (leolihao@arizona.edu), All Rights Reserved.
7
+ */
8
+ import React, { useState, useEffect } from 'react';
9
+ import { useAppContext } from '../appContext';
10
+
11
+ function RatioBox({ props, style }) {
12
+ const { debug } = props;
13
+
14
+ const { style: blockStyle } = props;
15
+ // if debug is true, then border is red, else no border
16
+ style = {
17
+ ...style,
18
+ ...blockStyle,
19
+ border: debug ? '1px solid red' : 'none',
20
+ };
21
+
22
+ const { title } = props;
23
+ const renderTitle = () => {
24
+ return title && <h3>{title}</h3>;
25
+ };
26
+
27
+ const { setSharedData } = useAppContext();
28
+ const { choice = [] } = props;
29
+
30
+ // State to keep track of selected choice
31
+ const [selectedChoice, setSelectedChoice] = useState(
32
+ choice.length > 0 ? choice[0] : null,
33
+ );
34
+
35
+ // Handler for radio button change
36
+ const handleRadioChange = (event) => {
37
+ setSelectedChoice(event.target.value);
38
+ };
39
+
40
+ const {
41
+ id,
42
+ val,
43
+ ratio_style = {
44
+ margin: 'auto 15px',
45
+ },
46
+ } = props;
47
+
48
+ // Store the selected choice in shared data whenever it changes
49
+ useEffect(() => {
50
+ if (val) {
51
+ setSharedData((prevSharedData) => {
52
+ return { ...prevSharedData, [val]: selectedChoice };
53
+ });
54
+ }
55
+ }, [selectedChoice, val, setSharedData]);
56
+
57
+ return (
58
+ <div
59
+ key={id}
60
+ style={style}
61
+ className='ratio-box-container'>
62
+ {renderTitle()}
63
+ {choice.map((item, index) => (
64
+ <label
65
+ key={index}
66
+ style={ratio_style}>
67
+ <input
68
+ type='radio'
69
+ value={item}
70
+ checked={selectedChoice === item}
71
+ onChange={handleRadioChange}
72
+ />
73
+ {item}
74
+ </label>
75
+ ))}
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export default RatioBox;