visualifyjs 2.5.3-2.dev → 2.5.3-9-dev

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of visualifyjs might be problematic. Click here for more details.

Files changed (139) hide show
  1. package/.github/workflows/{static.yml.bak → build.yaml} +51 -51
  2. package/LICENSE +674 -674
  3. package/README.md +40 -58
  4. package/config-overrides.js +31 -31
  5. package/dist/visualify.js +3 -3
  6. package/docs/CLI.md +15 -0
  7. package/docs/{docs/README.md → README.md} +41 -65
  8. package/docs/{docs/Rechart → Rechart}/bar.md +190 -190
  9. package/docs/{docs/Rechart → Rechart}/funnel.md +193 -241
  10. package/docs/{docs/Rechart → Rechart}/line.md +355 -355
  11. package/docs/{docs/Rechart → Rechart}/pie.md +225 -225
  12. package/docs/{docs/Rechart → Rechart}/radar.md +253 -253
  13. package/docs/{docs/_404.md → _404.md} +51 -51
  14. package/docs/{docs/_coverpage.md → _coverpage.md} +11 -11
  15. package/docs/{docs/_sidebar.md → _sidebar.md} +42 -44
  16. package/docs/{docs/components → components}/dotBio.md +34 -34
  17. package/docs/{docs/components → components}/echart.md +82 -82
  18. package/docs/{docs/components → components}/html.md +34 -34
  19. package/docs/{docs/components → components}/macaron.md +145 -145
  20. package/docs/components/markdown.md +0 -0
  21. package/docs/{docs/components → components}/more.md +142 -142
  22. package/docs/{docs/components → components}/plotly.md +62 -62
  23. package/docs/{docs/components → components}/scatterL.md +70 -70
  24. package/docs/{docs/components → components}/visium.md +56 -56
  25. package/docs/{docs/configuration.md → configuration.md} +123 -121
  26. package/docs/{docs/deploy.md → deploy.md} +23 -31
  27. package/docs/index.html +70 -70
  28. package/docs/log.md +1 -0
  29. package/docs/manifest.json +23 -23
  30. package/docs/{docs/more-pages.md → more-pages.md} +23 -23
  31. package/docs/{docs/quickstart.md → quickstart.md} +115 -124
  32. package/docs/{docs/rechart-attributes.md → rechart-attributes.md} +74 -74
  33. package/docs/{docs/rechart-basic-usage.md → rechart-basic-usage.md} +162 -162
  34. package/docs/static/css/fluff-stuff.css +169 -169
  35. package/docs/static/css/font-awesome.min.css +4 -4
  36. package/docs/static/css/visualify.css +25 -25
  37. package/docs/static/js/configuration.js +448 -448
  38. package/docs/static/js/visualify.js +24 -23
  39. package/docs/theme.md +3 -0
  40. package/package.json +74 -83
  41. package/rollup.config.mjs +75 -75
  42. package/src/_css/404.css +115 -115
  43. package/src/_css/App.css +37 -37
  44. package/src/_css/autoSuggestion.css +26 -26
  45. package/src/_css/circular-progress.css +32 -32
  46. package/src/_css/index.css +36 -36
  47. package/src/_css/modern.css +24 -24
  48. package/src/_media/corner.svg +8 -8
  49. package/src/_media/download.svg +3 -3
  50. package/src/_media/logo.svg +14 -14
  51. package/src/_test/App.test.js +15 -15
  52. package/src/_utils/reportWebVitals.js +13 -13
  53. package/src/core/appContext.js +27 -27
  54. package/src/core/components/Scatter.js +188 -188
  55. package/src/core/components/ScatterBio.js +572 -572
  56. package/src/core/components/VisiumPlot.js +165 -165
  57. package/src/core/components/browser.js +42 -42
  58. package/src/core/components/dotplot.js +413 -413
  59. package/src/core/components/html.js +29 -29
  60. package/src/core/components/list.js +178 -178
  61. package/src/core/components/macaron.js +201 -201
  62. package/src/core/components/markdown.js +56 -56
  63. package/src/core/components/parser.scatterBio.js +579 -587
  64. package/src/core/components/ratio.js +80 -80
  65. package/src/core/components/scatterL.js +173 -173
  66. package/src/core/components/searchbar.js +131 -131
  67. package/src/core/components/selection.js +193 -193
  68. package/src/core/components/timeline.js +281 -281
  69. package/src/core/components/visium.js +97 -97
  70. package/src/core/fetch/condfetch.js +82 -82
  71. package/src/core/fetch/fetch.js +92 -92
  72. package/src/core/fetch/json.js +29 -29
  73. package/src/core/fetch/vfetch.js +42 -42
  74. package/src/core/liveEditor.js +44 -44
  75. package/src/core/modules/codeEditorWithPreview.js +104 -104
  76. package/src/core/modules/echarts/common.js +20 -20
  77. package/src/core/modules/echarts/presetHandler.js +41 -41
  78. package/src/core/modules/echarts/presets/esodev.chromium.js +172 -172
  79. package/src/core/modules/echarts/presets/esodev.codex.js +130 -130
  80. package/src/core/modules/echarts/presets/esodev.visium.js +123 -123
  81. package/src/core/modules/echarts/presets/mmtrbc.js +186 -186
  82. package/src/core/modules/echarts.js +71 -71
  83. package/src/core/modules/echartsUtils.js +43 -43
  84. package/src/core/modules/echartswitcher.js +152 -152
  85. package/src/core/modules/replotly/presetHandler.js +24 -24
  86. package/src/core/modules/replotly/presets/minimum.js +18 -18
  87. package/src/core/modules/replotly/presets/mmtrbc.dot.js +114 -114
  88. package/src/core/modules/replotly/presets/mmtrbc.violin.js +100 -100
  89. package/src/core/modules/replotly.js +71 -71
  90. package/src/core/pages/404.js +50 -50
  91. package/src/core/pages/error.js +27 -27
  92. package/src/core/pages/jsonPage.js +62 -62
  93. package/src/core/pages/loading.js +44 -44
  94. package/src/core/parser/echart.data.js +183 -183
  95. package/src/core/parser/echart.features.js +125 -125
  96. package/src/core/parser/echart.general.js +143 -147
  97. package/src/core/parser/echart.hilbert.js +57 -57
  98. package/src/core/parser/echart.parser.js +210 -210
  99. package/src/core/parser/echart.series.js +67 -67
  100. package/src/core/parser/echart.types.js +76 -76
  101. package/src/core/parser/plotly.config.js +10 -10
  102. package/src/core/parser/plotly.data.js +132 -132
  103. package/src/core/parser/plotly.layout.js +9 -9
  104. package/src/core/parser/plotly.violin.js +18 -18
  105. package/src/core/recharts.js +62 -62
  106. package/src/core/router/alias.js +49 -49
  107. package/src/core/router/jsonRouter.js +31 -31
  108. package/src/core/themes/modern.js +32 -32
  109. package/src/core/themes/themeSelector.js +33 -33
  110. package/src/core/visualify.js +47 -47
  111. package/src/core/widgets/circularProgress.js +23 -23
  112. package/src/core/widgets/controller.js +83 -83
  113. package/src/core/widgets/errorBoundary.js +36 -36
  114. package/src/core/widgets/footer.js +177 -177
  115. package/src/core/widgets/header.js +234 -234
  116. package/src/core/widgets/layout/Grid.js +31 -31
  117. package/src/core/widgets/layout.js +36 -36
  118. package/src/core/widgets/mapping.js +42 -42
  119. package/src/index.js +62 -62
  120. package/src/setupTests.js +5 -5
  121. package/docs/docs/CLI.md +0 -34
  122. package/docs/docs/Rechart/scatter.md +0 -298
  123. package/docs/docs/log.md +0 -9
  124. package/docs/docs/static/logo/favicon.ico +0 -0
  125. package/docs/docs/static/logo/logo_128x128.png +0 -0
  126. package/docs/docs/static/logo/logo_192x192.png +0 -0
  127. package/docs/docs/static/logo/logo_256x256.png +0 -0
  128. package/docs/docs/static/logo/logo_512x512.png +0 -0
  129. package/docs/docs/static/logo/logo_64x64.png +0 -0
  130. package/docs/docs/theme.md +0 -5
  131. /package/docs/{docs/Rechart → Rechart}/geo.md +0 -0
  132. /package/docs/{docs/Rechart → Rechart}/liquidfill.md +0 -0
  133. /package/docs/{docs/Rechart → Rechart}/polar.md +0 -0
  134. /package/docs/{docs/Rechart → Rechart}/sankey.md +0 -0
  135. /package/docs/{docs/Rechart/sunburst.md → Rechart/scatter.md} +0 -0
  136. /package/docs/{docs/Rechart/tree.md → Rechart/sunburst.md} +0 -0
  137. /package/docs/{docs/Rechart/wordcloud.md → Rechart/tree.md} +0 -0
  138. /package/docs/{docs/components/markdown.md → Rechart/wordcloud.md} +0 -0
  139. /package/docs/{docs/static → static}/_images/deploy-github-pages.png +0 -0
