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,202 @@
1
+ /**
2
+ * @class ColorSwatchPicker - one-click curated color palette popover, with a
3
+ * native <input type="color"> escape hatch for exact/custom hex values.
4
+ */
5
+ class ColorSwatchPicker {
6
+ constructor(mount, options = {}) {
7
+ this.mount = mount;
8
+ this.value = options.value || "#2388ff";
9
+ this.presets = options.presets || ColorSwatchPicker.DEFAULT_PRESETS;
10
+ this.title = options.title || "Channel color";
11
+ this.onChange = options.onChange || (() => {});
12
+ this.isOpen = false;
13
+ this.render();
14
+ this.bindDismissHandlers();
15
+ }
16
+
17
+ render() {
18
+ this.mount.innerHTML = "";
19
+ this.mount.classList.add("color-swatch-mount");
20
+
21
+ this.button = document.createElement("button");
22
+ this.button.type = "button";
23
+ this.button.className = "channel-color-swatch";
24
+ this.button.title = this.title;
25
+ this.button.setAttribute("aria-label", this.title);
26
+ this.button.style.setProperty("--swatch-color", this.value);
27
+ this.button.setAttribute("aria-haspopup", "true");
28
+ this.button.setAttribute("aria-expanded", "false");
29
+ this.button.addEventListener("click", (event) => {
30
+ event.stopPropagation();
31
+ this.toggle();
32
+ });
33
+ this.mount.appendChild(this.button);
34
+
35
+ this.popover = document.createElement("div");
36
+ this.popover.className = "color-swatch-popover";
37
+ this.popover.hidden = true;
38
+ this.popover.addEventListener("click", (event) => event.stopPropagation());
39
+
40
+ const grid = document.createElement("div");
41
+ grid.className = "color-swatch-grid";
42
+ this.presets.forEach((preset) => {
43
+ const swatch = document.createElement("button");
44
+ swatch.type = "button";
45
+ swatch.className = "color-swatch-option";
46
+ swatch.title = preset.label;
47
+ swatch.style.setProperty("--swatch-color", preset.hex);
48
+ swatch.classList.toggle("is-selected", preset.hex.toLowerCase() === this.value.toLowerCase());
49
+ swatch.addEventListener("click", () => this.selectColor(preset.hex));
50
+ grid.appendChild(swatch);
51
+ });
52
+ this.popover.appendChild(grid);
53
+
54
+ const customRow = document.createElement("label");
55
+ customRow.className = "color-swatch-custom-row";
56
+ const customLabel = document.createElement("span");
57
+ customLabel.textContent = "Custom";
58
+ customRow.appendChild(customLabel);
59
+ this.customInput = document.createElement("input");
60
+ this.customInput.type = "color";
61
+ this.customInput.value = this.value;
62
+ this.customInput.addEventListener("input", (event) => this.selectColor(event.target.value, { keepOpen: true }));
63
+ customRow.appendChild(this.customInput);
64
+ this.popover.appendChild(customRow);
65
+
66
+ // Appended to <body> (a positioning "portal"), not this.mount: a disabled/dimmed
67
+ // ancestor row has opacity < 1, which creates its own CSS stacking context and
68
+ // would trap this popover's z-index behind the *next* row otherwise.
69
+ document.body.appendChild(this.popover);
70
+ }
71
+
72
+ toggle() {
73
+ if (this.isOpen) {
74
+ this.close();
75
+ } else {
76
+ this.open();
77
+ }
78
+ }
79
+
80
+ open() {
81
+ if (ColorSwatchPicker.activeInstance && ColorSwatchPicker.activeInstance !== this) {
82
+ ColorSwatchPicker.activeInstance.close();
83
+ }
84
+ ColorSwatchPicker.activeInstance = this;
85
+ this.isOpen = true;
86
+ this.positionPopover();
87
+ this.popover.hidden = false;
88
+ requestAnimationFrame(() => this.popover.classList.add("is-open"));
89
+ this.button.setAttribute("aria-expanded", "true");
90
+ this.bindDismissListeners();
91
+ }
92
+
93
+ positionPopover() {
94
+ const rect = this.button.getBoundingClientRect();
95
+ this.popover.style.left = `${rect.left}px`;
96
+ this.popover.style.top = `${rect.bottom + 6}px`;
97
+ }
98
+
99
+ close() {
100
+ this.isOpen = false;
101
+ if (!this.popover) return;
102
+ this.popover.classList.remove("is-open");
103
+ this.button.setAttribute("aria-expanded", "false");
104
+ window.setTimeout(() => {
105
+ // The popover may have been destroyed inside this delay -- closing
106
+ // is the first thing destroy() does.
107
+ if (!this.isOpen && this.popover) this.popover.hidden = true;
108
+ }, 150);
109
+ if (ColorSwatchPicker.activeInstance === this) {
110
+ ColorSwatchPicker.activeInstance = null;
111
+ }
112
+ this.unbindDismissListeners();
113
+ }
114
+
115
+ bindDismissListeners() {
116
+ if (this._dismissScroll) return;
117
+ this._dismissScroll = (event) => {
118
+ if (event.target === this.popover || this.popover.contains(event.target)) return;
119
+ this.close();
120
+ };
121
+ this._dismissResize = () => this.close();
122
+ window.addEventListener("scroll", this._dismissScroll, true);
123
+ window.addEventListener("resize", this._dismissResize);
124
+ }
125
+
126
+ unbindDismissListeners() {
127
+ if (!this._dismissScroll) return;
128
+ window.removeEventListener("scroll", this._dismissScroll, true);
129
+ window.removeEventListener("resize", this._dismissResize);
130
+ this._dismissScroll = null;
131
+ this._dismissResize = null;
132
+ }
133
+
134
+ selectColor(hex, opts = {}) {
135
+ this.setValue(hex);
136
+ this.onChange(hex);
137
+ if (!opts.keepOpen) this.close();
138
+ }
139
+
140
+ setValue(hex) {
141
+ this.value = hex;
142
+ this.button.style.setProperty("--swatch-color", hex);
143
+ this.customInput.value = hex;
144
+ this.popover.querySelectorAll(".color-swatch-option").forEach((el) => {
145
+ el.classList.toggle("is-selected", el.style.getPropertyValue("--swatch-color").toLowerCase() === hex.toLowerCase());
146
+ });
147
+ }
148
+
149
+ bindDismissHandlers() {
150
+ // Kept on the instance so destroy() can take them off again. Anonymous
151
+ // handlers here meant every picker ever built stayed subscribed to
152
+ // document for the life of the page, which is invisible while pickers
153
+ // are per-channel and created once, and is not once a list rebuilds a
154
+ // row of them on every keystroke.
155
+ this._documentClick = (event) => {
156
+ if (this.isOpen && !this.mount.contains(event.target) && !this.popover.contains(event.target)) {
157
+ this.close();
158
+ }
159
+ };
160
+ this._documentKeydown = (event) => {
161
+ if (event.key === "Escape" && this.isOpen) {
162
+ this.close();
163
+ }
164
+ };
165
+ document.addEventListener("click", this._documentClick);
166
+ document.addEventListener("keydown", this._documentKeydown);
167
+ }
168
+
169
+ /**
170
+ * Take this picker off the page for good.
171
+ *
172
+ * The popover lives on <body> rather than inside the mount (see render), so
173
+ * removing the row that owns it leaves the popover behind. Callers that
174
+ * rebuild their rows must call this, or the orphans accumulate.
175
+ */
176
+ destroy() {
177
+ this.close();
178
+ this.unbindDismissListeners();
179
+ document.removeEventListener("click", this._documentClick);
180
+ document.removeEventListener("keydown", this._documentKeydown);
181
+ this.popover?.remove();
182
+ this.popover = null;
183
+ if (ColorSwatchPicker.activeInstance === this) {
184
+ ColorSwatchPicker.activeInstance = null;
185
+ }
186
+ }
187
+ }
188
+
189
+ ColorSwatchPicker.DEFAULT_PRESETS = [
190
+ { label: "Blue", hex: "#2388ff" },
191
+ { label: "Red", hex: "#ff2d2d" },
192
+ { label: "Green", hex: "#2bd46f" },
193
+ { label: "White", hex: "#ffffff" },
194
+ { label: "Yellow", hex: "#ffd60a" },
195
+ { label: "Magenta", hex: "#ec4899" },
196
+ { label: "Cyan", hex: "#22e6e6" },
197
+ { label: "Orange", hex: "#f97316" },
198
+ { label: "Violet", hex: "#a78bfa" },
199
+ { label: "Gray", hex: "#94a3b8" },
200
+ ];
201
+
202
+ ColorSwatchPicker.activeInstance = null;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * columnClassifier.js -- which columns are markers, and which are metadata.
3
+ *
4
+ * Two boxes side by side. The server predicts the split (adapters/classify.py)
5
+ * and the user corrects it by dragging; the metadata box additionally carries
6
+ * the role slots, because a role always names a metadata column -- the cell id
7
+ * and the coordinates are measurements *about* a cell, never markers measured
8
+ * on one.
9
+ *
10
+ * Mounted by three callers -- the CSV import step, the requirements modal, and
11
+ * the project edit page -- which is why it is a component rather than page
12
+ * code. All three are asking the same question and must store the same answer.
13
+ *
14
+ * Uses window.Sortable (vendored via webpack's vendor bundle, loaded by
15
+ * base.html on every page).
16
+ */
17
+ window.PlexoraColumnClassifier = (function () {
18
+ /**
19
+ * @param mount element to render into
20
+ * @param options.markers predicted marker columns, in table order
21
+ * @param options.metadata predicted metadata columns, in table order
22
+ * @param options.roles role -> column name, as already recorded
23
+ * @param options.roleLabels role -> human label (from the server, so the
24
+ * wording matches the requirements modal)
25
+ * @param options.onChange called after every edit
26
+ * @returns { value(), destroy() }
27
+ */
28
+ function mount(mount, options = {}) {
29
+ const roleLabels = options.roleLabels || {};
30
+ const roleNames = Object.keys(roleLabels);
31
+ const roles = { ...(options.roles || {}) };
32
+ let markers = [...(options.markers || [])];
33
+ let metadata = [...(options.metadata || [])];
34
+ const sortables = [];
35
+
36
+ mount.innerHTML = `
37
+ <div class="column-classifier">
38
+ <div class="column-box" data-group="metadata">
39
+ <div class="column-box-head">
40
+ <h2>Metadata</h2>
41
+ <span class="field-hint">Identifiers, coordinates, morphology, annotations.</span>
42
+ </div>
43
+ <div class="column-roles"></div>
44
+ <ul class="column-list" data-group="metadata"></ul>
45
+ </div>
46
+ <div class="column-box" data-group="markers">
47
+ <div class="column-box-head">
48
+ <h2>Markers</h2>
49
+ <span class="field-hint">Per-cell intensities that can be plotted or thresholded.</span>
50
+ </div>
51
+ <ul class="column-list" data-group="markers"></ul>
52
+ </div>
53
+ </div>
54
+ <p class="column-classifier-hint">
55
+ Drag a column between the boxes to correct it.
56
+ </p>
57
+ `;
58
+
59
+ const lists = {
60
+ metadata: mount.querySelector('.column-list[data-group="metadata"]'),
61
+ markers: mount.querySelector('.column-list[data-group="markers"]'),
62
+ };
63
+ const rolesMount = mount.querySelector('.column-roles');
64
+
65
+ function item(name) {
66
+ const li = document.createElement('li');
67
+ li.className = 'column-chip';
68
+ li.dataset.column = name;
69
+ li.textContent = name;
70
+ return li;
71
+ }
72
+
73
+ function renderList(group, names) {
74
+ lists[group].replaceChildren(...names.map(item));
75
+ }
76
+
77
+ function renderRoles() {
78
+ // Rebuilt whenever the metadata box changes: a role can only name a
79
+ // column that is in it, and dragging a column out has to clear any
80
+ // role pointing at it rather than leave a dangling name.
81
+ rolesMount.replaceChildren(...roleNames.map((role) => {
82
+ const field = document.createElement('label');
83
+ field.className = 'column-role';
84
+ field.textContent = roleLabels[role];
85
+
86
+ const select = document.createElement('select');
87
+ select.className = 'form-select';
88
+ select.dataset.role = role;
89
+ select.append(new Option('—', ''));
90
+ metadata.forEach((name) => {
91
+ select.append(new Option(name, name, false, roles[role] === name));
92
+ });
93
+ if (roles[role] && !metadata.includes(roles[role])) {
94
+ roles[role] = null;
95
+ }
96
+ select.addEventListener('change', () => {
97
+ roles[role] = select.value || null;
98
+ options.onChange?.();
99
+ });
100
+ field.appendChild(select);
101
+ return field;
102
+ }));
103
+ }
104
+
105
+ function readLists() {
106
+ metadata = [...lists.metadata.children].map((li) => li.dataset.column);
107
+ markers = [...lists.markers.children].map((li) => li.dataset.column);
108
+ renderRoles();
109
+ options.onChange?.();
110
+ }
111
+
112
+ renderList('metadata', metadata);
113
+ renderList('markers', markers);
114
+ renderRoles();
115
+
116
+ // One shared group name is what makes the two lists exchange items;
117
+ // without it each list only reorders within itself.
118
+ Object.values(lists).forEach((list) => {
119
+ sortables.push(new Sortable(list, {
120
+ group: 'plexora-columns',
121
+ animation: 150,
122
+ ghostClass: 'column-chip-ghost',
123
+ onSort: readLists,
124
+ }));
125
+ });
126
+
127
+ return {
128
+ value() {
129
+ // Only the roles this instance actually drew. A caller that
130
+ // narrows `roleLabels` is saying "I am not the surface that
131
+ // asks about the rest" -- and echoing an unrendered role back
132
+ // would have the server record it as confirmed, retiring a
133
+ // question nobody was ever shown.
134
+ const answered = Object.fromEntries(
135
+ roleNames.map((role) => [role, roles[role] ?? null]));
136
+ return { markers: [...markers], metadata: [...metadata], roles: answered };
137
+ },
138
+ destroy() {
139
+ sortables.forEach((s) => s.destroy());
140
+ mount.innerHTML = '';
141
+ },
142
+ };
143
+ }
144
+
145
+ return { mount };
146
+ })();
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The "where are the cell coordinates" control.
3
+ *
4
+ * Shared by the requirements modal and the project edit page, the same way
5
+ * columnClassifier.js is -- both ask this question and neither should own a
6
+ * private version of the answer shape, which is a read spec the server has to
7
+ * be able to apply either way.
8
+ *
9
+ * Why it is one control rather than the two column selects it replaces:
10
+ *
11
+ * A file can put a cell's position in either of two places. `obs` holds it as
12
+ * a pair of columns, which two selects express fine. `obsm` holds it as one
13
+ * (n, 2) array holding BOTH axes -- and "which column is X" has no answer for
14
+ * that. So the obsm case could previously only appear as a blank option with
15
+ * the detected key printed on it: visible, unpickable, never confirmed. The
16
+ * source got decided by matching the key name against a hardcoded list.
17
+ *
18
+ * That is not a safe way to decide it. A store routinely carries `spatial`
19
+ * and `X_umap` as identically-shaped (n, 2) float32 arrays, so nothing about
20
+ * the data separates a position from an embedding -- and a file whose UMAP
21
+ * happens to be named `spatial` would have had every cell drawn in UMAP space
22
+ * with nothing said. Hence: list every candidate with its shape, and let the
23
+ * person who knows the file choose.
24
+ *
25
+ * mount() returns { value() }, where value() is the payload the server takes:
26
+ *
27
+ * { source: "obsm", obsm_key: "spatial" }
28
+ * { source: "obs", x_column: "X_centroid", y_column: "Y_centroid" }
29
+ *
30
+ * or null while the choice is incomplete, so a caller can tell "not answered
31
+ * yet" from "answered" without inspecting the controls.
32
+ */
33
+ window.PlexoraCoordinateField = (function () {
34
+
35
+ function el(tag, className, text) {
36
+ const node = document.createElement(tag);
37
+ if (className) node.className = className;
38
+ if (text) node.textContent = text;
39
+ return node;
40
+ }
41
+
42
+ /** `obsm["spatial"] · 42609 × 2` -- the name, and the shape that is the
43
+ * only other thing on offer to tell candidates apart. */
44
+ function describe(entry) {
45
+ const shape = (entry.shape || []).join(" × ");
46
+ return shape ? `obsm["${entry.name}"] · ${shape}` : `obsm["${entry.name}"]`;
47
+ }
48
+
49
+ /**
50
+ * @param container element to render into
51
+ * @param options { obsm: [{name, shape}], obs: [name], current: {...},
52
+ * name: unique radio-group name, onChange(value) }
53
+ */
54
+ function mount(container, options) {
55
+ const obsmEntries = options.obsm || [];
56
+ const obsColumns = options.obs || [];
57
+ const current = options.current || {};
58
+ const onChange = options.onChange || function () {};
59
+ const groupName = options.name || "coordinates";
60
+
61
+ const root = el("div", "coordinate-field");
62
+
63
+ const obsmSelect = el("select", "form-select");
64
+ obsmSelect.append(new Option("Choose an array…", ""));
65
+ obsmEntries.forEach((entry) => {
66
+ obsmSelect.append(new Option(describe(entry), entry.name));
67
+ });
68
+
69
+ const obsRow = el("div", "coordinate-field-pair");
70
+ const xSelect = el("select", "form-select");
71
+ const ySelect = el("select", "form-select");
72
+ [[xSelect, "X column…"], [ySelect, "Y column…"]].forEach(([select, blank]) => {
73
+ select.append(new Option(blank, ""));
74
+ obsColumns.forEach((name) => select.append(new Option(name, name)));
75
+ obsRow.appendChild(select);
76
+ });
77
+
78
+ let source = current.source === "obs" ? "obs" : "obsm";
79
+
80
+ function value() {
81
+ if (source === "obsm") {
82
+ return obsmSelect.value
83
+ ? { source: "obsm", obsm_key: obsmSelect.value } : null;
84
+ }
85
+ return (xSelect.value && ySelect.value)
86
+ ? { source: "obs", x_column: xSelect.value, y_column: ySelect.value }
87
+ : null;
88
+ }
89
+
90
+ function show(next) {
91
+ source = next;
92
+ obsmSelect.hidden = next !== "obsm";
93
+ obsRow.hidden = next !== "obs";
94
+ onChange(value());
95
+ }
96
+
97
+ // A radio rather than a third dropdown: the choice changes what the
98
+ // control below it is asking, and the user has to see both places at
99
+ // once to know the other one exists.
100
+ const radios = el("div", "coordinate-field-source");
101
+ [["obsm", ".obsm"], ["obs", ".obs"]].forEach(([choice, label]) => {
102
+ const wrapper = el("label", "coordinate-field-radio");
103
+ const input = document.createElement("input");
104
+ input.type = "radio";
105
+ input.name = groupName;
106
+ input.value = choice;
107
+ input.checked = choice === source;
108
+ input.addEventListener("change", () => {
109
+ if (input.checked) show(choice);
110
+ });
111
+ wrapper.appendChild(input);
112
+ wrapper.appendChild(el("span", null, label));
113
+ radios.appendChild(wrapper);
114
+ });
115
+
116
+ if (current.obsm_key) obsmSelect.value = current.obsm_key;
117
+ if (current.x_column) xSelect.value = current.x_column;
118
+ if (current.y_column) ySelect.value = current.y_column;
119
+ [obsmSelect, xSelect, ySelect].forEach((select) => {
120
+ select.addEventListener("change", () => onChange(value()));
121
+ });
122
+
123
+ root.appendChild(radios);
124
+ root.appendChild(obsmSelect);
125
+ root.appendChild(obsRow);
126
+ container.appendChild(root);
127
+
128
+ show(source);
129
+ return { value };
130
+ }
131
+
132
+ return { mount };
133
+ }());