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,513 @@
1
+ /**
2
+ * @class GatingSidebarController - gating module's sidebar extension: gate-marker
3
+ * selection, threshold slider/distribution plot, and "Save Gates to AnnData". Composed
4
+ * against the core ViewerSidebar's public API rather than being part of it -- see
5
+ * setGateMarker() below for the one confirmed cross-wire (mirroring the active gate
6
+ * marker into channel slot 1 via sidebar.setSlotMarker()), which stays an explicit,
7
+ * opportunistic call from the gating side into a stable core API, not the reverse.
8
+ *
9
+ * Registered with the core sidebar via ViewerSidebar#registerModule() -- see
10
+ * pluginRegistry.js's `createSidebarController(ctx)` hook and its call site in main.js.
11
+ */
12
+
13
+ // Datasource kinds whose gates can be written back to the source file, i.e.
14
+ // those backed by an AnnData group: an .h5ad, or the one table a SpatialData
15
+ // import selected out of its .zarr store. Must stay in step with the same
16
+ // check in the gating plugin's routes.py. CSV has nowhere to write to.
17
+ const SAVEABLE_SOURCE_TYPES = ["anndata", "spatialdata"];
18
+
19
+ class GatingSidebarController {
20
+ constructor(ctx) {
21
+ this.ctx = ctx;
22
+ this.sidebar = ctx.sidebar;
23
+ this.gatingList = ctx.moduleInstance;
24
+ this.dataLayer = ctx.dataLayer;
25
+ this.api = new GatingApi(ctx);
26
+ this.eventHandler = ctx.eventHandler;
27
+ this.config = ctx.config;
28
+ this.gateMarker = null;
29
+ this.gateSlider = null;
30
+ this.gateMarkerSelect = null;
31
+ this.gateMarkerChangeTimer = null;
32
+ this._saveGatingTimer = null;
33
+ this._gatingSaveChain = null;
34
+ this.gateDistributionScale = null;
35
+ }
36
+
37
+ // Called once from ViewerSidebar#init(), before the saved-state restore below.
38
+ setup() {
39
+ this.populateGateSelect();
40
+ this.bindSaveToAnndata();
41
+
42
+ // Thresholding is opened via the Tools menu (?tool=gating, see
43
+ // tool_routes.py / toolLoader.js). Closing just hides the panel --
44
+ // toolLoader.js owns the show/hide bookkeeping (and knows whether this
45
+ // was a lazy client-side open or a direct/bookmarked server-rendered
46
+ // one); this controller and its DOM stay alive so reopening later in
47
+ // the same session is instant, no re-fetch/re-init.
48
+ document.getElementById("gate_marker_close")?.addEventListener("click", () => {
49
+ window.PlexoraToolLoader?.hideToolPanel("gating");
50
+ });
51
+
52
+ const gateAuto = document.getElementById("gate_auto_button");
53
+ gateAuto.addEventListener("click", async () => {
54
+ gateAuto.disabled = true;
55
+ gateAuto.classList.add("auto-loading");
56
+ try {
57
+ await this.autoGate();
58
+ } finally {
59
+ gateAuto.disabled = false;
60
+ gateAuto.classList.remove("auto-loading");
61
+ }
62
+ });
63
+
64
+ // Registered through the plugin's cleanup list so deactivating the
65
+ // plugin detaches it; this was previously a permanent window listener.
66
+ const onResize = () => this.redrawGateSlider();
67
+ window.addEventListener("resize", onResize);
68
+ this.ctx?.onCleanup?.(() => window.removeEventListener("resize", onResize));
69
+ }
70
+
71
+ // Called by toolLoader.js right after unhiding the panel (both on first lazy
72
+ // load and on every reopen). The slider/distribution plot measure their own
73
+ // width via getBoundingClientRect() (redrawGateSlider/drawGateDistribution
74
+ // below), which returns 0 while the panel is display:none -- redraw now that
75
+ // it's actually visible so they don't render collapsed to zero width.
76
+ onShow() {
77
+ this.redrawGateSlider();
78
+ this.drawGateDistribution();
79
+ }
80
+
81
+ // ViewerSidebar#init() restore-flow hooks (see registerModule()'s doc comment).
82
+ fetchSaved() {
83
+ return this.api.getSavedGatingList();
84
+ }
85
+
86
+ async applyOrDefault(savedGating) {
87
+ if (savedGating && savedGating.length) {
88
+ this.applySavedGating(savedGating);
89
+ return;
90
+ }
91
+ // No gates saved in Plexora's own DB yet -- for an AnnData-backed
92
+ // datasource, check whether adata.uns[table_name] already has gates
93
+ // from outside Plexora (e.g. set on the source file before import)
94
+ // before falling back to a blank default marker. Wrapped: unlike
95
+ // the old fully-synchronous version, a rejected fetch here (network
96
+ // hiccup, stale-cached dataLayer.js missing the method, unexpected
97
+ // response shape) must still fall through to the default marker
98
+ // below -- not leave the sidebar with nothing selected at all.
99
+ if (SAVEABLE_SOURCE_TYPES.includes(this.ctx.dataset?.table?.sourceKind)) {
100
+ try {
101
+ const gates = await this.api.getGatesFromAnndata();
102
+ if (gates && Object.keys(gates).length) {
103
+ this.applyAnndataGates(gates);
104
+ return;
105
+ }
106
+ } catch (error) {
107
+ console.error("Error loading gates from AnnData", error);
108
+ }
109
+ }
110
+ this.setGateMarker(this.getGateMarkerNames()[1] || this.getGateMarkerNames()[0], { enableSlot: false });
111
+ }
112
+
113
+ // Seeds gating_channels with previously-saved AnnData gates (lower bound
114
+ // only -- see anndata_gates.load_gates_from_anndata) so the marker
115
+ // dropdown's gated indicator and slider reflect prior work done outside
116
+ // Plexora as soon as the tool opens. Each channel's own current max
117
+ // stands in for the missing upper bound. Marker selection still defers
118
+ // to the normal default -- if that default happens to be a gated
119
+ // channel its slider already reflects the loaded value; other gated
120
+ // channels show via the dropdown's gated-indicator dot until picked.
121
+ applyAnndataGates(gates) {
122
+ for (const [channel, lowerBound] of Object.entries(gates)) {
123
+ const range = this.getGateRange(channel);
124
+ this.gatingList.gating_channels[channel] = [lowerBound, range[1]];
125
+ }
126
+ this.setGateMarker(this.getGateMarkerNames()[1] || this.getGateMarkerNames()[0], { enableSlot: false });
127
+ }
128
+
129
+ persistIfNeeded(hadSaved) {
130
+ if (!hadSaved) this.persistGatingList();
131
+ }
132
+
133
+ // Wires the "Save Gates to AnnData" button/panel (adata.uns[table_name],
134
+ // lower gate bound only, one column per image -- see anndata_gates.py).
135
+ // Only meaningful for AnnData-backed datasources. The gates payload is
136
+ // computed fresh from getCustomGatedChannels() at click time -- not from
137
+ // the persisted GatingList row -- so every marker the user has actually
138
+ // gated is included, not just whichever one is currently on screen.
139
+ bindSaveToAnndata() {
140
+ const button = document.getElementById("save_gates_anndata_button");
141
+ const panel = document.getElementById("gating_save_anndata_panel");
142
+ const confirmButton = document.getElementById("save_anndata_confirm");
143
+ const cancelButton = document.getElementById("save_anndata_cancel");
144
+ const exitButton = document.getElementById("save_anndata_exit");
145
+ const tableNameInput = document.getElementById("save_anndata_table_name");
146
+ const status = document.getElementById("save_anndata_status");
147
+
148
+ if (SAVEABLE_SOURCE_TYPES.includes(this.ctx.dataset?.table?.sourceKind)) {
149
+ button.hidden = false;
150
+ }
151
+
152
+ // One way in, several ways out. The panel opens over the bottom of a
153
+ // scrolling sidebar, so its dismiss control has to be visible from the
154
+ // moment it opens rather than below the fold with the action row -- and
155
+ // once a save has succeeded "Cancel" reads like it would undo it.
156
+ const closePanel = () => {
157
+ panel.hidden = true;
158
+ cancelButton.textContent = "Cancel";
159
+ };
160
+
161
+ button.addEventListener("click", () => {
162
+ status.textContent = "";
163
+ status.className = "";
164
+ cancelButton.textContent = "Cancel";
165
+ panel.hidden = !panel.hidden;
166
+ });
167
+
168
+ cancelButton.addEventListener("click", closePanel);
169
+ exitButton.addEventListener("click", closePanel);
170
+
171
+ panel.addEventListener("keydown", (event) => {
172
+ if (event.key === "Escape") {
173
+ event.stopPropagation();
174
+ closePanel();
175
+ }
176
+ });
177
+
178
+ confirmButton.addEventListener("click", async () => {
179
+ confirmButton.disabled = true;
180
+ status.className = "";
181
+ status.textContent = "Saving...";
182
+ try {
183
+ // The server derives gates from the persisted GatingList row
184
+ // (the DB), not from anything sent here -- flush the current
185
+ // in-memory state first so a gate set moments ago (still
186
+ // inside the 400ms autosave debounce) isn't missed.
187
+ await this.api.saveGatingList(this.gatingList.gating_channels, this.gatingList.selections);
188
+ const result = await this.saveWithImageId(tableNameInput.value.trim() || "gates");
189
+ status.className = "success";
190
+ status.textContent = `Saved column "${result.image_id}" (${result.n_active_gates} markers).`;
191
+ // Nothing left to cancel -- the write already happened.
192
+ cancelButton.textContent = "Close";
193
+ } catch (error) {
194
+ status.className = "error";
195
+ status.textContent = error.message || "Failed to save gates to AnnData";
196
+ } finally {
197
+ confirmButton.disabled = false;
198
+ }
199
+ });
200
+ }
201
+
202
+ /**
203
+ * Save, asking the host for an image-id column if the project has not
204
+ * recorded one.
205
+ *
206
+ * This plugin declares `role:image_id` as an optional requirement, so core
207
+ * owns collecting and storing it -- one column name, asked once, and every
208
+ * other plugin then finds it already answered. The panel used to carry its
209
+ * own free-text box defaulting to the literal "imageid", which asked the
210
+ * user for something the host already had a place to keep.
211
+ */
212
+ async saveWithImageId(tableName) {
213
+ try {
214
+ return await this.api.saveGatesToAnndata(tableName);
215
+ } catch (error) {
216
+ if (!/image ID column/i.test(error.message || "")) throw error;
217
+ const collected = await this.ctx.requirements.require(["role:image_id"]);
218
+ if (!collected) throw error;
219
+ return this.api.saveGatesToAnndata(tableName);
220
+ }
221
+ }
222
+
223
+ populateGateSelect() {
224
+ const names = this.getGateMarkerNames();
225
+ const mount = document.getElementById("gate_marker_select");
226
+ if (!this.gateMarkerSelect) {
227
+ this.gateMarkerSelect = new SearchableSelect(mount, {
228
+ options: names,
229
+ value: this.gateMarker || "",
230
+ placeholder: "Search markers…",
231
+ getIndicator: (name) => this.describeGateIndicator(name),
232
+ onChange: (name) => {
233
+ window.clearTimeout(this.gateMarkerChangeTimer);
234
+ this.gateMarkerChangeTimer = window.setTimeout(() => {
235
+ this.setGateMarker(name);
236
+ }, 0);
237
+ },
238
+ });
239
+ } else {
240
+ this.gateMarkerSelect.setOptions(names);
241
+ }
242
+ }
243
+
244
+ getGateMarkerNames() {
245
+ // sidebar.columns is the image channel list -- gate-able markers are
246
+ // the feature table's own columns (e.g. adata.var_names), which are
247
+ // frequently a different set of strings entirely. CSVGatingList holds
248
+ // exactly that list (see its init, which takes the project's recorded
249
+ // marker/metadata split off ctx.dataset), so reuse it here rather than
250
+ // deriving a second answer that could disagree with the sliders.
251
+ return [...this.gatingList.columns];
252
+ }
253
+
254
+ setGateMarker(name, options = {}) {
255
+ if (!name) return;
256
+ if (name === this.gateMarker && !options.force) return;
257
+ const enableSlot = options.enableSlot !== false;
258
+ this.gateMarker = name;
259
+ if (this.gateMarkerSelect) {
260
+ this.gateMarkerSelect.setValue(name);
261
+ }
262
+ this.ensureGateSelection(name);
263
+ this.redrawGateSlider();
264
+ this.drawGateDistribution();
265
+ // Gating always works off the feature-table column (ensureGateSelection
266
+ // above), independent of the image -- a gate marker is very often not
267
+ // an image channel at all (adata.var_names vs. the image's channel
268
+ // names are frequently different strings/lengths). Only mirror the
269
+ // marker into a rendering slot when its name genuinely matches a real
270
+ // image channel; otherwise leave the channel section untouched and let
271
+ // the user find and enable the right channel themselves to verify the
272
+ // gate visually. The correct check is against the image channel
273
+ // vocabulary (imageChannels, keyed by full channel name -> tile
274
+ // index), not the marker vocabulary.
275
+ const hasMatchingImageChannel = this.ctx.dataset.image.has(this.dataLayer.getFullChannelName(name));
276
+ if (options.syncSlot !== false && hasMatchingImageChannel) {
277
+ this.sidebar.setSlotMarker(1, name, { keepColor: true, enable: enableSlot, reveal: enableSlot });
278
+ }
279
+ this.scheduleSaveGating();
280
+ }
281
+
282
+ ensureGateSelection(name) {
283
+ const fullName = this.dataLayer.getFullChannelName(name);
284
+ const range = this.gatingList.gating_channels[fullName] || this.getGateRange(name);
285
+ this.gatingList.selections = {};
286
+ this.gatingList.gating_channels[fullName] = range;
287
+ this.gatingList.selections[fullName] = range;
288
+ this.updateGateReadout(range);
289
+ this.eventHandler.trigger(CSVGatingList.events.GATING_BRUSH_MOVE, this.gatingList.selections);
290
+ this.eventHandler.trigger(CSVGatingList.events.SELECTION_CHANGED, this.gatingList.selections);
291
+ }
292
+
293
+ redrawGateSlider() {
294
+ if (!this.gateMarker) return;
295
+ const target = document.getElementById("gate_slider");
296
+ target.innerHTML = "";
297
+ const range = this.getGateRange(this.gateMarker);
298
+ const values = this.gatingList.gating_channels[this.dataLayer.getFullChannelName(this.gateMarker)] || range;
299
+ const width = Math.max(180, target.getBoundingClientRect().width - 16);
300
+ const slider = d3.sliderBottom()
301
+ .min(range[0])
302
+ .max(range[1])
303
+ .width(width)
304
+ .ticks(0)
305
+ .tickValues([])
306
+ .default(values)
307
+ .fill("#f36f45")
308
+ .handle(d3.symbol().type(d3.symbolCircle).size(120))
309
+ .on("onchange", (value) => this.setGateRange(value, CSVGatingList.events.GATING_BRUSH_MOVE))
310
+ .on("end", (value) => this.setGateRange(value, CSVGatingList.events.SELECTION_CHANGED));
311
+
312
+ this.gateSlider = slider;
313
+ d3.select(target)
314
+ .append("svg")
315
+ .attr("width", width + 16)
316
+ .attr("height", 44)
317
+ .append("g")
318
+ .attr("transform", "translate(8,18)")
319
+ .call(slider);
320
+ this.updateGateReadout(values);
321
+ }
322
+
323
+ setGateRange(values, eventName) {
324
+ const fullName = this.dataLayer.getFullChannelName(this.gateMarker);
325
+ const normalized = this.normalizeGateRange(values, this.getGateRange(this.gateMarker));
326
+ this.gatingList.gating_channels[fullName] = normalized;
327
+ this.gatingList.selections = {};
328
+ this.gatingList.selections[fullName] = normalized;
329
+ this.updateGateReadout(normalized);
330
+ this.updateGateThresholdLines(normalized);
331
+ this.eventHandler.trigger(eventName, this.gatingList.selections);
332
+ if (eventName === CSVGatingList.events.SELECTION_CHANGED) {
333
+ this.scheduleSaveGating();
334
+ }
335
+ }
336
+
337
+ async autoGate() {
338
+ if (!this.gateMarker) return;
339
+ if (!(this.gateMarker in this.gatingList.hasGatingGMM)) {
340
+ await this.gatingList.getGatingGMM(this.gateMarker);
341
+ }
342
+ const packet = this.gatingList.hasGatingGMM[this.gateMarker];
343
+ if (!packet || packet.gate === undefined) return;
344
+ const range = this.getGateRange(this.gateMarker);
345
+ const factor = Math.pow(10, this.dataLayer.gateDecimals(range));
346
+ const gate = Math.floor(parseFloat(packet.gate) * factor) / factor;
347
+ const values = [gate, range[1]];
348
+ if (this.gateSlider) {
349
+ this.gateSlider.silentValue(values);
350
+ }
351
+ this.setGateRange(values, CSVGatingList.events.SELECTION_CHANGED);
352
+ this.redrawGateSlider();
353
+ }
354
+
355
+ drawGateDistribution() {
356
+ const target = document.getElementById("gate_distribution_plot");
357
+ target.innerHTML = "";
358
+ this.gateDistributionScale = null;
359
+ if (!this.gateMarker) return;
360
+ const fullName = this.dataLayer.getFullChannelName(this.gateMarker);
361
+ const desc = this.sidebar.databaseDescription[fullName];
362
+ const histogram = desc?.histogram || [];
363
+ if (!histogram.length) return;
364
+
365
+ const box = target.getBoundingClientRect();
366
+ const width = Math.max(220, box.width || 280);
367
+ const height = 120;
368
+ const margin = { top: 12, right: 10, bottom: 24, left: 28 };
369
+ const innerWidth = width - margin.left - margin.right;
370
+ const innerHeight = height - margin.top - margin.bottom;
371
+ const xDomain = d3.extent(histogram, (d) => d.x);
372
+ const yMax = d3.max(histogram, (d) => d.y);
373
+ const xScale = d3.scaleLinear().domain(xDomain).range([0, innerWidth]);
374
+ const yScale = d3.scaleLinear().domain([0, yMax]).range([innerHeight, 0]);
375
+ const line = d3.line()
376
+ .x((d) => xScale(d.x))
377
+ .y((d) => yScale(d.y))
378
+ .curve(d3.curveMonotoneX);
379
+ const values = this.gatingList.gating_channels[fullName] || this.getGateRange(this.gateMarker);
380
+
381
+ const svg = d3.select(target)
382
+ .append("svg")
383
+ .attr("width", width)
384
+ .attr("height", height);
385
+ const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
386
+ g.append("path")
387
+ .datum(histogram)
388
+ .attr("class", "sidebar-distribution-line")
389
+ .attr("d", line);
390
+ g.append("g")
391
+ .attr("class", "gate-threshold-lines")
392
+ .selectAll("line")
393
+ .data(values)
394
+ .enter()
395
+ .append("line")
396
+ .attr("class", "gate-threshold-line")
397
+ .attr("x1", (value) => xScale(value))
398
+ .attr("x2", (value) => xScale(value))
399
+ .attr("y1", 0)
400
+ .attr("y2", innerHeight);
401
+ g.append("g")
402
+ .attr("class", "distribution-axis")
403
+ .attr("transform", `translate(0,${innerHeight})`)
404
+ .call(d3.axisBottom(xScale).ticks(3).tickFormat(d3.format(".2f")));
405
+
406
+ this.gateDistributionScale = xScale;
407
+ }
408
+
409
+ // Cheap per-tick update during a drag: reposition the existing threshold lines instead of
410
+ // tearing down and rebuilding the whole histogram/axis (which was the source of drag lag).
411
+ updateGateThresholdLines(values) {
412
+ if (!this.gateDistributionScale) {
413
+ this.drawGateDistribution();
414
+ return;
415
+ }
416
+ const xScale = this.gateDistributionScale;
417
+ d3.select("#gate_distribution_plot")
418
+ .selectAll(".gate-threshold-line")
419
+ .data(values)
420
+ .attr("x1", (value) => xScale(value))
421
+ .attr("x2", (value) => xScale(value));
422
+ }
423
+
424
+ applySavedGating(rows) {
425
+ let activeRow = null;
426
+ rows.forEach((row) => {
427
+ if (!row || !row.channel || row.channel === "Lasso") return;
428
+ this.gatingList.gating_channels[row.channel] = [row.gate_start, row.gate_end];
429
+ if (row.gate_active) {
430
+ activeRow = row;
431
+ }
432
+ });
433
+ const marker = activeRow
434
+ ? this.dataLayer.getShortChannelName(activeRow.channel)
435
+ : (this.getGateMarkerNames()[1] || this.getGateMarkerNames()[0]);
436
+ // syncSlot:false - applySavedChannels already placed every active channel (including
437
+ // this one, if it was active) in its correct restored slot; letting setGateMarker's
438
+ // normal slot-1 mirroring run here would clobber whatever channel actually belongs there.
439
+ this.setGateMarker(marker, { force: true, syncSlot: false });
440
+ }
441
+
442
+ persistGatingList() {
443
+ return this.api.saveGatingList(this.gatingList.gating_channels, this.gatingList.selections);
444
+ }
445
+
446
+ scheduleSaveGating() {
447
+ if (this.sidebar.isRestoring()) return;
448
+ window.clearTimeout(this._saveGatingTimer);
449
+ this._saveGatingTimer = window.setTimeout(() => {
450
+ this._gatingSaveChain = (this._gatingSaveChain || Promise.resolve()).then(() => this.persistGatingList());
451
+ }, 400);
452
+ }
453
+
454
+ updateGateReadout(values) {
455
+ document.getElementById("gate_min_value").textContent = this.sidebar.formatValue(values[0]);
456
+ document.getElementById("gate_max_value").textContent = this.sidebar.formatValue(values[1]);
457
+ }
458
+
459
+ getGateRange(name) {
460
+ const fullName = this.dataLayer.getFullChannelName(name);
461
+ const desc = this.sidebar.databaseDescription[fullName] || {};
462
+ return [desc.min || 0, desc.max || 1];
463
+ }
464
+
465
+ // A marker only counts as "gated" once its stored range differs from its
466
+ // own full data range (getGateRange) -- ensureGateSelection() seeds every
467
+ // marker the user merely browses to in the dropdown with that same full
468
+ // range, so membership in gating_channels alone would over-count markers
469
+ // nobody actually narrowed.
470
+ hasCustomGate(name) {
471
+ const fullName = this.dataLayer.getFullChannelName(name);
472
+ const range = this.gatingList.gating_channels[fullName];
473
+ if (!range) return false;
474
+ const defaultRange = this.getGateRange(name);
475
+ return range[0] !== defaultRange[0] || range[1] !== defaultRange[1];
476
+ }
477
+
478
+ // {fullChannelName: [low, high]} for every marker with a real, user-set
479
+ // gate -- independent of gatingList.selections, which only ever holds
480
+ // the single currently-displayed marker (ensureGateSelection() resets it
481
+ // on every marker switch, by design, for the live single-marker slider/
482
+ // segmentation-outline view). Used for exports (e.g. save-to-AnnData)
483
+ // that need *all* gated markers, not just the one on screen right now.
484
+ getCustomGatedChannels() {
485
+ const result = {};
486
+ for (const name of this.gatingList.columns) {
487
+ if (!this.hasCustomGate(name)) continue;
488
+ const fullName = this.dataLayer.getFullChannelName(name);
489
+ result[fullName] = this.gatingList.gating_channels[fullName];
490
+ }
491
+ return result;
492
+ }
493
+
494
+ // Hover-tooltip text for the marker dropdown's gated-indicator dot; null
495
+ // means "don't show a dot" (SearchableSelect skips rendering it).
496
+ describeGateIndicator(name) {
497
+ if (!this.hasCustomGate(name)) return null;
498
+ const fullName = this.dataLayer.getFullChannelName(name);
499
+ const range = this.gatingList.gating_channels[fullName];
500
+ return `Gated ${this.sidebar.formatValue(range[0])}–${this.sidebar.formatValue(range[1])}`;
501
+ }
502
+
503
+ // Gate-specific rounding: precision is derived from the channel's own
504
+ // observed range (dataLayer.gateDecimals) instead of the isTransformed
505
+ // config flag, so it can't silently drift out of sync with the data's
506
+ // actual scale. Floors the low handle / ceils the high handle (at that
507
+ // precision) so rounding never excludes boundary cells.
508
+ normalizeGateRange(values, range) {
509
+ const sorted = [...values].map((value) => parseFloat(value)).sort((a, b) => a - b);
510
+ const factor = Math.pow(10, this.dataLayer.gateDecimals(range));
511
+ return [Math.floor(sorted[0] * factor) / factor, Math.ceil(sorted[1] * factor) / factor];
512
+ }
513
+ }
@@ -0,0 +1,26 @@
1
+ <div id="csv_channel_list_wrapper">
2
+ <div id="lowRow3" class="row">
3
+ <div id="boxHeading3" class="col-auto">
4
+ <span class="box-heading">CSV Gating</span>
5
+ </div>
6
+ <div id="download-gating-header-div"></div>
7
+ </div>
8
+
9
+ <div class="list" id="csv_gating_list">
10
+ <div id="csv_gating_row" class="row">
11
+ <div class="col-auto">
12
+ <div class="lds-ellipsis" id="csv_gating_list_loader">
13
+ <div></div>
14
+ <div></div>
15
+ <div></div>
16
+ <div></div>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ <div id="drag-and-drop-info">
21
+ <div class="d-flex justify-content-md-center align-items-center vh-100">
22
+ <span id="drag-and-drop-text">Drag and Drop A Gating CSV File</span>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
@@ -0,0 +1,69 @@
1
+ <section class="sidebar-section" id="gate_marker_section">
2
+ <div class="section-heading">
3
+ <h2><span class="fas fa-sliders section-heading-icon"></span>Gate Marker</h2>
4
+ <div class="section-actions">
5
+ <button id="gating_upload_icon" class="icon-button" title="Load gating from CSV" type="button">
6
+ <span class="fas fa-file-arrow-up"></span>
7
+ </button>
8
+ <button id="gating_download_icon" class="icon-button" title="Download gating CSV" type="button">
9
+ <span class="fas fa-file-arrow-down"></span>
10
+ </button>
11
+ <button id="gate_marker_close" class="icon-button" title="Close Thresholding" type="button">
12
+ <span class="fas fa-xmark"></span>
13
+ </button>
14
+ </div>
15
+ </div>
16
+ <div id="gating_download_panel">
17
+ <a id="gating_exit"><span class="fas fa-xmark"></span></a>
18
+ <label>Download gated channel ranges:
19
+ <span class="download_flex">
20
+ <input id="download_input1" type="text" autocomplete="false" spellcheck="false"
21
+ value="{{ data.datasource }}_gated_channel_ranges.csv">
22
+ <a id="download_gated_channel_ranges" class="download_button"><span class="fas fa-arrow-up-from-bracket"></span></a>
23
+ </span>
24
+ </label>
25
+ <label>Download gated cell encodings:
26
+ <span class="download_flex">
27
+ <input id="download_input2" type="text" autocomplete="false" spellcheck="false"
28
+ value="{{ data.datasource }}_gated_cell_encodings.csv">
29
+ <a id="download_gated_cell_encodings" class="download_button"><span class="fas fa-arrow-up-from-bracket"></span></a>
30
+ </span>
31
+ </label>
32
+ <label>Cell Encoding:
33
+ <select id="encoding">
34
+ <option value="binary" selected>Binary Values</option>
35
+ <option value="intensity">Intensity Values</option>
36
+ </select>
37
+ </label>
38
+ </div>
39
+ <form id="arrow-upload-form">
40
+ <input id="gating-upload-from-arrow" class="hidden" type="file">
41
+ <input type="hidden" id="upload-arrow-datasource" name="upload-arrow-datasource">
42
+ </form>
43
+ <label class="control-label">Marker</label>
44
+ <div id="gate_marker_select" class="marker-combobox-mount"></div>
45
+ <div class="range-readout">
46
+ <span id="gate_min_value">0.00</span>
47
+ <span id="gate_max_value">0.00</span>
48
+ </div>
49
+ <div id="gate_slider" class="sidebar-slider"></div>
50
+ <button id="gate_auto_button" class="sidebar-action" type="button">Auto Threshold</button>
51
+ <div id="gate_distribution_plot" class="distribution-plot"></div>
52
+ <button id="save_gates_anndata_button" class="sidebar-action-fancy" type="button" hidden>
53
+ <span class="fas fa-file-export"></span>
54
+ Save Gates to Source File
55
+ </button>
56
+ <div id="gating_save_anndata_panel" hidden>
57
+ <button id="save_anndata_exit" class="icon-button" title="Close" type="button">
58
+ <span class="fas fa-xmark"></span>
59
+ </button>
60
+ <label>Table name:
61
+ <input id="save_anndata_table_name" type="text" autocomplete="false" spellcheck="false" value="gates">
62
+ </label>
63
+ <div class="save-anndata-actions">
64
+ <button id="save_anndata_confirm" class="sidebar-action" type="button">Confirm</button>
65
+ <button id="save_anndata_cancel" class="sidebar-action secondary" type="button">Cancel</button>
66
+ </div>
67
+ <div id="save_anndata_status"></div>
68
+ </div>
69
+ </section>