visualifyjs 2.5.3 → 3.0.0

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.
Files changed (252) hide show
  1. package/.claude/mem/TIMELINE.md +36 -0
  2. package/.claude/mem/notes/2026-02-11-3d-visualization-docs-fix-external-script-solution.md +24 -0
  3. package/.claude/mem/notes/2026-02-11-3d-visualization-docs-fix-session-summary.md +43 -0
  4. package/.claude/mem/notes/2026-02-11-cli-fix-editor-command-alias.md +26 -0
  5. package/.claude/mem/notes/2026-02-11-phase-3-developer-experience-completed.md +51 -0
  6. package/.claude/mem/notes/2026-02-11-phase-4-web-workers-implementation-complete.md +59 -0
  7. package/.claude/mem/notes/2026-02-11-visualify-phase-2-3d-visualization-complete.md +50 -0
  8. package/.claude/mem/notes/2026-02-11-visualify-phase-2-committed-ready-for-phase-3.md +33 -0
  9. package/.claude/mem/notes/2026-02-11-visualify-phase-3-complete-developer-experience.md +52 -0
  10. package/.claude/mem/notes/2026-02-11-visualify-repository-cleanup-complete.md +28 -0
  11. package/.claude/mem/notes/2026-02-18-codebase-cleanup-docsify-plugin-documentation.md +37 -0
  12. package/.claude/mem/notes/2026-02-19-css-grid-layout-fix-displaycontents-on-vcontroller.md +18 -0
  13. package/.claude/mem/notes/2026-02-19-docsify-plugin-fixes-latex-and-visualify-code-bloc.md +26 -0
  14. package/.claude/mem/notes/2026-02-19-page-mode-docs-update-decisions.md +23 -0
  15. package/.claude/mem/notes/2026-02-19-react-context-infinite-re-render-loop-fix-pattern.md +31 -0
  16. package/.claude/mem/notes/2026-02-19-version-300-bump-and-build-fixes.md +32 -0
  17. package/.claude/mem/notes/2026-02-19-visualify-build-deployment-architecture-bug-fixes.md +25 -0
  18. package/.claude/mem/notes/2026-02-19-visualify-dist-iife-self-contained-build-config.md +30 -0
  19. package/.claude/mem/notes/2026-02-19-visualify-infinite-loop-i18n-fixes.md +31 -0
  20. package/.claude/mem/notes/2026-02-19-visualify-v3-bundle-splitting-docs-restructuring.md +32 -0
  21. package/.claude/mem/notes/2026-02-20-bundle-externalization-final-architecture.md +29 -0
  22. package/.claude/mem/notes/2026-02-20-chromium-page-fix-unstable-keys-and-double-event-b.md +27 -0
  23. package/.claude/mem/notes/2026-02-20-console-cleanup-bundle-optimization-commit.md +20 -0
  24. package/.claude/mem/notes/2026-02-20-dotbio-dot-plot-fix-useeffect-dependency.md +21 -0
  25. package/.claude/mem/notes/2026-02-20-public-folder-cleanup-and-readme-rewrite.md +25 -0
  26. package/.claude/mem/notes/2026-02-20-v300-release-and-beta-channel-strategy.md +29 -0
  27. package/.claude/mem/notes/2026-02-20-visium-background-image-unknown-legend-fix.md +19 -0
  28. package/.claude/mem/notes/2026-02-20-visualify-cdn-loader-bundle-externalization.md +34 -0
  29. package/.claude/mem/sessions/session-2026-02-20-031524.md +54 -0
  30. package/.claude/settings.local.json +21 -0
  31. package/.github/workflows/static.yml.bak +51 -51
  32. package/.sisyphus/boulder.json +65 -0
  33. package/.sisyphus/plans/phase-4-advanced-optimizations.md +217 -0
  34. package/LICENSE +674 -674
  35. package/README.md +94 -59
  36. package/config-overrides.js +31 -31
  37. package/dist/stats.html +4949 -0
  38. package/dist/visualify-3d.esm.js +1 -0
  39. package/dist/visualify-3d.js +1 -0
  40. package/dist/visualify-core.esm.js +1 -0
  41. package/dist/visualify-core.js +1 -0
  42. package/dist/visualify-docs.esm.js +1 -0
  43. package/dist/visualify-docs.js +1 -0
  44. package/dist/visualify-loader.js +1 -0
  45. package/dist/visualify-pages.esm.js +1 -0
  46. package/dist/visualify-pages.js +1 -0
  47. package/dist/visualify-portal.esm.js +1 -0
  48. package/dist/visualify-portal.js +1 -0
  49. package/dist/visualify-shared.js +26571 -0
  50. package/dist/visualify.js +1 -188
  51. package/docs/CHANGELOG.md +148 -0
  52. package/docs/cli/commands.md +513 -0
  53. package/docs/configuration/visualify-json.md +474 -0
  54. package/docs/docs/3d-visualization.md +374 -0
  55. package/docs/docs/CLI.md +303 -34
  56. package/docs/docs/README.md +65 -65
  57. package/docs/docs/Rechart/bar.md +190 -190
  58. package/docs/docs/Rechart/funnel.md +241 -193
  59. package/docs/docs/Rechart/line.md +355 -355
  60. package/docs/docs/Rechart/pie.md +225 -225
  61. package/docs/docs/Rechart/radar.md +253 -253
  62. package/docs/docs/Rechart/scatter.md +298 -0
  63. package/docs/docs/_404.md +51 -51
  64. package/docs/docs/_coverpage.md +11 -11
  65. package/docs/docs/_sidebar.md +54 -43
  66. package/docs/docs/components/dotBio.md +87 -34
  67. package/docs/docs/components/echart.md +171 -82
  68. package/docs/docs/components/html.md +61 -34
  69. package/docs/docs/components/macaron.md +156 -145
  70. package/docs/docs/components/markdown.md +42 -0
  71. package/docs/docs/components/more.md +183 -142
  72. package/docs/docs/components/plotly.md +132 -62
  73. package/docs/docs/components/scatterL.md +171 -70
  74. package/docs/docs/components/visium.md +112 -57
  75. package/docs/docs/configuration.md +121 -123
  76. package/docs/docs/deploy.md +31 -31
  77. package/docs/docs/docsify-plugin.md +655 -0
  78. package/docs/docs/hmr.md +165 -0
  79. package/docs/docs/i18n.md +332 -0
  80. package/docs/docs/log.md +30 -1
  81. package/docs/docs/more-pages.md +23 -23
  82. package/docs/docs/quickstart.md +148 -119
  83. package/docs/docs/rechart-attributes.md +74 -74
  84. package/docs/docs/rechart-basic-usage.md +160 -162
  85. package/docs/docs/theme.md +5 -5
  86. package/docs/docs/typescript.md +306 -0
  87. package/docs/docs/visual-editor.md +359 -0
  88. package/docs/index.html +85 -71
  89. package/docs/manifest.json +23 -23
  90. package/docs/migration/v3-migration.md +392 -0
  91. package/docs/static/css/fluff-stuff.css +169 -169
  92. package/docs/static/css/font-awesome.min.css +4 -4
  93. package/docs/static/css/visualify.css +6 -25
  94. package/docs/static/js/3d-viz-examples.js +181 -0
  95. package/docs/static/js/configuration.js +630 -448
  96. package/docs/static/js/visualify.js +1 -188
  97. package/package.json +106 -84
  98. package/rollup.config.mjs +766 -76
  99. package/src/_css/404.css +115 -115
  100. package/src/_css/App.css +37 -37
  101. package/src/_css/autoSuggestion.css +26 -26
  102. package/src/_css/circular-progress.css +32 -32
  103. package/src/_css/index.css +36 -36
  104. package/src/_css/modern.css +350 -25
  105. package/src/_media/corner.svg +8 -8
  106. package/src/_media/download.svg +3 -3
  107. package/src/_media/logo.svg +14 -14
  108. package/src/_test/App.test.js +15 -15
  109. package/src/_utils/reportWebVitals.js +13 -13
  110. package/src/a11y/README.md +177 -0
  111. package/src/a11y/aria-labels.js +339 -0
  112. package/src/a11y/color-contrast.js +535 -0
  113. package/src/a11y/index.js +197 -0
  114. package/src/a11y/keyboard-nav.js +523 -0
  115. package/src/a11y/styles.css +165 -0
  116. package/src/cli/commands/dev.js +214 -0
  117. package/src/cli/commands/docs.js +521 -0
  118. package/src/cli/commands/edit.js +379 -0
  119. package/src/cli/commands/init.js +213 -0
  120. package/src/cli/commands/portal.js +236 -0
  121. package/src/cli/dev-server.js +530 -0
  122. package/src/cli/hmr.js +456 -0
  123. package/src/cli/index.js +180 -0
  124. package/src/cli/utils/config.js +207 -0
  125. package/src/cli/utils/logger.js +241 -0
  126. package/src/config/defaults.ts +122 -0
  127. package/src/config/index.ts +72 -0
  128. package/src/config/loader.ts +478 -0
  129. package/src/config/schema.ts +227 -0
  130. package/src/config/validator.ts +337 -0
  131. package/src/core/appContext.js +34 -27
  132. package/src/core/components/Bar.js +383 -0
  133. package/src/core/components/Bar3D.js +473 -0
  134. package/src/core/components/LargeDatasetChart.js +296 -0
  135. package/src/core/components/Line3D.js +310 -0
  136. package/src/core/components/Scatter.js +392 -188
  137. package/src/core/components/Scatter3D.js +455 -0
  138. package/src/core/components/ScatterBio.js +601 -572
  139. package/src/core/components/Surface3D.js +326 -0
  140. package/src/core/components/ThreeCustom.js +648 -0
  141. package/src/core/components/ThreeScene.js +459 -0
  142. package/src/core/components/VisiumPlot.js +191 -165
  143. package/src/core/components/browser.js +42 -42
  144. package/src/core/components/dotplot.js +413 -413
  145. package/src/core/components/html.js +29 -29
  146. package/src/core/components/list.js +178 -178
  147. package/src/core/components/macaron.js +206 -201
  148. package/src/core/components/markdown.js +56 -56
  149. package/src/core/components/parser.scatterBio.js +582 -579
  150. package/src/core/components/ratio.js +82 -80
  151. package/src/core/components/scatterL.js +206 -173
  152. package/src/core/components/searchbar.js +156 -131
  153. package/src/core/components/selection.js +310 -193
  154. package/src/core/components/timeline.js +236 -281
  155. package/src/core/components/visium.js +114 -97
  156. package/src/core/data-processor.js +413 -0
  157. package/src/core/fetch/condfetch.js +82 -82
  158. package/src/core/fetch/fetch.js +92 -92
  159. package/src/core/fetch/json.js +29 -29
  160. package/src/core/fetch/vfetch.js +42 -42
  161. package/src/core/hmr-client.js +724 -0
  162. package/src/core/liveEditor.js +44 -44
  163. package/src/core/modules/codeEditorWithPreview.js +104 -104
  164. package/src/core/modules/echarts/common.js +20 -20
  165. package/src/core/modules/echarts/gl.js +228 -0
  166. package/src/core/modules/echarts/presetHandler.js +41 -41
  167. package/src/core/modules/echarts/presets/esodev.chromium.js +172 -172
  168. package/src/core/modules/echarts/presets/esodev.codex.js +130 -130
  169. package/src/core/modules/echarts/presets/esodev.visium.js +123 -123
  170. package/src/core/modules/echarts/presets/mmtrbc.js +186 -186
  171. package/src/core/modules/echarts.js +70 -71
  172. package/src/core/modules/echartsUtils.js +43 -43
  173. package/src/core/modules/echartswitcher.js +227 -152
  174. package/src/core/modules/replotly/presetHandler.js +24 -24
  175. package/src/core/modules/replotly/presets/minimum.js +18 -18
  176. package/src/core/modules/replotly/presets/mmtrbc.dot.js +114 -114
  177. package/src/core/modules/replotly/presets/mmtrbc.violin.js +100 -100
  178. package/src/core/modules/replotly.js +74 -71
  179. package/src/core/modules/threejs/Camera.js +373 -0
  180. package/src/core/modules/threejs/Lighting.js +459 -0
  181. package/src/core/modules/threejs/Renderer.js +364 -0
  182. package/src/core/modules/threejs/Scene.js +266 -0
  183. package/src/core/modules/threejs/index.js +155 -0
  184. package/src/core/pages/404.js +50 -50
  185. package/src/core/pages/error.js +27 -27
  186. package/src/core/pages/jsonPage.js +62 -62
  187. package/src/core/pages/loading.js +44 -44
  188. package/src/core/parser/echart.data.js +204 -183
  189. package/src/core/parser/echart.features.js +125 -125
  190. package/src/core/parser/echart.general.js +147 -143
  191. package/src/core/parser/echart.hilbert.js +57 -57
  192. package/src/core/parser/echart.parser.js +210 -210
  193. package/src/core/parser/echart.series.js +67 -67
  194. package/src/core/parser/echart.types.js +76 -76
  195. package/src/core/parser/plotly.config.js +10 -10
  196. package/src/core/parser/plotly.data.js +132 -132
  197. package/src/core/parser/plotly.layout.js +9 -9
  198. package/src/core/parser/plotly.violin.js +18 -18
  199. package/src/core/recharts.js +361 -62
  200. package/src/core/router/alias.js +49 -49
  201. package/src/core/router/jsonRouter.js +31 -31
  202. package/src/core/themes/modern.js +32 -32
  203. package/src/core/themes/themeSelector.js +33 -33
  204. package/src/core/visualify.js +213 -47
  205. package/src/core/widgets/circularProgress.js +23 -23
  206. package/src/core/widgets/controller.js +116 -83
  207. package/src/core/widgets/errorBoundary.js +36 -36
  208. package/src/core/widgets/footer.js +185 -177
  209. package/src/core/widgets/header.js +238 -234
  210. package/src/core/widgets/layout/Grid.js +31 -31
  211. package/src/core/widgets/layout.js +36 -36
  212. package/src/core/widgets/mapping.js +56 -42
  213. package/src/core/workers/data-worker.js +349 -0
  214. package/src/core/workers/worker-pool.js +396 -0
  215. package/src/docsify/bundle.js +215 -0
  216. package/src/docsify/markdown.js +271 -0
  217. package/src/docsify/plugin.js +268 -0
  218. package/src/editor/README.md +172 -0
  219. package/src/editor/components/ChartBuilder.jsx +341 -0
  220. package/src/editor/components/ChartTypeSidebar.jsx +91 -0
  221. package/src/editor/components/Editor.jsx +367 -0
  222. package/src/editor/components/Preview.jsx +446 -0
  223. package/src/editor/components/PropertyPanel.jsx +468 -0
  224. package/src/editor/components/StatusBar.jsx +85 -0
  225. package/src/editor/context/EditorContext.js +248 -0
  226. package/src/editor/hooks/useDebounce.js +32 -0
  227. package/src/editor/index.js +315 -0
  228. package/src/editor/styles/editor.css +637 -0
  229. package/src/editor/utils/chartValidator.js +263 -0
  230. package/src/entries/charts3d.js +70 -0
  231. package/src/entries/core.js +78 -0
  232. package/src/entries/docs.js +154 -0
  233. package/src/entries/pages.js +93 -0
  234. package/src/entries/portal.js +204 -0
  235. package/src/entries/shared.js +50 -0
  236. package/src/i18n/formatters.js +455 -0
  237. package/src/i18n/index.js +169 -0
  238. package/src/i18n/locales/ar.json +137 -0
  239. package/src/i18n/locales/de.json +137 -0
  240. package/src/i18n/locales/en.json +137 -0
  241. package/src/i18n/locales/es.json +137 -0
  242. package/src/i18n/locales/he.json +137 -0
  243. package/src/i18n/locales/zh.json +137 -0
  244. package/src/i18n/rtl.css +183 -0
  245. package/src/index.js +82 -62
  246. package/src/loader.js +103 -0
  247. package/src/setupTests.js +5 -5
  248. package/tsconfig.json +51 -0
  249. package/types/charts.d.ts +569 -0
  250. package/types/components.d.ts +441 -0
  251. package/types/config.d.ts +199 -0
  252. package/types/index.d.ts +353 -0
