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,372 @@
1
+ /**
2
+ * cellExplorerState.js - what is being shown, and what the user has chosen.
3
+ *
4
+ * One object holds both halves so nothing else has to reconcile them: the
5
+ * descriptors the server sent, and the display preferences that came out of the
6
+ * plugin store. The panel reads from here, the LUT is built from here, and the
7
+ * autosave writes from here.
8
+ *
9
+ * The `generation` counter is the guard against a stale response. It is bumped
10
+ * whenever what is being shown changes -- a new column, an overridden kind, a
11
+ * dataset refresh -- and every in-flight request is compared against it on
12
+ * arrival. See cellExplorerApi.js for the other half.
13
+ *
14
+ * Preferences are stored per column, not globally. `cluster = "1"` and
15
+ * `grade = "1"` are different things that happen to share a label, and a colour
16
+ * keyed only by the label would force them to look the same.
17
+ */
18
+ class CellExplorerState {
19
+
20
+ constructor() {
21
+ this.generation = 0;
22
+ this.revision = 0;
23
+ this.settings = CellExplorerState.defaults();
24
+ this.descriptors = [];
25
+ this.canDraw = { segmentation: false, segmentation_pending: false, centroids: false };
26
+ //: Which column is showing, and the values behind it. `data` is null
27
+ //: while a fetch is in flight, which is what the panel renders its
28
+ //: loading state from.
29
+ this.column = null;
30
+ this.data = null;
31
+ this.status = "idle";
32
+ this.error = null;
33
+ //: Set when a preference changes and cleared when it is written. The
34
+ //: autosave reads it so a debounce that fires with nothing to say costs
35
+ //: no request.
36
+ this.dirty = false;
37
+ //: True when the stored document is from a newer Plexora. The panel
38
+ //: still works; it must not save over it.
39
+ this.readOnly = false;
40
+ }
41
+
42
+ static defaults() {
43
+ return {
44
+ selected: null,
45
+ display: { mode: null, opacity: 0.7 },
46
+ overrides: {},
47
+ categorical: {},
48
+ continuous: {},
49
+ };
50
+ }
51
+
52
+ /** Bump and return the generation a request should carry. */
53
+ nextGeneration() {
54
+ this.generation += 1;
55
+ return this.generation;
56
+ }
57
+
58
+ isCurrent(generation) {
59
+ return generation === this.generation;
60
+ }
61
+
62
+ // -- descriptors --------------------------------------------------------
63
+
64
+ /**
65
+ * One column's descriptor, by name.
66
+ *
67
+ * Indexed rather than scanned. The dropdown asks for a hint and a warning
68
+ * per option, per keystroke of its filter, so a linear scan here is
69
+ * quadratic in the width of the table -- which used to be a short list and
70
+ * is now every metadata column there is. The index is rebuilt whenever the
71
+ * array is replaced, which is the only way descriptors ever change.
72
+ */
73
+ descriptor(column) {
74
+ if (this._index?.source !== this.descriptors) {
75
+ this._index = {
76
+ source: this.descriptors,
77
+ byName: new Map(this.descriptors.map((entry) => [entry.name, entry])),
78
+ };
79
+ }
80
+ return this._index.byName.get(column) || null;
81
+ }
82
+
83
+ get current() {
84
+ return this.descriptor(this.column);
85
+ }
86
+
87
+ /**
88
+ * How a column is being read: the user's override if they set one,
89
+ * otherwise the server's inference.
90
+ *
91
+ * An override that names something the descriptor cannot supply is ignored
92
+ * rather than honoured -- the payload for it was never computed, and a
93
+ * saved preference outlives the data it was made against.
94
+ */
95
+ kindFor(column) {
96
+ const descriptor = this.descriptor(column);
97
+ if (!descriptor) return null;
98
+ const override = this.settings.overrides[column];
99
+ if (override && override !== descriptor.kind && descriptor.ambiguous) {
100
+ return override;
101
+ }
102
+ return descriptor.kind;
103
+ }
104
+
105
+ /** The kind to ask the server for, or null to take its own answer. */
106
+ requestedKind(column) {
107
+ const descriptor = this.descriptor(column);
108
+ const kind = this.kindFor(column);
109
+ return descriptor && kind !== descriptor.kind ? kind : null;
110
+ }
111
+
112
+ setOverride(column, kind) {
113
+ if (kind) {
114
+ this.settings.overrides[column] = kind;
115
+ } else {
116
+ delete this.settings.overrides[column];
117
+ }
118
+ this.dirty = true;
119
+ }
120
+
121
+ // -- per-column preferences ---------------------------------------------
122
+
123
+ /**
124
+ * This column's category colours and hidden set, created on first use.
125
+ *
126
+ * Guarded against a null column so a stray call cannot file preferences
127
+ * under the string "null" -- which would then be written to the store and
128
+ * come back forever, keyed to a column that does not exist.
129
+ */
130
+ categorical(column) {
131
+ if (!column) return { colors: {}, hidden: [] };
132
+ let entry = this.settings.categorical[column];
133
+ if (!entry) {
134
+ entry = this.settings.categorical[column] = { colors: {}, hidden: [] };
135
+ }
136
+ entry.colors = entry.colors || {};
137
+ entry.hidden = entry.hidden || [];
138
+ return entry;
139
+ }
140
+
141
+ /** As `categorical`, and null-guarded for the same reason.
142
+ *
143
+ * `hidden` is a boolean here where a categorical entry's is a list of
144
+ * labels. Not an inconsistency: a ramp has no rows to hide one of, so the
145
+ * column is the only unit there is. */
146
+ continuous(column) {
147
+ if (!column) {
148
+ return { palette: "viridis", custom: { low: null, high: null },
149
+ range: { mode: "auto" }, hidden: false };
150
+ }
151
+ let entry = this.settings.continuous[column];
152
+ if (!entry) {
153
+ entry = this.settings.continuous[column] = {
154
+ palette: "viridis", custom: { low: null, high: null },
155
+ range: { mode: "auto" }, hidden: false,
156
+ };
157
+ }
158
+ entry.custom = entry.custom || { low: null, high: null };
159
+ entry.range = entry.range || { mode: "auto" };
160
+ entry.hidden = Boolean(entry.hidden);
161
+ return entry;
162
+ }
163
+
164
+ hiddenSet(column) {
165
+ return new Set(this.categorical(column).hidden || []);
166
+ }
167
+
168
+ setHidden(column, label, hidden) {
169
+ const entry = this.categorical(column);
170
+ const current = new Set(entry.hidden);
171
+ if (hidden) current.add(label); else current.delete(label);
172
+ entry.hidden = Array.from(current).sort();
173
+ this.dirty = true;
174
+ }
175
+
176
+ setAllHidden(column, hidden) {
177
+ const entry = this.categorical(column);
178
+ entry.hidden = hidden ? this.allLabels(column).sort() : [];
179
+ this.dirty = true;
180
+ }
181
+
182
+ setColor(column, label, hex) {
183
+ const entry = this.categorical(column);
184
+ if (hex) entry.colors[label] = hex; else delete entry.colors[label];
185
+ this.dirty = true;
186
+ }
187
+
188
+ setPalette(column, palette) {
189
+ this.continuous(column).palette = palette;
190
+ this.dirty = true;
191
+ }
192
+
193
+ setCustomColor(column, end, hex) {
194
+ this.continuous(column).custom[end] = hex;
195
+ this.dirty = true;
196
+ }
197
+
198
+ /** Draw this numeric column's overlay, or not. The counterpart of the
199
+ * legend's per-category eye, for a kind of column that has no rows. */
200
+ setContinuousHidden(column, hidden) {
201
+ this.continuous(column).hidden = Boolean(hidden);
202
+ this.dirty = true;
203
+ }
204
+
205
+ setRange(column, low, high) {
206
+ this.continuous(column).range = (low === null || high === null)
207
+ ? { mode: "auto" }
208
+ : { mode: "manual", min: low, max: high };
209
+ this.dirty = true;
210
+ }
211
+
212
+ setOpacity(value) {
213
+ this.settings.display.opacity = value;
214
+ this.dirty = true;
215
+ }
216
+
217
+ setMode(mode) {
218
+ this.settings.display.mode = mode;
219
+ this.dirty = true;
220
+ }
221
+
222
+ /**
223
+ * The low/high a continuous column is drawn between.
224
+ *
225
+ * The stored manual range when there is a usable one, otherwise the
226
+ * server's robust percentiles -- p01/p99 rather than the literal extremes,
227
+ * because one outlying cell otherwise compresses every other cell into a
228
+ * few percent of the ramp.
229
+ *
230
+ * A stored manual range that no longer makes sense against this data falls
231
+ * back to auto rather than refusing to draw. The source can change under a
232
+ * saved preference.
233
+ */
234
+ domainFor(column) {
235
+ const stats = this.descriptor(column)?.stats || {};
236
+ const range = this.continuous(column).range;
237
+ if (range.mode === "manual"
238
+ && Number.isFinite(range.min) && Number.isFinite(range.max)
239
+ && range.min < range.max) {
240
+ return [range.min, range.max];
241
+ }
242
+ const low = Number.isFinite(stats.p01) ? stats.p01 : stats.min;
243
+ const high = Number.isFinite(stats.p99) ? stats.p99 : stats.max;
244
+ return [low, high];
245
+ }
246
+
247
+ isAutoRange(column) {
248
+ return this.continuous(column).range.mode !== "manual";
249
+ }
250
+
251
+ /**
252
+ * The lookup table for whatever is currently showing, or null.
253
+ *
254
+ * Everything the user can change without new data lands here: this is
255
+ * rebuilt and handed to core, and nothing is refetched.
256
+ */
257
+ buildLUT() {
258
+ const column = this.column;
259
+ const data = this.data;
260
+ if (!column || !data) return null;
261
+
262
+ if (data.kind === "continuous") {
263
+ const entry = this.continuous(column);
264
+ return CellExplorerColors.buildLUT({
265
+ kind: "continuous",
266
+ ids: data.ids,
267
+ values: data.values,
268
+ domain: this.domainFor(column),
269
+ palette: entry.palette,
270
+ custom: entry.custom,
271
+ blank: entry.hidden,
272
+ });
273
+ }
274
+
275
+ const entry = this.categorical(column);
276
+ return CellExplorerColors.buildLUT({
277
+ kind: "categorical",
278
+ ids: data.ids,
279
+ codes: data.codes,
280
+ categories: this.descriptor(column)?.categories || [],
281
+ colors: entry.colors,
282
+ hidden: this.hiddenSet(column),
283
+ });
284
+ }
285
+
286
+ /** What gets written to the store. Only preferences -- never values. */
287
+ toSettings() {
288
+ return {
289
+ selected: this.column,
290
+ display: { ...this.settings.display },
291
+ overrides: { ...this.settings.overrides },
292
+ categorical: this.settings.categorical,
293
+ continuous: this.settings.continuous,
294
+ };
295
+ }
296
+
297
+ /**
298
+ * Adopt what came back from the store.
299
+ *
300
+ * Unknown columns are kept rather than filtered against the current table:
301
+ * a column that is temporarily absent comes back, and dropping the colours
302
+ * somebody chose the one time it was missing throws away deliberate work.
303
+ */
304
+ adopt(stored) {
305
+ const defaults = CellExplorerState.defaults();
306
+ this.revision = Number.isInteger(stored?.revision) ? stored.revision : 0;
307
+ this.readOnly = Boolean(stored?.unreadable);
308
+ this.settings = {
309
+ selected: stored?.selected ?? defaults.selected,
310
+ display: { ...defaults.display, ...(stored?.display || {}) },
311
+ overrides: { ...(stored?.overrides || {}) },
312
+ categorical: { ...(stored?.categorical || {}) },
313
+ continuous: { ...(stored?.continuous || {}) },
314
+ };
315
+ this.dirty = false;
316
+ }
317
+
318
+ /**
319
+ * Whether every category is showing, none is, or it is a mix.
320
+ *
321
+ * The All/None control reads this: it reports what the legend is in as well
322
+ * as offering to change it, and "mixed" -- neither lit -- is the ordinary
323
+ * state once anybody has hidden a single row.
324
+ */
325
+ visibilityState(column) {
326
+ const labels = this.allLabels(column);
327
+ if (!labels.length) return "all";
328
+ const hidden = this.hiddenSet(column);
329
+ const count = labels.filter((label) => hidden.has(label)).length;
330
+ if (count === 0) return "all";
331
+ return count === labels.length ? "none" : "mixed";
332
+ }
333
+
334
+ /** Every row the legend can show, including Unassigned when there is one. */
335
+ allLabels(column) {
336
+ const descriptor = this.descriptor(column);
337
+ if (!descriptor) return [];
338
+ const labels = (descriptor.categories || []).map((entry) => entry.value);
339
+ if (descriptor.n_missing) labels.push(CellExplorerColors.UNASSIGNED_LABEL);
340
+ return labels;
341
+ }
342
+
343
+ /**
344
+ * Which column to open on.
345
+ *
346
+ * In order: what was showing last time, if the table still has it; the
347
+ * project's annotation column, which is what a cell-type column is for; the
348
+ * first categorical that was not a guess; the first anything.
349
+ *
350
+ * A saved selection is honoured whatever it is -- including an
351
+ * identifier-like column, which somebody picked on purpose the last time
352
+ * they were here. The automatic steps skip those: opening the panel and
353
+ * finding a million colours drawn from a barcode column is not a default
354
+ * anybody would have asked for. If every column looks like an identifier,
355
+ * nothing is chosen and the user picks.
356
+ */
357
+ chooseColumn(celltypeColumn) {
358
+ const saved = this.settings.selected;
359
+ if (saved && this.descriptors.some((entry) => entry.name === saved)) return saved;
360
+
361
+ const usable = this.descriptors.filter((entry) => !entry.identifier_like);
362
+ const celltype = usable.find(
363
+ (entry) => entry.name === celltypeColumn && entry.kind === "categorical");
364
+ if (celltype) return celltype.name;
365
+
366
+ const confident = usable.find(
367
+ (entry) => entry.kind === "categorical" && !entry.ambiguous);
368
+ if (confident) return confident.name;
369
+
370
+ return usable.length ? usable[0].name : null;
371
+ }
372
+ }