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,572 @@
1
+ /**
2
+ * requirementsModal.js -- asking for what a plugin needs and the project lacks.
3
+ *
4
+ * A plugin declares its requirements (plexora/api/plugin.py's Requires); the
5
+ * server works out which are unmet and hands over a list of typed descriptors.
6
+ * This renders a form from that list and posts the answers back, which the
7
+ * server stores on the project -- so the next plugin wanting the same thing
8
+ * never asks again.
9
+ *
10
+ * Core knows nothing about any particular plugin here. Every field is built
11
+ * from a requirement's `kind`, which is why adding a requirement to a plugin
12
+ * needs no change in this file.
13
+ *
14
+ * Three properties the design turns on:
15
+ *
16
+ * - **Nothing already answered is shown.** A requirement the user has answered
17
+ * is absent from the payload rather than rendered as a filled-in field, so
18
+ * they are never asked to re-confirm what they already gave.
19
+ * - **A guess is not an answer.** Most of a conventionally-named table is
20
+ * filled in by the column predictor, and those arrive in `confirm`:
21
+ * prefilled, shown once, and never again once saved. Without this tier the
22
+ * first launch of a tool would run silently on five guesses, which is what a
23
+ * well-named file used to produce.
24
+ * - **It loops.** Attaching a data file makes new questions askable (which
25
+ * column is the cell id — meaningless before there were columns), so after
26
+ * each save it re-asks the server what remains and re-renders. That is why
27
+ * the server reports roles and markers only once a table exists.
28
+ */
29
+ window.PlexoraRequirements = (function () {
30
+ let openDialog = null;
31
+
32
+ // The image-id select's "only one image" choice. Written as an escape so
33
+ // this file stays plain ASCII: a literal NUL byte makes grep and diff
34
+ // treat the whole source as binary. A leading NUL cannot occur in a real
35
+ // obs column name, so the option can never collide with one -- and the
36
+ // value is client-only regardless, translated into the payload's
37
+ // `single_image` flag rather than sent as it stands.
38
+ const SINGLE_IMAGE = "\u0000single-image";
39
+
40
+ // The cell-id select's "number the rows" choice, on the same terms and for
41
+ // the same reason. It used to be the wording on the blank option -- which
42
+ // made a select nobody touched indistinguishable from one deliberately
43
+ // left on the row number, and is how a project kept a positional cell id
44
+ // while its mask carried the label values from an obs column. Translated
45
+ // into the payload's `row_number_ids` flag, never sent as it stands.
46
+ const ROW_NUMBER = "\u0000row-number";
47
+
48
+ function el(tag, className, text) {
49
+ const node = document.createElement(tag);
50
+ if (className) node.className = className;
51
+ if (text) node.textContent = text;
52
+ return node;
53
+ }
54
+
55
+ /**
56
+ * One row of the form: its label on the left, its control on the right.
57
+ *
58
+ * Every field is a row rather than a label stacked over a control, because
59
+ * the form is a short list of "which column is this?" questions and reading
60
+ * them down a single column of labels is how it stays scannable. "Optional"
61
+ * rides on the label for the same reason -- as its own line under the
62
+ * control it doubled the height of every field that had it.
63
+ */
64
+ function fieldRow(requirement) {
65
+ const field = el("div", "requirement-field");
66
+ const label = el("label", null, requirement.label);
67
+ if (requirement.optional) {
68
+ label.appendChild(el("span", "field-hint", "optional"));
69
+ }
70
+ field.appendChild(label);
71
+ return field;
72
+ }
73
+
74
+ /** A mask path: one input, one Browse, nothing to ask about it. */
75
+ function maskField(requirement, state) {
76
+ const field = fieldRow(requirement);
77
+
78
+ const row = el("div", "import-field-row");
79
+ const input = el("input", "form-control");
80
+ input.type = "text";
81
+ input.placeholder = "/path/to/segmentation.ome.tif";
82
+ input.addEventListener("input", () => {
83
+ state.segmentation = input.value.trim();
84
+ });
85
+ row.appendChild(input);
86
+
87
+ const button = el("button", "browse-button", "Browse…");
88
+ button.type = "button";
89
+ if (typeof attachBrowseButton === "function") {
90
+ attachBrowseButton(button, input, { mode: "file", filter: "image" });
91
+ }
92
+ row.appendChild(button);
93
+
94
+ field.appendChild(row);
95
+ return field;
96
+ }
97
+
98
+ /**
99
+ * The data file, and whatever the file itself leaves open.
100
+ *
101
+ * Rendered by the shared control (dataSourceField.js) rather than as a
102
+ * bare input, because a path is not always enough to read a file by: a
103
+ * .zarr store with several tables, or a table spanning several images,
104
+ * has to be narrowed before anything can be loaded. This used to be an
105
+ * input and two buttons, so picking such a store posted a path the server
106
+ * could only refuse -- with a message telling the user to choose, and
107
+ * nothing anywhere to choose from.
108
+ */
109
+ function dataField(requirement, state, controls) {
110
+ const field = fieldRow(requirement);
111
+ // The control grows a row per open question, so the label aligns to
112
+ // the path input rather than to the middle of the stack.
113
+ field.classList.add("requirement-field-stacked");
114
+ const mount = el("div", "requirement-stack");
115
+ field.appendChild(mount);
116
+ controls.data = window.PlexoraDataSourceField.mount(mount, {
117
+ onChange: (value) => {
118
+ state.data = value.data;
119
+ state.table = value.table;
120
+ state.subset_column = value.subset_column;
121
+ state.subset_value = value.subset_value;
122
+ },
123
+ });
124
+ return field;
125
+ }
126
+
127
+ /**
128
+ * A select over the columns this project answers role questions with.
129
+ *
130
+ * The list arrives ready-made in `roleColumns` and is used exactly as
131
+ * given: for AnnData and SpatialData it is the source file's `obs`,
132
+ * unfiltered, and which of those columns holds the cell id is not
133
+ * something a name heuristic here gets to narrow down. The prefilled
134
+ * answer is the guess; the list is everything.
135
+ */
136
+ function roleField(requirement, needs, state) {
137
+ const field = fieldRow(requirement);
138
+
139
+ const select = el("select", "form-select");
140
+ select.append(new Option("Choose a column…", ""));
141
+
142
+ // The answers that name no column. Each is offered as its own option
143
+ // rather than carried by the blank, because a blank that satisfies a
144
+ // question is indistinguishable from one the user never looked at --
145
+ // and both of these are the answer a conventionally-shaped file needs,
146
+ // so the blank was reached by default and read as agreement.
147
+ //
148
+ // "There is only one image" cannot be inferred either: deciding it
149
+ // needs to know which column identifies the image, which is this
150
+ // question. "Number the rows" is right only when the mask's labels run
151
+ // in file order, which nothing here can check.
152
+ const isImageId = requirement.role === "image_id";
153
+ if (isImageId) {
154
+ select.append(new Option("This data has only one image", SINGLE_IMAGE));
155
+ }
156
+ // Present only for the formats whose adapter can number the rows
157
+ // itself; a CSV's cell id is always one of its own columns.
158
+ const fallback = (needs.roleDefaults || {})[requirement.role];
159
+ if (fallback) {
160
+ select.append(new Option(fallback, ROW_NUMBER));
161
+ }
162
+
163
+ const candidates = needs.roleColumns || [];
164
+ candidates.forEach((name) => select.append(new Option(name, name)));
165
+
166
+ // Prefilled with whatever the project holds, which for a `confirm`
167
+ // requirement is the predictor's guess -- the thing the user is here to
168
+ // look at. Seeded into state too, so accepting it unchanged (no change
169
+ // event) still posts the value rather than clearing the role.
170
+ const current = (needs.roleAnswers || {})[requirement.role] || "";
171
+ if (current && candidates.includes(current)) {
172
+ select.value = current;
173
+ state.roles[requirement.role] = current;
174
+ } else if (isImageId && needs.singleImage) {
175
+ select.value = SINGLE_IMAGE;
176
+ state.single_image = true;
177
+ } else if (fallback && needs.rowNumberIds) {
178
+ select.value = ROW_NUMBER;
179
+ state.row_number_ids = true;
180
+ }
181
+ select.addEventListener("change", () => {
182
+ if (isImageId) {
183
+ // Mutually exclusive: each write clears the other, so the
184
+ // payload never claims one image AND names a column.
185
+ state.single_image = select.value === SINGLE_IMAGE;
186
+ state.roles[requirement.role] =
187
+ select.value === SINGLE_IMAGE ? null : (select.value || null);
188
+ return;
189
+ }
190
+ if (fallback) {
191
+ // Same pairing for the cell id.
192
+ state.row_number_ids = select.value === ROW_NUMBER;
193
+ state.roles[requirement.role] =
194
+ select.value === ROW_NUMBER ? null : (select.value || null);
195
+ return;
196
+ }
197
+ state.roles[requirement.role] = select.value || null;
198
+ });
199
+
200
+ field.appendChild(select);
201
+ return field;
202
+ }
203
+
204
+ /**
205
+ * Where each cell's position is read from.
206
+ *
207
+ * One question, not the two column roles it used to be -- see
208
+ * coordinateField.js for why, and for the shape of the answer. Rendered by
209
+ * the shared control so this surface and the project edit page cannot
210
+ * disagree about what they are asking or what they post back.
211
+ */
212
+ function coordinatesField(requirement, needs, state) {
213
+ const field = fieldRow(requirement);
214
+ field.classList.add("requirement-field-stacked");
215
+ const mount = el("div", "requirement-stack");
216
+ field.appendChild(mount);
217
+ // Seeded up front, like every other field here: accepting a prefilled
218
+ // value fires no change event, and without this a confirmed-unchanged
219
+ // answer would post nothing and be asked again on the next open.
220
+ const control = window.PlexoraCoordinateField.mount(mount, {
221
+ ...(needs.coordinateOptions || {}),
222
+ name: `coordinates-${requirement.key}`,
223
+ onChange: (value) => { state.coordinates = value; },
224
+ });
225
+ state.coordinates = control.value();
226
+ return field;
227
+ }
228
+
229
+ /**
230
+ * Which matrix the marker values are read from, and whether to log them.
231
+ *
232
+ * Two controls on one row because they are one decision: "what numbers am
233
+ * I thresholding". A file commonly holds raw counts in `X` and a
234
+ * log-transformed copy in a layer, and nothing about the numbers themselves
235
+ * says which is which -- so a threshold set on the wrong one is not a
236
+ * cosmetic mistake, it is a meaningless number. The log switch covers the
237
+ * other half: a file with only raw counts has no layer to pick, and the
238
+ * values still need transforming before a threshold on them reads sensibly.
239
+ *
240
+ * The select is dropped when the file carries only `X` -- one option is not
241
+ * a choice -- but the switch stays, because it is still a real question.
242
+ */
243
+ function featuresField(requirement, needs, state) {
244
+ const field = fieldRow(requirement);
245
+ const row = el("div", "requirement-inline");
246
+
247
+ const options = needs.featureOptions || [];
248
+ // Seeded into state up front: accepting what is shown fires no change
249
+ // event, and the answer still has to reach the server.
250
+ state.features_layer = needs.featureSource || "X";
251
+ if (options.length > 1) {
252
+ const select = el("select", "form-select");
253
+ options.forEach((option) => select.append(new Option(option.label, option.value)));
254
+ select.value = state.features_layer;
255
+ select.addEventListener("change", () => {
256
+ state.features_layer = select.value;
257
+ });
258
+ row.appendChild(select);
259
+ }
260
+
261
+ const toggle = el("label", "requirement-inline-check");
262
+ const box = el("input");
263
+ box.type = "checkbox";
264
+ box.checked = Boolean(needs.featureLog);
265
+ box.title = "Apply log1p to the values as they are read.";
266
+ state.features_log = box.checked;
267
+ box.addEventListener("change", () => {
268
+ state.features_log = box.checked;
269
+ });
270
+ toggle.append(box, el("span", null, "log1p"));
271
+ row.appendChild(toggle);
272
+
273
+ field.appendChild(row);
274
+ return field;
275
+ }
276
+
277
+ /**
278
+ * The marker/metadata split -- and only that.
279
+ *
280
+ * The classifier can also carry the role selects (the edit page uses it
281
+ * that way), but not here: this form already renders a field per role the
282
+ * plugin asked for, so handing it labels drew every one of them a second
283
+ * time inside the metadata box. It also drew the ones nothing asked for --
284
+ * a CSV import showed a "Cell type column" select that no installed plugin
285
+ * reads and the user had no reason to answer.
286
+ */
287
+ function classificationField(requirement, needs, state) {
288
+ const field = el("div", "requirement-field requirement-field-wide");
289
+ field.appendChild(el("label", null, requirement.label));
290
+ const mount = el("div");
291
+ field.appendChild(mount);
292
+
293
+ const classifier = PlexoraColumnClassifier.mount(mount, {
294
+ markers: (needs.columns || {}).markers || [],
295
+ metadata: (needs.columns || {}).metadata || [],
296
+ roleLabels: {},
297
+ onChange: () => {
298
+ const value = classifier.value();
299
+ state.columns = { markers: value.markers, metadata: value.metadata };
300
+ Object.assign(state.roles, value.roles);
301
+ },
302
+ });
303
+ // Seed immediately: a user who accepts the prediction unchanged never
304
+ // fires onChange, and the form would otherwise post nothing.
305
+ const initial = classifier.value();
306
+ state.columns = { markers: initial.markers, metadata: initial.metadata };
307
+ Object.assign(state.roles, initial.roles);
308
+ return field;
309
+ }
310
+
311
+ /**
312
+ * Draw the form, and hand back the controls the save step has to consult.
313
+ *
314
+ * Only the data field is in there: it is the one control whose answer is
315
+ * not complete the moment it is typed (see dataSourceField.js), so saving
316
+ * has to ask it whether it is ready. Kept out of `state`, which is posted
317
+ * as it stands.
318
+ */
319
+ function render(dialog, needs, state) {
320
+ const controls = {};
321
+ const body = dialog.querySelector(".requirements-body");
322
+ body.replaceChildren();
323
+
324
+ // Missing first, then the prefilled ones, then what is merely offered:
325
+ // the empty fields are what block the tool, so they lead.
326
+ const all = [...(needs.missing || []), ...(needs.confirm || []),
327
+ ...(needs.optional || [])];
328
+ // Every key rendered is a key the user has now been asked about, blank
329
+ // answer included -- that is what stops an optional field they chose to
330
+ // skip from reappearing on every open.
331
+ state.confirm = all.map((requirement) => requirement.key);
332
+
333
+ all.forEach((requirement) => {
334
+ if (requirement.kind === "data") {
335
+ body.appendChild(dataField(requirement, state, controls));
336
+ } else if (requirement.kind === "segmentation") {
337
+ body.appendChild(maskField(requirement, state));
338
+ } else if (requirement.kind === "role") {
339
+ body.appendChild(roleField(requirement, needs, state));
340
+ } else if (requirement.kind === "coordinates") {
341
+ body.appendChild(coordinatesField(requirement, needs, state));
342
+ } else if (requirement.kind === "features") {
343
+ body.appendChild(featuresField(requirement, needs, state));
344
+ } else if (requirement.kind === "classification") {
345
+ body.appendChild(classificationField(requirement, needs, state));
346
+ }
347
+ });
348
+
349
+ const blocking = (needs.missing || []).length;
350
+ dialog.querySelector(".requirements-title").textContent =
351
+ blocking ? "Before this tool can open" : `Set up ${needs.label}`;
352
+ // Three cases, not two. Blocking is core's to word. A prefilled form is
353
+ // core's too -- "we guessed, check it" is true of every plugin. A form
354
+ // made entirely of things nobody has to fill in is the one core cannot
355
+ // word: the generic line claims Plexora filled them in from the data,
356
+ // and for a plugin whose fields are all optional nothing was filled in
357
+ // and nothing is required, so the sentence is false twice over. That
358
+ // one comes from the plugin (Plugin.intro).
359
+ const optionalOnly = !blocking && !(needs.confirm || []).length;
360
+ dialog.querySelector(".requirements-subtitle").textContent = blocking
361
+ ? `${needs.label} needs a little more about this project.`
362
+ : (optionalOnly && needs.intro)
363
+ ? needs.intro
364
+ : `Check what ${needs.label} will use. Plexora filled these in from the data — you will not be asked again.`;
365
+
366
+ // On a form where nothing is required, "Cancel" is the wrong word for
367
+ // the only button that gets you to the tool. It reads as "do not open
368
+ // this", and taking it literally would leave a plugin that requires
369
+ // nothing permanently unopenable: the caller re-enters on a true
370
+ // result, so declining has to be RECORDED, not just obeyed. `data-skip`
371
+ // routes the button through the same save as Continue, with whatever
372
+ // the user did not fill in left blank -- which is what marks the offer
373
+ // answered and stops it coming back on every open.
374
+ const cancel = dialog.querySelector('[data-action="cancel"]');
375
+ cancel.dataset.skip = optionalOnly ? "true" : "false";
376
+ cancel.textContent = optionalOnly ? "Skip" : "Cancel";
377
+
378
+ return controls;
379
+ }
380
+
381
+ function buildDialog() {
382
+ const dialog = document.createElement("dialog");
383
+ dialog.className = "requirements-modal";
384
+ dialog.innerHTML = `
385
+ <form method="dialog" class="requirements-form">
386
+ <h2 class="requirements-title">Before this tool can open</h2>
387
+ <p class="requirements-subtitle"></p>
388
+ <div class="requirements-body"></div>
389
+ <div class="requirements-error" role="alert" hidden></div>
390
+ <div class="requirements-actions">
391
+ <button type="button" class="btn btn-secondary" data-action="cancel">Cancel</button>
392
+ <button type="button" class="btn btn-primary" data-action="save">Continue</button>
393
+ </div>
394
+ </form>
395
+ `;
396
+ document.body.appendChild(dialog);
397
+ return dialog;
398
+ }
399
+
400
+ /**
401
+ * Ask for everything `needs` reports missing; resolve true once the tool
402
+ * can open.
403
+ *
404
+ * @param datasource the project name
405
+ * @param needs the payload from tool_routes' `_needs()`
406
+ * @returns Promise<boolean> — false if the user cancelled
407
+ */
408
+ function collect(datasource, needs) {
409
+ if (openDialog) openDialog.remove();
410
+ const dialog = openDialog = buildDialog();
411
+ const error = dialog.querySelector(".requirements-error");
412
+ const save = dialog.querySelector('[data-action="save"]');
413
+ let state = { roles: {} };
414
+ let controls = render(dialog, needs, state);
415
+ dialog.showModal();
416
+
417
+ return new Promise((resolve) => {
418
+ function close(result) {
419
+ dialog.close();
420
+ dialog.remove();
421
+ if (openDialog === dialog) openDialog = null;
422
+ resolve(result);
423
+ }
424
+
425
+ const cancel = dialog.querySelector('[data-action="cancel"]');
426
+ cancel.addEventListener("click", () => {
427
+ // "Skip" on an all-optional form is an empty Continue: it saves
428
+ // nothing but the list of keys the form showed, which is what
429
+ // records the offer as declined. See render().
430
+ if (cancel.dataset.skip === "true") return submit();
431
+ close(false);
432
+ });
433
+ // Escape stays a plain way out, on every form. It leaves the offer
434
+ // standing for next time, which is the right reading of a keypress
435
+ // that might have been aimed at something else -- and it is safe
436
+ // because a false result means the caller does not re-enter.
437
+ dialog.addEventListener("cancel", () => close(false));
438
+
439
+ save.addEventListener("click", () => submit());
440
+
441
+ async function submit() {
442
+ save.disabled = true;
443
+ cancel.disabled = true;
444
+ error.hidden = true;
445
+ // A data file the user has not finished describing -- a store
446
+ // whose table is still unpicked -- is not something to post.
447
+ // The server would reject it with a message asking for a
448
+ // choice, which reads as nonsense when the control offering
449
+ // that choice is on screen and untouched.
450
+ const waiting = controls.data?.blocking?.();
451
+ if (waiting) {
452
+ error.textContent = waiting;
453
+ error.hidden = false;
454
+ save.disabled = false;
455
+ cancel.disabled = false;
456
+ return;
457
+ }
458
+ const task = window.PlexoraStatus?.begin("Saving");
459
+ try {
460
+ const payload = { tool: needs.tool, ...state };
461
+ const response = await fetch(
462
+ plexoraUrl(`${encodeURIComponent(datasource)}/requirements`),
463
+ {
464
+ method: "POST",
465
+ headers: { "Content-Type": "application/json" },
466
+ body: JSON.stringify(payload),
467
+ },
468
+ );
469
+ const result = await response.json();
470
+ if (!response.ok || !result.success) {
471
+ throw new Error(result.error || "Could not save");
472
+ }
473
+ task?.done();
474
+
475
+ // An answer that changed what gets read means the page is
476
+ // holding the previous table's statistics, and the tool
477
+ // about to open is drawn from them. Re-fetch before handing
478
+ // back, so the caller never renders a panel of the numbers
479
+ // the user just stopped reading.
480
+ if (result.reloaded) {
481
+ await window.__plexora?.refreshDataset?.();
482
+ }
483
+
484
+ // Both lists, because they are different questions and the
485
+ // second one only becomes askable once the first is
486
+ // answered: naming a data file is what makes "which column
487
+ // is the cell id" a question with answers, and for a plugin
488
+ // that merely OFFERS that column it arrives as optional
489
+ // rather than missing. Closing on `stillMissing` alone shut
490
+ // the form the instant the file was attached, one question
491
+ // short of the point.
492
+ if (!(result.stillMissing || []).length
493
+ && !(result.stillOptional || []).length) {
494
+ return close(true);
495
+ }
496
+ // Answering one question can reveal others -- naming the
497
+ // data file is what makes "which column is the cell id"
498
+ // answerable. Re-ask rather than guessing what is next.
499
+ needs = await fetchNeeds(datasource, needs.tool);
500
+ if (!needs) return close(true);
501
+ state = { roles: {} };
502
+ controls = render(dialog, needs, state);
503
+ // Say why the form came back. Leaving a blocking select on
504
+ // "Choose a column…" lands here, and re-rendering the same
505
+ // fields with nothing said reads as the button doing
506
+ // nothing at all.
507
+ const open = (needs.missing || []).map((r) => r.label);
508
+ if (open.length) {
509
+ error.textContent = `Still needed: ${open.join(", ")}.`;
510
+ error.hidden = false;
511
+ }
512
+ save.disabled = false;
513
+ cancel.disabled = false;
514
+ } catch (e) {
515
+ task?.fail("Save failed");
516
+ error.textContent = e.message;
517
+ error.hidden = false;
518
+ save.disabled = false;
519
+ cancel.disabled = false;
520
+ }
521
+ }
522
+ });
523
+ }
524
+
525
+ /** The tool's outstanding requirements, or null when it can just open. */
526
+ async function fetchNeeds(datasource, tool) {
527
+ const response = await fetch(plexoraUrl(
528
+ `${encodeURIComponent(datasource)}/tools/${encodeURIComponent(tool)}/requirements`));
529
+ const payload = await response.json();
530
+ if (!payload.success) return null;
531
+ // `optional` counts. It is the only list a plugin that requires nothing
532
+ // ever fills, so leaving it out made "outstanding" mean "blocking" and
533
+ // handed back null the moment a data file was attached -- closing the
534
+ // form on the pass that was about to ask which column holds the cell id.
535
+ const outstanding = (payload.missing || []).length
536
+ + (payload.confirm || []).length
537
+ + (payload.optional || []).length;
538
+ return outstanding ? payload : null;
539
+ }
540
+
541
+ /**
542
+ * Ask for named requirements mid-session, after a tool is already open.
543
+ *
544
+ * Gating uses this: an image-id column only matters when the user chooses
545
+ * to write gates back to the source file, long after the panel opened.
546
+ * Resolves true once the named keys are satisfied.
547
+ */
548
+ async function require(datasource, tool, keys) {
549
+ // `keys` asks a different question from the ordinary lists: not "what
550
+ // has the user not been asked yet" but "what does this action need
551
+ // right now". An optional field they were offered and skipped is
552
+ // absent from the first and present in the second, which is the whole
553
+ // reason this takes the explicit route rather than filtering `optional`.
554
+ const url = `${encodeURIComponent(datasource)}/tools/${encodeURIComponent(tool)}`
555
+ + `/requirements?keys=${encodeURIComponent(keys.join(","))}`;
556
+ const full = await fetch(plexoraUrl(url)).then((r) => r.json());
557
+ if (!full.success) return false;
558
+
559
+ const wanted = full.requested || [];
560
+ if (!wanted.length) return true;
561
+ // Shown as required here even for an optional input: the caller cannot
562
+ // do what the user just asked for without it.
563
+ return collect(datasource, {
564
+ ...full,
565
+ missing: wanted.map((r) => ({ ...r, optional: false })),
566
+ confirm: [],
567
+ optional: [],
568
+ });
569
+ }
570
+
571
+ return { collect, require };
572
+ })();