@@ -1,579 +1,582 @@
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
- };
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
+ ${
65
+ params.data.Cell_ID
66
+ ? '<strong>' + params.data.Cell_ID + '</strong> <br/>'
67
+ : ''
68
+ }
69
+ ${params.data.Cell_Type != null ? ' Type: <strong>' + params.data.Cell_Type + '</strong><br/>' : ''}
70
+ ${params.data.Stage != null ? ' Stage: <strong>' + params.data.Stage + '</strong><br/>' : ''}
71
+ ${params.data.UMI != null ? ' #UMI: <strong>' + params.data.UMI + '</strong><br/>' : ''}
72
+ ${params.data.Gene != null ? ' #Gene: <strong>' + params.data.Gene + '</strong><br/>' : ''}
73
+ ${params.data.MT != null ? ' MT%: <strong>' + params.data.MT + '</strong><br/>' : ''}
74
+ ${express != null ? 'Expression: <strong>' + express + '</strong>': ''
75
+ }
76
+ </div>
77
+ `;
78
+ },
79
+ },
80
+ title: {
81
+ textStyle: {
82
+ fontSize: 20,
83
+ },
84
+ left: 'center',
85
+ top: 0,
86
+ },
87
+ animation: false,
88
+ legend: {
89
+ textStyle: {
90
+ fontSize: 18,
91
+ },
92
+ orient: 'horizontal',
93
+ right: 'center', // "top" | "bottom" | "center"
94
+ itemWidth: 20,
95
+ width: 600,
96
+ top: 20,
97
+ },
98
+
99
+ xAxis: {
100
+ name: 'UMAP 1',
101
+ type: 'value',
102
+ nameLocation: 'middle',
103
+ nameGap: 10,
104
+ nameTextStyle: {
105
+ fontSize: 18, // Set font size
106
+ fontWeight: 'bold', // Set font weight to bold
107
+ },
108
+ axisLine: { show: false },
109
+ axisTick: { show: false },
110
+ splitLine: { show: false },
111
+ axisLabel: { show: false },
112
+ //min: -20,
113
+ //max: 20,
114
+ },
115
+ yAxis: {
116
+ name: 'UMAP 2',
117
+ type: 'value',
118
+ nameLocation: 'middle',
119
+ nameGap: 10,
120
+ nameTextStyle: {
121
+ fontSize: 18, // Set font size
122
+ fontWeight: 'bold', // Set font weight to bold
123
+ },
124
+ //min: -20,
125
+ //max: 20,
126
+ axisLine: { show: false },
127
+ axisTick: { show: false },
128
+ splitLine: { show: false },
129
+ axisLabel: { show: false },
130
+ },
131
+ grid: {
132
+ top: '20%',
133
+ bottom: '12%',
134
+ left: '10%',
135
+ right: '10%',
136
+ },
137
+ ...echart,
138
+ };
139
+
140
+ return option;
141
+ };
142
+
143
+ export const handleSimplyLoad = (simpleload) => {
144
+ // fetch data direcly from config
145
+ // fetch data from simply api
146
+ };
147
+
148
+ export const handleAPI = async (config, sharedData, bbox = false) => {
149
+ let fetched_dat = {};
150
+ let dependencies = {};
151
+
152
+ // Get corresponding api and its attributes
153
+ for (const [item, attr] of Object.entries(config.api)) {
154
+ const { href, val, dep = null } = attr;
155
+ let id = sharedData[val];
156
+
157
+ if (typeof id === 'object' && config.mapping && config.mapping.api) {
158
+ id = [...new Set(id.map((i) => config.mapping.api[i] ?? i))];
159
+ id = id[0] ? id : null;
160
+ } else if (config.mapping && config.mapping.api)
161
+ id = config.mapping.api[id] ?? id;
162
+
163
+ //console.log(`item: ${item}, href: ${href}, val: ${val}, id: ${id}`);
164
+ dependencies[item] = id;
165
+
166
+ if (id && typeof id === 'object') {
167
+ //console.log(`id for ${item} : `, id);
168
+ const promises = id.map(async (i) => {
169
+ //console.log(`request for ${item} : `, i);
170
+ return await simplefetch(href, {
171
+ id: i,
172
+ type: item,
173
+ debug: false,
174
+ bbox: bbox,
175
+ });
176
+ });
177
+ //console.log(`promises for ${item} : `, promises);
178
+
179
+ const resp = await Promise.all(promises);
180
+ const result = resp.reduce((acc, cur) => {
181
+ return {
182
+ ...acc,
183
+ ...cur,
184
+ };
185
+ }, {});
186
+ //console.log(`result for ${item} : `, result);
187
+ fetched_dat[item] = result;
188
+ } else if (id) {
189
+ //console.log(`dep2 id for ${item} : `, id, dependencies);
190
+
191
+ const result = await simplefetch(href, {
192
+ id: dependencies[dep] + '/' + id,
193
+ type: item,
194
+ debug: sharedData.debug,
195
+ bbox: bbox,
196
+ });
197
+ fetched_dat[item] = result;
198
+ dependencies[item] = id;
199
+ }
200
+ }
201
+
202
+ if (isEmpty(fetched_dat)) {
203
+ throw new Error(
204
+ config.startup_msg ?? 'Please select metadata to load data',
205
+ );
206
+ } else {
207
+ fetched_dat.fetched_ID = dependencies;
208
+ return fetched_dat;
209
+ }
210
+
211
+ //wait for user to provide values for api
212
+ };
213
+
214
+ // Function to output the axis values
215
+ export const outputAxisValues = (myChart) => {
216
+ if (!myChart) {
217
+ return;
218
+ }
219
+
220
+ const yAxis = myChart.getModel().getComponent('yAxis').axis;
221
+ const yAxisMin = yAxis.scale.getExtent()[0];
222
+ const yAxisMax = yAxis.scale.getExtent()[1];
223
+
224
+ const xAxis = myChart.getModel().getComponent('xAxis').axis;
225
+ const XAxisMin = Math.round(xAxis.scale.getExtent()[0]);
226
+ const XAxisMax = Math.round(xAxis.scale.getExtent()[1]);
227
+
228
+ //console.log(
229
+ // `${msgPrefix} X Axis Display Range: [${XAxisMin}, ${XAxisMax}]`,
230
+ // `${msgPrefix} Y Axis Display Range: [${yAxisMin}, ${yAxisMax}]`,
231
+ //);
232
+
233
+ return {
234
+ xMin: XAxisMin,
235
+ yMin: yAxisMin,
236
+ xMax: XAxisMax,
237
+ yMax: yAxisMax,
238
+ };
239
+ };
240
+
241
+ export const validateConfig = (config) => {
242
+ if (!config) {
243
+ throw new Error('config is required, minial config is {}');
244
+ }
245
+ // api or simpleload is required
246
+ if (!config.api && !config.simpleload) {
247
+ throw new Error('config.api or config.simpleload is required');
248
+ }
249
+ };
250
+
251
+ export const parseData = (fetched, config, sharedData) => {
252
+ if (!fetched) {
253
+ throw new Error('fetched data is not valid: ' + fetched);
254
+ }
255
+
256
+ //console.log(`fetched: `, fetched);
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, config.visualmap),
296
+ title: fetched?.fetched_ID?.gene ?? '',
297
+ };
298
+ } catch (error) {
299
+ console.error(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, visualmap = {}) => {
545
+ if (!genes) return [];
546
+ // Initialize max and min variables with the first value in the object
547
+
548
+ let maxValue = Number.NEGATIVE_INFINITY;
549
+ let minValue = Number.POSITIVE_INFINITY;
550
+ let visualMap = {};
551
+ // Iterate through the values of the object
552
+ for (const value of Object.values(genes)) {
553
+ // Compare the current value to the max and min values
554
+ if (value > maxValue) {
555
+ maxValue = value;
556
+ }
557
+ if (value < minValue) {
558
+ minValue = value;
559
+ }
560
+ }
561
+ //console.log(`handlesVisualMap: `, maxValue, minValue);
562
+ visualMap.min = minValue;
563
+ visualMap.max = maxValue;
564
+ visualMap.dimension = 2;
565
+ visualMap.orient = 'vertical';
566
+ visualMap.top = 'center';
567
+ visualMap.left = 0;
568
+ visualMap.text = ['log2\n(tpm+1)', ''];
569
+ visualMap.textGap = 10;
570
+ visualMap.calculable = true;
571
+ visualMap.inRange = {
572
+ color: ['#808080', '#FFA500', '#FF0000'],
573
+ };
574
+ visualMap.textStyle = {
575
+ writingMode: 'vertical-lr',
576
+ };
577
+
578
+ visualMap = { ...visualMap, ...visualmap };
579
+
580
+ //console.log(`handlesVisualMap: `, visualMap);
581
+ return visualMap;
582
+ };