plexora 0.0.1__py3-none-any.whl

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 (248) hide show
  1. plexora/__init__.py +129 -0
  2. plexora/__main__.py +15 -0
  3. plexora/_url.py +105 -0
  4. plexora/api/__init__.py +55 -0
  5. plexora/api/dataset.py +398 -0
  6. plexora/api/http.py +19 -0
  7. plexora/api/plugin.py +541 -0
  8. plexora/api/store.py +221 -0
  9. plexora/cli.py +777 -0
  10. plexora/client/dist/354_bundle.js +2 -0
  11. plexora/client/dist/354_bundle.js.LICENSE.txt +20 -0
  12. plexora/client/dist/418_bundle.js +2 -0
  13. plexora/client/dist/418_bundle.js.LICENSE.txt +1 -0
  14. plexora/client/dist/693b935cb1f907814be9c6199a68c217.svg +19 -0
  15. plexora/client/dist/713c134ff47cd328a5b711526f151082.svg +18 -0
  16. plexora/client/dist/770_bundle.js +1 -0
  17. plexora/client/dist/ff743f408972e0e96ca9153ed5ae4c83.svg +23 -0
  18. plexora/client/dist/vendor_bundle.js +2 -0
  19. plexora/client/dist/vendor_bundle.js.LICENSE.txt +308 -0
  20. plexora/client/external/openseadragon-bin-2.4.0/LICENSE.txt +28 -0
  21. plexora/client/external/openseadragon-bin-2.4.0/canvas-overlay-hd.js +140 -0
  22. plexora/client/external/openseadragon-bin-2.4.0/changelog.txt +501 -0
  23. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/.gitattributes +17 -0
  24. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/LICENSE.txt +116 -0
  25. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/Toolbar.png +0 -0
  26. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/fullpage.psd +0 -0
  27. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/home.psd +0 -0
  28. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/next.psd +0 -0
  29. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/previous.psd +0 -0
  30. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateleft.psd +0 -0
  31. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/rotateright.psd +0 -0
  32. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/Photoshop/zoom.psd +0 -0
  33. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/README.md +9 -0
  34. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_grouphover.png +0 -0
  35. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_hover.png +0 -0
  36. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_pressed.png +0 -0
  37. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/fullpage_rest.png +0 -0
  38. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_grouphover.png +0 -0
  39. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_hover.png +0 -0
  40. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_pressed.png +0 -0
  41. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/home_rest.png +0 -0
  42. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_grouphover.png +0 -0
  43. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_hover.png +0 -0
  44. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_pressed.png +0 -0
  45. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/next_rest.png +0 -0
  46. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_grouphover.png +0 -0
  47. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_hover.png +0 -0
  48. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_pressed.png +0 -0
  49. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/previous_rest.png +0 -0
  50. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_grouphover.png +0 -0
  51. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_hover.png +0 -0
  52. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_pressed.png +0 -0
  53. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateleft_rest.png +0 -0
  54. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_grouphover.png +0 -0
  55. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_hover.png +0 -0
  56. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_pressed.png +0 -0
  57. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/rotateright_rest.png +0 -0
  58. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_grouphover.png +0 -0
  59. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_hover.png +0 -0
  60. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_pressed.png +0 -0
  61. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomin_rest.png +0 -0
  62. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_grouphover.png +0 -0
  63. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_hover.png +0 -0
  64. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_pressed.png +0 -0
  65. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-flat-toolbar-icons-master/images/zoomout_rest.png +0 -0
  66. plexora/client/external/openseadragon-bin-2.4.0/openseadragon-scalebar.js +562 -0
  67. plexora/client/src/css/import.css +474 -0
  68. plexora/client/src/css/main.css +903 -0
  69. plexora/client/src/css/openProject.css +363 -0
  70. plexora/client/src/css/quickView.css +166 -0
  71. plexora/client/src/css/tokens.css +51 -0
  72. plexora/client/src/css/viewer.css +1655 -0
  73. plexora/client/src/img/apple-touch-icon.png +0 -0
  74. plexora/client/src/img/favicon.ico +0 -0
  75. plexora/client/src/img/logo.ai +8892 -33
  76. plexora/client/src/img/logo.svg +1 -0
  77. plexora/client/src/img/logo_with_text.ai +9011 -34
  78. plexora/client/src/img/logo_with_text.svg +1 -0
  79. plexora/client/src/js/main.js +704 -0
  80. plexora/client/src/js/pluginRegistry.js +108 -0
  81. plexora/client/src/js/services/appStatus.js +286 -0
  82. plexora/client/src/js/services/browsePicker.js +73 -0
  83. plexora/client/src/js/services/dataLayer.js +400 -0
  84. plexora/client/src/js/services/datasetContext.js +143 -0
  85. plexora/client/src/js/services/glRenderer.js +143 -0
  86. plexora/client/src/js/services/importFormValidation.js +257 -0
  87. plexora/client/src/js/services/numericData.js +100 -0
  88. plexora/client/src/js/services/passVariablesToFrontend.js +20 -0
  89. plexora/client/src/js/services/simpleEventHandler.js +25 -0
  90. plexora/client/src/js/vendor.js +29 -0
  91. plexora/client/src/js/views/channelList.js +663 -0
  92. plexora/client/src/js/views/colorSwatchPicker.js +202 -0
  93. plexora/client/src/js/views/columnClassifier.js +146 -0
  94. plexora/client/src/js/views/coordinateField.js +133 -0
  95. plexora/client/src/js/views/dataSourceField.js +276 -0
  96. plexora/client/src/js/views/imageViewer.js +3306 -0
  97. plexora/client/src/js/views/miniMap.js +787 -0
  98. plexora/client/src/js/views/navbarControls.js +178 -0
  99. plexora/client/src/js/views/openProjectPage.js +239 -0
  100. plexora/client/src/js/views/projectEdit.js +335 -0
  101. plexora/client/src/js/views/quickViewLanding.js +136 -0
  102. plexora/client/src/js/views/rainbow.js +326 -0
  103. plexora/client/src/js/views/requirementsModal.js +572 -0
  104. plexora/client/src/js/views/rgbImageViewer.js +115 -0
  105. plexora/client/src/js/views/searchableSelect.js +411 -0
  106. plexora/client/src/js/views/segmentationProgress.js +122 -0
  107. plexora/client/src/js/views/toolLoader.js +747 -0
  108. plexora/client/src/js/views/viewerControls.js +689 -0
  109. plexora/client/src/js/views/viewerManager.js +434 -0
  110. plexora/client/src/js/views/viewerSidebar.js +1117 -0
  111. plexora/client/src/js/workers/tileDecoder.js +130 -0
  112. plexora/client/src/shaders/frag.glsl +528 -0
  113. plexora/client/src/shaders/vert.glsl +12 -0
  114. plexora/client/templates/base.html +212 -0
  115. plexora/client/templates/index.html +238 -0
  116. plexora/client/templates/open_project.html +96 -0
  117. plexora/client/templates/project_columns.html +86 -0
  118. plexora/client/templates/project_edit.html +196 -0
  119. plexora/client/templates/upload.html +118 -0
  120. plexora/connect.py +528 -0
  121. plexora/datasource.py +782 -0
  122. plexora/jupyter.py +406 -0
  123. plexora/notebook_env.py +232 -0
  124. plexora/paths.py +486 -0
  125. plexora/plugins/__init__.py +12 -0
  126. plexora/plugins/cell_explorer/__init__.py +100 -0
  127. plexora/plugins/cell_explorer/server/__init__.py +11 -0
  128. plexora/plugins/cell_explorer/server/routes.py +167 -0
  129. plexora/plugins/cell_explorer/server/state.py +281 -0
  130. plexora/plugins/cell_explorer/server/values.py +145 -0
  131. plexora/plugins/cell_explorer/server/variables.py +367 -0
  132. plexora/plugins/cell_explorer/static/cellExplorerApi.js +158 -0
  133. plexora/plugins/cell_explorer/static/cellExplorerColors.js +316 -0
  134. plexora/plugins/cell_explorer/static/cellExplorerContinuous.js +403 -0
  135. plexora/plugins/cell_explorer/static/cellExplorerFigureBridge.js +133 -0
  136. plexora/plugins/cell_explorer/static/cellExplorerLegend.js +188 -0
  137. plexora/plugins/cell_explorer/static/cellExplorerRoiBridge.js +822 -0
  138. plexora/plugins/cell_explorer/static/cellExplorerSidebarController.js +826 -0
  139. plexora/plugins/cell_explorer/static/cellExplorerState.js +372 -0
  140. plexora/plugins/cell_explorer/static/cell_explorer.css +752 -0
  141. plexora/plugins/cell_explorer/templates/cell_explorer/panel.html +137 -0
  142. plexora/plugins/figure_builder/__init__.py +125 -0
  143. plexora/plugins/figure_builder/server/__init__.py +7 -0
  144. plexora/plugins/figure_builder/server/compose.py +365 -0
  145. plexora/plugins/figure_builder/server/export.py +784 -0
  146. plexora/plugins/figure_builder/server/export_jobs.py +148 -0
  147. plexora/plugins/figure_builder/server/operations.py +556 -0
  148. plexora/plugins/figure_builder/server/pixels.py +142 -0
  149. plexora/plugins/figure_builder/server/provenance.py +174 -0
  150. plexora/plugins/figure_builder/server/render.py +278 -0
  151. plexora/plugins/figure_builder/server/repository.py +701 -0
  152. plexora/plugins/figure_builder/server/routes.py +594 -0
  153. plexora/plugins/figure_builder/server/schema.py +1023 -0
  154. plexora/plugins/figure_builder/server/sources.py +120 -0
  155. plexora/plugins/figure_builder/server/textmetrics.py +155 -0
  156. plexora/plugins/figure_builder/static/figureActions.js +363 -0
  157. plexora/plugins/figure_builder/static/figureBuilderApi.js +255 -0
  158. plexora/plugins/figure_builder/static/figureCanvas.js +2362 -0
  159. plexora/plugins/figure_builder/static/figureCaptureBoxes.js +369 -0
  160. plexora/plugins/figure_builder/static/figureCaptureDock.js +466 -0
  161. plexora/plugins/figure_builder/static/figureCaptureTool.js +1034 -0
  162. plexora/plugins/figure_builder/static/figureConfirm.js +136 -0
  163. plexora/plugins/figure_builder/static/figureContextBar.js +1193 -0
  164. plexora/plugins/figure_builder/static/figureContextMenu.js +253 -0
  165. plexora/plugins/figure_builder/static/figureDocumentState.js +275 -0
  166. plexora/plugins/figure_builder/static/figureExportUi.js +174 -0
  167. plexora/plugins/figure_builder/static/figureLibrary.js +247 -0
  168. plexora/plugins/figure_builder/static/figureQuickEdit.js +628 -0
  169. plexora/plugins/figure_builder/static/figureRichText.js +634 -0
  170. plexora/plugins/figure_builder/static/figureSceneSnapshot.js +361 -0
  171. plexora/plugins/figure_builder/static/figureSchema.js +218 -0
  172. plexora/plugins/figure_builder/static/figureSidebarController.js +1159 -0
  173. plexora/plugins/figure_builder/static/figureTextEditor.js +503 -0
  174. plexora/plugins/figure_builder/static/figureTextPanel.js +491 -0
  175. plexora/plugins/figure_builder/static/figureViewOptions.js +273 -0
  176. plexora/plugins/figure_builder/static/figureWorkspace.js +2030 -0
  177. plexora/plugins/figure_builder/static/figure_builder.css +3292 -0
  178. plexora/plugins/figure_builder/templates/figure_builder/library.html +74 -0
  179. plexora/plugins/figure_builder/templates/figure_builder/workspace.html +27 -0
  180. plexora/plugins/figure_builder/templates/figure_builder/workspace_body.html +458 -0
  181. plexora/plugins/gating/__init__.py +76 -0
  182. plexora/plugins/gating/server/__init__.py +0 -0
  183. plexora/plugins/gating/server/anndata_gates.py +367 -0
  184. plexora/plugins/gating/server/database.py +16 -0
  185. plexora/plugins/gating/server/model.py +320 -0
  186. plexora/plugins/gating/server/routes.py +235 -0
  187. plexora/plugins/gating/static/csvGatingList.js +947 -0
  188. plexora/plugins/gating/static/gating.css +289 -0
  189. plexora/plugins/gating/static/gatingApi.js +221 -0
  190. plexora/plugins/gating/static/gatingSidebarController.js +513 -0
  191. plexora/plugins/gating/templates/gating/legacy.html +26 -0
  192. plexora/plugins/gating/templates/gating/panel.html +69 -0
  193. plexora/plugins/roi/__init__.py +95 -0
  194. plexora/plugins/roi/server/__init__.py +8 -0
  195. plexora/plugins/roi/server/adapters.py +655 -0
  196. plexora/plugins/roi/server/geojson.py +267 -0
  197. plexora/plugins/roi/server/geometry.py +181 -0
  198. plexora/plugins/roi/server/mapping.py +169 -0
  199. plexora/plugins/roi/server/operations.py +293 -0
  200. plexora/plugins/roi/server/repository.py +215 -0
  201. plexora/plugins/roi/server/routes.py +409 -0
  202. plexora/plugins/roi/server/schema.py +323 -0
  203. plexora/plugins/roi/static/roi.css +583 -0
  204. plexora/plugins/roi/static/roiApi.js +177 -0
  205. plexora/plugins/roi/static/roiFigureBridge.js +107 -0
  206. plexora/plugins/roi/static/roiGeometry.js +350 -0
  207. plexora/plugins/roi/static/roiRenderer.js +278 -0
  208. plexora/plugins/roi/static/roiSidebarController.js +1038 -0
  209. plexora/plugins/roi/static/roiState.js +518 -0
  210. plexora/plugins/roi/static/roiTools.js +993 -0
  211. plexora/plugins/roi/templates/roi/panel.html +240 -0
  212. plexora/proxy.py +57 -0
  213. plexora/server/models/adapters/__init__.py +94 -0
  214. plexora/server/models/adapters/anndata_adapter.py +388 -0
  215. plexora/server/models/adapters/base.py +93 -0
  216. plexora/server/models/adapters/classify.py +192 -0
  217. plexora/server/models/adapters/csv_adapter.py +105 -0
  218. plexora/server/models/adapters/inspection.py +265 -0
  219. plexora/server/models/adapters/spatialdata_adapter.py +248 -0
  220. plexora/server/models/centroid_tiles.py +305 -0
  221. plexora/server/models/data_model.py +1659 -0
  222. plexora/server/models/database_model.py +169 -0
  223. plexora/server/models/project.py +1270 -0
  224. plexora/server/plugins.py +199 -0
  225. plexora/server/routes/browse_routes.py +41 -0
  226. plexora/server/routes/data_routes.py +294 -0
  227. plexora/server/routes/import_routes.py +603 -0
  228. plexora/server/routes/page_routes.py +139 -0
  229. plexora/server/routes/project_routes.py +438 -0
  230. plexora/server/routes/quick_view_routes.py +62 -0
  231. plexora/server/routes/system_routes.py +39 -0
  232. plexora/server/routes/tool_routes.py +418 -0
  233. plexora/server/utils/addHEColumns.py +11 -0
  234. plexora/server/utils/fast_png.py +97 -0
  235. plexora/server/utils/fullConversion.py +50 -0
  236. plexora/server/utils/native_dialog.py +168 -0
  237. plexora/server/utils/pre_normalization.py +47 -0
  238. plexora/server/utils/segmentation_pyramid.py +598 -0
  239. plexora/server/utils/smallestenclosingcircle.py +126 -0
  240. plexora/server/utils/tiffsurgeon.py +372 -0
  241. plexora/server_cli.py +48 -0
  242. plexora/test.py +0 -0
  243. plexora-0.0.1.dist-info/METADATA +341 -0
  244. plexora-0.0.1.dist-info/RECORD +248 -0
  245. plexora-0.0.1.dist-info/WHEEL +5 -0
  246. plexora-0.0.1.dist-info/entry_points.txt +12 -0
  247. plexora-0.0.1.dist-info/licenses/LICENSE +207 -0
  248. plexora-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,316 @@