@@ -1,587 +1,579 @@
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
- };
1
+ /*
2
+ * @Author : Lihao leolihao@arizona.edu
3
+ * @Date : 2023-11-12 17:35:02
4
+ * @FilePath : /visualifyjs/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(`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
+ const metadata = fetched.metadata;
259
+ const genes = fetched.gene;
260
+
261
+ const {
262
+ colors: givenColors = [],
263
+ exclusion = [],
264
+ dotsize = 'auto',
265
+ } = config;
266
+ //console.log(`givenColors: `, givenColors);
267
+ let colourby = config.colourby;
268
+
269
+ if (colourby && sharedData[config.colourby]) {
270
+ colourby = sharedData[config.colourby].replace(' ', '_');
271
+ }
272
+ //console.log(`colourby: `, colourby);
273
+
274
+ // Exclude items if given by config
275
+ const category = [...new Set(metadata[colourby])].filter(
276
+ (_category) => !exclusion.includes(_category),
277
+ );
278
+ //console.log(`category: `, category);
279
+
280
+ const seriesAttr = processSeriesAttr(givenColors, category);
281
+ //console.log(`seriescolor: `, seriesAttr);
282
+
283
+ //console.log(fetched);
284
+ try {
285
+ return {
286
+ series: processData(
287
+ metadata,
288
+ genes,
289
+ colourby,
290
+ seriesAttr,
291
+ dotsize,
292
+ config?.mapping?.axis,
293
+ ),
294
+ legend: handleLegend(category),
295
+ visualMap: handleVisualMap(genes),
296
+ title: fetched?.fetched_ID?.gene ?? '',
297
+ };
298
+ } catch (error) {
299
+ console.log(error);
300
+ throw new Error('Failed to parse data');
301
+ }
302
+ };
303
+
304
+ const processSymbolSize = (dotsize, totalcount) => {
305
+ if (typeof dotsize === 'number') {
306
+ return dotsize;
307
+ } else if (dotsize === 'auto' || typeof dotsize === 'object') {
308
+ const { dotFactor = 500, min = 2, max = 10 } = dotsize;
309
+ const sizeFactor = Math.max(1 - totalcount / dotFactor, 0.2);
310
+ return (max - min) * sizeFactor + min;
311
+ } else {
312
+ return 3;
313
+ }
314
+ };
315
+
316
+ const processSeriesAttr = (givenColors, category) => {
317
+ var __colors = givenColors;
318
+ let visualcolor = [];
319
+ let seriescolor = {};
320
+
321
+ if (!isEmpty(cached_color)) {
322
+ //console.log(`cached_color: `, cached_color);
323
+ seriescolor = cached_color;
324
+ }
325
+
326
+ if (Array.isArray(__colors) && __colors.length > 0) {
327
+ __colors.forEach((item) => {
328
+ if (item.color) visualcolor.push(item.color);
329
+ else visualcolor.push(rcolor());
330
+ });
331
+ category.forEach((celltype, index) => {
332
+ const cell = __colors.find((item) => item.name === celltype);
333
+ const _color = cell?.color ?? visualcolor[index];
334
+ const _symbol =
335
+ __colors.find((item) => item.name === celltype)?.symbol ??
336
+ 'circle';
337
+ seriescolor[celltype] = {
338
+ color: _color,
339
+ symbol: _symbol,
340
+ };
341
+ });
342
+ } else {
343
+ let no_zcolor = false;
344
+ if (visualcolor.length === 0) {
345
+ no_zcolor = true;
346
+ }
347
+ category.forEach((celltype, index) => {
348
+ if (no_zcolor) {
349
+ visualcolor.push(rcolor());
350
+ }
351
+ if (!seriescolor[celltype]) {
352
+ seriescolor[celltype] = {
353
+ color: rcolor(),
354
+ symbol: 'circle',
355
+ };
356
+ }
357
+ });
358
+ }
359
+
360
+ cached_color = seriescolor;
361
+ return seriescolor;
362
+ };
363
+
364
+ const processData = (
365
+ metadata,
366
+ genes,
367
+ colourby,
368
+ seriesAttr,
369
+ dotsize,
370
+ mapping,
371
+ ) => {
372
+ const { x = 'x', y = 'y', extra = [] } = mapping;
373
+ // get the key from metadata
374
+ const keys = Object.keys(metadata);
375
+ //console.log(`keys: `, keys);
376
+ // if colourby is not given, randomly pick one from keys
377
+ colourby = colourby ?? keys[Math.floor(Math.random() * keys.length)];
378
+ //console.log(`colourby: `, colourby);
379
+
380
+ // Aggregate data based on the "colourby" attribute
381
+ const aggregatedData = {};
382
+
383
+ if (metadata[colourby] === undefined) return Object.values(aggregatedData);
384
+
385
+ metadata[colourby].forEach((item, index) => {
386
+ if (!aggregatedData[item]) {
387
+ aggregatedData[item] = {
388
+ name: item,
389
+ type: 'scatter',
390
+ data: [],
391
+ itemStyle: {
392
+ color: seriesAttr[item].color,
393
+ },
394
+ symbol: seriesAttr[item].symbol,
395
+ symbolSize: processSymbolSize(
396
+ dotsize,
397
+ metadata[colourby].length,
398
+ ),
399
+ };
400
+ }
401
+
402
+ // console.log(metadata['Cell_ID'][index]);
403
+ // Todo: Leave for gene expression later
404
+ const extraProperties = {};
405
+ for (const property in mapping.extra) {
406
+ if (metadata[extra[property]]) {
407
+ extraProperties[property] = metadata[extra[property]][index];
408
+ }
409
+ }
410
+ // Todo: Leave for z-index of visualmap later
411
+
412
+ let Expression = genes ? genes[metadata['Cell_ID'][index]] : null;
413
+ aggregatedData[item].data.push({
414
+ value: [metadata[x][index], metadata[y][index], Expression ?? 0],
415
+ ...extraProperties,
416
+ Expression: Expression,
417
+ });
418
+ });
419
+
420
+ //console.log(`aggregatedData: `, aggregatedData);
421
+ // Convert aggregated data to an array of series
422
+ return Object.values(aggregatedData);
423
+ };
424
+
425
+ export async function onDataZoom(
426
+ props,
427
+ sharedData,
428
+ fetched_dat,
429
+ myChart,
430
+ option,
431
+ ) {
432
+ const bbox = outputAxisValues(myChart);
433
+
434
+ fetched_dat = await handleAPI(props.config, sharedData, bbox);
435
+
436
+ if (isEmpty(fetched_dat)) return;
437
+
438
+ var {
439
+ series: newseries,
440
+ legend,
441
+ visualMap,
442
+ title,
443
+ } = parseData(fetched_dat, props.config, sharedData);
444
+
445
+ const MAX_POINTS = 8000;
446
+
447
+ if (props.config.merge) {
448
+ // Ensure that both option.series and newseries are arrays
449
+ option.series = Array.isArray(option.series) ? option.series : [];
450
+ newseries = Array.isArray(newseries) ? newseries : [];
451
+
452
+ // Merge the existing series and new series while ensuring uniqueness based on the series name
453
+ const mergedSeries = [...option.series, ...newseries].reduce(
454
+ (uniqueSeries, currentSeries) => {
455
+ const existingIndex = uniqueSeries.findIndex(
456
+ (series) => series.name === currentSeries.name,
457
+ );
458
+
459
+ if (existingIndex === -1) {
460
+ // Series with this name doesn't exist yet, add it to the uniqueSeries array
461
+ uniqueSeries.push(currentSeries);
462
+ } else {
463
+ // Series with this name already exists, update its data
464
+ uniqueSeries[existingIndex].data = currentSeries.data;
465
+ }
466
+
467
+ return uniqueSeries;
468
+ },
469
+ [],
470
+ );
471
+
472
+ // Calculate the total number of points in the merged series
473
+ const totalPoints = mergedSeries.reduce(
474
+ (count, series) =>
475
+ (count += Array.isArray(series.data) ? series.data.length : 0),
476
+ 0,
477
+ );
478
+
479
+ // Check if the total number of points exceeds the MAX_POINTS limit
480
+ if (totalPoints > MAX_POINTS) {
481
+ // Calculate the number of excess points
482
+ let excess = totalPoints - MAX_POINTS;
483
+
484
+ // Iterate over the last option.series and reduce their data points
485
+ for (const series of option.series.slice().reverse()) {
486
+ if (excess > 0 && Array.isArray(series.data)) {
487
+ const removedPoints = series.data.splice(-excess);
488
+ excess -= removedPoints.length;
489
+ }
490
+ }
491
+ }
492
+
493
+ // Set the option's series to the merged series
494
+ option.series = mergedSeries;
495
+ } else {
496
+ option.series = newseries;
497
+ }
498
+
499
+ option.legend = {
500
+ ...option.legend,
501
+ ...legend,
502
+ };
503
+
504
+ option.visualMap = visualMap;
505
+ option.title = {
506
+ ...option.title,
507
+ text: title,
508
+ };
509
+ myChart.setOption(option);
510
+ return bbox;
511
+ }
512
+
513
+ const handleLegend = (category) => {
514
+ let legend = {};
515
+ let legendType = category.length > 10 ? 'scroll' : 'plain';
516
+
517
+ const sortedItems = category.slice().sort((a, b) => {
518
+ // Extract the character part from the strings
519
+ const charPartA = a.match(/[^0-9]+/)[0];
520
+ const charPartB = b.match(/[^0-9]+/)[0];
521
+
522
+ // Compare the character parts alphabetically
523
+ const charComparison = charPartA.localeCompare(charPartB);
524
+
525
+ if (charComparison !== 0) {
526
+ return charComparison;
527
+ }
528
+
529
+ // If the character parts are the same, extract and compare the numeric part
530
+ const numPartA = parseInt(a.match(/\d+/)[0], 10);
531
+ const numPartB = parseInt(b.match(/\d+/)[0], 10);
532
+
533
+ return numPartA - numPartB;
534
+ });
535
+
536
+ legend.orient = 'horizontal';
537
+ legend.top = 25;
538
+ legend.right = 'center';
539
+ legend.type = legendType;
540
+ legend.data = sortedItems;
541
+ return legend;
542
+ };
543
+
544
+ const handleVisualMap = (genes) => {
545
+ if (!genes) return [];
546
+ // Initialize max and min variables with the first value in the object
547
+ let maxValue = Number.NEGATIVE_INFINITY;
548
+ let minValue = Number.POSITIVE_INFINITY;
549
+ let visualMap = {};
550
+ // Iterate through the values of the object
551
+ for (const value of Object.values(genes)) {
552
+ // Compare the current value to the max and min values
553
+ if (value > maxValue) {
554
+ maxValue = value;
555
+ }
556
+ if (value < minValue) {
557
+ minValue = value;
558
+ }
559
+ }
560
+ //console.log(`handlesVisualMap: `, maxValue, minValue);
561
+ visualMap.min = minValue;
562
+ visualMap.max = maxValue;
563
+ visualMap.dimension = 2;
564
+ visualMap.orient = 'vertical';
565
+ visualMap.top = 'center';
566
+ visualMap.left = 0;
567
+ visualMap.text = ['log2\n(tpm+1)', ''];
568
+ visualMap.textGap = 10;
569
+ visualMap.calculable = true;
570
+ visualMap.inRange = {
571
+ color: ['#808080', '#FFA500', '#FF0000'],
572
+ };
573
+ visualMap.textStyle = {
574
+ writingMode: 'vertical-lr',
575
+ };
576
+
577
+ //console.log(`handlesVisualMap: `, visualMap);
578
+ return visualMap;
579
+ };