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,572 +1,601 @@
1
- import React, { useEffect, useState } from 'react';
2
- import Scatter from './Scatter';
3
- import CircularProgress from '../widgets/circularProgress';
4
- import { useAppContext } from '../appContext';
5
- import simplefetch from '../fetch/fetch';
6
-
7
- function rcolor(seed = 0) {
8
- const maxHexValue = 16777215;
9
- const randomSeed = seed || Math.random();
10
- const randomColor = Math.floor(randomSeed * maxHexValue).toString(16);
11
- const color = '#' + randomColor.padStart(6, '0');
12
-
13
- return color;
14
- }
15
-
16
- function isEmpty(obj) {
17
- return Object.keys(obj).length === 0 && obj.constructor === Object;
18
- }
19
-
20
- function ScatterBio({ props, style, reset }) {
21
- // Declare states at the beginning
22
- const { sharedData } = useAppContext();
23
- const { debug } = props;
24
-
25
- const [updatedProps, setUpdatedProps] = useState(props);
26
- const [loading, setLoading] = useState(true);
27
- const [error, setError] = useState(null);
28
-
29
- useEffect(() => {
30
- // ------------------------------ Fetch Data ------------------------------
31
- const {
32
- meta,
33
- simpleload = true,
34
- gene,
35
- geneval,
36
- metaval,
37
- colour = 'Cell_Type', // 'Cell_Type' or 'Stage'
38
- colourval = undefined,
39
- axis_mapping,
40
- config = {},
41
- exclude_celltype = [],
42
- entry_mapping = undefined,
43
- exp_condition = 0,
44
- startup_msg = '',
45
- } = props;
46
-
47
- // error handling for missing config --------------------------------------
48
-
49
- // if meta is missing
50
- if (!meta) {
51
- setError({ message: 'meta is missing', data: { meta: meta } });
52
- setLoading(false);
53
- return;
54
- } else if (!gene) {
55
- setError({ message: 'gene is missing', data: { gene: gene } });
56
- setLoading(false);
57
- return;
58
- }
59
- // missing axis_mapping
60
- // that are required for this widget to work
61
- // represented as x, y while processing the data
62
- else if (!axis_mapping || !axis_mapping.x || !axis_mapping.y) {
63
- setError({
64
- message: 'axis_mapping is missing/incorrect',
65
- data: { axis_mapping: axis_mapping },
66
- });
67
- setLoading(false);
68
- return;
69
- }
70
- // if not simpleload, then metaval is required
71
- else if (!simpleload && !metaval) {
72
- setError({
73
- message: 'metaval is missing',
74
- data: { metaval: metaval },
75
- });
76
- setLoading(false);
77
- return;
78
- }
79
-
80
- /*
81
- // missing config
82
- else if (!config) {
83
- setError({message: "config is missing", data: {config: config}});
84
- setLoading(false);
85
- return;
86
- }
87
-
88
- // config.xAxis & config.yAxis are required for this widget to work
89
- else if (!config.xAxis || !config.yAxis) {
90
- setError({message: "config.xAxis or config.yAxis is missing", data: {config: config}});
91
- setLoading(false);
92
- return;
93
- }
94
- */
95
-
96
- // ------------------------------------------------------------------------
97
-
98
- const _query_gene = geneval ? sharedData[geneval] : sharedData.gene;
99
- const _query_meta = metaval ? sharedData[metaval] : sharedData.meta;
100
-
101
- const updatePlot = async () => {
102
- try {
103
- setLoading(true);
104
-
105
- let metadata = {};
106
- let genedata = {};
107
-
108
- // if not simpleload, then we wait user to provide the data
109
- if (!simpleload && (!_query_meta || _query_meta.length === 0)) {
110
- setLoading({
111
- message: `Please select ${startup_msg} to load data`,
112
- });
113
- return;
114
- } else if (simpleload) {
115
- metadata = await simplefetch(meta, {
116
- id: _query_meta,
117
- type: 'metadata',
118
- debug: debug,
119
- });
120
-
121
- genedata = await simplefetch(gene, {
122
- id: _query_gene,
123
- key: 'gene', //TODO: obtain key from config which can override the type
124
- type: 'gene',
125
- debug: debug,
126
- });
127
- } else {
128
- //console.log("ScatterBio: _query_meta", _query_meta);
129
- //console.log("ScatterBio: nodes_mapping", nodes_mapping);
130
-
131
- // Mapping logic to filter and transform IDs
132
- const mapped_nodes = entry_mapping
133
- ? _query_meta.map((node) => entry_mapping[node])
134
- : _query_meta;
135
-
136
- //removed duplicated IDs
137
- const _filted_query_meta = [...new Set(mapped_nodes)];
138
-
139
- //console.log("ScatterBio: _filted_query_meta", _filted_query_meta);
140
-
141
- const metadataPromises = _filted_query_meta.map(
142
- async (metaId) => {
143
- return await simplefetch(meta, {
144
- id: metaId,
145
- type: 'metadata',
146
- debug: debug,
147
- });
148
- },
149
- );
150
-
151
- const metadataResponses = await Promise.all(
152
- metadataPromises,
153
- );
154
-
155
- // Combine the metadata from different IDs
156
- metadata = metadataResponses.reduce(
157
- (combined, metadata) => {
158
- return {
159
- ...combined,
160
- ...metadata,
161
- };
162
- },
163
- {},
164
- );
165
-
166
- if (
167
- _query_gene &&
168
- _query_gene.length > 0 &&
169
- !_query_gene.includes('None')
170
- ) {
171
- if (debug)
172
- console.log('ScatterBio: _query_gene', _query_gene);
173
-
174
- const genedataPromises = _query_meta.map(
175
- async (metaId) => {
176
- const _mapped_metaId = entry_mapping
177
- ? entry_mapping[metaId]
178
- : metaId;
179
- const _merged_query_gene =
180
- _mapped_metaId + '/' + _query_gene;
181
- //console.log(_merged_query_gene);
182
- return await simplefetch(gene, {
183
- id: _merged_query_gene,
184
- key: 'gene', //TODO: obtain key from config which can override the type
185
- type: 'gene',
186
- debug: debug,
187
- });
188
- },
189
- );
190
-
191
- genedata = await Promise.any(genedataPromises);
192
- //console.log("ScatterBio: genedata", genedata);
193
- }
194
- }
195
-
196
- const _nogene = isEmpty(genedata);
197
- if (debug) console.log('ScatterBio: _nogene', _nogene);
198
- if (debug)
199
- console.log(
200
- 'ScatterBio: genedata',
201
- genedata,
202
- 'metadata',
203
- metadata,
204
- );
205
-
206
- // ------------------------------ Proccess Data ------------------------------
207
- const { colors: __colors = [] } = config;
208
-
209
- const filted_colour = sharedData[colourval]
210
- ? sharedData[colourval].replace(' ', '_')
211
- : colour;
212
-
213
- if (debug)
214
- console.log('ScatterBio - Colour by:', filted_colour);
215
-
216
- const __celltypes = [
217
- ...new Set(metadata[filted_colour]),
218
- ].filter((celltype) => !exclude_celltype.includes(celltype));
219
-
220
- let { zcolor: visualcolor = [] } = config;
221
- let seriescolor = {};
222
-
223
- if (Array.isArray(__colors) && __colors.length > 0) {
224
- __colors.forEach((item) => {
225
- if (item.color) visualcolor.push(item.color);
226
- else visualcolor.push(rcolor());
227
- });
228
- __celltypes.forEach((celltype, index) => {
229
- const cell = __colors.find(
230
- (item) => item.name === celltype,
231
- );
232
- const _color = cell?.color ?? visualcolor[index];
233
- const _symbol =
234
- __colors.find((item) => item.name === celltype)
235
- ?.symbol ?? 'circle';
236
- seriescolor[celltype] = {
237
- color: _color,
238
- symbol: _symbol,
239
- };
240
- });
241
- } else {
242
- let no_zcolor = false;
243
- if (visualcolor.length === 0) {
244
- no_zcolor = true;
245
- }
246
- __celltypes.forEach((celltype, index) => {
247
- if (no_zcolor) {
248
- visualcolor.push(rcolor());
249
- }
250
- seriescolor[celltype] = {
251
- color: rcolor(),
252
- symbol: 'circle',
253
- };
254
- });
255
- }
256
-
257
- // console.log("ScatterBio: axis_mapping", axis_mapping);
258
- // console.log("metadata: ", metadata);
259
- const { extra } = axis_mapping;
260
-
261
- let processedData;
262
-
263
- try {
264
- processedData = metadata['Cell_ID'].map((cellId, index) => {
265
- const _expression =
266
- genedata[cellId] >= exp_condition
267
- ? genedata[cellId]
268
- : _nogene
269
- ? 999
270
- : -1;
271
- const _Cell_Type = metadata[filted_colour] ?? 'Unknown';
272
- if (
273
- !__celltypes.includes('Unknown') &&
274
- __celltypes.length === 0
275
- ) {
276
- __celltypes.push('Unknown');
277
- seriescolor['Unknown'] = {
278
- color: '#000000',
279
- symbol: 'circle',
280
- };
281
- }
282
- // const _UMI = metadata.N_UMI ? Math.round(metadata.N_UMI[index] * 100) / 100 : 0;
283
- // round to 2 decimal places
284
- const extraProperties = {};
285
- for (const property in extra) {
286
- if (metadata[extra[property]]) {
287
- extraProperties[property] =
288
- metadata[extra[property]][index];
289
- }
290
- }
291
-
292
- const z_index =
293
- _expression === -1 || _expression === 999
294
- ? config.visium_cell_fraction
295
- ? extraProperties[
296
- config.visium_cell_fraction
297
- ] ?? _expression
298
- : _expression
299
- : _expression;
300
-
301
- return {
302
- name: cellId,
303
- value: [
304
- metadata[axis_mapping.x][index],
305
- metadata[axis_mapping.y][index],
306
- z_index,
307
- ],
308
- Type:
309
- _Cell_Type === 'Unknown'
310
- ? 'Unknown'
311
- : _Cell_Type[index],
312
- Expression: _expression,
313
- ...extraProperties,
314
- };
315
- });
316
-
317
- //console.log("ScatterBio: processedData", processedData[0], processedData[1]);
318
- } catch (err) {
319
- // if TypeError, then set error message to remind user to check the api
320
- setError({
321
- message:
322
- 'Please check the API, \n' +
323
- 'Your API may not return the correct data format. \n' +
324
- meta +
325
- '\n' +
326
- err,
327
- });
328
- setLoading(false);
329
- return;
330
- }
331
-
332
- if (debug) console.log(processedData[2], processedData[3]);
333
-
334
- const {
335
- dotsize,
336
- dotFactor = 2000,
337
- minSymbolSize = 2,
338
- maxSymbolSize = 10,
339
- } = config;
340
- const pointCount = processedData.length;
341
-
342
- // Calculate the symbol size based on the number of points
343
- let symbolSize;
344
- if (dotsize === 'auto') {
345
- const sizeFactor = Math.max(
346
- 1 - (pointCount - 1000) / dotFactor,
347
- 0.2,
348
- );
349
- symbolSize =
350
- (maxSymbolSize - minSymbolSize) * sizeFactor +
351
- minSymbolSize;
352
- //console.log(pointCount, symbolSize);
353
- } else {
354
- symbolSize = typeof dotsize === 'number' ? dotsize : 5;
355
- }
356
-
357
- // Create a series for each unique cell type
358
- const series = __celltypes.map((cellType) => {
359
- return {
360
- name: cellType,
361
- type: 'scatter',
362
- data: processedData.filter(
363
- (data) =>
364
- data.Type === cellType && data.Expression > -1,
365
- ),
366
- itemStyle: {
367
- color: seriescolor[cellType].color,
368
- },
369
- symbol: seriescolor[cellType].symbol,
370
- symbolSize: symbolSize,
371
- };
372
- });
373
-
374
- //console.log("series: ", series);
375
-
376
- const zshift =
377
- -config.zshift || -Math.max(config.chartWidth * 0.02, 10);
378
-
379
- const auto_zmax = Math.max(
380
- ...processedData.map((item) => item.value[2]),
381
- );
382
- const auto_zmin = Math.min(
383
- ...processedData.map((item) => item.value[2]),
384
- );
385
-
386
- //console.log("ScatterBio: auto_zmax", auto_zmax, "auto_zmin", auto_zmin, config.visium_cell_fraction);
387
-
388
- let visualMap =
389
- _nogene && !config.visium_cell_fraction
390
- ? []
391
- : {
392
- min: config.zmin ?? auto_zmin,
393
- max: config.zmax ?? auto_zmax,
394
- dimension: 2,
395
- orient: 'vertical',
396
- top: 'center',
397
- right: zshift,
398
- text: config.labels
399
- ? config.labels.z ?? ''
400
- : '',
401
- textGap: 10,
402
- calculable: true,
403
- inRange: {
404
- color: visualcolor,
405
- },
406
- textStyle: {
407
- writingMode: 'vertical-lr',
408
- },
409
- ...config.visualMap,
410
- };
411
-
412
- const { legend: _config_legend = {} } = config;
413
- const legendItemCount = __celltypes.length;
414
- const { type: _legendType, ...__config_legend } =
415
- _config_legend;
416
- let legendType = 'scroll';
417
- if (_legendType === 'auto') {
418
- legendType = legendItemCount > 10 ? 'scroll' : 'plain';
419
- }
420
-
421
- // console.log("__celltypes:", __celltypes);
422
- // Sort __celltypes by extracting and comparing the numerical part
423
- const sortedCellTypes = __celltypes.slice().sort((a, b) => {
424
- // Handle null, undefined, or other unexpected values
425
- if (a === null || b === null) {
426
- return 0; // Handle the case as per your requirement
427
- }
428
- const numA = parseInt(a.substring(1), 10);
429
- const numB = parseInt(b.substring(1), 10);
430
- return numA - numB;
431
- });
432
-
433
- // legend
434
- const _legend =
435
- _config_legend === false
436
- ? false
437
- : {
438
- data: sortedCellTypes, //__celltypes,
439
- orient: 'horizontal',
440
- top: 5, // "top" | "bottom" | "center"
441
- right: 'center', // "top" | "bottom" | "center",
442
- ...__config_legend,
443
- type: legendType,
444
- };
445
- //console.log("ScatterBio: legend", _legend);
446
-
447
- const gridSettings = [
448
- {
449
- top: '5%',
450
- bottom: '5%',
451
- left: '5%',
452
- right: '5%',
453
- ...(config.grid || {}), // Merge with config.grid if available, otherwise an empty object
454
- },
455
- ];
456
-
457
- const title = config.title ?? '';
458
- // Update props for Scatter
459
-
460
- let scatterBioProps = {
461
- ...props,
462
- config: {
463
- chartWidth: 800,
464
- chartHeight: 600,
465
- ...config,
466
- series: series,
467
- legend: _legend,
468
- visualMap: visualMap,
469
- title: _nogene
470
- ? title
471
- : {
472
- ...title,
473
- text: _query_gene,
474
- },
475
- // Use formatter from config if provided, otherwise use the default formatter
476
- formatter: config.formatter
477
- ? (params) => config.formatter(params)
478
- : (params) => {
479
- return `
480
- ${params.name}
481
- <br/> Type: ${params.data.Type}
482
- <br/> UMI: ${params.data.UMI ?? 0}
483
- `;
484
- },
485
- grid: gridSettings,
486
- },
487
- };
488
-
489
- /*
490
- const imageDom = new Image();
491
- imageDom.src = './resources/YY0-44_image.png';
492
- scatterBioProps.config.backgroundColor = {
493
- image: imageDom,
494
- repeat: 'no-repeat' // or 'repeat', 'repeat-x', 'repeat-y' depending on your needs
495
- };
496
- */
497
-
498
- //console.log("ScatterBio: scatterBioProps.config", scatterBioProps.config);
499
- setUpdatedProps(scatterBioProps);
500
-
501
- setLoading(false);
502
- } catch (err) {
503
- console.log(err);
504
- if (err.name === 'TypeError' && simpleload)
505
- setError({
506
- message: `invalid URL for metadata/gene,\nplease check your URL or set simpleload to false`,
507
- });
508
- else setError(err);
509
- setLoading(false);
510
- }
511
- };
512
- updatePlot();
513
- }, [props, sharedData, debug]);
514
-
515
- if (reset) {
516
- reset(props.id, (vals) => {
517
- // logic to reset the data
518
- const { gene: rec_gene = ' ' } = vals;
519
- console.log('received data', sharedData[rec_gene]);
520
- });
521
- }
522
-
523
- if (loading.message) {
524
- return (
525
- <div style={{ marginTop: '10px', ...style }}>{loading.message}</div>
526
- );
527
- } else if (loading) {
528
- // Render a loading animation
529
- return (
530
- <div
531
- style={{
532
- display: 'flex',
533
- flexDirection: 'column',
534
- alignItems: 'center',
535
- justifyContent: 'center',
536
- height: '100%',
537
- ...style,
538
- }}>
539
- <CircularProgress color='primary' />
540
- </div>
541
- );
542
- }
543
-
544
- if (error) {
545
- const errorLines = error.message.split('\n');
546
- const errorElements = errorLines.map((line, index) => (
547
- <p
548
- key={index}
549
- style={{ color: 'red' }}>
550
- {line}
551
- </p>
552
- ));
553
- return (
554
- <div style={style}>
555
- An error has occurred: <br />
556
- <br /> {errorElements}
557
- </div>
558
- );
559
- }
560
- // Only render Scatter when not loading
561
- return (
562
- <>
563
- <Scatter
564
- key={updatedProps.id + '.inherent'}
565
- props={updatedProps}
566
- style={style}
567
- />
568
- </>
569
- );
570
- }
571
-
572
- export default ScatterBio;
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import Scatter from './Scatter';
3
+ import CircularProgress from '../widgets/circularProgress';
4
+ import { useAppContext } from '../appContext';
5
+ import simplefetch from '../fetch/fetch';
6
+
7
+ function rcolor(seed = 0) {
8
+ const maxHexValue = 16777215;
9
+ const randomSeed = seed || Math.random();
10
+ const randomColor = Math.floor(randomSeed * maxHexValue).toString(16);
11
+ const color = '#' + randomColor.padStart(6, '0');
12
+
13
+ return color;
14
+ }
15
+
16
+ function isEmpty(obj) {
17
+ return Object.keys(obj).length === 0 && obj.constructor === Object;
18
+ }
19
+
20
+ function ScatterBio({ props, style, reset }) {
21
+ // Declare states at the beginning
22
+ const { sharedData } = useAppContext();
23
+ const { debug } = props;
24
+
25
+ const [updatedProps, setUpdatedProps] = useState(props);
26
+ const [loading, setLoading] = useState(true);
27
+ const [error, setError] = useState(null);
28
+
29
+ // Use refs for sharedData/props to access latest without depending on them
30
+ const sharedDataRef = useRef(sharedData);
31
+ sharedDataRef.current = sharedData;
32
+ const propsRef = useRef(props);
33
+ propsRef.current = props;
34
+
35
+ // Extract stable keys from props for dependency tracking
36
+ const { geneval, metaval, colourval } = props;
37
+ const visiumCellFraction = props.config?.visium_cell_fraction;
38
+ // Track config overrides that affect rendering (e.g. VisiumPlot background image, legend, axes)
39
+ const configLegend = props.config?.legend;
40
+ const configHasBg = !!props.config?.backgroundColor;
41
+ // Build snapshot of relevant sharedData keys
42
+ const relevantKeys = [geneval, metaval, colourval].filter(Boolean);
43
+ const sharedDataSnapshot = JSON.stringify(
44
+ relevantKeys.reduce((acc, key) => {
45
+ acc[key] = sharedData[key];
46
+ return acc;
47
+ }, {}),
48
+ );
49
+
50
+ useEffect(() => {
51
+ const currentProps = propsRef.current;
52
+ const currentSharedData = sharedDataRef.current;
53
+ // ------------------------------ Fetch Data ------------------------------
54
+ const {
55
+ meta,
56
+ simpleload = true,
57
+ gene,
58
+ geneval,
59
+ metaval,
60
+ colour = 'Cell_Type', // 'Cell_Type' or 'Stage'
61
+ colourval = undefined,
62
+ axis_mapping,
63
+ config = {},
64
+ exclude_celltype = [],
65
+ entry_mapping = undefined,
66
+ exp_condition = 0,
67
+ startup_msg = '',
68
+ } = currentProps;
69
+
70
+ // error handling for missing config --------------------------------------
71
+
72
+ // if meta is missing
73
+ if (!meta) {
74
+ setError({ message: 'meta is missing', data: { meta: meta } });
75
+ setLoading(false);
76
+ return;
77
+ } else if (!gene) {
78
+ setError({ message: 'gene is missing', data: { gene: gene } });
79
+ setLoading(false);
80
+ return;
81
+ }
82
+ // missing axis_mapping
83
+ // that are required for this widget to work
84
+ // represented as x, y while processing the data
85
+ else if (!axis_mapping || !axis_mapping.x || !axis_mapping.y) {
86
+ setError({
87
+ message: 'axis_mapping is missing/incorrect',
88
+ data: { axis_mapping: axis_mapping },
89
+ });
90
+ setLoading(false);
91
+ return;
92
+ }
93
+ // if not simpleload, then metaval is required
94
+ else if (!simpleload && !metaval) {
95
+ setError({
96
+ message: 'metaval is missing',
97
+ data: { metaval: metaval },
98
+ });
99
+ setLoading(false);
100
+ return;
101
+ }
102
+
103
+ /*
104
+ // missing config
105
+ else if (!config) {
106
+ setError({message: "config is missing", data: {config: config}});
107
+ setLoading(false);
108
+ return;
109
+ }
110
+
111
+ // config.xAxis & config.yAxis are required for this widget to work
112
+ else if (!config.xAxis || !config.yAxis) {
113
+ setError({message: "config.xAxis or config.yAxis is missing", data: {config: config}});
114
+ setLoading(false);
115
+ return;
116
+ }
117
+ */
118
+
119
+ // ------------------------------------------------------------------------
120
+
121
+ const _query_gene = geneval ? currentSharedData[geneval] : currentSharedData.gene;
122
+ const _query_meta = metaval ? currentSharedData[metaval] : currentSharedData.meta;
123
+
124
+ const updatePlot = async () => {
125
+ try {
126
+ setLoading(true);
127
+
128
+ let metadata = {};
129
+ let genedata = {};
130
+
131
+ // if not simpleload, then we wait user to provide the data
132
+ if (!simpleload && (!_query_meta || _query_meta.length === 0)) {
133
+ setLoading({
134
+ message: `Please select ${startup_msg} to load data`,
135
+ });
136
+ return;
137
+ } else if (simpleload) {
138
+ metadata = await simplefetch(meta, {
139
+ id: _query_meta,
140
+ type: 'metadata',
141
+ debug: debug,
142
+ });
143
+
144
+ genedata = await simplefetch(gene, {
145
+ id: _query_gene,
146
+ key: 'gene', //TODO: obtain key from config which can override the type
147
+ type: 'gene',
148
+ debug: debug,
149
+ });
150
+ } else {
151
+ //console.log("ScatterBio: _query_meta", _query_meta);
152
+ //console.log("ScatterBio: nodes_mapping", nodes_mapping);
153
+
154
+ // Mapping logic to filter and transform IDs
155
+ const mapped_nodes = entry_mapping
156
+ ? _query_meta.map((node) => entry_mapping[node])
157
+ : _query_meta;
158
+
159
+ //removed duplicated IDs
160
+ const _filted_query_meta = [...new Set(mapped_nodes)];
161
+
162
+ //console.log("ScatterBio: _filted_query_meta", _filted_query_meta);
163
+
164
+ const metadataPromises = _filted_query_meta.map(
165
+ async (metaId) => {
166
+ return await simplefetch(meta, {
167
+ id: metaId,
168
+ type: 'metadata',
169
+ debug: debug,
170
+ });
171
+ },
172
+ );
173
+
174
+ const metadataResponses = await Promise.all(
175
+ metadataPromises,
176
+ );
177
+
178
+ // Combine the metadata from different IDs
179
+ metadata = metadataResponses.reduce(
180
+ (combined, metadata) => {
181
+ return {
182
+ ...combined,
183
+ ...metadata,
184
+ };
185
+ },
186
+ {},
187
+ );
188
+
189
+ if (
190
+ _query_gene &&
191
+ _query_gene.length > 0 &&
192
+ !_query_gene.includes('None')
193
+ ) {
194
+ if (debug)
195
+ console.log('ScatterBio: _query_gene', _query_gene);
196
+
197
+ const genedataPromises = _query_meta.map(
198
+ async (metaId) => {
199
+ const _mapped_metaId = entry_mapping
200
+ ? entry_mapping[metaId]
201
+ : metaId;
202
+ const _merged_query_gene =
203
+ _mapped_metaId + '/' + _query_gene;
204
+ //console.log(_merged_query_gene);
205
+ return await simplefetch(gene, {
206
+ id: _merged_query_gene,
207
+ key: 'gene', //TODO: obtain key from config which can override the type
208
+ type: 'gene',
209
+ debug: debug,
210
+ });
211
+ },
212
+ );
213
+
214
+ genedata = await Promise.any(genedataPromises);
215
+ //console.log("ScatterBio: genedata", genedata);
216
+ }
217
+ }
218
+
219
+ const _nogene = isEmpty(genedata);
220
+ if (debug) console.log('ScatterBio: _nogene', _nogene);
221
+ if (debug)
222
+ console.log(
223
+ 'ScatterBio: genedata',
224
+ genedata,
225
+ 'metadata',
226
+ metadata,
227
+ );
228
+
229
+ // ------------------------------ Proccess Data ------------------------------
230
+ const { colors: __colors = [] } = config;
231
+
232
+ const filted_colour = currentSharedData[colourval]
233
+ ? currentSharedData[colourval].replace(' ', '_')
234
+ : colour;
235
+
236
+ if (debug)
237
+ console.log('ScatterBio - Colour by:', filted_colour);
238
+
239
+ const __celltypes = [
240
+ ...new Set(metadata[filted_colour]),
241
+ ].filter((celltype) => !exclude_celltype.includes(celltype));
242
+
243
+ let { zcolor: visualcolor = [] } = config;
244
+ let seriescolor = {};
245
+
246
+ if (Array.isArray(__colors) && __colors.length > 0) {
247
+ __colors.forEach((item) => {
248
+ if (item.color) visualcolor.push(item.color);
249
+ else visualcolor.push(rcolor());
250
+ });
251
+ __celltypes.forEach((celltype, index) => {
252
+ const cell = __colors.find(
253
+ (item) => item.name === celltype,
254
+ );
255
+ const _color = cell?.color ?? visualcolor[index];
256
+ const _symbol =
257
+ __colors.find((item) => item.name === celltype)
258
+ ?.symbol ?? 'circle';
259
+ seriescolor[celltype] = {
260
+ color: _color,
261
+ symbol: _symbol,
262
+ };
263
+ });
264
+ } else {
265
+ let no_zcolor = false;
266
+ if (visualcolor.length === 0) {
267
+ no_zcolor = true;
268
+ }
269
+ __celltypes.forEach((celltype, index) => {
270
+ if (no_zcolor) {
271
+ visualcolor.push(rcolor());
272
+ }
273
+ seriescolor[celltype] = {
274
+ color: rcolor(),
275
+ symbol: 'circle',
276
+ };
277
+ });
278
+ }
279
+
280
+ // console.log("ScatterBio: axis_mapping", axis_mapping);
281
+ // console.log("metadata: ", metadata);
282
+ const { extra } = axis_mapping;
283
+
284
+ let processedData;
285
+
286
+ try {
287
+ processedData = metadata['Cell_ID'].map((cellId, index) => {
288
+ const _expression =
289
+ genedata[cellId] >= exp_condition
290
+ ? genedata[cellId]
291
+ : _nogene
292
+ ? 999
293
+ : -1;
294
+ const _Cell_Type = metadata[filted_colour] ?? 'Unknown';
295
+ if (
296
+ !__celltypes.includes('Unknown') &&
297
+ __celltypes.length === 0
298
+ ) {
299
+ __celltypes.push('Unknown');
300
+ seriescolor['Unknown'] = {
301
+ color: '#000000',
302
+ symbol: 'circle',
303
+ };
304
+ }
305
+ // const _UMI = metadata.N_UMI ? Math.round(metadata.N_UMI[index] * 100) / 100 : 0;
306
+ // round to 2 decimal places
307
+ const extraProperties = {};
308
+ for (const property in extra) {
309
+ if (metadata[extra[property]]) {
310
+ extraProperties[property] =
311
+ metadata[extra[property]][index];
312
+ }
313
+ }
314
+
315
+ const z_index =
316
+ _expression === -1 || _expression === 999
317
+ ? config.visium_cell_fraction
318
+ ? extraProperties[
319
+ config.visium_cell_fraction
320
+ ] ?? _expression
321
+ : _expression
322
+ : _expression;
323
+
324
+ return {
325
+ name: cellId,
326
+ value: [
327
+ metadata[axis_mapping.x][index],
328
+ metadata[axis_mapping.y][index],
329
+ z_index,
330
+ ],
331
+ Type:
332
+ _Cell_Type === 'Unknown'
333
+ ? 'Unknown'
334
+ : _Cell_Type[index],
335
+ Expression: _expression,
336
+ ...extraProperties,
337
+ };
338
+ });
339
+
340
+ //console.log("ScatterBio: processedData", processedData[0], processedData[1]);
341
+ } catch (err) {
342
+ // if TypeError, then set error message to remind user to check the api
343
+ setError({
344
+ message:
345
+ 'Please check the API, \n' +
346
+ 'Your API may not return the correct data format. \n' +
347
+ meta +
348
+ '\n' +
349
+ err,
350
+ });
351
+ setLoading(false);
352
+ return;
353
+ }
354
+
355
+ if (debug) console.log(processedData[2], processedData[3]);
356
+
357
+ const {
358
+ dotsize,
359
+ dotFactor = 2000,
360
+ minSymbolSize = 2,
361
+ maxSymbolSize = 10,
362
+ } = config;
363
+ const pointCount = processedData.length;
364
+
365
+ // Calculate the symbol size based on the number of points
366
+ let symbolSize;
367
+ if (dotsize === 'auto') {
368
+ const sizeFactor = Math.max(
369
+ 1 - (pointCount - 1000) / dotFactor,
370
+ 0.2,
371
+ );
372
+ symbolSize =
373
+ (maxSymbolSize - minSymbolSize) * sizeFactor +
374
+ minSymbolSize;
375
+ //console.log(pointCount, symbolSize);
376
+ } else {
377
+ symbolSize = typeof dotsize === 'number' ? dotsize : 5;
378
+ }
379
+
380
+ // Create a series for each unique cell type
381
+ const series = __celltypes.map((cellType) => {
382
+ const seriesItem = {
383
+ name: cellType,
384
+ type: 'scatter',
385
+ data: processedData.filter(
386
+ (data) =>
387
+ data.Type === cellType && data.Expression > -1,
388
+ ),
389
+ symbol: seriescolor[cellType]?.symbol ?? 'circle',
390
+ symbolSize: symbolSize,
391
+ };
392
+ // When legend is disabled (e.g. Visium spatial mode), skip itemStyle.color
393
+ // to allow visualMap to control dot coloring by expression/cell fraction
394
+ if (config.legend !== false && seriescolor[cellType]) {
395
+ seriesItem.itemStyle = {
396
+ color: seriescolor[cellType].color,
397
+ };
398
+ }
399
+ return seriesItem;
400
+ });
401
+
402
+ //console.log("series: ", series);
403
+
404
+ const zshift =
405
+ -config.zshift || -Math.max(config.chartWidth * 0.02, 10);
406
+
407
+ const auto_zmax = Math.max(
408
+ ...processedData.map((item) => item.value[2]),
409
+ );
410
+ const auto_zmin = Math.min(
411
+ ...processedData.map((item) => item.value[2]),
412
+ );
413
+
414
+ //console.log("ScatterBio: auto_zmax", auto_zmax, "auto_zmin", auto_zmin, config.visium_cell_fraction);
415
+
416
+ let visualMap =
417
+ _nogene && !config.visium_cell_fraction
418
+ ? []
419
+ : {
420
+ min: config.zmin ?? auto_zmin,
421
+ max: config.zmax ?? auto_zmax,
422
+ dimension: 2,
423
+ orient: 'vertical',
424
+ top: 'center',
425
+ right: zshift,
426
+ text: config.labels
427
+ ? config.labels.z ?? ''
428
+ : '',
429
+ textGap: 10,
430
+ calculable: true,
431
+ inRange: {
432
+ color: visualcolor,
433
+ },
434
+ textStyle: {
435
+ writingMode: 'vertical-lr',
436
+ },
437
+ ...config.visualMap,
438
+ };
439
+
440
+ const { legend: _config_legend = {} } = config;
441
+ const legendItemCount = __celltypes.length;
442
+ const { type: _legendType, ...__config_legend } =
443
+ _config_legend;
444
+ let legendType = 'scroll';
445
+ if (_legendType === 'auto') {
446
+ legendType = legendItemCount > 10 ? 'scroll' : 'plain';
447
+ }
448
+
449
+ // console.log("__celltypes:", __celltypes);
450
+ // Sort __celltypes by extracting and comparing the numerical part
451
+ const sortedCellTypes = __celltypes.slice().sort((a, b) => {
452
+ // Handle null, undefined, or other unexpected values
453
+ if (a === null || b === null) {
454
+ return 0; // Handle the case as per your requirement
455
+ }
456
+ const numA = parseInt(a.substring(1), 10);
457
+ const numB = parseInt(b.substring(1), 10);
458
+ return numA - numB;
459
+ });
460
+
461
+ // legend
462
+ const _legend =
463
+ _config_legend === false
464
+ ? false
465
+ : {
466
+ data: sortedCellTypes, //__celltypes,
467
+ orient: 'horizontal',
468
+ top: 5, // "top" | "bottom" | "center"
469
+ right: 'center', // "top" | "bottom" | "center",
470
+ ...__config_legend,
471
+ type: legendType,
472
+ };
473
+ //console.log("ScatterBio: legend", _legend);
474
+
475
+ const gridSettings = [
476
+ {
477
+ top: '5%',
478
+ bottom: '5%',
479
+ left: '5%',
480
+ right: '5%',
481
+ ...(config.grid || {}), // Merge with config.grid if available, otherwise an empty object
482
+ },
483
+ ];
484
+
485
+ const title = config.title ?? '';
486
+ // Update props for Scatter
487
+
488
+ let scatterBioProps = {
489
+ ...currentProps,
490
+ config: {
491
+ chartWidth: 800,
492
+ chartHeight: 600,
493
+ ...config,
494
+ series: series,
495
+ legend: _legend,
496
+ visualMap: visualMap,
497
+ title: _nogene
498
+ ? title
499
+ : {
500
+ ...title,
501
+ text: _query_gene,
502
+ },
503
+ // Use formatter from config if provided, otherwise use the default formatter
504
+ formatter: config.formatter
505
+ ? (params) => config.formatter(params)
506
+ : (params) => {
507
+ return `
508
+ ${params.name}
509
+ <br/> Type: ${params.data.Type}
510
+ <br/> UMI: ${params.data.UMI ?? 0}
511
+ `;
512
+ },
513
+ grid: gridSettings,
514
+ },
515
+ };
516
+
517
+ /*
518
+ const imageDom = new Image();
519
+ imageDom.src = './resources/YY0-44_image.png';
520
+ scatterBioProps.config.backgroundColor = {
521
+ image: imageDom,
522
+ repeat: 'no-repeat' // or 'repeat', 'repeat-x', 'repeat-y' depending on your needs
523
+ };
524
+ */
525
+
526
+ //console.log("ScatterBio: scatterBioProps.config", scatterBioProps.config);
527
+ setUpdatedProps(scatterBioProps);
528
+
529
+ setLoading(false);
530
+ } catch (err) {
531
+ console.error(err);
532
+ if (err.name === 'TypeError' && simpleload)
533
+ setError({
534
+ message: `invalid URL for metadata/gene,\nplease check your URL or set simpleload to false`,
535
+ });
536
+ else setError(err);
537
+ setLoading(false);
538
+ }
539
+ };
540
+ updatePlot();
541
+ // eslint-disable-next-line react-hooks/exhaustive-deps
542
+ }, [sharedDataSnapshot, visiumCellFraction, configLegend, configHasBg, debug]);
543
+
544
+ if (reset) {
545
+ reset(props.id, (vals) => {
546
+ // logic to reset the data
547
+ const { gene: rec_gene = ' ' } = vals;
548
+ // reset callback
549
+ });
550
+ }
551
+
552
+ if (loading.message) {
553
+ return (
554
+ <div style={{ marginTop: '10px', ...style }}>{loading.message}</div>
555
+ );
556
+ } else if (loading) {
557
+ // Render a loading animation
558
+ return (
559
+ <div
560
+ style={{
561
+ display: 'flex',
562
+ flexDirection: 'column',
563
+ alignItems: 'center',
564
+ justifyContent: 'center',
565
+ height: '100%',
566
+ ...style,
567
+ }}>
568
+ <CircularProgress color='primary' />
569
+ </div>
570
+ );
571
+ }
572
+
573
+ if (error) {
574
+ const errorLines = error.message.split('\n');
575
+ const errorElements = errorLines.map((line, index) => (
576
+ <p
577
+ key={index}
578
+ style={{ color: 'red' }}>
579
+ {line}
580
+ </p>
581
+ ));
582
+ return (
583
+ <div style={style}>
584
+ An error has occurred: <br />
585
+ <br /> {errorElements}
586
+ </div>
587
+ );
588
+ }
589
+ // Only render Scatter when not loading
590
+ return (
591
+ <>
592
+ <Scatter
593
+ key={updatedProps.id + '.inherent'}
594
+ props={updatedProps}
595
+ style={style}
596
+ />
597
+ </>
598
+ );
599
+ }
600
+
601
+ export default ScatterBio;