1
+ /**
2
+ * cellExplorerColors.js - palettes, and the lookup table core draws from.
3
+ *
4
+ * `buildLUT` is the whole point of the plugin's client half. Everything the
5
+ * user can change without touching the data -- palette, category colour,
6
+ * category visibility, continuous range -- is a rebuild of this table and a
7
+ * redraw, with no request and no geometry rebuilt. The values themselves are
8
+ * fetched once per column and cached, so those interactions cost microseconds
9
+ * rather than a round trip.
10
+ *
11
+ * Two shapes, because a dense table over cell ids is the fast lookup and cell
12
+ * ids are not guaranteed to be dense. Below the threshold it is a flat
13
+ * Uint8Array indexed by id; above it, a Map. Core reads either (see
14
+ * ImageViewer.setCellColorLUT).
15
+ *
16
+ * Alpha 0 is the single "do not draw this cell" channel. Hidden category, no
17
+ * value, a value that is NaN, a cell the table has no row for -- all of them
18
+ * arrive as alpha 0, so the renderer needs exactly one test.
19
+ */
20
+ class CellExplorerColors {
21
+
22
+ /**
23
+ * Categorical palette. Paul Tol's qualitative sets, which are built to stay
24
+ * distinguishable under the common colour-vision deficiencies -- the usual
25
+ * default (a rainbow, or matplotlib's tab20) puts a red and a green next to
26
+ * each other and asks the user to tell two cell populations apart by them.
27
+ *
28
+ * Ordered so the first handful are maximally separated: most variables have
29
+ * fewer than eight categories, and those are the ones that must not be
30
+ * confusable. Past 24 the palette cycles, which is honest -- there is no
31
+ * set of 60 colours anybody can tell apart, and the legend's search and
32
+ * isolate controls are the real answer at that size.
33
+ */
34
+ static CATEGORICAL = [
35
+ "#4477aa", "#ee6677", "#228833", "#ccbb44", "#66ccee", "#aa3377",
36
+ "#ee7733", "#0077bb", "#009988", "#cc3311", "#33bbee", "#ee3377",
37
+ "#88ccee", "#44aa99", "#117733", "#999933", "#ddcc77", "#cc6677",
38
+ "#882255", "#aa4499", "#6699cc", "#994455", "#997700", "#66aa55",
39
+ ];
40
+
41
+ /**
42
+ * The quick picks in a category's colour popover, in the shape core's
43
+ * ColorSwatchPicker takes.
44
+ *
45
+ * Drawn from CATEGORICAL rather than invented, so the ten one-click choices
46
+ * are the same ten colours the legend already assigned by itself: swapping
47
+ * two categories over is picking one of the colours on screen, not matching
48
+ * a new one to them by eye. The neutral last is UNASSIGNED, which is the
49
+ * one deliberate "this means nothing" colour in the set.
50
+ */
51
+ static SWATCH_PRESETS = [
52
+ { label: "Blue", hex: "#4477aa" },
53
+ { label: "Red", hex: "#ee6677" },
54
+ { label: "Green", hex: "#228833" },
55
+ { label: "Yellow", hex: "#ccbb44" },
56
+ { label: "Cyan", hex: "#66ccee" },
57
+ { label: "Purple", hex: "#aa3377" },
58
+ { label: "Orange", hex: "#ee7733" },
59
+ { label: "Teal", hex: "#009988" },
60
+ { label: "Magenta", hex: "#ee3377" },
61
+ { label: "Neutral grey", hex: "#5b6675" },
62
+ ];
63
+
64
+ /** Missing values. Neutral and desaturated, so "we do not know" never reads
65
+ * as a category with a finding attached to it. */
66
+ static UNASSIGNED = "#5b6675";
67
+
68
+ /** The label the panel and the LUT both use for missing values. */
69
+ static UNASSIGNED_LABEL = "Unassigned";
70
+
71
+ /**
72
+ * Continuous ramps, as anchor colours interpolated to 256 stops at use.
73
+ *
74
+ * Four, deliberately: one perceptually uniform default, one warm, one
75
+ * colour-vision-safe, one diverging, plus a custom two-colour option. Forty
76
+ * matplotlib colormaps is a menu, not a choice, and most of them are
77
+ * perceptually non-uniform in ways that invent structure in the data.
78
+ */
79
+ static RAMPS = {
80
+ viridis: ["#440154", "#472d7b", "#3b528b", "#2c728e", "#21918c",
81
+ "#28ae80", "#5ec962", "#addc30", "#fde725"],
82
+ magma: ["#000004", "#1c1044", "#4f127b", "#812581", "#b5367a",
83
+ "#e55964", "#fb8761", "#fec287", "#fcfdbf"],
84
+ cividis: ["#00224e", "#123570", "#3b496c", "#575d6d", "#707173",
85
+ "#8a8678", "#a59c74", "#c3b369", "#fee838"],
86
+ coolwarm: ["#3b4cc0", "#6788ee", "#9abbff", "#c9d7f0", "#edd1c2",
87
+ "#f7a889", "#e26952", "#b40426"],
88
+ };
89
+
90
+ static PALETTE_LABELS = {
91
+ viridis: "Viridis",
92
+ magma: "Magma",
93
+ cividis: "Cividis (colour-vision safe)",
94
+ coolwarm: "Cool-warm (diverging)",
95
+ custom: "Custom",
96
+ };
97
+
98
+ /** Fallback ends for the custom ramp, before the user picks anything. */
99
+ static CUSTOM_LOW = "#1b2a4a";
100
+ static CUSTOM_HIGH = "#f7c948";
101
+
102
+ /**
103
+ * Above this, a dense table stops being worth allocating: 8M ids is 32 MB
104
+ * of Uint8Array, and a table whose ids run that high is sparse by
105
+ * definition (they are usually source label values, not row numbers).
106
+ */
107
+ static DENSE_MAX_ID = 8_000_000;
108
+
109
+ static RAMP_STOPS = 256;
110
+
111
+ // -- colour utilities ---------------------------------------------------
112
+
113
+ static parseHex(hex) {
114
+ const text = String(hex || "").trim();
115
+ if (!/^#[0-9a-f]{6}$/i.test(text)) return null;
116
+ return [
117
+ parseInt(text.slice(1, 3), 16),
118
+ parseInt(text.slice(3, 5), 16),
119
+ parseInt(text.slice(5, 7), 16),
120
+ ];
121
+ }
122
+
123
+ static toHex([r, g, b]) {
124
+ const part = (v) => Math.max(0, Math.min(255, Math.round(v)))
125
+ .toString(16).padStart(2, "0");
126
+ return `#${part(r)}${part(g)}${part(b)}`;
127
+ }
128
+
129
+ /**
130
+ * The colour a category gets when nobody has chosen one.
131
+ *
132
+ * From its POSITION in the legend, not a hash of its name. Two things
133
+ * follow, and both are what a user expects: the same project opened twice
134
+ * gives the same colours, and `cluster = "1"` and `grade = "1"` are not
135
+ * forced to share one just because they share a label.
136
+ */
137
+ static defaultCategoryColor(index) {
138
+ return CellExplorerColors.CATEGORICAL[index % CellExplorerColors.CATEGORICAL.length];
139
+ }
140
+
141
+ /**
142
+ * 256 RGB stops for a palette, built by interpolating its anchors.
143
+ *
144
+ * Anchors rather than 256 literal entries per ramp: four ramps at 256
145
+ * colours each is 3 kB of source that nobody can read or check, and the
146
+ * interpolation is exact at every anchor.
147
+ */
148
+ static ramp(palette, custom) {
149
+ const anchors = palette === "custom"
150
+ ? [custom?.low || CellExplorerColors.CUSTOM_LOW,
151
+ custom?.high || CellExplorerColors.CUSTOM_HIGH]
152
+ : (CellExplorerColors.RAMPS[palette] || CellExplorerColors.RAMPS.viridis);
153
+
154
+ const points = anchors
155
+ .map((hex) => CellExplorerColors.parseHex(hex))
156
+ .filter(Boolean);
157
+ if (points.length === 0) points.push([0, 0, 0], [255, 255, 255]);
158
+ if (points.length === 1) points.push(points[0]);
159
+
160
+ const stops = CellExplorerColors.RAMP_STOPS;
161
+ const out = new Uint8Array(stops * 3);
162
+ const span = points.length - 1;
163
+ for (let i = 0; i < stops; i += 1) {
164
+ const position = (i / (stops - 1)) * span;
165
+ const lower = Math.min(Math.floor(position), span - 1);
166
+ const t = position - lower;
167
+ const a = points[lower];
168
+ const b = points[lower + 1];
169
+ out[i * 3] = a[0] + (b[0] - a[0]) * t;
170
+ out[i * 3 + 1] = a[1] + (b[1] - a[1]) * t;
171
+ out[i * 3 + 2] = a[2] + (b[2] - a[2]) * t;
172
+ }
173
+ return out;
174
+ }
175
+
176
+ /** One ramp stop as a hex string, for the panel's own swatches. */
177
+ static rampStop(palette, custom, fraction) {
178
+ const ramp = CellExplorerColors.ramp(palette, custom);
179
+ const index = Math.max(0, Math.min(CellExplorerColors.RAMP_STOPS - 1,
180
+ Math.round(fraction * (CellExplorerColors.RAMP_STOPS - 1))));
181
+ return CellExplorerColors.toHex([
182
+ ramp[index * 3], ramp[index * 3 + 1], ramp[index * 3 + 2]]);
183
+ }
184
+
185
+ // -- the lookup table ---------------------------------------------------
186
+
187
+ /**
188
+ * Build the cell-id -> colour table core draws from.
189
+ *
190
+ * @param spec
191
+ * ids Uint32Array, one per cell, from the values response
192
+ * kind "categorical" | "continuous"
193
+ * codes Uint16Array parallel to ids (categorical)
194
+ * values Float32Array parallel to ids (continuous)
195
+ * categories [{value}] in the server's order -- codes index into this
196
+ * colors { category: "#rrggbb" } overrides
197
+ * hidden Set of category labels to leave undrawn
198
+ * domain [low, high] for continuous
199
+ * palette ramp name, or "custom"
200
+ * custom { low, high } hex ends for "custom"
201
+ * showMissing whether cells with no value are drawn at all
202
+ * blank draw nothing: a table of the right shape, transparent
203
+ * @returns { colors, maxId } | { map } | null
204
+ */
205
+ static buildLUT(spec) {
206
+ const ids = spec?.ids;
207
+ if (!ids || !ids.length) return null;
208
+
209
+ let maxId = 0;
210
+ for (let i = 0; i < ids.length; i += 1) {
211
+ if (ids[i] > maxId) maxId = ids[i];
212
+ }
213
+
214
+ const dense = maxId <= CellExplorerColors.DENSE_MAX_ID;
215
+ const table = dense ? new Uint8Array(4 * (maxId + 1)) : null;
216
+ const map = dense ? null : new Map();
217
+ const write = dense
218
+ ? (id, r, g, b, a) => {
219
+ const offset = id * 4;
220
+ table[offset] = r;
221
+ table[offset + 1] = g;
222
+ table[offset + 2] = b;
223
+ table[offset + 3] = a;
224
+ }
225
+ // Alpha 0 is simply an absent entry in the sparse form: core treats
226
+ // "no entry" and "alpha 0" identically, so storing hidden cells
227
+ // would be bytes spent to say nothing.
228
+ : (id, r, g, b, a) => { if (a) map.set(id, [r, g, b, a]); };
229
+
230
+ // Nothing written is the whole of "drawn but hidden": a zeroed dense
231
+ // table is alpha 0 for every id, and an empty sparse map says the same.
232
+ // A table rather than a null LUT, because null means "this plugin is
233
+ // not colouring cells" -- core would then draw the layer in its own
234
+ // default white, which is not what hiding an overlay looks like.
235
+ if (!spec.blank) {
236
+ if (spec.kind === "continuous") {
237
+ CellExplorerColors._fillContinuous(spec, ids, write);
238
+ } else {
239
+ CellExplorerColors._fillCategorical(spec, ids, write);
240
+ }
241
+ }
242
+ return dense ? { colors: table, maxId } : { map };
243
+ }
244
+
245
+ static _fillCategorical(spec, ids, write) {
246
+ const categories = spec.categories || [];
247
+ const hidden = spec.hidden || new Set();
248
+ const overrides = spec.colors || {};
249
+
250
+ // One RGBA per code, resolved before the loop over cells. Doing the
251
+ // override lookup and the hex parse per cell instead would be a string
252
+ // operation per cell per redraw.
253
+ const swatches = categories.map((entry, index) => {
254
+ const label = entry.value;
255
+ if (hidden.has(label)) return [0, 0, 0, 0];
256
+ const rgb = CellExplorerColors.parseHex(overrides[label])
257
+ || CellExplorerColors.parseHex(
258
+ CellExplorerColors.defaultCategoryColor(index));
259
+ return [rgb[0], rgb[1], rgb[2], 255];
260
+ });
261
+
262
+ const missingHidden = spec.showMissing === false
263
+ || hidden.has(CellExplorerColors.UNASSIGNED_LABEL);
264
+ const missingRgb = CellExplorerColors.parseHex(
265
+ overrides[CellExplorerColors.UNASSIGNED_LABEL]
266
+ || CellExplorerColors.UNASSIGNED);
267
+ const missing = missingHidden
268
+ ? [0, 0, 0, 0] : [missingRgb[0], missingRgb[1], missingRgb[2], 255];
269
+
270
+ const codes = spec.codes;
271
+ for (let i = 0; i < ids.length; i += 1) {
272
+ const code = codes[i];
273
+ // Anything not in the dictionary -- the missing sentinel, or a code
274
+ // from a stale payload -- is Unassigned rather than a guess at a
275
+ // nearby category.
276
+ const swatch = code < swatches.length ? swatches[code] : missing;
277
+ write(ids[i], swatch[0], swatch[1], swatch[2], swatch[3]);
278
+ }
279
+ }
280
+
281
+ static _fillContinuous(spec, ids, write) {
282
+ const ramp = CellExplorerColors.ramp(spec.palette, spec.custom);
283
+ const stops = CellExplorerColors.RAMP_STOPS;
284
+ const values = spec.values;
285
+ let [low, high] = spec.domain || [0, 1];
286
+ if (!Number.isFinite(low) || !Number.isFinite(high) || high <= low) {
287
+ // A constant column, or a range that stopped making sense. Every
288
+ // cell takes the same stop rather than dividing by zero -- the
289
+ // panel says so in words, which is more use than a uniform picture
290
+ // the user has to work out for themselves.
291
+ const flat = Math.floor(stops * 0.75) * 3;
292
+ for (let i = 0; i < ids.length; i += 1) {
293
+ if (!Number.isFinite(values[i])) continue;
294
+ write(ids[i], ramp[flat], ramp[flat + 1], ramp[flat + 2], 255);
295
+ }
296
+ return;
297
+ }
298
+
299
+ const scale = (stops - 1) / (high - low);
300
+ for (let i = 0; i < ids.length; i += 1) {
301
+ const value = values[i];
302
+ // NaN is how the server says "no value", including the infinities
303
+ // it folded in. Left undrawn rather than clamped to an end, which
304
+ // would put a handful of cells at the extreme colour and say
305
+ // nothing about why.
306
+ if (!Number.isFinite(value)) continue;
307
+ // Clipping is display-only. The stored value is untouched; a cell
308
+ // past the top of the range simply takes the top colour.
309
+ let index = Math.round((value - low) * scale);
310
+ if (index < 0) index = 0;
311
+ else if (index >= stops) index = stops - 1;
312
+ const offset = index * 3;
313
+ write(ids[i], ramp[offset], ramp[offset + 1], ramp[offset + 2], 255);
314
+ }
315
+ }
316
+ }