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,276 @@
1
+ /**
2
+ * dataSourceField.js -- "which data file?", asked the same way everywhere.
3
+ *
4
+ * A path alone does not name a table. A .zarr store can hold several, and a
5
+ * table can span several images, and neither is answerable from the file --
6
+ * the server refuses to import until one is picked (see import_routes'
7
+ * replace_project_data). The upload page has asked both questions since it was
8
+ * written; the requirements modal and the edit page asked neither, so choosing
9
+ * a multi-table store on either one posted a path the server could only
10
+ * reject, with the message "choose which one to load" and nothing to choose
11
+ * from.
12
+ *
13
+ * So the question lives here, once, and all three surfaces mount it: path
14
+ * input, its two pickers, and the table/image selects that appear only when
15
+ * the file itself forces the choice. What it produces is exactly the four keys
16
+ * the server reads -- `data`, `table`, `subset_column`, `subset_value`.
17
+ *
18
+ * Two properties worth keeping:
19
+ *
20
+ * - **Nothing is inspected until the path changes.** The edit page mounts this
21
+ * with the project's stored file, already imported and already answered;
22
+ * re-opening it on load would re-read a multi-gigabyte store to ask a
23
+ * question that has an answer.
24
+ * - **It reports what it is still waiting for.** `blocking()` is the caller's
25
+ * way to refuse to save rather than post something the server will reject --
26
+ * and to say why, in the same words the picker beside it is asking.
27
+ */
28
+ window.PlexoraDataSourceField = (function () {
29
+ const DATA_TYPE_LABELS = {
30
+ csv: "CSV table",
31
+ anndata: "AnnData",
32
+ spatialdata: "SpatialData store",
33
+ };
34
+
35
+ const DEFAULT_HINT = "CSV, AnnData (.h5ad) or SpatialData (.zarr).";
36
+
37
+ function el(tag, className, text) {
38
+ const node = document.createElement(tag);
39
+ if (className) node.className = className;
40
+ if (text) node.textContent = text;
41
+ return node;
42
+ }
43
+
44
+ /**
45
+ * @param container where to render
46
+ * @param options `value` the path already stored (never inspected),
47
+ * `id` for the input when a caller needs to address it, `hint` the
48
+ * resting note under the field, and `onChange` called with value()
49
+ * whenever any of the four keys changes.
50
+ * @returns { value(), blocking(), element }
51
+ */
52
+ function mount(container, options = {}) {
53
+ const initial = (options.value || "").trim();
54
+ const onChange = options.onChange || function () {};
55
+
56
+ const root = el("div", "data-source-field");
57
+ const row = el("div", "import-field-row");
58
+ const input = el("input", "form-control");
59
+ input.type = "text";
60
+ input.placeholder = options.placeholder || "/path/to/cells.csv";
61
+ input.value = initial;
62
+ if (options.id) input.id = options.id;
63
+ row.appendChild(input);
64
+
65
+ // A .zarr store is a directory and a .csv/.h5ad is a file, and one
66
+ // input takes both -- so the picker offers each in turn, same as the
67
+ // upload page.
68
+ [["file", "File…", "data"], ["directory", "Store…", "any"]]
69
+ .forEach(([mode, label, filter]) => {
70
+ const button = el("button", "browse-button", label);
71
+ button.type = "button";
72
+ if (typeof attachBrowseButton === "function") {
73
+ attachBrowseButton(button, input, { mode, filter });
74
+ }
75
+ row.appendChild(button);
76
+ });
77
+ root.appendChild(row);
78
+
79
+ const note = el("span", "field-hint", options.hint || DEFAULT_HINT);
80
+ root.appendChild(note);
81
+
82
+ const tableField = el("div", "data-source-extra");
83
+ const tableSelect = el("select", "form-select");
84
+ tableField.append(tableSelect,
85
+ el("span", "field-hint",
86
+ "This store holds several tables. Choose which one holds the cells."));
87
+ tableField.hidden = true;
88
+ root.appendChild(tableField);
89
+
90
+ const subsetField = el("div", "data-source-extra");
91
+ const subsetSelect = el("select", "form-select");
92
+ const subsetHint = el("span", "field-hint");
93
+ subsetField.append(subsetSelect, subsetHint);
94
+ subsetField.hidden = true;
95
+ root.appendChild(subsetField);
96
+
97
+ container.appendChild(root);
98
+
99
+ const state = {
100
+ table: null,
101
+ subsetColumn: null,
102
+ subsetValue: null,
103
+ blocking: null,
104
+ };
105
+
106
+ // Replies can arrive out of order when someone types quickly, and a
107
+ // stale one would re-show a picker for a file no longer in the box.
108
+ let token = 0;
109
+ let timer = null;
110
+ let pending = false;
111
+
112
+ function value() {
113
+ return {
114
+ data: input.value.trim(),
115
+ table: state.table || undefined,
116
+ subset_column: state.subsetColumn || undefined,
117
+ subset_value: state.subsetValue || undefined,
118
+ };
119
+ }
120
+
121
+ /** What still stops this being saved, or null. */
122
+ function blocking() {
123
+ if (pending) return "Still reading the data file — try again in a moment.";
124
+ return state.blocking;
125
+ }
126
+
127
+ function emit() {
128
+ onChange(value());
129
+ }
130
+
131
+ function reset() {
132
+ state.table = null;
133
+ state.subsetColumn = null;
134
+ state.subsetValue = null;
135
+ state.blocking = null;
136
+ tableField.hidden = true;
137
+ subsetField.hidden = true;
138
+ }
139
+
140
+ function schedule() {
141
+ clearTimeout(timer);
142
+ const path = input.value.trim();
143
+ // The stored file is already imported and already answered. Only a
144
+ // change is a question, which is also what keeps a mount free.
145
+ if (path === initial) {
146
+ reset();
147
+ input.classList.remove("is-valid", "is-invalid");
148
+ note.textContent = options.hint || DEFAULT_HINT;
149
+ emit();
150
+ return;
151
+ }
152
+ reset();
153
+ emit();
154
+ if (!path) {
155
+ input.classList.remove("is-valid", "is-invalid");
156
+ note.textContent = options.hint || DEFAULT_HINT;
157
+ return;
158
+ }
159
+ // Every inspection opens the file, and an .h5ad is not cheap to
160
+ // open -- so wait for a pause in typing rather than per keystroke.
161
+ timer = setTimeout(() => inspect(null), 250);
162
+ }
163
+
164
+ /**
165
+ * @param table which table inside a .zarr store to look at, on the
166
+ * second pass. Everything below the table picker is a question about
167
+ * a table, so a multi-table store cannot answer any of it until one
168
+ * is named.
169
+ */
170
+ async function inspect(table) {
171
+ const mine = ++token;
172
+ const path = input.value.trim();
173
+ if (!path) return;
174
+ pending = true;
175
+
176
+ let payload;
177
+ try {
178
+ const response = await fetch(plexoraUrl("inspect_data"), {
179
+ method: "POST",
180
+ headers: { "Content-Type": "application/json" },
181
+ body: JSON.stringify({ path, table: table || null }),
182
+ });
183
+ payload = await response.json();
184
+ } catch (e) {
185
+ pending = false;
186
+ return; // transport failures are reported by PlexoraStatus
187
+ }
188
+ if (mine !== token) return; // a newer path is in the box
189
+ pending = false;
190
+
191
+ if (!payload.ok) {
192
+ input.classList.remove("is-valid");
193
+ input.classList.add("is-invalid");
194
+ note.textContent = payload.error || "Unreadable";
195
+ state.blocking = payload.error || "That data file cannot be read.";
196
+ emit();
197
+ return;
198
+ }
199
+
200
+ input.classList.remove("is-invalid");
201
+ input.classList.add("is-valid");
202
+ note.textContent = DATA_TYPE_LABELS[payload.data_type] || payload.data_type;
203
+ state.blocking = null;
204
+
205
+ if (!table && (payload.tables || []).length > 1) {
206
+ // Choosing re-runs this with the table, which is what puts the
207
+ // image question -- a store with several tables has no answer
208
+ // to it until one is named.
209
+ showTables(payload.tables);
210
+ emit();
211
+ return;
212
+ }
213
+ state.table = table || payload.table || null;
214
+ if ((payload.ambiguous || []).length) {
215
+ showSubset(payload.ambiguous[0]);
216
+ }
217
+ emit();
218
+ }
219
+
220
+ function showTables(tables) {
221
+ tableSelect.replaceChildren();
222
+ tableSelect.append(new Option("Choose a table…", ""));
223
+ tables.forEach((table) => {
224
+ tableSelect.append(new Option(
225
+ `${table.name} — ${table.n_obs} cells × ${table.n_var} markers`,
226
+ table.name,
227
+ ));
228
+ });
229
+ // Assigned rather than added: this runs again on every
230
+ // re-inspection, and addEventListener would stack a handler each
231
+ // time.
232
+ tableSelect.onchange = () => {
233
+ subsetField.hidden = true;
234
+ state.subsetColumn = null;
235
+ state.subsetValue = null;
236
+ state.table = tableSelect.value || null;
237
+ state.blocking = tableSelect.value
238
+ ? null : "Choose which table in the store holds the cells.";
239
+ emit();
240
+ if (tableSelect.value) inspect(tableSelect.value);
241
+ };
242
+ tableField.hidden = false;
243
+ state.table = null;
244
+ state.blocking = "Choose which table in the store holds the cells.";
245
+ }
246
+
247
+ function showSubset(ambiguous) {
248
+ subsetHint.textContent =
249
+ `This table covers several images (column "${ambiguous.column}"). `
250
+ + "Choose the one this project's image shows.";
251
+ subsetSelect.replaceChildren();
252
+ subsetSelect.append(new Option("Choose an image…", ""));
253
+ (ambiguous.values || []).forEach(
254
+ (item) => subsetSelect.append(new Option(item, item)));
255
+ subsetSelect.onchange = () => {
256
+ state.subsetValue = subsetSelect.value || null;
257
+ state.blocking = subsetSelect.value
258
+ ? null : "Choose which image in the table this project shows.";
259
+ emit();
260
+ };
261
+ subsetField.hidden = false;
262
+ state.subsetColumn = ambiguous.column;
263
+ state.subsetValue = null;
264
+ state.blocking = "Choose which image in the table this project shows.";
265
+ }
266
+
267
+ input.addEventListener("input", schedule);
268
+ // browsePicker dispatches both; `input` alone would miss nothing, but
269
+ // the upload page's fields listen on keyup and this stays in step.
270
+ input.addEventListener("keyup", schedule);
271
+
272
+ return { value, blocking, element: root, input };
273
+ }
274
+
275
+ return { mount };
276
+ })();