@jax-data-science/sci-image-visualizer 0.15.0 → 0.17.0

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 (47) hide show
  1. package/esm2022/lib/contracts/display-types.mjs +2 -1
  2. package/esm2022/lib/implementations/napari-js/napari-loading-state.mjs +1 -1
  3. package/esm2022/lib/implementations/napari-js/napari-scene.mjs +1 -1
  4. package/esm2022/lib/implementations/napari-js/napari-spatial-2d-scene.mjs +10 -8
  5. package/esm2022/lib/implementations/napari-js/napari-spatial-3d-scene.mjs +226 -39
  6. package/esm2022/lib/implementations/napari-js/napari-spatial-3d-transcripts.mjs +269 -0
  7. package/esm2022/lib/implementations/napari-js/napari-spatial-encoding.mjs +11 -4
  8. package/esm2022/lib/implementations/napari-js/napari-spatial-tiles.mjs +6 -7
  9. package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +2 -1
  10. package/esm2022/lib/implementations/napari-js/napari-volume-region-meshes.mjs +33 -8
  11. package/esm2022/lib/implementations/napari-js/napari-volume-scene.mjs +6 -2
  12. package/esm2022/lib/implementations/napari-js/spatial-tiles/cell-layers.mjs +10 -6
  13. package/esm2022/lib/implementations/napari-js/spatial-tiles/transcript-layers.mjs +9 -5
  14. package/esm2022/lib/spatial/spatial-controls.facade.mjs +7 -4
  15. package/esm2022/lib/spatial/spatial-encoding.mjs +18 -1
  16. package/esm2022/lib/spatial-controls/spatial-charts/spatial-charts.component.mjs +6 -2
  17. package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +7 -3
  18. package/esm2022/lib/spatial-controls/spatial-gene-style/spatial-gene-style.component.mjs +7 -3
  19. package/esm2022/lib/spatial-controls/spatial-gene-tree/spatial-gene-tree.component.mjs +67 -19
  20. package/esm2022/lib/spatial-controls/spatial-groups-panel/spatial-groups-panel.component.mjs +61 -8
  21. package/esm2022/lib/spatial-controls/spatial-observations-panel/spatial-observations-panel.component.mjs +3 -3
  22. package/esm2022/lib/toolbar/toolbar.component.mjs +13 -4
  23. package/esm2022/lib/toolbar/view-controls/view-controls.component.mjs +7 -7
  24. package/fesm2022/{jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-qUeMuBb0.mjs → jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DHrHO2ay.mjs} +795 -165
  25. package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DHrHO2ay.mjs.map +1 -0
  26. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-U1ODcfYD.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-CKEUwNta.mjs} +2 -2
  27. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-U1ODcfYD.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-CKEUwNta.mjs.map} +1 -1
  28. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +1 -1
  29. package/lib/contracts/display-types.d.ts +9 -2
  30. package/lib/implementations/napari-js/napari-loading-state.d.ts +1 -1
  31. package/lib/implementations/napari-js/napari-scene.d.ts +2 -0
  32. package/lib/implementations/napari-js/napari-spatial-2d-scene.d.ts +3 -3
  33. package/lib/implementations/napari-js/napari-spatial-3d-scene.d.ts +82 -9
  34. package/lib/implementations/napari-js/napari-spatial-3d-transcripts.d.ts +62 -0
  35. package/lib/implementations/napari-js/napari-spatial-encoding.d.ts +9 -2
  36. package/lib/implementations/napari-js/napari-spatial-tiles.d.ts +5 -6
  37. package/lib/implementations/napari-js/napari-volume-region-meshes.d.ts +16 -3
  38. package/lib/implementations/napari-js/spatial-tiles/cell-layers.d.ts +1 -1
  39. package/lib/spatial/spatial-encoding.d.ts +10 -0
  40. package/lib/spatial-controls/spatial-controls.component.d.ts +2 -0
  41. package/lib/spatial-controls/spatial-gene-style/spatial-gene-style.component.d.ts +3 -1
  42. package/lib/spatial-controls/spatial-gene-tree/spatial-gene-tree.component.d.ts +25 -0
  43. package/lib/spatial-controls/spatial-groups-panel/spatial-groups-panel.component.d.ts +14 -3
  44. package/lib/toolbar/toolbar.component.d.ts +6 -0
  45. package/lib/toolbar/view-controls/view-controls.component.d.ts +3 -3
  46. package/package.json +2 -2
  47. package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-qUeMuBb0.mjs.map +0 -1
@@ -729,6 +729,7 @@ const DEFAULT_SPATIAL_VIEW = {
729
729
  transcriptGenes: [],
730
730
  transcriptHiddenGenes: [],
731
731
  transcriptGeneColors: {},
732
+ groupColors: {},
732
733
  transcriptGeneGroups: [],
733
734
  transcriptAllGenes: false,
734
735
  transcriptAllGenesHidden: [],
@@ -2831,6 +2832,23 @@ function resolveCategoryColors(meta, palette = DEFAULT_CATEGORICAL_PALETTE) {
2831
2832
  return fallbackColorFor(name, palette);
2832
2833
  });
2833
2834
  }
2835
+ /**
2836
+ * Categorical column `column`'s colours ({@link resolveCategoryColors}) with the user's choices
2837
+ * for its groups over them. Every place a grouping is coloured goes through this, so a group
2838
+ * recoloured in the list is that colour on the cells, the markers, the legend and the charts.
2839
+ */
2840
+ function categoryColorsFor(column, meta, overrides) {
2841
+ const base = resolveCategoryColors(meta);
2842
+ const own = overrides?.[column];
2843
+ if (!own)
2844
+ return base;
2845
+ return meta.categories.map((name, i) => own[name] || base[i]);
2846
+ }
2847
+ /** What a cache keyed on a grouping's colours must include of the user's choices for it. */
2848
+ function groupColorsKey(column, overrides) {
2849
+ const own = column ? overrides?.[column] : undefined;
2850
+ return own ? JSON.stringify(own) : '';
2851
+ }
2834
2852
  /** Categorical column → flat per-point RGBA (length `4 * codes.length`). */
2835
2853
  function encodeCategorical(codes, opts) {
2836
2854
  const n = codes.length;
@@ -10580,7 +10598,7 @@ function frameToRgba(cached, frameIndex) {
10580
10598
  /** Lazy-import so onnxruntime-web is never pulled into unit tests or the
10581
10599
  * initial bundle — only when segmentation actually runs. */
10582
10600
  const onnxSessionFactory = async () => {
10583
- const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-U1ODcfYD.mjs');
10601
+ const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-CKEUwNta.mjs');
10584
10602
  return new OnnxSamSession();
10585
10603
  };
10586
10604
  /** Thrown inside a load that a model switch superseded. */
@@ -13348,12 +13366,12 @@ class ViewControlsComponent {
13348
13366
  this.activeDragMode = null;
13349
13367
  /** The 3D camera mode (`turntable` / `orbit` / …). */
13350
13368
  this.activeSurface3dMode = 'turntable';
13351
- /** A napari-js WebGPU view (axes toggle, napari camera). */
13352
- this.isNapariMode = false;
13353
13369
  /** The napari-js surface (shows the wireframe toggle). */
13354
13370
  this.isNapariSurfaceMode = false;
13355
13371
  /** A napari-js 3D view (shows the Resolution dropdown). */
13356
13372
  this.isNapari3dMode = false;
13373
+ /** Whether the view draws the 3D axes gizmo (shows its toggle). */
13374
+ this.axesToggle = false;
13357
13375
  /** The napari 3D axes gizmo is shown. */
13358
13376
  this.axesVisible = true;
13359
13377
  /** The napari surface wireframe is on. */
@@ -13380,11 +13398,11 @@ class ViewControlsComponent {
13380
13398
  this.decimateOptions = NAPARI_DECIMATE_OPTIONS;
13381
13399
  }
13382
13400
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ViewControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
13383
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ViewControlsComponent, isStandalone: true, selector: "toolbar-view-controls", inputs: { isHeatmap: "isHeatmap", zoomTools: "zoomTools", isImageView: "isImageView", activeDragMode: "activeDragMode", activeSurface3dMode: "activeSurface3dMode", isNapariMode: "isNapariMode", isNapariSurfaceMode: "isNapariSurfaceMode", isNapari3dMode: "isNapari3dMode", axesVisible: "axesVisible", wireframeActive: "wireframeActive", resolutionScale: "resolutionScale" }, outputs: { toggleDragMode: "toggleDragMode", zoomIn: "zoomIn", zoomOut: "zoomOut", toggleSurface3dMode: "toggleSurface3dMode", resetSurfaceCamera: "resetSurfaceCamera", toggleAxes: "toggleAxes", toggleWireframe: "toggleWireframe", selectResolution: "selectResolution" }, ngImport: i0, template: "<!-- Zoom tools. Plotly rubber-band drag-zoom: on OpenSeadragon the box-zoom\n covers this and a bare drag pans natively, so it's hidden in image view. -->\n<ng-container *ngIf=\"isHeatmap && zoomTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n *ngIf=\"!isImageView\"\n icon=\"pi pi-search\"\n size=\"small\"\n [text]=\"activeDragMode !== 'zoom'\"\n ariaLabel=\"Zoom drag\"\n pTooltip=\"Zoom drag\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoom')\"\n >\n </p-button>\n <p-button\n size=\"small\"\n [text]=\"activeDragMode !== 'zoomToBox'\"\n ariaLabel=\"Zoom box\"\n pTooltip=\"Zoom box (<kbd>b</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoomToBox')\"\n >\n <img\n src=\"assets/plotting/zoom-box.svg\"\n [class]=\"activeDragMode === 'zoomToBox' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"\n />\n </p-button>\n <p-button\n icon=\"pi pi-arrows-alt\"\n size=\"small\"\n [text]=\"activeDragMode !== 'pan'\"\n ariaLabel=\"Pan the image\"\n pTooltip=\"Pan the image (<kbd>p</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('pan')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-search-plus\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Zoom in\"\n pTooltip=\"Zoom in (<kbd>+</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"zoomIn.emit()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-search-minus\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Zoom out\"\n pTooltip=\"Zoom out (<kbd>-</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"zoomOut.emit()\"\n >\n </p-button>\n</ng-container>\n\n<!-- Surface 3D controls -->\n<ng-container *ngIf=\"!isHeatmap\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n icon=\"pi pi-search\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'zoom'\"\n ariaLabel=\"Zoom\"\n pTooltip=\"Zoom\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('zoom')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-arrows-alt\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'pan'\"\n ariaLabel=\"Pan\"\n pTooltip=\"Pan\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('pan')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-globe\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'orbit'\"\n ariaLabel=\"Orbital rotation\"\n pTooltip=\"Orbital rotation\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('orbit')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-sync\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'turntable'\"\n ariaLabel=\"Turntable rotation\"\n pTooltip=\"Turntable rotation\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('turntable')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-home\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Reset camera\"\n pTooltip=\"Reset camera to default\"\n tooltipPosition=\"bottom\"\n (onClick)=\"resetSurfaceCamera.emit()\"\n >\n </p-button>\n <!-- 3D coordinate-axes / scale gizmo toggle (napari-js WebGPU volume/isosurface only).\n `p-button-icon-only` gives the same square sizing as the neighbouring pi-icon buttons\n (the projected <img> doesn't trigger PrimeNG's automatic icon-only sizing). -->\n <p-button\n size=\"small\"\n *ngIf=\"isNapariMode\"\n styleClass=\"p-button-icon-only\"\n [text]=\"!axesVisible\"\n ariaLabel=\"Toggle 3D axes\"\n pTooltip=\"Toggle 3D axes / scale\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleAxes.emit()\"\n >\n <img\n src=\"assets/plotting/3d-coordinates.svg\"\n [class]=\"axesVisible ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"\n />\n </p-button>\n <!-- Surface wireframe toggle (napari-js WebGPU surface only). -->\n <p-button\n icon=\"pi pi-share-alt\"\n size=\"small\"\n *ngIf=\"isNapariSurfaceMode\"\n [text]=\"!wireframeActive\"\n ariaLabel=\"Toggle wireframe\"\n pTooltip=\"Toggle wireframe\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleWireframe.emit()\"\n >\n </p-button>\n <!-- Resolution (decimate factor) for the napari 3D types \u2014 reloads at coarser/finer sampling. -->\n <ng-container *ngIf=\"isNapari3dMode\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Res</span>\n <p-dropdown\n [options]=\"decimateOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"resolutionScale\"\n appendTo=\"body\"\n ariaLabel=\"Resolution\"\n pTooltip=\"Resolution (decimate factor)\"\n tooltipPosition=\"bottom\"\n [style]=\"{ 'min-width': '6rem' }\"\n (onChange)=\"selectResolution.emit($event.value)\"\n >\n </p-dropdown>\n </ng-container>\n</ng-container>\n", styles: [".toolbar-text{color:var(--primary-color);font-size:.875rem}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-icon-active{filter:brightness(0) invert(1)}.wand-sensitivity-value{min-width:2rem;text-align:right}:host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i5$2.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13401
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ViewControlsComponent, isStandalone: true, selector: "toolbar-view-controls", inputs: { isHeatmap: "isHeatmap", zoomTools: "zoomTools", isImageView: "isImageView", activeDragMode: "activeDragMode", activeSurface3dMode: "activeSurface3dMode", isNapariSurfaceMode: "isNapariSurfaceMode", isNapari3dMode: "isNapari3dMode", axesToggle: "axesToggle", axesVisible: "axesVisible", wireframeActive: "wireframeActive", resolutionScale: "resolutionScale" }, outputs: { toggleDragMode: "toggleDragMode", zoomIn: "zoomIn", zoomOut: "zoomOut", toggleSurface3dMode: "toggleSurface3dMode", resetSurfaceCamera: "resetSurfaceCamera", toggleAxes: "toggleAxes", toggleWireframe: "toggleWireframe", selectResolution: "selectResolution" }, ngImport: i0, template: "<!-- Zoom tools. Plotly rubber-band drag-zoom: on OpenSeadragon the box-zoom\n covers this and a bare drag pans natively, so it's hidden in image view. -->\n<ng-container *ngIf=\"isHeatmap && zoomTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n *ngIf=\"!isImageView\"\n icon=\"pi pi-search\"\n size=\"small\"\n [text]=\"activeDragMode !== 'zoom'\"\n ariaLabel=\"Zoom drag\"\n pTooltip=\"Zoom drag\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoom')\"\n >\n </p-button>\n <p-button\n size=\"small\"\n [text]=\"activeDragMode !== 'zoomToBox'\"\n ariaLabel=\"Zoom box\"\n pTooltip=\"Zoom box (<kbd>b</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoomToBox')\"\n >\n <img\n src=\"assets/plotting/zoom-box.svg\"\n [class]=\"activeDragMode === 'zoomToBox' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"\n />\n </p-button>\n <p-button\n icon=\"pi pi-arrows-alt\"\n size=\"small\"\n [text]=\"activeDragMode !== 'pan'\"\n ariaLabel=\"Pan the image\"\n pTooltip=\"Pan the image (<kbd>p</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('pan')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-search-plus\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Zoom in\"\n pTooltip=\"Zoom in (<kbd>+</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"zoomIn.emit()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-search-minus\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Zoom out\"\n pTooltip=\"Zoom out (<kbd>-</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"zoomOut.emit()\"\n >\n </p-button>\n</ng-container>\n\n<!-- Surface 3D controls -->\n<ng-container *ngIf=\"!isHeatmap\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n icon=\"pi pi-search\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'zoom'\"\n ariaLabel=\"Zoom\"\n pTooltip=\"Zoom\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('zoom')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-arrows-alt\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'pan'\"\n ariaLabel=\"Pan\"\n pTooltip=\"Pan\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('pan')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-globe\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'orbit'\"\n ariaLabel=\"Orbital rotation\"\n pTooltip=\"Orbital rotation\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('orbit')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-sync\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'turntable'\"\n ariaLabel=\"Turntable rotation\"\n pTooltip=\"Turntable rotation\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('turntable')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-home\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Reset camera\"\n pTooltip=\"Reset camera to default\"\n tooltipPosition=\"bottom\"\n (onClick)=\"resetSurfaceCamera.emit()\"\n >\n </p-button>\n <!-- 3D coordinate-axes / scale gizmo toggle (napari-js WebGPU volume/isosurface, and the 3D\n spatial cloud).\n `p-button-icon-only` gives the same square sizing as the neighbouring pi-icon buttons\n (the projected <img> doesn't trigger PrimeNG's automatic icon-only sizing). -->\n <p-button\n size=\"small\"\n *ngIf=\"axesToggle\"\n styleClass=\"p-button-icon-only\"\n [text]=\"!axesVisible\"\n ariaLabel=\"Toggle 3D axes\"\n pTooltip=\"Toggle 3D axes / scale\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleAxes.emit()\"\n >\n <img\n src=\"assets/plotting/3d-coordinates.svg\"\n [class]=\"axesVisible ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"\n />\n </p-button>\n <!-- Surface wireframe toggle (napari-js WebGPU surface only). -->\n <p-button\n icon=\"pi pi-share-alt\"\n size=\"small\"\n *ngIf=\"isNapariSurfaceMode\"\n [text]=\"!wireframeActive\"\n ariaLabel=\"Toggle wireframe\"\n pTooltip=\"Toggle wireframe\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleWireframe.emit()\"\n >\n </p-button>\n <!-- Resolution (decimate factor) for the napari 3D types \u2014 reloads at coarser/finer sampling. -->\n <ng-container *ngIf=\"isNapari3dMode\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Res</span>\n <p-dropdown\n [options]=\"decimateOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"resolutionScale\"\n appendTo=\"body\"\n ariaLabel=\"Resolution\"\n pTooltip=\"Resolution (decimate factor)\"\n tooltipPosition=\"bottom\"\n [style]=\"{ 'min-width': '6rem' }\"\n (onChange)=\"selectResolution.emit($event.value)\"\n >\n </p-dropdown>\n </ng-container>\n</ng-container>\n", styles: [".toolbar-text{color:var(--primary-color);font-size:.875rem}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-icon-active{filter:brightness(0) invert(1)}.wand-sensitivity-value{min-width:2rem;text-align:right}:host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i5$2.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13384
13402
  }
13385
13403
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ViewControlsComponent, decorators: [{
13386
13404
  type: Component,
13387
- args: [{ selector: 'toolbar-view-controls', standalone: true, imports: [CommonModule, FormsModule, ButtonModule, DropdownModule, TooltipModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Zoom tools. Plotly rubber-band drag-zoom: on OpenSeadragon the box-zoom\n covers this and a bare drag pans natively, so it's hidden in image view. -->\n<ng-container *ngIf=\"isHeatmap && zoomTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n *ngIf=\"!isImageView\"\n icon=\"pi pi-search\"\n size=\"small\"\n [text]=\"activeDragMode !== 'zoom'\"\n ariaLabel=\"Zoom drag\"\n pTooltip=\"Zoom drag\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoom')\"\n >\n </p-button>\n <p-button\n size=\"small\"\n [text]=\"activeDragMode !== 'zoomToBox'\"\n ariaLabel=\"Zoom box\"\n pTooltip=\"Zoom box (<kbd>b</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoomToBox')\"\n >\n <img\n src=\"assets/plotting/zoom-box.svg\"\n [class]=\"activeDragMode === 'zoomToBox' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"\n />\n </p-button>\n <p-button\n icon=\"pi pi-arrows-alt\"\n size=\"small\"\n [text]=\"activeDragMode !== 'pan'\"\n ariaLabel=\"Pan the image\"\n pTooltip=\"Pan the image (<kbd>p</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('pan')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-search-plus\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Zoom in\"\n pTooltip=\"Zoom in (<kbd>+</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"zoomIn.emit()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-search-minus\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Zoom out\"\n pTooltip=\"Zoom out (<kbd>-</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"zoomOut.emit()\"\n >\n </p-button>\n</ng-container>\n\n<!-- Surface 3D controls -->\n<ng-container *ngIf=\"!isHeatmap\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n icon=\"pi pi-search\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'zoom'\"\n ariaLabel=\"Zoom\"\n pTooltip=\"Zoom\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('zoom')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-arrows-alt\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'pan'\"\n ariaLabel=\"Pan\"\n pTooltip=\"Pan\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('pan')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-globe\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'orbit'\"\n ariaLabel=\"Orbital rotation\"\n pTooltip=\"Orbital rotation\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('orbit')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-sync\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'turntable'\"\n ariaLabel=\"Turntable rotation\"\n pTooltip=\"Turntable rotation\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('turntable')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-home\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Reset camera\"\n pTooltip=\"Reset camera to default\"\n tooltipPosition=\"bottom\"\n (onClick)=\"resetSurfaceCamera.emit()\"\n >\n </p-button>\n <!-- 3D coordinate-axes / scale gizmo toggle (napari-js WebGPU volume/isosurface only).\n `p-button-icon-only` gives the same square sizing as the neighbouring pi-icon buttons\n (the projected <img> doesn't trigger PrimeNG's automatic icon-only sizing). -->\n <p-button\n size=\"small\"\n *ngIf=\"isNapariMode\"\n styleClass=\"p-button-icon-only\"\n [text]=\"!axesVisible\"\n ariaLabel=\"Toggle 3D axes\"\n pTooltip=\"Toggle 3D axes / scale\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleAxes.emit()\"\n >\n <img\n src=\"assets/plotting/3d-coordinates.svg\"\n [class]=\"axesVisible ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"\n />\n </p-button>\n <!-- Surface wireframe toggle (napari-js WebGPU surface only). -->\n <p-button\n icon=\"pi pi-share-alt\"\n size=\"small\"\n *ngIf=\"isNapariSurfaceMode\"\n [text]=\"!wireframeActive\"\n ariaLabel=\"Toggle wireframe\"\n pTooltip=\"Toggle wireframe\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleWireframe.emit()\"\n >\n </p-button>\n <!-- Resolution (decimate factor) for the napari 3D types \u2014 reloads at coarser/finer sampling. -->\n <ng-container *ngIf=\"isNapari3dMode\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Res</span>\n <p-dropdown\n [options]=\"decimateOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"resolutionScale\"\n appendTo=\"body\"\n ariaLabel=\"Resolution\"\n pTooltip=\"Resolution (decimate factor)\"\n tooltipPosition=\"bottom\"\n [style]=\"{ 'min-width': '6rem' }\"\n (onChange)=\"selectResolution.emit($event.value)\"\n >\n </p-dropdown>\n </ng-container>\n</ng-container>\n", styles: [".toolbar-text{color:var(--primary-color);font-size:.875rem}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-icon-active{filter:brightness(0) invert(1)}.wand-sensitivity-value{min-width:2rem;text-align:right}:host{display:contents}\n"] }]
13405
+ args: [{ selector: 'toolbar-view-controls', standalone: true, imports: [CommonModule, FormsModule, ButtonModule, DropdownModule, TooltipModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Zoom tools. Plotly rubber-band drag-zoom: on OpenSeadragon the box-zoom\n covers this and a bare drag pans natively, so it's hidden in image view. -->\n<ng-container *ngIf=\"isHeatmap && zoomTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n *ngIf=\"!isImageView\"\n icon=\"pi pi-search\"\n size=\"small\"\n [text]=\"activeDragMode !== 'zoom'\"\n ariaLabel=\"Zoom drag\"\n pTooltip=\"Zoom drag\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoom')\"\n >\n </p-button>\n <p-button\n size=\"small\"\n [text]=\"activeDragMode !== 'zoomToBox'\"\n ariaLabel=\"Zoom box\"\n pTooltip=\"Zoom box (<kbd>b</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('zoomToBox')\"\n >\n <img\n src=\"assets/plotting/zoom-box.svg\"\n [class]=\"activeDragMode === 'zoomToBox' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"\n />\n </p-button>\n <p-button\n icon=\"pi pi-arrows-alt\"\n size=\"small\"\n [text]=\"activeDragMode !== 'pan'\"\n ariaLabel=\"Pan the image\"\n pTooltip=\"Pan the image (<kbd>p</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('pan')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-search-plus\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Zoom in\"\n pTooltip=\"Zoom in (<kbd>+</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"zoomIn.emit()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-search-minus\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Zoom out\"\n pTooltip=\"Zoom out (<kbd>-</kbd>)\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"zoomOut.emit()\"\n >\n </p-button>\n</ng-container>\n\n<!-- Surface 3D controls -->\n<ng-container *ngIf=\"!isHeatmap\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n icon=\"pi pi-search\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'zoom'\"\n ariaLabel=\"Zoom\"\n pTooltip=\"Zoom\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('zoom')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-arrows-alt\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'pan'\"\n ariaLabel=\"Pan\"\n pTooltip=\"Pan\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('pan')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-globe\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'orbit'\"\n ariaLabel=\"Orbital rotation\"\n pTooltip=\"Orbital rotation\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('orbit')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-sync\"\n size=\"small\"\n [text]=\"activeSurface3dMode !== 'turntable'\"\n ariaLabel=\"Turntable rotation\"\n pTooltip=\"Turntable rotation\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSurface3dMode.emit('turntable')\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-home\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Reset camera\"\n pTooltip=\"Reset camera to default\"\n tooltipPosition=\"bottom\"\n (onClick)=\"resetSurfaceCamera.emit()\"\n >\n </p-button>\n <!-- 3D coordinate-axes / scale gizmo toggle (napari-js WebGPU volume/isosurface, and the 3D\n spatial cloud).\n `p-button-icon-only` gives the same square sizing as the neighbouring pi-icon buttons\n (the projected <img> doesn't trigger PrimeNG's automatic icon-only sizing). -->\n <p-button\n size=\"small\"\n *ngIf=\"axesToggle\"\n styleClass=\"p-button-icon-only\"\n [text]=\"!axesVisible\"\n ariaLabel=\"Toggle 3D axes\"\n pTooltip=\"Toggle 3D axes / scale\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleAxes.emit()\"\n >\n <img\n src=\"assets/plotting/3d-coordinates.svg\"\n [class]=\"axesVisible ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"\n />\n </p-button>\n <!-- Surface wireframe toggle (napari-js WebGPU surface only). -->\n <p-button\n icon=\"pi pi-share-alt\"\n size=\"small\"\n *ngIf=\"isNapariSurfaceMode\"\n [text]=\"!wireframeActive\"\n ariaLabel=\"Toggle wireframe\"\n pTooltip=\"Toggle wireframe\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleWireframe.emit()\"\n >\n </p-button>\n <!-- Resolution (decimate factor) for the napari 3D types \u2014 reloads at coarser/finer sampling. -->\n <ng-container *ngIf=\"isNapari3dMode\">\n <span class=\"flex align-items-center mx-1 toolbar-text\">Res</span>\n <p-dropdown\n [options]=\"decimateOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"resolutionScale\"\n appendTo=\"body\"\n ariaLabel=\"Resolution\"\n pTooltip=\"Resolution (decimate factor)\"\n tooltipPosition=\"bottom\"\n [style]=\"{ 'min-width': '6rem' }\"\n (onChange)=\"selectResolution.emit($event.value)\"\n >\n </p-dropdown>\n </ng-container>\n</ng-container>\n", styles: [".toolbar-text{color:var(--primary-color);font-size:.875rem}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-icon-active{filter:brightness(0) invert(1)}.wand-sensitivity-value{min-width:2rem;text-align:right}:host{display:contents}\n"] }]
13388
13406
  }], propDecorators: { isHeatmap: [{
13389
13407
  type: Input
13390
13408
  }], zoomTools: [{
@@ -13395,12 +13413,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
13395
13413
  type: Input
13396
13414
  }], activeSurface3dMode: [{
13397
13415
  type: Input
13398
- }], isNapariMode: [{
13399
- type: Input
13400
13416
  }], isNapariSurfaceMode: [{
13401
13417
  type: Input
13402
13418
  }], isNapari3dMode: [{
13403
13419
  type: Input
13420
+ }], axesToggle: [{
13421
+ type: Input
13404
13422
  }], axesVisible: [{
13405
13423
  type: Input
13406
13424
  }], wireframeActive: [{
@@ -14012,6 +14030,15 @@ class ToolbarComponent {
14012
14030
  get isIsosurfaceMode() {
14013
14031
  return this.effectivePlotType === PlotType.ISOSURFACE || isNapariIsosurface(this.effectivePlotType);
14014
14032
  }
14033
+ /**
14034
+ * The views that draw the 3D axes gizmo (an `Axes3dGizmo`): napari-js's volume, isosurface and
14035
+ * surface, and the 3D spatial cloud. Not every napari-js view: its 2D image and scatter and its
14036
+ * 3D scatter draw none, and a toggle there would do nothing.
14037
+ */
14038
+ get showsAxesToggle() {
14039
+ const type = this.effectivePlotType;
14040
+ return isNapariVolume(type) || isNapariIsosurface(type) || isNapariSurface(type) || isSpatialOmics3d(type);
14041
+ }
14015
14042
  /** The napari-js WebGPU surface — shows the wireframe toggle. */
14016
14043
  get isNapariSurfaceMode() {
14017
14044
  return isNapariSurface(this.effectivePlotType);
@@ -14029,7 +14056,7 @@ class ToolbarComponent {
14029
14056
  this.displayHelpDialog = true;
14030
14057
  }
14031
14058
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
14032
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ToolbarComponent, isStandalone: true, selector: "plotting-toolbar", inputs: { imageInfo: "imageInfo", hasSpatialDataset: "hasSpatialDataset", plotTypeOptions: "plotTypeOptions", selectedPlotType: "selectedPlotType", basePlotType: "basePlotType", isoRange: "isoRange", maxIndex: "maxIndex", zIndex: "zIndex", isHeatmap: "isHeatmap", is3dRegions: "is3dRegions", isVolumeView: "isVolumeView", sliceEditor: "sliceEditor", activeDragMode: "activeDragMode", activeSurface3dMode: "activeSurface3dMode", axesVisible: "axesVisible", wireframeActive: "wireframeActive", resolutionScale: "resolutionScale", wandSensitivity: "wandSensitivity", brushSize: "brushSize", vertexEraserRadius: "vertexEraserRadius", canUndo: "canUndo", canRedo: "canRedo", stackOptions: "stackOptions", tools: "tools", imageSmoothingEnabled: "imageSmoothingEnabled", samModels: "samModels", samModelId: "samModelId", contributedTools: "contributedTools", dialogTools: "dialogTools", openDialogToolId: "openDialogToolId", toolModelIds: "toolModelIds" }, outputs: { selectPlotType: "selectPlotType", addProfileLine: "addProfileLine", toggleImageSmoothing: "toggleImageSmoothing", isoRangeChange: "isoRangeChange", openChannelHistogram: "openChannelHistogram", openSpatialControls: "openSpatialControls", openRegionEditor: "openRegionEditor", toggleSliceEditor: "toggleSliceEditor", selectStackOption: "selectStackOption", zScrub: "zScrub", zSlide: "zSlide", zIndexInput: "zIndexInput", reloadAndPlot: "reloadAndPlot", downloadImage: "downloadImage", autoscaleImage: "autoscaleImage", toggleDragMode: "toggleDragMode", zoomIn: "zoomIn", zoomOut: "zoomOut", toggleSurface3dMode: "toggleSurface3dMode", resetSurfaceCamera: "resetSurfaceCamera", toggleAxes: "toggleAxes", toggleWireframe: "toggleWireframe", selectResolution: "selectResolution", deleteRegion: "deleteRegion", undoRegion: "undoRegion", redoRegion: "redoRegion", segmentRegions: "segmentRegions", segmentCellpose: "segmentCellpose", runTool: "runTool", toggleDialogTool: "toggleDialogTool", toolModelChange: "toolModelChange", openToolParams: "openToolParams", samModelChange: "samModelChange", wandSensitivityChange: "wandSensitivityChange", brushSizeChange: "brushSizeChange", vertexEraserRadiusChange: "vertexEraserRadiusChange" }, ngImport: i0, template: "<p-toolbar>\n <div class=\"p-toolbar-group-start flex-wrap align-items-center gap-1\">\n <!-- Host-provided custom toolbar buttons, shown first (e.g. app-specific\n actions the visualization view itself doesn't own). -->\n <ng-content></ng-content>\n\n <!-- Contributed dialog tools (TOOLBAR_TOOLS, kind: 'dialog'): app-level actions\n like the host's own buttons above, so they sit right beside them. Image view\n only, the view their dialog draws over. Highlighted while the dialog is open. -->\n <ng-container *ngIf=\"isImageView\">\n <p-button\n *ngFor=\"let tool of dialogTools; trackBy: trackToolById\"\n size=\"small\"\n [text]=\"openDialogToolId !== tool.id\"\n [pTooltip]=\"tool.tooltip\"\n tooltipPosition=\"bottom\"\n [ariaLabel]=\"tool.label\"\n [attr.aria-pressed]=\"openDialogToolId === tool.id\"\n (onClick)=\"toggleDialogTool.emit(tool.id)\"\n >\n <img\n *ngIf=\"tool.icon.src\"\n [src]=\"tool.icon.src\"\n style=\"width: 1rem\"\n [alt]=\"tool.label\"\n [class]=\"openDialogToolId === tool.id ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n <i *ngIf=\"tool.icon.pi\" class=\"pi {{ tool.icon.pi }}\"></i>\n </p-button>\n </ng-container>\n\n <!-- Plot type: available for any image (grayscale or RGB), and for a spatial dataset\n that brings none (its spatial modes are still a choice). -->\n <toolbar-plot-type-selector\n *ngIf=\"showPlotTypes\"\n [options]=\"plotTypeOptions\"\n [selected]=\"selectedPlotType\"\n [isosurface]=\"isIsosurfaceMode\"\n [isoRange]=\"isoRange\"\n [intensityLines]=\"isIntensityCapable\"\n (selectPlotType)=\"selectPlotType.emit($event)\"\n (isoRangeChange)=\"isoRangeChange.emit($event)\"\n (addProfileLine)=\"addProfileLine.emit()\"\n >\n </toolbar-plot-type-selector>\n <!-- Channels & Histogram: brightness/contrast, gamma, per-channel display,\n colormap/reverse/invert \u2014 opens the floating dialog. -->\n <p-button\n *ngIf=\"tools.specialTools\"\n pRipple\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Channels &amp; Histogram\"\n pTooltip=\"Channels &amp; Histogram (brightness/contrast, colormap)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openChannelHistogram.emit()\"\n >\n <img src=\"assets/plotting/invert.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n\n <!-- Spatial-omics controls. Only meaningful in the spatial mode, which is\n itself only offered while a dataset is loaded \u2014 so this button appears\n exactly when there is something for it to control. -->\n <p-button\n *ngIf=\"tools.specialTools && isSpatialMode\"\n pRipple\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Spatial omics controls\"\n pTooltip=\"Spatial omics (colour by column or gene, legend, selection, distributions)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openSpatialControls.emit()\"\n >\n <img src=\"assets/plotting/spatial-omics.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n\n <!-- Smoothen toggle: bilinear smoothing vs nearest-neighbour raw pixels (Image\n view only \u2014 it's an OpenSeadragon drawer setting). Highlighted when smoothing on. -->\n <p-button\n *ngIf=\"tools.specialTools && isImageView\"\n size=\"small\"\n [text]=\"!imageSmoothingEnabled\"\n ariaLabel=\"Smoothen\"\n pTooltip=\"Smoothen (turn on image smoothing)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleImageSmoothing.emit()\"\n >\n <img\n src=\"assets/plotting/smooth.svg\"\n style=\"width: 1rem\"\n [class]=\"imageSmoothingEnabled ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n </p-button>\n\n <!-- Stack controls -->\n <toolbar-stack-controls\n *ngIf=\"imageInfo?.isStack\"\n [liveScrubber]=\"showsLiveSliceScrubber\"\n [stackToggle]=\"!showsLiveSliceScrubber && !isNapariMode && !isSpatialMode\"\n [showStack]=\"!!imageInfo?.showStack\"\n [stackOptions]=\"stackOptions\"\n [zIndex]=\"zIndex\"\n [maxIndex]=\"maxIndex\"\n (zScrub)=\"zScrub.emit($event)\"\n (zSlide)=\"zSlide.emit($event)\"\n (selectStackOption)=\"selectStackOption.emit($event)\"\n (zIndexInput)=\"zIndexInput.emit($event)\"\n (reloadAndPlot)=\"reloadAndPlot.emit()\"\n >\n </toolbar-stack-controls>\n\n <!-- Image actions (special tools) -->\n <ng-container *ngIf=\"tools.specialTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n icon=\"pi pi-download\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Download image as PNG\"\n pTooltip=\"Download image as PNG\"\n tooltipPosition=\"bottom\"\n (onClick)=\"downloadImage.emit()\"\n >\n </p-button>\n <p-button\n *ngIf=\"isHeatmap\"\n icon=\"pi pi-window-maximize\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Fit to view\"\n pTooltip=\"Fit to view\"\n tooltipPosition=\"bottom\"\n (onClick)=\"autoscaleImage.emit()\"\n >\n </p-button>\n </ng-container>\n\n <toolbar-view-controls\n [isHeatmap]=\"isHeatmap\"\n [zoomTools]=\"tools.zoomTools\"\n [isImageView]=\"isImageView\"\n [activeDragMode]=\"activeDragMode\"\n [activeSurface3dMode]=\"activeSurface3dMode\"\n [isNapariMode]=\"isNapariMode\"\n [isNapariSurfaceMode]=\"isNapariSurfaceMode\"\n [isNapari3dMode]=\"isNapari3dMode\"\n [axesVisible]=\"axesVisible\"\n [wireframeActive]=\"wireframeActive\"\n [resolutionScale]=\"resolutionScale\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (zoomIn)=\"zoomIn.emit()\"\n (zoomOut)=\"zoomOut.emit()\"\n (toggleSurface3dMode)=\"toggleSurface3dMode.emit($event)\"\n (resetSurfaceCamera)=\"resetSurfaceCamera.emit()\"\n (toggleAxes)=\"toggleAxes.emit()\"\n (toggleWireframe)=\"toggleWireframe.emit()\"\n (selectResolution)=\"selectResolution.emit($event)\"\n >\n </toolbar-view-controls>\n\n <!-- A volume view draws no regions; the Region Editor shows them in it in 3D, and the\n selection tool picks their surfaces there. The slice editor splits the view: the slice,\n with every region tool, beside the 3D view. -->\n <ng-container *ngIf=\"isVolumeView && !isHeatmap && !is3dRegions && tools.regionTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n size=\"small\"\n [text]=\"!sliceEditor\"\n ariaLabel=\"Slice editor\"\n [attr.aria-pressed]=\"sliceEditor\"\n [pTooltip]=\"\n sliceEditor\n ? 'Close the slice editor'\n : 'Slice editor: the slice and the sections through x and y beside the 3D view'\n \"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSliceEditor.emit()\"\n >\n <img\n src=\"assets/plotting/slice.svg\"\n style=\"width: 1rem\"\n alt=\"\"\n [class]=\"sliceEditor ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n </p-button>\n <toolbar-region-tools\n *ngIf=\"!sliceEditor\"\n [activeDragMode]=\"activeDragMode\"\n [selectOnly]=\"true\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (openRegionEditor)=\"openRegionEditor.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (undoRegion)=\"undoRegion.emit()\"\n (redoRegion)=\"redoRegion.emit()\"\n (deleteRegion)=\"deleteRegion.emit()\"\n >\n </toolbar-region-tools>\n </ng-container>\n <ng-container *ngIf=\"(isHeatmap || is3dRegions || (isVolumeView && sliceEditor)) && tools.regionTools\">\n <toolbar-region-tools\n [activeDragMode]=\"activeDragMode\"\n [is3dRegions]=\"is3dRegions\"\n [vertexTools]=\"supportsRegionVertexTools || (isVolumeView && sliceEditor)\"\n [wandSensitivity]=\"wandSensitivity\"\n [brushSize]=\"brushSize\"\n [vertexEraserRadius]=\"vertexEraserRadius\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (openRegionEditor)=\"openRegionEditor.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (undoRegion)=\"undoRegion.emit()\"\n (redoRegion)=\"redoRegion.emit()\"\n (deleteRegion)=\"deleteRegion.emit()\"\n (wandSensitivityChange)=\"wandSensitivityChange.emit($event)\"\n (brushSizeChange)=\"brushSizeChange.emit($event)\"\n (vertexEraserRadiusChange)=\"vertexEraserRadiusChange.emit($event)\"\n >\n </toolbar-region-tools>\n <toolbar-segmentation-tools\n [activeDragMode]=\"activeDragMode\"\n [samModels]=\"samModels\"\n [samModelId]=\"samModelId\"\n [contributedTools]=\"contributedTools\"\n [toolModelIds]=\"toolModelIds\"\n (segmentRegions)=\"segmentRegions.emit()\"\n (segmentCellpose)=\"segmentCellpose.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (samModelChange)=\"samModelChange.emit($event)\"\n (runTool)=\"runTool.emit($event)\"\n (toolModelChange)=\"toolModelChange.emit($event)\"\n (openToolParams)=\"openToolParams.emit($event)\"\n >\n </toolbar-segmentation-tools>\n </ng-container>\n\n <ng-container *ngIf=\"tools.help\">\n <span class=\"toolbar-separator\"></span>\n\n <!-- Help -->\n <p-button\n icon=\"pi pi-question-circle\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Help\"\n pTooltip=\"Help\"\n tooltipPosition=\"bottom\"\n (onClick)=\"showHelp()\"\n >\n </p-button>\n </ng-container>\n </div>\n</p-toolbar>\n<toolbar-help-dialog\n [(visible)]=\"displayHelpDialog\"\n [contributedTools]=\"contributedTools\"\n [dialogTools]=\"dialogTools\"\n>\n</toolbar-help-dialog>\n", styles: [".toolbar-text{color:var(--primary-color);font-size:.875rem}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-icon-active{filter:brightness(0) invert(1)}.wand-sensitivity-value{min-width:2rem;text-align:right}:host{display:block}:host ::ng-deep .p-toolbar{padding:.25rem .5rem}::ng-deep .toolbar-input .p-inputtext{color:var(--primary-color);border-color:var(--primary-color)}::ng-deep .p-menu.model-menu{width:auto;min-width:12.5rem}::ng-deep .model-menu-item{display:flex;align-items:center;gap:.5rem}::ng-deep .model-menu-item .p-menuitem-text{flex:1 1 auto;white-space:nowrap}::ng-deep .model-menu-item .model-info-icon{flex:0 0 auto;font-size:.875rem;opacity:.55;cursor:help;appearance:none;background:none;border:0;padding:0;margin:0;color:inherit;line-height:1}::ng-deep .model-menu-item .model-info-icon:hover{opacity:1}::ng-deep .model-menu-item .model-info-icon:focus-visible{opacity:1;outline:2px solid var(--primary-color, #2196f3);outline-offset:2px;border-radius:2px}::ng-deep .model-info-tooltip .p-tooltip-text{min-width:18rem;max-width:24rem;line-height:1.4}::ng-deep .wand-sensitivity-slider{width:8rem;align-self:center;margin:0 .25rem}span.colormap{width:50px;height:auto;display:inline-block}img.colormap{width:50px;height:auto}.colormap{background:no-repeat;background-size:100%;vertical-align:middle}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: RippleModule }, { kind: "directive", type: i3$1.Ripple, selector: "[pRipple]" }, { kind: "ngmodule", type: ToolbarModule }, { kind: "component", type: i4$1.Toolbar, selector: "p-toolbar", inputs: ["style", "styleClass", "ariaLabelledBy"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: ToolbarHelpDialogComponent, selector: "toolbar-help-dialog", inputs: ["visible", "contributedTools", "dialogTools"], outputs: ["visibleChange"] }, { kind: "component", type: PlotTypeSelectorComponent, selector: "toolbar-plot-type-selector", inputs: ["options", "selected", "isosurface", "isoRange", "intensityLines"], outputs: ["selectPlotType", "isoRangeChange", "addProfileLine"] }, { kind: "component", type: StackControlsComponent, selector: "toolbar-stack-controls", inputs: ["liveScrubber", "stackToggle", "showStack", "stackOptions", "zIndex", "maxIndex"], outputs: ["zScrub", "zSlide", "selectStackOption", "zIndexInput", "reloadAndPlot"] }, { kind: "component", type: ViewControlsComponent, selector: "toolbar-view-controls", inputs: ["isHeatmap", "zoomTools", "isImageView", "activeDragMode", "activeSurface3dMode", "isNapariMode", "isNapariSurfaceMode", "isNapari3dMode", "axesVisible", "wireframeActive", "resolutionScale"], outputs: ["toggleDragMode", "zoomIn", "zoomOut", "toggleSurface3dMode", "resetSurfaceCamera", "toggleAxes", "toggleWireframe", "selectResolution"] }, { kind: "component", type: RegionToolsComponent, selector: "toolbar-region-tools", inputs: ["activeDragMode", "is3dRegions", "selectOnly", "vertexTools", "wandSensitivity", "brushSize", "vertexEraserRadius", "canUndo", "canRedo"], outputs: ["openRegionEditor", "toggleDragMode", "undoRegion", "redoRegion", "deleteRegion", "wandSensitivityChange", "brushSizeChange", "vertexEraserRadiusChange"] }, { kind: "component", type: SegmentationToolsComponent, selector: "toolbar-segmentation-tools", inputs: ["activeDragMode", "samModels", "samModelId", "contributedTools", "toolModelIds"], outputs: ["segmentRegions", "segmentCellpose", "toggleDragMode", "samModelChange", "runTool", "toolModelChange", "openToolParams"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14059
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ToolbarComponent, isStandalone: true, selector: "plotting-toolbar", inputs: { imageInfo: "imageInfo", hasSpatialDataset: "hasSpatialDataset", plotTypeOptions: "plotTypeOptions", selectedPlotType: "selectedPlotType", basePlotType: "basePlotType", isoRange: "isoRange", maxIndex: "maxIndex", zIndex: "zIndex", isHeatmap: "isHeatmap", is3dRegions: "is3dRegions", isVolumeView: "isVolumeView", sliceEditor: "sliceEditor", activeDragMode: "activeDragMode", activeSurface3dMode: "activeSurface3dMode", axesVisible: "axesVisible", wireframeActive: "wireframeActive", resolutionScale: "resolutionScale", wandSensitivity: "wandSensitivity", brushSize: "brushSize", vertexEraserRadius: "vertexEraserRadius", canUndo: "canUndo", canRedo: "canRedo", stackOptions: "stackOptions", tools: "tools", imageSmoothingEnabled: "imageSmoothingEnabled", samModels: "samModels", samModelId: "samModelId", contributedTools: "contributedTools", dialogTools: "dialogTools", openDialogToolId: "openDialogToolId", toolModelIds: "toolModelIds" }, outputs: { selectPlotType: "selectPlotType", addProfileLine: "addProfileLine", toggleImageSmoothing: "toggleImageSmoothing", isoRangeChange: "isoRangeChange", openChannelHistogram: "openChannelHistogram", openSpatialControls: "openSpatialControls", openRegionEditor: "openRegionEditor", toggleSliceEditor: "toggleSliceEditor", selectStackOption: "selectStackOption", zScrub: "zScrub", zSlide: "zSlide", zIndexInput: "zIndexInput", reloadAndPlot: "reloadAndPlot", downloadImage: "downloadImage", autoscaleImage: "autoscaleImage", toggleDragMode: "toggleDragMode", zoomIn: "zoomIn", zoomOut: "zoomOut", toggleSurface3dMode: "toggleSurface3dMode", resetSurfaceCamera: "resetSurfaceCamera", toggleAxes: "toggleAxes", toggleWireframe: "toggleWireframe", selectResolution: "selectResolution", deleteRegion: "deleteRegion", undoRegion: "undoRegion", redoRegion: "redoRegion", segmentRegions: "segmentRegions", segmentCellpose: "segmentCellpose", runTool: "runTool", toggleDialogTool: "toggleDialogTool", toolModelChange: "toolModelChange", openToolParams: "openToolParams", samModelChange: "samModelChange", wandSensitivityChange: "wandSensitivityChange", brushSizeChange: "brushSizeChange", vertexEraserRadiusChange: "vertexEraserRadiusChange" }, ngImport: i0, template: "<p-toolbar>\n <div class=\"p-toolbar-group-start flex-wrap align-items-center gap-1\">\n <!-- Host-provided custom toolbar buttons, shown first (e.g. app-specific\n actions the visualization view itself doesn't own). -->\n <ng-content></ng-content>\n\n <!-- Contributed dialog tools (TOOLBAR_TOOLS, kind: 'dialog'): app-level actions\n like the host's own buttons above, so they sit right beside them. Image view\n only, the view their dialog draws over. Highlighted while the dialog is open. -->\n <ng-container *ngIf=\"isImageView\">\n <p-button\n *ngFor=\"let tool of dialogTools; trackBy: trackToolById\"\n size=\"small\"\n [text]=\"openDialogToolId !== tool.id\"\n [pTooltip]=\"tool.tooltip\"\n tooltipPosition=\"bottom\"\n [ariaLabel]=\"tool.label\"\n [attr.aria-pressed]=\"openDialogToolId === tool.id\"\n (onClick)=\"toggleDialogTool.emit(tool.id)\"\n >\n <img\n *ngIf=\"tool.icon.src\"\n [src]=\"tool.icon.src\"\n style=\"width: 1rem\"\n [alt]=\"tool.label\"\n [class]=\"openDialogToolId === tool.id ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n <i *ngIf=\"tool.icon.pi\" class=\"pi {{ tool.icon.pi }}\"></i>\n </p-button>\n </ng-container>\n\n <!-- Plot type: available for any image (grayscale or RGB), and for a spatial dataset\n that brings none (its spatial modes are still a choice). -->\n <toolbar-plot-type-selector\n *ngIf=\"showPlotTypes\"\n [options]=\"plotTypeOptions\"\n [selected]=\"selectedPlotType\"\n [isosurface]=\"isIsosurfaceMode\"\n [isoRange]=\"isoRange\"\n [intensityLines]=\"isIntensityCapable\"\n (selectPlotType)=\"selectPlotType.emit($event)\"\n (isoRangeChange)=\"isoRangeChange.emit($event)\"\n (addProfileLine)=\"addProfileLine.emit()\"\n >\n </toolbar-plot-type-selector>\n <!-- Channels & Histogram: brightness/contrast, gamma, per-channel display,\n colormap/reverse/invert \u2014 opens the floating dialog. -->\n <p-button\n *ngIf=\"tools.specialTools\"\n pRipple\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Channels &amp; Histogram\"\n pTooltip=\"Channels &amp; Histogram (brightness/contrast, colormap)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openChannelHistogram.emit()\"\n >\n <img src=\"assets/plotting/invert.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n\n <!-- Spatial-omics controls. Only meaningful in the spatial mode, which is\n itself only offered while a dataset is loaded \u2014 so this button appears\n exactly when there is something for it to control. -->\n <p-button\n *ngIf=\"tools.specialTools && isSpatialMode\"\n pRipple\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Spatial omics controls\"\n pTooltip=\"Spatial omics (colour by column or gene, legend, selection, distributions)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openSpatialControls.emit()\"\n >\n <img src=\"assets/plotting/spatial-omics.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n\n <!-- Smoothen toggle: bilinear smoothing vs nearest-neighbour raw pixels (Image\n view only \u2014 it's an OpenSeadragon drawer setting). Highlighted when smoothing on. -->\n <p-button\n *ngIf=\"tools.specialTools && isImageView\"\n size=\"small\"\n [text]=\"!imageSmoothingEnabled\"\n ariaLabel=\"Smoothen\"\n pTooltip=\"Smoothen (turn on image smoothing)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleImageSmoothing.emit()\"\n >\n <img\n src=\"assets/plotting/smooth.svg\"\n style=\"width: 1rem\"\n [class]=\"imageSmoothingEnabled ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n </p-button>\n\n <!-- Stack controls -->\n <toolbar-stack-controls\n *ngIf=\"imageInfo?.isStack\"\n [liveScrubber]=\"showsLiveSliceScrubber\"\n [stackToggle]=\"!showsLiveSliceScrubber && !isNapariMode && !isSpatialMode\"\n [showStack]=\"!!imageInfo?.showStack\"\n [stackOptions]=\"stackOptions\"\n [zIndex]=\"zIndex\"\n [maxIndex]=\"maxIndex\"\n (zScrub)=\"zScrub.emit($event)\"\n (zSlide)=\"zSlide.emit($event)\"\n (selectStackOption)=\"selectStackOption.emit($event)\"\n (zIndexInput)=\"zIndexInput.emit($event)\"\n (reloadAndPlot)=\"reloadAndPlot.emit()\"\n >\n </toolbar-stack-controls>\n\n <!-- Image actions (special tools) -->\n <ng-container *ngIf=\"tools.specialTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n icon=\"pi pi-download\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Download image as PNG\"\n pTooltip=\"Download image as PNG\"\n tooltipPosition=\"bottom\"\n (onClick)=\"downloadImage.emit()\"\n >\n </p-button>\n <p-button\n *ngIf=\"isHeatmap\"\n icon=\"pi pi-window-maximize\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Fit to view\"\n pTooltip=\"Fit to view\"\n tooltipPosition=\"bottom\"\n (onClick)=\"autoscaleImage.emit()\"\n >\n </p-button>\n </ng-container>\n\n <toolbar-view-controls\n [isHeatmap]=\"isHeatmap\"\n [zoomTools]=\"tools.zoomTools\"\n [isImageView]=\"isImageView\"\n [activeDragMode]=\"activeDragMode\"\n [activeSurface3dMode]=\"activeSurface3dMode\"\n [axesToggle]=\"showsAxesToggle\"\n [isNapariSurfaceMode]=\"isNapariSurfaceMode\"\n [isNapari3dMode]=\"isNapari3dMode\"\n [axesVisible]=\"axesVisible\"\n [wireframeActive]=\"wireframeActive\"\n [resolutionScale]=\"resolutionScale\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (zoomIn)=\"zoomIn.emit()\"\n (zoomOut)=\"zoomOut.emit()\"\n (toggleSurface3dMode)=\"toggleSurface3dMode.emit($event)\"\n (resetSurfaceCamera)=\"resetSurfaceCamera.emit()\"\n (toggleAxes)=\"toggleAxes.emit()\"\n (toggleWireframe)=\"toggleWireframe.emit()\"\n (selectResolution)=\"selectResolution.emit($event)\"\n >\n </toolbar-view-controls>\n\n <!-- A volume view draws no regions; the Region Editor shows them in it in 3D, and the\n selection tool picks their surfaces there. The slice editor splits the view: the slice,\n with every region tool, beside the 3D view. -->\n <ng-container *ngIf=\"isVolumeView && !isHeatmap && !is3dRegions && tools.regionTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n size=\"small\"\n [text]=\"!sliceEditor\"\n ariaLabel=\"Slice editor\"\n [attr.aria-pressed]=\"sliceEditor\"\n [pTooltip]=\"\n sliceEditor\n ? 'Close the slice editor'\n : 'Slice editor: the slice and the sections through x and y beside the 3D view'\n \"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSliceEditor.emit()\"\n >\n <img\n src=\"assets/plotting/slice.svg\"\n style=\"width: 1rem\"\n alt=\"\"\n [class]=\"sliceEditor ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n </p-button>\n <toolbar-region-tools\n *ngIf=\"!sliceEditor\"\n [activeDragMode]=\"activeDragMode\"\n [selectOnly]=\"true\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (openRegionEditor)=\"openRegionEditor.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (undoRegion)=\"undoRegion.emit()\"\n (redoRegion)=\"redoRegion.emit()\"\n (deleteRegion)=\"deleteRegion.emit()\"\n >\n </toolbar-region-tools>\n </ng-container>\n <ng-container *ngIf=\"(isHeatmap || is3dRegions || (isVolumeView && sliceEditor)) && tools.regionTools\">\n <toolbar-region-tools\n [activeDragMode]=\"activeDragMode\"\n [is3dRegions]=\"is3dRegions\"\n [vertexTools]=\"supportsRegionVertexTools || (isVolumeView && sliceEditor)\"\n [wandSensitivity]=\"wandSensitivity\"\n [brushSize]=\"brushSize\"\n [vertexEraserRadius]=\"vertexEraserRadius\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (openRegionEditor)=\"openRegionEditor.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (undoRegion)=\"undoRegion.emit()\"\n (redoRegion)=\"redoRegion.emit()\"\n (deleteRegion)=\"deleteRegion.emit()\"\n (wandSensitivityChange)=\"wandSensitivityChange.emit($event)\"\n (brushSizeChange)=\"brushSizeChange.emit($event)\"\n (vertexEraserRadiusChange)=\"vertexEraserRadiusChange.emit($event)\"\n >\n </toolbar-region-tools>\n <toolbar-segmentation-tools\n [activeDragMode]=\"activeDragMode\"\n [samModels]=\"samModels\"\n [samModelId]=\"samModelId\"\n [contributedTools]=\"contributedTools\"\n [toolModelIds]=\"toolModelIds\"\n (segmentRegions)=\"segmentRegions.emit()\"\n (segmentCellpose)=\"segmentCellpose.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (samModelChange)=\"samModelChange.emit($event)\"\n (runTool)=\"runTool.emit($event)\"\n (toolModelChange)=\"toolModelChange.emit($event)\"\n (openToolParams)=\"openToolParams.emit($event)\"\n >\n </toolbar-segmentation-tools>\n </ng-container>\n\n <ng-container *ngIf=\"tools.help\">\n <span class=\"toolbar-separator\"></span>\n\n <!-- Help -->\n <p-button\n icon=\"pi pi-question-circle\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Help\"\n pTooltip=\"Help\"\n tooltipPosition=\"bottom\"\n (onClick)=\"showHelp()\"\n >\n </p-button>\n </ng-container>\n </div>\n</p-toolbar>\n<toolbar-help-dialog\n [(visible)]=\"displayHelpDialog\"\n [contributedTools]=\"contributedTools\"\n [dialogTools]=\"dialogTools\"\n>\n</toolbar-help-dialog>\n", styles: [".toolbar-text{color:var(--primary-color);font-size:.875rem}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-icon-active{filter:brightness(0) invert(1)}.wand-sensitivity-value{min-width:2rem;text-align:right}:host{display:block}:host ::ng-deep .p-toolbar{padding:.25rem .5rem}::ng-deep .toolbar-input .p-inputtext{color:var(--primary-color);border-color:var(--primary-color)}::ng-deep .p-menu.model-menu{width:auto;min-width:12.5rem}::ng-deep .model-menu-item{display:flex;align-items:center;gap:.5rem}::ng-deep .model-menu-item .p-menuitem-text{flex:1 1 auto;white-space:nowrap}::ng-deep .model-menu-item .model-info-icon{flex:0 0 auto;font-size:.875rem;opacity:.55;cursor:help;appearance:none;background:none;border:0;padding:0;margin:0;color:inherit;line-height:1}::ng-deep .model-menu-item .model-info-icon:hover{opacity:1}::ng-deep .model-menu-item .model-info-icon:focus-visible{opacity:1;outline:2px solid var(--primary-color, #2196f3);outline-offset:2px;border-radius:2px}::ng-deep .model-info-tooltip .p-tooltip-text{min-width:18rem;max-width:24rem;line-height:1.4}::ng-deep .wand-sensitivity-slider{width:8rem;align-self:center;margin:0 .25rem}span.colormap{width:50px;height:auto;display:inline-block}img.colormap{width:50px;height:auto}.colormap{background:no-repeat;background-size:100%;vertical-align:middle}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: RippleModule }, { kind: "directive", type: i3$1.Ripple, selector: "[pRipple]" }, { kind: "ngmodule", type: ToolbarModule }, { kind: "component", type: i4$1.Toolbar, selector: "p-toolbar", inputs: ["style", "styleClass", "ariaLabelledBy"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: ToolbarHelpDialogComponent, selector: "toolbar-help-dialog", inputs: ["visible", "contributedTools", "dialogTools"], outputs: ["visibleChange"] }, { kind: "component", type: PlotTypeSelectorComponent, selector: "toolbar-plot-type-selector", inputs: ["options", "selected", "isosurface", "isoRange", "intensityLines"], outputs: ["selectPlotType", "isoRangeChange", "addProfileLine"] }, { kind: "component", type: StackControlsComponent, selector: "toolbar-stack-controls", inputs: ["liveScrubber", "stackToggle", "showStack", "stackOptions", "zIndex", "maxIndex"], outputs: ["zScrub", "zSlide", "selectStackOption", "zIndexInput", "reloadAndPlot"] }, { kind: "component", type: ViewControlsComponent, selector: "toolbar-view-controls", inputs: ["isHeatmap", "zoomTools", "isImageView", "activeDragMode", "activeSurface3dMode", "isNapariSurfaceMode", "isNapari3dMode", "axesToggle", "axesVisible", "wireframeActive", "resolutionScale"], outputs: ["toggleDragMode", "zoomIn", "zoomOut", "toggleSurface3dMode", "resetSurfaceCamera", "toggleAxes", "toggleWireframe", "selectResolution"] }, { kind: "component", type: RegionToolsComponent, selector: "toolbar-region-tools", inputs: ["activeDragMode", "is3dRegions", "selectOnly", "vertexTools", "wandSensitivity", "brushSize", "vertexEraserRadius", "canUndo", "canRedo"], outputs: ["openRegionEditor", "toggleDragMode", "undoRegion", "redoRegion", "deleteRegion", "wandSensitivityChange", "brushSizeChange", "vertexEraserRadiusChange"] }, { kind: "component", type: SegmentationToolsComponent, selector: "toolbar-segmentation-tools", inputs: ["activeDragMode", "samModels", "samModelId", "contributedTools", "toolModelIds"], outputs: ["segmentRegions", "segmentCellpose", "toggleDragMode", "samModelChange", "runTool", "toolModelChange", "openToolParams"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
14033
14060
  }
14034
14061
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ToolbarComponent, decorators: [{
14035
14062
  type: Component,
@@ -14045,7 +14072,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
14045
14072
  ViewControlsComponent,
14046
14073
  RegionToolsComponent,
14047
14074
  SegmentationToolsComponent,
14048
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p-toolbar>\n <div class=\"p-toolbar-group-start flex-wrap align-items-center gap-1\">\n <!-- Host-provided custom toolbar buttons, shown first (e.g. app-specific\n actions the visualization view itself doesn't own). -->\n <ng-content></ng-content>\n\n <!-- Contributed dialog tools (TOOLBAR_TOOLS, kind: 'dialog'): app-level actions\n like the host's own buttons above, so they sit right beside them. Image view\n only, the view their dialog draws over. Highlighted while the dialog is open. -->\n <ng-container *ngIf=\"isImageView\">\n <p-button\n *ngFor=\"let tool of dialogTools; trackBy: trackToolById\"\n size=\"small\"\n [text]=\"openDialogToolId !== tool.id\"\n [pTooltip]=\"tool.tooltip\"\n tooltipPosition=\"bottom\"\n [ariaLabel]=\"tool.label\"\n [attr.aria-pressed]=\"openDialogToolId === tool.id\"\n (onClick)=\"toggleDialogTool.emit(tool.id)\"\n >\n <img\n *ngIf=\"tool.icon.src\"\n [src]=\"tool.icon.src\"\n style=\"width: 1rem\"\n [alt]=\"tool.label\"\n [class]=\"openDialogToolId === tool.id ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n <i *ngIf=\"tool.icon.pi\" class=\"pi {{ tool.icon.pi }}\"></i>\n </p-button>\n </ng-container>\n\n <!-- Plot type: available for any image (grayscale or RGB), and for a spatial dataset\n that brings none (its spatial modes are still a choice). -->\n <toolbar-plot-type-selector\n *ngIf=\"showPlotTypes\"\n [options]=\"plotTypeOptions\"\n [selected]=\"selectedPlotType\"\n [isosurface]=\"isIsosurfaceMode\"\n [isoRange]=\"isoRange\"\n [intensityLines]=\"isIntensityCapable\"\n (selectPlotType)=\"selectPlotType.emit($event)\"\n (isoRangeChange)=\"isoRangeChange.emit($event)\"\n (addProfileLine)=\"addProfileLine.emit()\"\n >\n </toolbar-plot-type-selector>\n <!-- Channels & Histogram: brightness/contrast, gamma, per-channel display,\n colormap/reverse/invert \u2014 opens the floating dialog. -->\n <p-button\n *ngIf=\"tools.specialTools\"\n pRipple\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Channels &amp; Histogram\"\n pTooltip=\"Channels &amp; Histogram (brightness/contrast, colormap)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openChannelHistogram.emit()\"\n >\n <img src=\"assets/plotting/invert.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n\n <!-- Spatial-omics controls. Only meaningful in the spatial mode, which is\n itself only offered while a dataset is loaded \u2014 so this button appears\n exactly when there is something for it to control. -->\n <p-button\n *ngIf=\"tools.specialTools && isSpatialMode\"\n pRipple\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Spatial omics controls\"\n pTooltip=\"Spatial omics (colour by column or gene, legend, selection, distributions)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openSpatialControls.emit()\"\n >\n <img src=\"assets/plotting/spatial-omics.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n\n <!-- Smoothen toggle: bilinear smoothing vs nearest-neighbour raw pixels (Image\n view only \u2014 it's an OpenSeadragon drawer setting). Highlighted when smoothing on. -->\n <p-button\n *ngIf=\"tools.specialTools && isImageView\"\n size=\"small\"\n [text]=\"!imageSmoothingEnabled\"\n ariaLabel=\"Smoothen\"\n pTooltip=\"Smoothen (turn on image smoothing)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleImageSmoothing.emit()\"\n >\n <img\n src=\"assets/plotting/smooth.svg\"\n style=\"width: 1rem\"\n [class]=\"imageSmoothingEnabled ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n </p-button>\n\n <!-- Stack controls -->\n <toolbar-stack-controls\n *ngIf=\"imageInfo?.isStack\"\n [liveScrubber]=\"showsLiveSliceScrubber\"\n [stackToggle]=\"!showsLiveSliceScrubber && !isNapariMode && !isSpatialMode\"\n [showStack]=\"!!imageInfo?.showStack\"\n [stackOptions]=\"stackOptions\"\n [zIndex]=\"zIndex\"\n [maxIndex]=\"maxIndex\"\n (zScrub)=\"zScrub.emit($event)\"\n (zSlide)=\"zSlide.emit($event)\"\n (selectStackOption)=\"selectStackOption.emit($event)\"\n (zIndexInput)=\"zIndexInput.emit($event)\"\n (reloadAndPlot)=\"reloadAndPlot.emit()\"\n >\n </toolbar-stack-controls>\n\n <!-- Image actions (special tools) -->\n <ng-container *ngIf=\"tools.specialTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n icon=\"pi pi-download\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Download image as PNG\"\n pTooltip=\"Download image as PNG\"\n tooltipPosition=\"bottom\"\n (onClick)=\"downloadImage.emit()\"\n >\n </p-button>\n <p-button\n *ngIf=\"isHeatmap\"\n icon=\"pi pi-window-maximize\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Fit to view\"\n pTooltip=\"Fit to view\"\n tooltipPosition=\"bottom\"\n (onClick)=\"autoscaleImage.emit()\"\n >\n </p-button>\n </ng-container>\n\n <toolbar-view-controls\n [isHeatmap]=\"isHeatmap\"\n [zoomTools]=\"tools.zoomTools\"\n [isImageView]=\"isImageView\"\n [activeDragMode]=\"activeDragMode\"\n [activeSurface3dMode]=\"activeSurface3dMode\"\n [isNapariMode]=\"isNapariMode\"\n [isNapariSurfaceMode]=\"isNapariSurfaceMode\"\n [isNapari3dMode]=\"isNapari3dMode\"\n [axesVisible]=\"axesVisible\"\n [wireframeActive]=\"wireframeActive\"\n [resolutionScale]=\"resolutionScale\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (zoomIn)=\"zoomIn.emit()\"\n (zoomOut)=\"zoomOut.emit()\"\n (toggleSurface3dMode)=\"toggleSurface3dMode.emit($event)\"\n (resetSurfaceCamera)=\"resetSurfaceCamera.emit()\"\n (toggleAxes)=\"toggleAxes.emit()\"\n (toggleWireframe)=\"toggleWireframe.emit()\"\n (selectResolution)=\"selectResolution.emit($event)\"\n >\n </toolbar-view-controls>\n\n <!-- A volume view draws no regions; the Region Editor shows them in it in 3D, and the\n selection tool picks their surfaces there. The slice editor splits the view: the slice,\n with every region tool, beside the 3D view. -->\n <ng-container *ngIf=\"isVolumeView && !isHeatmap && !is3dRegions && tools.regionTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n size=\"small\"\n [text]=\"!sliceEditor\"\n ariaLabel=\"Slice editor\"\n [attr.aria-pressed]=\"sliceEditor\"\n [pTooltip]=\"\n sliceEditor\n ? 'Close the slice editor'\n : 'Slice editor: the slice and the sections through x and y beside the 3D view'\n \"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSliceEditor.emit()\"\n >\n <img\n src=\"assets/plotting/slice.svg\"\n style=\"width: 1rem\"\n alt=\"\"\n [class]=\"sliceEditor ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n </p-button>\n <toolbar-region-tools\n *ngIf=\"!sliceEditor\"\n [activeDragMode]=\"activeDragMode\"\n [selectOnly]=\"true\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (openRegionEditor)=\"openRegionEditor.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (undoRegion)=\"undoRegion.emit()\"\n (redoRegion)=\"redoRegion.emit()\"\n (deleteRegion)=\"deleteRegion.emit()\"\n >\n </toolbar-region-tools>\n </ng-container>\n <ng-container *ngIf=\"(isHeatmap || is3dRegions || (isVolumeView && sliceEditor)) && tools.regionTools\">\n <toolbar-region-tools\n [activeDragMode]=\"activeDragMode\"\n [is3dRegions]=\"is3dRegions\"\n [vertexTools]=\"supportsRegionVertexTools || (isVolumeView && sliceEditor)\"\n [wandSensitivity]=\"wandSensitivity\"\n [brushSize]=\"brushSize\"\n [vertexEraserRadius]=\"vertexEraserRadius\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (openRegionEditor)=\"openRegionEditor.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (undoRegion)=\"undoRegion.emit()\"\n (redoRegion)=\"redoRegion.emit()\"\n (deleteRegion)=\"deleteRegion.emit()\"\n (wandSensitivityChange)=\"wandSensitivityChange.emit($event)\"\n (brushSizeChange)=\"brushSizeChange.emit($event)\"\n (vertexEraserRadiusChange)=\"vertexEraserRadiusChange.emit($event)\"\n >\n </toolbar-region-tools>\n <toolbar-segmentation-tools\n [activeDragMode]=\"activeDragMode\"\n [samModels]=\"samModels\"\n [samModelId]=\"samModelId\"\n [contributedTools]=\"contributedTools\"\n [toolModelIds]=\"toolModelIds\"\n (segmentRegions)=\"segmentRegions.emit()\"\n (segmentCellpose)=\"segmentCellpose.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (samModelChange)=\"samModelChange.emit($event)\"\n (runTool)=\"runTool.emit($event)\"\n (toolModelChange)=\"toolModelChange.emit($event)\"\n (openToolParams)=\"openToolParams.emit($event)\"\n >\n </toolbar-segmentation-tools>\n </ng-container>\n\n <ng-container *ngIf=\"tools.help\">\n <span class=\"toolbar-separator\"></span>\n\n <!-- Help -->\n <p-button\n icon=\"pi pi-question-circle\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Help\"\n pTooltip=\"Help\"\n tooltipPosition=\"bottom\"\n (onClick)=\"showHelp()\"\n >\n </p-button>\n </ng-container>\n </div>\n</p-toolbar>\n<toolbar-help-dialog\n [(visible)]=\"displayHelpDialog\"\n [contributedTools]=\"contributedTools\"\n [dialogTools]=\"dialogTools\"\n>\n</toolbar-help-dialog>\n", styles: [".toolbar-text{color:var(--primary-color);font-size:.875rem}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-icon-active{filter:brightness(0) invert(1)}.wand-sensitivity-value{min-width:2rem;text-align:right}:host{display:block}:host ::ng-deep .p-toolbar{padding:.25rem .5rem}::ng-deep .toolbar-input .p-inputtext{color:var(--primary-color);border-color:var(--primary-color)}::ng-deep .p-menu.model-menu{width:auto;min-width:12.5rem}::ng-deep .model-menu-item{display:flex;align-items:center;gap:.5rem}::ng-deep .model-menu-item .p-menuitem-text{flex:1 1 auto;white-space:nowrap}::ng-deep .model-menu-item .model-info-icon{flex:0 0 auto;font-size:.875rem;opacity:.55;cursor:help;appearance:none;background:none;border:0;padding:0;margin:0;color:inherit;line-height:1}::ng-deep .model-menu-item .model-info-icon:hover{opacity:1}::ng-deep .model-menu-item .model-info-icon:focus-visible{opacity:1;outline:2px solid var(--primary-color, #2196f3);outline-offset:2px;border-radius:2px}::ng-deep .model-info-tooltip .p-tooltip-text{min-width:18rem;max-width:24rem;line-height:1.4}::ng-deep .wand-sensitivity-slider{width:8rem;align-self:center;margin:0 .25rem}span.colormap{width:50px;height:auto;display:inline-block}img.colormap{width:50px;height:auto}.colormap{background:no-repeat;background-size:100%;vertical-align:middle}\n"] }]
14075
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p-toolbar>\n <div class=\"p-toolbar-group-start flex-wrap align-items-center gap-1\">\n <!-- Host-provided custom toolbar buttons, shown first (e.g. app-specific\n actions the visualization view itself doesn't own). -->\n <ng-content></ng-content>\n\n <!-- Contributed dialog tools (TOOLBAR_TOOLS, kind: 'dialog'): app-level actions\n like the host's own buttons above, so they sit right beside them. Image view\n only, the view their dialog draws over. Highlighted while the dialog is open. -->\n <ng-container *ngIf=\"isImageView\">\n <p-button\n *ngFor=\"let tool of dialogTools; trackBy: trackToolById\"\n size=\"small\"\n [text]=\"openDialogToolId !== tool.id\"\n [pTooltip]=\"tool.tooltip\"\n tooltipPosition=\"bottom\"\n [ariaLabel]=\"tool.label\"\n [attr.aria-pressed]=\"openDialogToolId === tool.id\"\n (onClick)=\"toggleDialogTool.emit(tool.id)\"\n >\n <img\n *ngIf=\"tool.icon.src\"\n [src]=\"tool.icon.src\"\n style=\"width: 1rem\"\n [alt]=\"tool.label\"\n [class]=\"openDialogToolId === tool.id ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n <i *ngIf=\"tool.icon.pi\" class=\"pi {{ tool.icon.pi }}\"></i>\n </p-button>\n </ng-container>\n\n <!-- Plot type: available for any image (grayscale or RGB), and for a spatial dataset\n that brings none (its spatial modes are still a choice). -->\n <toolbar-plot-type-selector\n *ngIf=\"showPlotTypes\"\n [options]=\"plotTypeOptions\"\n [selected]=\"selectedPlotType\"\n [isosurface]=\"isIsosurfaceMode\"\n [isoRange]=\"isoRange\"\n [intensityLines]=\"isIntensityCapable\"\n (selectPlotType)=\"selectPlotType.emit($event)\"\n (isoRangeChange)=\"isoRangeChange.emit($event)\"\n (addProfileLine)=\"addProfileLine.emit()\"\n >\n </toolbar-plot-type-selector>\n <!-- Channels & Histogram: brightness/contrast, gamma, per-channel display,\n colormap/reverse/invert \u2014 opens the floating dialog. -->\n <p-button\n *ngIf=\"tools.specialTools\"\n pRipple\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Channels &amp; Histogram\"\n pTooltip=\"Channels &amp; Histogram (brightness/contrast, colormap)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openChannelHistogram.emit()\"\n >\n <img src=\"assets/plotting/invert.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n\n <!-- Spatial-omics controls. Only meaningful in the spatial mode, which is\n itself only offered while a dataset is loaded \u2014 so this button appears\n exactly when there is something for it to control. -->\n <p-button\n *ngIf=\"tools.specialTools && isSpatialMode\"\n pRipple\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Spatial omics controls\"\n pTooltip=\"Spatial omics (colour by column or gene, legend, selection, distributions)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openSpatialControls.emit()\"\n >\n <img src=\"assets/plotting/spatial-omics.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n\n <!-- Smoothen toggle: bilinear smoothing vs nearest-neighbour raw pixels (Image\n view only \u2014 it's an OpenSeadragon drawer setting). Highlighted when smoothing on. -->\n <p-button\n *ngIf=\"tools.specialTools && isImageView\"\n size=\"small\"\n [text]=\"!imageSmoothingEnabled\"\n ariaLabel=\"Smoothen\"\n pTooltip=\"Smoothen (turn on image smoothing)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleImageSmoothing.emit()\"\n >\n <img\n src=\"assets/plotting/smooth.svg\"\n style=\"width: 1rem\"\n [class]=\"imageSmoothingEnabled ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n </p-button>\n\n <!-- Stack controls -->\n <toolbar-stack-controls\n *ngIf=\"imageInfo?.isStack\"\n [liveScrubber]=\"showsLiveSliceScrubber\"\n [stackToggle]=\"!showsLiveSliceScrubber && !isNapariMode && !isSpatialMode\"\n [showStack]=\"!!imageInfo?.showStack\"\n [stackOptions]=\"stackOptions\"\n [zIndex]=\"zIndex\"\n [maxIndex]=\"maxIndex\"\n (zScrub)=\"zScrub.emit($event)\"\n (zSlide)=\"zSlide.emit($event)\"\n (selectStackOption)=\"selectStackOption.emit($event)\"\n (zIndexInput)=\"zIndexInput.emit($event)\"\n (reloadAndPlot)=\"reloadAndPlot.emit()\"\n >\n </toolbar-stack-controls>\n\n <!-- Image actions (special tools) -->\n <ng-container *ngIf=\"tools.specialTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n icon=\"pi pi-download\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Download image as PNG\"\n pTooltip=\"Download image as PNG\"\n tooltipPosition=\"bottom\"\n (onClick)=\"downloadImage.emit()\"\n >\n </p-button>\n <p-button\n *ngIf=\"isHeatmap\"\n icon=\"pi pi-window-maximize\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Fit to view\"\n pTooltip=\"Fit to view\"\n tooltipPosition=\"bottom\"\n (onClick)=\"autoscaleImage.emit()\"\n >\n </p-button>\n </ng-container>\n\n <toolbar-view-controls\n [isHeatmap]=\"isHeatmap\"\n [zoomTools]=\"tools.zoomTools\"\n [isImageView]=\"isImageView\"\n [activeDragMode]=\"activeDragMode\"\n [activeSurface3dMode]=\"activeSurface3dMode\"\n [axesToggle]=\"showsAxesToggle\"\n [isNapariSurfaceMode]=\"isNapariSurfaceMode\"\n [isNapari3dMode]=\"isNapari3dMode\"\n [axesVisible]=\"axesVisible\"\n [wireframeActive]=\"wireframeActive\"\n [resolutionScale]=\"resolutionScale\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (zoomIn)=\"zoomIn.emit()\"\n (zoomOut)=\"zoomOut.emit()\"\n (toggleSurface3dMode)=\"toggleSurface3dMode.emit($event)\"\n (resetSurfaceCamera)=\"resetSurfaceCamera.emit()\"\n (toggleAxes)=\"toggleAxes.emit()\"\n (toggleWireframe)=\"toggleWireframe.emit()\"\n (selectResolution)=\"selectResolution.emit($event)\"\n >\n </toolbar-view-controls>\n\n <!-- A volume view draws no regions; the Region Editor shows them in it in 3D, and the\n selection tool picks their surfaces there. The slice editor splits the view: the slice,\n with every region tool, beside the 3D view. -->\n <ng-container *ngIf=\"isVolumeView && !isHeatmap && !is3dRegions && tools.regionTools\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n size=\"small\"\n [text]=\"!sliceEditor\"\n ariaLabel=\"Slice editor\"\n [attr.aria-pressed]=\"sliceEditor\"\n [pTooltip]=\"\n sliceEditor\n ? 'Close the slice editor'\n : 'Slice editor: the slice and the sections through x and y beside the 3D view'\n \"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleSliceEditor.emit()\"\n >\n <img\n src=\"assets/plotting/slice.svg\"\n style=\"width: 1rem\"\n alt=\"\"\n [class]=\"sliceEditor ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n />\n </p-button>\n <toolbar-region-tools\n *ngIf=\"!sliceEditor\"\n [activeDragMode]=\"activeDragMode\"\n [selectOnly]=\"true\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (openRegionEditor)=\"openRegionEditor.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (undoRegion)=\"undoRegion.emit()\"\n (redoRegion)=\"redoRegion.emit()\"\n (deleteRegion)=\"deleteRegion.emit()\"\n >\n </toolbar-region-tools>\n </ng-container>\n <ng-container *ngIf=\"(isHeatmap || is3dRegions || (isVolumeView && sliceEditor)) && tools.regionTools\">\n <toolbar-region-tools\n [activeDragMode]=\"activeDragMode\"\n [is3dRegions]=\"is3dRegions\"\n [vertexTools]=\"supportsRegionVertexTools || (isVolumeView && sliceEditor)\"\n [wandSensitivity]=\"wandSensitivity\"\n [brushSize]=\"brushSize\"\n [vertexEraserRadius]=\"vertexEraserRadius\"\n [canUndo]=\"canUndo\"\n [canRedo]=\"canRedo\"\n (openRegionEditor)=\"openRegionEditor.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (undoRegion)=\"undoRegion.emit()\"\n (redoRegion)=\"redoRegion.emit()\"\n (deleteRegion)=\"deleteRegion.emit()\"\n (wandSensitivityChange)=\"wandSensitivityChange.emit($event)\"\n (brushSizeChange)=\"brushSizeChange.emit($event)\"\n (vertexEraserRadiusChange)=\"vertexEraserRadiusChange.emit($event)\"\n >\n </toolbar-region-tools>\n <toolbar-segmentation-tools\n [activeDragMode]=\"activeDragMode\"\n [samModels]=\"samModels\"\n [samModelId]=\"samModelId\"\n [contributedTools]=\"contributedTools\"\n [toolModelIds]=\"toolModelIds\"\n (segmentRegions)=\"segmentRegions.emit()\"\n (segmentCellpose)=\"segmentCellpose.emit()\"\n (toggleDragMode)=\"toggleDragMode.emit($event)\"\n (samModelChange)=\"samModelChange.emit($event)\"\n (runTool)=\"runTool.emit($event)\"\n (toolModelChange)=\"toolModelChange.emit($event)\"\n (openToolParams)=\"openToolParams.emit($event)\"\n >\n </toolbar-segmentation-tools>\n </ng-container>\n\n <ng-container *ngIf=\"tools.help\">\n <span class=\"toolbar-separator\"></span>\n\n <!-- Help -->\n <p-button\n icon=\"pi pi-question-circle\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Help\"\n pTooltip=\"Help\"\n tooltipPosition=\"bottom\"\n (onClick)=\"showHelp()\"\n >\n </p-button>\n </ng-container>\n </div>\n</p-toolbar>\n<toolbar-help-dialog\n [(visible)]=\"displayHelpDialog\"\n [contributedTools]=\"contributedTools\"\n [dialogTools]=\"dialogTools\"\n>\n</toolbar-help-dialog>\n", styles: [".toolbar-text{color:var(--primary-color);font-size:.875rem}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-icon-active{filter:brightness(0) invert(1)}.wand-sensitivity-value{min-width:2rem;text-align:right}:host{display:block}:host ::ng-deep .p-toolbar{padding:.25rem .5rem}::ng-deep .toolbar-input .p-inputtext{color:var(--primary-color);border-color:var(--primary-color)}::ng-deep .p-menu.model-menu{width:auto;min-width:12.5rem}::ng-deep .model-menu-item{display:flex;align-items:center;gap:.5rem}::ng-deep .model-menu-item .p-menuitem-text{flex:1 1 auto;white-space:nowrap}::ng-deep .model-menu-item .model-info-icon{flex:0 0 auto;font-size:.875rem;opacity:.55;cursor:help;appearance:none;background:none;border:0;padding:0;margin:0;color:inherit;line-height:1}::ng-deep .model-menu-item .model-info-icon:hover{opacity:1}::ng-deep .model-menu-item .model-info-icon:focus-visible{opacity:1;outline:2px solid var(--primary-color, #2196f3);outline-offset:2px;border-radius:2px}::ng-deep .model-info-tooltip .p-tooltip-text{min-width:18rem;max-width:24rem;line-height:1.4}::ng-deep .wand-sensitivity-slider{width:8rem;align-self:center;margin:0 .25rem}span.colormap{width:50px;height:auto;display:inline-block}img.colormap{width:50px;height:auto}.colormap{background:no-repeat;background-size:100%;vertical-align:middle}\n"] }]
14049
14076
  }], propDecorators: { imageInfo: [{
14050
14077
  type: Input
14051
14078
  }], hasSpatialDataset: [{
@@ -20970,6 +20997,8 @@ class SpatialChartsComponent {
20970
20997
  .pipe(takeUntilDestroyed(this.destroyRef))
20971
20998
  .subscribe(([view, selection]) => {
20972
20999
  const sourceChanged = view.colorBy?.kind !== this.view.colorBy?.kind || view.colorBy?.name !== this.view.colorBy?.name;
21000
+ // A group recoloured in the Cells panel: the categories are fetched again in their new colours.
21001
+ const groupColorsChanged = this.primed && JSON.stringify(view.groupColors) !== JSON.stringify(this.view.groupColors);
20973
21002
  this.view = view;
20974
21003
  this.colorBy = view.colorBy;
20975
21004
  this.selection = selection;
@@ -20978,9 +21007,11 @@ class SpatialChartsComponent {
20978
21007
  // needs its vector fetched first. The FIRST emission always reloads:
20979
21008
  // otherwise `null -> null` reads as "unchanged" and the component sits
20980
21009
  // with no data and no explanation until something else moves.
20981
- if (!this.primed || sourceChanged) {
21010
+ if (!this.primed || sourceChanged || groupColorsChanged) {
20982
21011
  this.primed = true;
20983
21012
  void this.reload();
21013
+ if (groupColorsChanged && this.groupBy)
21014
+ void this.onGroupBy(this.groupBy);
20984
21015
  }
20985
21016
  else {
20986
21017
  // A selection or log-scale change needs only a redraw of the same vector.
@@ -22715,13 +22746,72 @@ function colormapNodeFor(options, value) {
22715
22746
  return null;
22716
22747
  }
22717
22748
 
22749
+ /**
22750
+ * One gene's icon and colour: the icon grid, a colour picker with its hex value, presets, and
22751
+ * a reset — the content of a popover its host opens on the gene's swatch. Writes nothing itself:
22752
+ * it reports the choice, and the host stores it where it belongs.
22753
+ */
22754
+ class SpatialGeneStyleComponent {
22755
+ constructor() {
22756
+ /** The gene, for the controls' labels. */
22757
+ this.gene = '';
22758
+ /** What the icons and colour are of ("Observation", "Transcript"), for the titles. */
22759
+ this.what = '';
22760
+ /** The gene's colour, `#rrggbb`. */
22761
+ this.color = '#ffffff';
22762
+ /** The gene's icon. */
22763
+ this.glyph = 'circle';
22764
+ /** A note under the icons, e.g. where they show; none when null. */
22765
+ this.iconHint = null;
22766
+ /** False for a colour alone (a cell group, or every gene drawn with one icon): no icon grid. */
22767
+ this.showIcons = true;
22768
+ this.colorChange = new EventEmitter();
22769
+ this.glyphChange = new EventEmitter();
22770
+ /** Back to the gene's default icon and colour. */
22771
+ this.resetStyle = new EventEmitter();
22772
+ this.glyphOptions = GLYPH_OPTIONS;
22773
+ this.colorPresets = DEFAULT_CATEGORICAL_PALETTE.slice(0, 10).map((c) => c.toLowerCase());
22774
+ }
22775
+ /** A typed hex colour, accepted as `#rrggbb` or `rrggbb`; anything else is ignored. */
22776
+ onHex(text) {
22777
+ const t = text.trim();
22778
+ const rgb = /^#?[0-9a-f]{6}$/i.test(t) ? parseCssColor(t) : null;
22779
+ if (rgb)
22780
+ this.colorChange.emit(rgbToHex(rgb));
22781
+ }
22782
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneStyleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
22783
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGeneStyleComponent, isStandalone: true, selector: "spatial-gene-style", inputs: { gene: "gene", what: "what", color: "color", glyph: "glyph", iconHint: "iconHint", showIcons: "showIcons" }, outputs: { colorChange: "colorChange", glyphChange: "glyphChange", resetStyle: "resetStyle" }, ngImport: i0, template: "<div class=\"sc-style-pop\">\n <ng-container *ngIf=\"showIcons\">\n <div class=\"sc-style-title\">{{ what }} Icon</div>\n <div class=\"sc-style-icons\" role=\"radiogroup\" [attr.aria-label]=\"'Icon for ' + gene\">\n <button\n type=\"button\"\n *ngFor=\"let opt of glyphOptions\"\n class=\"sc-style-icon\"\n role=\"radio\"\n [attr.aria-checked]=\"opt.value === glyph\"\n [class.active]=\"opt.value === glyph\"\n [title]=\"opt.label\"\n (click)=\"glyphChange.emit(opt.value)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"opt.points\"></polygon>\n </svg>\n </button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"iconHint\">{{ iconHint }}</small>\n </ng-container>\n <div class=\"sc-style-title\">{{ what }} Color</div>\n <div class=\"sc-style-color\">\n <label class=\"sc-style-swatch\" [style.background]=\"color\" title=\"Pick a colour\">\n <input type=\"color\" [value]=\"color\" (change)=\"colorChange.emit($any($event.target).value)\" />\n </label>\n <input\n pInputText\n class=\"sc-style-hex\"\n [value]=\"color\"\n maxlength=\"7\"\n [attr.aria-label]=\"'Hex colour for ' + gene\"\n (change)=\"onHex($any($event.target).value)\"\n (keydown.enter)=\"onHex($any($event.target).value)\"\n />\n </div>\n <div class=\"sc-style-title sc-style-sub\">Presets</div>\n <div class=\"sc-style-presets\">\n <button\n type=\"button\"\n *ngFor=\"let c of colorPresets\"\n class=\"sc-style-preset\"\n [style.background]=\"c\"\n [class.active]=\"c === color.toLowerCase()\"\n [title]=\"c\"\n [attr.aria-label]=\"'Colour ' + c\"\n (click)=\"colorChange.emit(c)\"\n ></button>\n </div>\n <p-button\n label=\"Reset to default\"\n icon=\"pi pi-refresh\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"sc-style-reset\"\n (onClick)=\"resetStyle.emit()\"\n ></p-button>\n</div>\n", styles: ["::ng-deep .sc-gene-style .p-overlaypanel-content{padding:.75rem}.sc-style-pop{display:flex;flex-direction:column;gap:.5rem;width:15rem}.sc-style-title{font-weight:600;font-size:.9rem}.sc-style-sub{font-size:.75rem;text-transform:uppercase;opacity:.7}.sc-style-icons,.sc-style-presets{display:grid;grid-template-columns:repeat(5,1fr);gap:.35rem}.sc-style-icon{display:inline-flex;align-items:center;justify-content:center;height:1.9rem;border:1px solid var(--surface-border, rgba(0, 0, 0, .15));border-radius:4px;background:transparent;color:var(--text-color, inherit);cursor:pointer}.sc-style-icon .sc-glyph-svg{width:1.1rem;height:1.1rem}.sc-style-icon.active{border-color:var(--primary-color, #3b82f6);background:var(--highlight-bg, rgba(59, 130, 246, .12))}.sc-style-color{display:flex;gap:.5rem;align-items:center}.sc-style-swatch{position:relative;flex:0 0 3rem;height:2rem;border-radius:4px;border:1px solid rgba(0,0,0,.2);cursor:pointer}.sc-style-swatch input{position:absolute;inset:0;opacity:0;width:100%;height:100%;cursor:pointer}.sc-style-hex{flex:1 1 auto;min-width:0;font-family:monospace}.sc-style-preset{width:1.4rem;height:1.4rem;justify-self:center;border-radius:50%;border:2px solid transparent;cursor:pointer}.sc-style-preset.active{border-color:var(--text-color, #000)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
22784
+ }
22785
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneStyleComponent, decorators: [{
22786
+ type: Component,
22787
+ args: [{ selector: 'spatial-gene-style', standalone: true, imports: [CommonModule, ButtonModule, InputTextModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"sc-style-pop\">\n <ng-container *ngIf=\"showIcons\">\n <div class=\"sc-style-title\">{{ what }} Icon</div>\n <div class=\"sc-style-icons\" role=\"radiogroup\" [attr.aria-label]=\"'Icon for ' + gene\">\n <button\n type=\"button\"\n *ngFor=\"let opt of glyphOptions\"\n class=\"sc-style-icon\"\n role=\"radio\"\n [attr.aria-checked]=\"opt.value === glyph\"\n [class.active]=\"opt.value === glyph\"\n [title]=\"opt.label\"\n (click)=\"glyphChange.emit(opt.value)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"opt.points\"></polygon>\n </svg>\n </button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"iconHint\">{{ iconHint }}</small>\n </ng-container>\n <div class=\"sc-style-title\">{{ what }} Color</div>\n <div class=\"sc-style-color\">\n <label class=\"sc-style-swatch\" [style.background]=\"color\" title=\"Pick a colour\">\n <input type=\"color\" [value]=\"color\" (change)=\"colorChange.emit($any($event.target).value)\" />\n </label>\n <input\n pInputText\n class=\"sc-style-hex\"\n [value]=\"color\"\n maxlength=\"7\"\n [attr.aria-label]=\"'Hex colour for ' + gene\"\n (change)=\"onHex($any($event.target).value)\"\n (keydown.enter)=\"onHex($any($event.target).value)\"\n />\n </div>\n <div class=\"sc-style-title sc-style-sub\">Presets</div>\n <div class=\"sc-style-presets\">\n <button\n type=\"button\"\n *ngFor=\"let c of colorPresets\"\n class=\"sc-style-preset\"\n [style.background]=\"c\"\n [class.active]=\"c === color.toLowerCase()\"\n [title]=\"c\"\n [attr.aria-label]=\"'Colour ' + c\"\n (click)=\"colorChange.emit(c)\"\n ></button>\n </div>\n <p-button\n label=\"Reset to default\"\n icon=\"pi pi-refresh\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"sc-style-reset\"\n (onClick)=\"resetStyle.emit()\"\n ></p-button>\n</div>\n", styles: ["::ng-deep .sc-gene-style .p-overlaypanel-content{padding:.75rem}.sc-style-pop{display:flex;flex-direction:column;gap:.5rem;width:15rem}.sc-style-title{font-weight:600;font-size:.9rem}.sc-style-sub{font-size:.75rem;text-transform:uppercase;opacity:.7}.sc-style-icons,.sc-style-presets{display:grid;grid-template-columns:repeat(5,1fr);gap:.35rem}.sc-style-icon{display:inline-flex;align-items:center;justify-content:center;height:1.9rem;border:1px solid var(--surface-border, rgba(0, 0, 0, .15));border-radius:4px;background:transparent;color:var(--text-color, inherit);cursor:pointer}.sc-style-icon .sc-glyph-svg{width:1.1rem;height:1.1rem}.sc-style-icon.active{border-color:var(--primary-color, #3b82f6);background:var(--highlight-bg, rgba(59, 130, 246, .12))}.sc-style-color{display:flex;gap:.5rem;align-items:center}.sc-style-swatch{position:relative;flex:0 0 3rem;height:2rem;border-radius:4px;border:1px solid rgba(0,0,0,.2);cursor:pointer}.sc-style-swatch input{position:absolute;inset:0;opacity:0;width:100%;height:100%;cursor:pointer}.sc-style-hex{flex:1 1 auto;min-width:0;font-family:monospace}.sc-style-preset{width:1.4rem;height:1.4rem;justify-self:center;border-radius:50%;border:2px solid transparent;cursor:pointer}.sc-style-preset.active{border-color:var(--text-color, #000)}\n"] }]
22788
+ }], propDecorators: { gene: [{
22789
+ type: Input
22790
+ }], what: [{
22791
+ type: Input
22792
+ }], color: [{
22793
+ type: Input
22794
+ }], glyph: [{
22795
+ type: Input
22796
+ }], iconHint: [{
22797
+ type: Input
22798
+ }], showIcons: [{
22799
+ type: Input
22800
+ }], colorChange: [{
22801
+ type: Output
22802
+ }], glyphChange: [{
22803
+ type: Output
22804
+ }], resetStyle: [{
22805
+ type: Output
22806
+ }] } });
22807
+
22718
22808
  /**
22719
22809
  * The cells' grouping, as Xenium Explorer's Groups section: the group picker (columns under
22720
22810
  * their section heading, a k-means family once, with its k beside it), CSV import of a new
22721
- * grouping, and the grouping's categories with their colours, cell counts and show/hide
22722
- * switches.
22811
+ * grouping, and the grouping's categories with their colours (each one's swatch opens a colour
22812
+ * picker), cell counts and show/hide switches.
22723
22813
  *
22724
- * Writes only through `controls.setViewState` (`cellTypeColumn`, `hiddenGroups`).
22814
+ * Writes only through `controls.setViewState` (`cellTypeColumn`, `hiddenGroups`, `groupColors`).
22725
22815
  */
22726
22816
  class SpatialGroupsPanelComponent {
22727
22817
  constructor(zone, cdr) {
@@ -22754,6 +22844,8 @@ class SpatialGroupsPanelComponent {
22754
22844
  this.groupRowsLoad = new Supersede();
22755
22845
  this.groupImportError = null;
22756
22846
  this.groupImporting = false;
22847
+ /** The group the open colour picker edits. */
22848
+ this.styleGroup = null;
22757
22849
  /** {@link SpatialViewState.hiddenGroups} as a set, for the per-row checks. */
22758
22850
  this.hiddenGroups = new Set();
22759
22851
  this.trackByLabel = (_i, row) => row.label;
@@ -22808,6 +22900,43 @@ class SpatialGroupsPanelComponent {
22808
22900
  onAllGroupsShown(on) {
22809
22901
  this.controls?.setViewState({ hiddenGroups: on ? [] : this.groupRows.map((r) => r.label) });
22810
22902
  }
22903
+ // ── per-group colour picker ──────────────────────────────────────────────
22904
+ openGroupStyle(event, label, panel) {
22905
+ if (this.styleGroup === label) {
22906
+ panel.toggle(event);
22907
+ return;
22908
+ }
22909
+ this.styleGroup = label;
22910
+ panel.hide();
22911
+ // Re-open anchored on the row that was clicked, once the panel has closed.
22912
+ setTimeout(() => panel.toggle(event));
22913
+ }
22914
+ /** The colour group `label` is drawn in now (the user's, else the column's or the palette's). */
22915
+ groupColorOf(label) {
22916
+ return this.groupRows.find((r) => r.label === label)?.color ?? '#999999';
22917
+ }
22918
+ onGroupColor(label, hex) {
22919
+ const column = this.activeCellTypeColumn;
22920
+ if (!column)
22921
+ return;
22922
+ const all = this.view.groupColors ?? {};
22923
+ this.controls?.setViewState({ groupColors: { ...all, [column]: { ...all[column], [label]: hex } } });
22924
+ }
22925
+ /** Back to the group's own colour (the column's, else the palette's). */
22926
+ resetGroupColor(label) {
22927
+ const column = this.activeCellTypeColumn;
22928
+ const own = column ? this.view.groupColors?.[column] : undefined;
22929
+ if (!column || !own || !(label in own))
22930
+ return;
22931
+ const rest = { ...own };
22932
+ delete rest[label];
22933
+ const all = { ...this.view.groupColors };
22934
+ if (Object.keys(rest).length)
22935
+ all[column] = rest;
22936
+ else
22937
+ delete all[column];
22938
+ this.controls?.setViewState({ groupColors: all });
22939
+ }
22811
22940
  get canImportGroups() {
22812
22941
  return !!this.controls?.importGroups;
22813
22942
  }
@@ -22850,9 +22979,11 @@ class SpatialGroupsPanelComponent {
22850
22979
  this.groupRowsFor = null;
22851
22980
  return;
22852
22981
  }
22853
- if (name === this.groupRowsFor)
22982
+ // The user's colours for the grouping are part of what the rows show.
22983
+ const key = `${name}\u0001${groupColorsKey(name, this.view.groupColors)}`;
22984
+ if (key === this.groupRowsFor)
22854
22985
  return;
22855
- this.groupRowsFor = name;
22986
+ this.groupRowsFor = key;
22856
22987
  const task = this.groupRowsLoad.next();
22857
22988
  try {
22858
22989
  const v = await this.controls.categoricalView(name);
@@ -22879,11 +23010,19 @@ class SpatialGroupsPanelComponent {
22879
23010
  });
22880
23011
  }
22881
23012
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGroupsPanelComponent, deps: [{ token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
22882
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGroupsPanelComponent, isStandalone: true, selector: "spatial-groups-panel", inputs: { controls: "controls", dataset: "dataset", view: "view" }, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"groupOptions.length\">\n <div class=\"sc-subhead\">Groups</div>\n <div class=\"sc-row sc-row-picker\">\n <p-dropdown\n [options]=\"groupOptions\"\n [group]=\"true\"\n optionLabel=\"label\"\n optionValue=\"value\"\n optionGroupLabel=\"label\"\n optionGroupChildren=\"items\"\n [ngModel]=\"activeGroupEntry\"\n (ngModelChange)=\"onGroupEntry($event)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n >\n <ng-template let-group pTemplate=\"group\">\n <span class=\"sc-group-heading\">{{ group.label }}</span>\n </ng-template>\n </p-dropdown>\n <p-dropdown\n *ngIf=\"groupVariantOptions.length\"\n [options]=\"groupVariantOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n styleClass=\"sc-variant\"\n [ngModel]=\"activeCellTypeColumn\"\n (ngModelChange)=\"onGroupVariant($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n <button\n *ngIf=\"canImportGroups\"\n type=\"button\"\n class=\"sc-icon-btn\"\n [disabled]=\"groupImporting\"\n (click)=\"groupFile.click()\"\n title=\"Import cell groups (CSV of cell_id, group)\"\n >\n <i\n class=\"pi\"\n [class.pi-plus-circle]=\"!groupImporting\"\n [class.pi-spin]=\"groupImporting\"\n [class.pi-spinner]=\"groupImporting\"\n ></i>\n </button>\n <input #groupFile type=\"file\" accept=\".csv,.tsv,.txt\" hidden (change)=\"onImportGroupsFile(groupFile)\" />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"groupImportError\">{{ groupImportError }}</small>\n\n <div class=\"sc-group-list\" *ngIf=\"groupRows.length\">\n <div class=\"sc-group-row sc-group-all\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"allGroupsShown\"\n (onChange)=\"onAllGroupsShown($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-group-label\">All groups</span>\n <span class=\"sc-count\">({{ groupTotal | number }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"groupsExpanded = !groupsExpanded\">\n <i class=\"pi\" [class.pi-chevron-down]=\"groupsExpanded\" [class.pi-chevron-right]=\"!groupsExpanded\"></i>\n </button>\n </div>\n <ng-container *ngIf=\"groupsExpanded\">\n <div class=\"sc-group-row\" *ngFor=\"let row of groupRows; trackBy: trackByLabel\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isGroupShown(row.label)\"\n (onChange)=\"onGroupShown(row.label, $event.checked)\"\n ></p-checkbox>\n <span class=\"sc-dot\" [style.background]=\"row.color\"></span>\n <span class=\"sc-group-label\" [title]=\"row.label\">{{ row.label }}</span>\n <span class=\"sc-count\">({{ row.count | number }})</span>\n </div>\n </ng-container>\n </div>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-dot{flex:0 0 auto;width:.7rem;height:.7rem;border-radius:50%;border:1px solid rgba(0,0,0,.2)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i5$2.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23013
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGroupsPanelComponent, isStandalone: true, selector: "spatial-groups-panel", inputs: { controls: "controls", dataset: "dataset", view: "view" }, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"groupOptions.length\">\n <div class=\"sc-subhead\">Groups</div>\n <div class=\"sc-row sc-row-picker\">\n <p-dropdown\n [options]=\"groupOptions\"\n [group]=\"true\"\n optionLabel=\"label\"\n optionValue=\"value\"\n optionGroupLabel=\"label\"\n optionGroupChildren=\"items\"\n [ngModel]=\"activeGroupEntry\"\n (ngModelChange)=\"onGroupEntry($event)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n >\n <ng-template let-group pTemplate=\"group\">\n <span class=\"sc-group-heading\">{{ group.label }}</span>\n </ng-template>\n </p-dropdown>\n <p-dropdown\n *ngIf=\"groupVariantOptions.length\"\n [options]=\"groupVariantOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n styleClass=\"sc-variant\"\n [ngModel]=\"activeCellTypeColumn\"\n (ngModelChange)=\"onGroupVariant($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n <button\n *ngIf=\"canImportGroups\"\n type=\"button\"\n class=\"sc-icon-btn\"\n [disabled]=\"groupImporting\"\n (click)=\"groupFile.click()\"\n title=\"Import cell groups (CSV of cell_id, group)\"\n >\n <i\n class=\"pi\"\n [class.pi-plus-circle]=\"!groupImporting\"\n [class.pi-spin]=\"groupImporting\"\n [class.pi-spinner]=\"groupImporting\"\n ></i>\n </button>\n <input #groupFile type=\"file\" accept=\".csv,.tsv,.txt\" hidden (change)=\"onImportGroupsFile(groupFile)\" />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"groupImportError\">{{ groupImportError }}</small>\n\n <div class=\"sc-group-list\" *ngIf=\"groupRows.length\">\n <div class=\"sc-group-row sc-group-all\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"allGroupsShown\"\n (onChange)=\"onAllGroupsShown($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-group-label\">All groups</span>\n <span class=\"sc-count\">({{ groupTotal | number }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"groupsExpanded = !groupsExpanded\">\n <i class=\"pi\" [class.pi-chevron-down]=\"groupsExpanded\" [class.pi-chevron-right]=\"!groupsExpanded\"></i>\n </button>\n </div>\n <ng-container *ngIf=\"groupsExpanded\">\n <div class=\"sc-group-row\" *ngFor=\"let row of groupRows; trackBy: trackByLabel\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isGroupShown(row.label)\"\n (onChange)=\"onGroupShown(row.label, $event.checked)\"\n ></p-checkbox>\n <!-- The group's colour; clicking it opens the colour picker. -->\n <button\n type=\"button\"\n class=\"sc-dot sc-dot-btn\"\n [style.background]=\"row.color\"\n [attr.aria-label]=\"'Colour for ' + row.label\"\n title=\"Colour\"\n (click)=\"openGroupStyle($event, row.label, groupPop)\"\n ></button>\n <span class=\"sc-group-label\" [title]=\"row.label\">{{ row.label }}</span>\n <span class=\"sc-count\">({{ row.count | number }})</span>\n </div>\n </ng-container>\n </div>\n <!-- One picker for every group row, filled for the group that opened it. -->\n <p-overlayPanel #groupPop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGroup as group\"\n [gene]=\"group\"\n what=\"Group\"\n [color]=\"groupColorOf(group)\"\n [showIcons]=\"false\"\n (colorChange)=\"onGroupColor(group, $event)\"\n (resetStyle)=\"resetGroupColor(group)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-dot{flex:0 0 auto;width:.7rem;height:.7rem;border-radius:50%;border:1px solid rgba(0,0,0,.2)}.sc-dot-btn{padding:0;cursor:pointer}.sc-dot-btn:hover,.sc-dot-btn:focus-visible{outline:2px solid var(--surface-border, rgba(0, 0, 0, .35));outline-offset:1px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i5$2.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "component", type: SpatialGeneStyleComponent, selector: "spatial-gene-style", inputs: ["gene", "what", "color", "glyph", "iconHint", "showIcons"], outputs: ["colorChange", "glyphChange", "resetStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
22883
23014
  }
22884
23015
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGroupsPanelComponent, decorators: [{
22885
23016
  type: Component,
22886
- args: [{ selector: 'spatial-groups-panel', standalone: true, imports: [CommonModule, FormsModule, SharedModule, CheckboxModule, DropdownModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"groupOptions.length\">\n <div class=\"sc-subhead\">Groups</div>\n <div class=\"sc-row sc-row-picker\">\n <p-dropdown\n [options]=\"groupOptions\"\n [group]=\"true\"\n optionLabel=\"label\"\n optionValue=\"value\"\n optionGroupLabel=\"label\"\n optionGroupChildren=\"items\"\n [ngModel]=\"activeGroupEntry\"\n (ngModelChange)=\"onGroupEntry($event)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n >\n <ng-template let-group pTemplate=\"group\">\n <span class=\"sc-group-heading\">{{ group.label }}</span>\n </ng-template>\n </p-dropdown>\n <p-dropdown\n *ngIf=\"groupVariantOptions.length\"\n [options]=\"groupVariantOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n styleClass=\"sc-variant\"\n [ngModel]=\"activeCellTypeColumn\"\n (ngModelChange)=\"onGroupVariant($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n <button\n *ngIf=\"canImportGroups\"\n type=\"button\"\n class=\"sc-icon-btn\"\n [disabled]=\"groupImporting\"\n (click)=\"groupFile.click()\"\n title=\"Import cell groups (CSV of cell_id, group)\"\n >\n <i\n class=\"pi\"\n [class.pi-plus-circle]=\"!groupImporting\"\n [class.pi-spin]=\"groupImporting\"\n [class.pi-spinner]=\"groupImporting\"\n ></i>\n </button>\n <input #groupFile type=\"file\" accept=\".csv,.tsv,.txt\" hidden (change)=\"onImportGroupsFile(groupFile)\" />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"groupImportError\">{{ groupImportError }}</small>\n\n <div class=\"sc-group-list\" *ngIf=\"groupRows.length\">\n <div class=\"sc-group-row sc-group-all\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"allGroupsShown\"\n (onChange)=\"onAllGroupsShown($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-group-label\">All groups</span>\n <span class=\"sc-count\">({{ groupTotal | number }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"groupsExpanded = !groupsExpanded\">\n <i class=\"pi\" [class.pi-chevron-down]=\"groupsExpanded\" [class.pi-chevron-right]=\"!groupsExpanded\"></i>\n </button>\n </div>\n <ng-container *ngIf=\"groupsExpanded\">\n <div class=\"sc-group-row\" *ngFor=\"let row of groupRows; trackBy: trackByLabel\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isGroupShown(row.label)\"\n (onChange)=\"onGroupShown(row.label, $event.checked)\"\n ></p-checkbox>\n <span class=\"sc-dot\" [style.background]=\"row.color\"></span>\n <span class=\"sc-group-label\" [title]=\"row.label\">{{ row.label }}</span>\n <span class=\"sc-count\">({{ row.count | number }})</span>\n </div>\n </ng-container>\n </div>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-dot{flex:0 0 auto;width:.7rem;height:.7rem;border-radius:50%;border:1px solid rgba(0,0,0,.2)}\n"] }]
23017
+ args: [{ selector: 'spatial-groups-panel', standalone: true, imports: [
23018
+ CommonModule,
23019
+ FormsModule,
23020
+ SharedModule,
23021
+ CheckboxModule,
23022
+ DropdownModule,
23023
+ OverlayPanelModule,
23024
+ SpatialGeneStyleComponent,
23025
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"groupOptions.length\">\n <div class=\"sc-subhead\">Groups</div>\n <div class=\"sc-row sc-row-picker\">\n <p-dropdown\n [options]=\"groupOptions\"\n [group]=\"true\"\n optionLabel=\"label\"\n optionValue=\"value\"\n optionGroupLabel=\"label\"\n optionGroupChildren=\"items\"\n [ngModel]=\"activeGroupEntry\"\n (ngModelChange)=\"onGroupEntry($event)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n >\n <ng-template let-group pTemplate=\"group\">\n <span class=\"sc-group-heading\">{{ group.label }}</span>\n </ng-template>\n </p-dropdown>\n <p-dropdown\n *ngIf=\"groupVariantOptions.length\"\n [options]=\"groupVariantOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n styleClass=\"sc-variant\"\n [ngModel]=\"activeCellTypeColumn\"\n (ngModelChange)=\"onGroupVariant($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n <button\n *ngIf=\"canImportGroups\"\n type=\"button\"\n class=\"sc-icon-btn\"\n [disabled]=\"groupImporting\"\n (click)=\"groupFile.click()\"\n title=\"Import cell groups (CSV of cell_id, group)\"\n >\n <i\n class=\"pi\"\n [class.pi-plus-circle]=\"!groupImporting\"\n [class.pi-spin]=\"groupImporting\"\n [class.pi-spinner]=\"groupImporting\"\n ></i>\n </button>\n <input #groupFile type=\"file\" accept=\".csv,.tsv,.txt\" hidden (change)=\"onImportGroupsFile(groupFile)\" />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"groupImportError\">{{ groupImportError }}</small>\n\n <div class=\"sc-group-list\" *ngIf=\"groupRows.length\">\n <div class=\"sc-group-row sc-group-all\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"allGroupsShown\"\n (onChange)=\"onAllGroupsShown($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-group-label\">All groups</span>\n <span class=\"sc-count\">({{ groupTotal | number }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"groupsExpanded = !groupsExpanded\">\n <i class=\"pi\" [class.pi-chevron-down]=\"groupsExpanded\" [class.pi-chevron-right]=\"!groupsExpanded\"></i>\n </button>\n </div>\n <ng-container *ngIf=\"groupsExpanded\">\n <div class=\"sc-group-row\" *ngFor=\"let row of groupRows; trackBy: trackByLabel\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isGroupShown(row.label)\"\n (onChange)=\"onGroupShown(row.label, $event.checked)\"\n ></p-checkbox>\n <!-- The group's colour; clicking it opens the colour picker. -->\n <button\n type=\"button\"\n class=\"sc-dot sc-dot-btn\"\n [style.background]=\"row.color\"\n [attr.aria-label]=\"'Colour for ' + row.label\"\n title=\"Colour\"\n (click)=\"openGroupStyle($event, row.label, groupPop)\"\n ></button>\n <span class=\"sc-group-label\" [title]=\"row.label\">{{ row.label }}</span>\n <span class=\"sc-count\">({{ row.count | number }})</span>\n </div>\n </ng-container>\n </div>\n <!-- One picker for every group row, filled for the group that opened it. -->\n <p-overlayPanel #groupPop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGroup as group\"\n [gene]=\"group\"\n what=\"Group\"\n [color]=\"groupColorOf(group)\"\n [showIcons]=\"false\"\n (colorChange)=\"onGroupColor(group, $event)\"\n (resetStyle)=\"resetGroupColor(group)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-dot{flex:0 0 auto;width:.7rem;height:.7rem;border-radius:50%;border:1px solid rgba(0,0,0,.2)}.sc-dot-btn{padding:0;cursor:pointer}.sc-dot-btn:hover,.sc-dot-btn:focus-visible{outline:2px solid var(--surface-border, rgba(0, 0, 0, .35));outline-offset:1px}\n"] }]
22887
23026
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { controls: [{
22888
23027
  type: Input
22889
23028
  }], dataset: [{
@@ -23315,63 +23454,10 @@ function clusterMarkers(rasters, slots, bin, area) {
23315
23454
  };
23316
23455
  }
23317
23456
 
23318
- /**
23319
- * One gene's icon and colour: the icon grid, a colour picker with its hex value, presets, and
23320
- * a reset — the content of a popover its host opens on the gene's swatch. Writes nothing itself:
23321
- * it reports the choice, and the host stores it where it belongs.
23322
- */
23323
- class SpatialGeneStyleComponent {
23324
- constructor() {
23325
- /** The gene, for the controls' labels. */
23326
- this.gene = '';
23327
- /** What the icons and colour are of ("Observation", "Transcript"), for the titles. */
23328
- this.what = '';
23329
- /** The gene's colour, `#rrggbb`. */
23330
- this.color = '#ffffff';
23331
- /** The gene's icon. */
23332
- this.glyph = 'circle';
23333
- /** A note under the icons, e.g. where they show; none when null. */
23334
- this.iconHint = null;
23335
- this.colorChange = new EventEmitter();
23336
- this.glyphChange = new EventEmitter();
23337
- /** Back to the gene's default icon and colour. */
23338
- this.resetStyle = new EventEmitter();
23339
- this.glyphOptions = GLYPH_OPTIONS;
23340
- this.colorPresets = DEFAULT_CATEGORICAL_PALETTE.slice(0, 10).map((c) => c.toLowerCase());
23341
- }
23342
- /** A typed hex colour, accepted as `#rrggbb` or `rrggbb`; anything else is ignored. */
23343
- onHex(text) {
23344
- const t = text.trim();
23345
- const rgb = /^#?[0-9a-f]{6}$/i.test(t) ? parseCssColor(t) : null;
23346
- if (rgb)
23347
- this.colorChange.emit(rgbToHex(rgb));
23348
- }
23349
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneStyleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
23350
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGeneStyleComponent, isStandalone: true, selector: "spatial-gene-style", inputs: { gene: "gene", what: "what", color: "color", glyph: "glyph", iconHint: "iconHint" }, outputs: { colorChange: "colorChange", glyphChange: "glyphChange", resetStyle: "resetStyle" }, ngImport: i0, template: "<div class=\"sc-style-pop\">\n <div class=\"sc-style-title\">{{ what }} Icon</div>\n <div class=\"sc-style-icons\" role=\"radiogroup\" [attr.aria-label]=\"'Icon for ' + gene\">\n <button\n type=\"button\"\n *ngFor=\"let opt of glyphOptions\"\n class=\"sc-style-icon\"\n role=\"radio\"\n [attr.aria-checked]=\"opt.value === glyph\"\n [class.active]=\"opt.value === glyph\"\n [title]=\"opt.label\"\n (click)=\"glyphChange.emit(opt.value)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"opt.points\"></polygon>\n </svg>\n </button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"iconHint\">{{ iconHint }}</small>\n <div class=\"sc-style-title\">{{ what }} Color</div>\n <div class=\"sc-style-color\">\n <label class=\"sc-style-swatch\" [style.background]=\"color\" title=\"Pick a colour\">\n <input type=\"color\" [value]=\"color\" (change)=\"colorChange.emit($any($event.target).value)\" />\n </label>\n <input\n pInputText\n class=\"sc-style-hex\"\n [value]=\"color\"\n maxlength=\"7\"\n [attr.aria-label]=\"'Hex colour for ' + gene\"\n (change)=\"onHex($any($event.target).value)\"\n (keydown.enter)=\"onHex($any($event.target).value)\"\n />\n </div>\n <div class=\"sc-style-title sc-style-sub\">Presets</div>\n <div class=\"sc-style-presets\">\n <button\n type=\"button\"\n *ngFor=\"let c of colorPresets\"\n class=\"sc-style-preset\"\n [style.background]=\"c\"\n [class.active]=\"c === color.toLowerCase()\"\n [title]=\"c\"\n [attr.aria-label]=\"'Colour ' + c\"\n (click)=\"colorChange.emit(c)\"\n ></button>\n </div>\n <p-button\n label=\"Reset to default\"\n icon=\"pi pi-refresh\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"sc-style-reset\"\n (onClick)=\"resetStyle.emit()\"\n ></p-button>\n</div>\n", styles: ["::ng-deep .sc-gene-style .p-overlaypanel-content{padding:.75rem}.sc-style-pop{display:flex;flex-direction:column;gap:.5rem;width:15rem}.sc-style-title{font-weight:600;font-size:.9rem}.sc-style-sub{font-size:.75rem;text-transform:uppercase;opacity:.7}.sc-style-icons,.sc-style-presets{display:grid;grid-template-columns:repeat(5,1fr);gap:.35rem}.sc-style-icon{display:inline-flex;align-items:center;justify-content:center;height:1.9rem;border:1px solid var(--surface-border, rgba(0, 0, 0, .15));border-radius:4px;background:transparent;color:var(--text-color, inherit);cursor:pointer}.sc-style-icon .sc-glyph-svg{width:1.1rem;height:1.1rem}.sc-style-icon.active{border-color:var(--primary-color, #3b82f6);background:var(--highlight-bg, rgba(59, 130, 246, .12))}.sc-style-color{display:flex;gap:.5rem;align-items:center}.sc-style-swatch{position:relative;flex:0 0 3rem;height:2rem;border-radius:4px;border:1px solid rgba(0,0,0,.2);cursor:pointer}.sc-style-swatch input{position:absolute;inset:0;opacity:0;width:100%;height:100%;cursor:pointer}.sc-style-hex{flex:1 1 auto;min-width:0;font-family:monospace}.sc-style-preset{width:1.4rem;height:1.4rem;justify-self:center;border-radius:50%;border:2px solid transparent;cursor:pointer}.sc-style-preset.active{border-color:var(--text-color, #000)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23351
- }
23352
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneStyleComponent, decorators: [{
23353
- type: Component,
23354
- args: [{ selector: 'spatial-gene-style', standalone: true, imports: [CommonModule, ButtonModule, InputTextModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"sc-style-pop\">\n <div class=\"sc-style-title\">{{ what }} Icon</div>\n <div class=\"sc-style-icons\" role=\"radiogroup\" [attr.aria-label]=\"'Icon for ' + gene\">\n <button\n type=\"button\"\n *ngFor=\"let opt of glyphOptions\"\n class=\"sc-style-icon\"\n role=\"radio\"\n [attr.aria-checked]=\"opt.value === glyph\"\n [class.active]=\"opt.value === glyph\"\n [title]=\"opt.label\"\n (click)=\"glyphChange.emit(opt.value)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"opt.points\"></polygon>\n </svg>\n </button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"iconHint\">{{ iconHint }}</small>\n <div class=\"sc-style-title\">{{ what }} Color</div>\n <div class=\"sc-style-color\">\n <label class=\"sc-style-swatch\" [style.background]=\"color\" title=\"Pick a colour\">\n <input type=\"color\" [value]=\"color\" (change)=\"colorChange.emit($any($event.target).value)\" />\n </label>\n <input\n pInputText\n class=\"sc-style-hex\"\n [value]=\"color\"\n maxlength=\"7\"\n [attr.aria-label]=\"'Hex colour for ' + gene\"\n (change)=\"onHex($any($event.target).value)\"\n (keydown.enter)=\"onHex($any($event.target).value)\"\n />\n </div>\n <div class=\"sc-style-title sc-style-sub\">Presets</div>\n <div class=\"sc-style-presets\">\n <button\n type=\"button\"\n *ngFor=\"let c of colorPresets\"\n class=\"sc-style-preset\"\n [style.background]=\"c\"\n [class.active]=\"c === color.toLowerCase()\"\n [title]=\"c\"\n [attr.aria-label]=\"'Colour ' + c\"\n (click)=\"colorChange.emit(c)\"\n ></button>\n </div>\n <p-button\n label=\"Reset to default\"\n icon=\"pi pi-refresh\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"sc-style-reset\"\n (onClick)=\"resetStyle.emit()\"\n ></p-button>\n</div>\n", styles: ["::ng-deep .sc-gene-style .p-overlaypanel-content{padding:.75rem}.sc-style-pop{display:flex;flex-direction:column;gap:.5rem;width:15rem}.sc-style-title{font-weight:600;font-size:.9rem}.sc-style-sub{font-size:.75rem;text-transform:uppercase;opacity:.7}.sc-style-icons,.sc-style-presets{display:grid;grid-template-columns:repeat(5,1fr);gap:.35rem}.sc-style-icon{display:inline-flex;align-items:center;justify-content:center;height:1.9rem;border:1px solid var(--surface-border, rgba(0, 0, 0, .15));border-radius:4px;background:transparent;color:var(--text-color, inherit);cursor:pointer}.sc-style-icon .sc-glyph-svg{width:1.1rem;height:1.1rem}.sc-style-icon.active{border-color:var(--primary-color, #3b82f6);background:var(--highlight-bg, rgba(59, 130, 246, .12))}.sc-style-color{display:flex;gap:.5rem;align-items:center}.sc-style-swatch{position:relative;flex:0 0 3rem;height:2rem;border-radius:4px;border:1px solid rgba(0,0,0,.2);cursor:pointer}.sc-style-swatch input{position:absolute;inset:0;opacity:0;width:100%;height:100%;cursor:pointer}.sc-style-hex{flex:1 1 auto;min-width:0;font-family:monospace}.sc-style-preset{width:1.4rem;height:1.4rem;justify-self:center;border-radius:50%;border:2px solid transparent;cursor:pointer}.sc-style-preset.active{border-color:var(--text-color, #000)}\n"] }]
23355
- }], propDecorators: { gene: [{
23356
- type: Input
23357
- }], what: [{
23358
- type: Input
23359
- }], color: [{
23360
- type: Input
23361
- }], glyph: [{
23362
- type: Input
23363
- }], iconHint: [{
23364
- type: Input
23365
- }], colorChange: [{
23366
- type: Output
23367
- }], glyphChange: [{
23368
- type: Output
23369
- }], resetStyle: [{
23370
- type: Output
23371
- }] } });
23372
-
23373
23457
  /** Rows of the all-genes list drawn at once; the filter narrows the rest. */
23374
23458
  const ALL_GENES_ROWS = 200;
23459
+ /** Names in reading order: case aside, and `G9` before `G10`. */
23460
+ const byName = (a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
23375
23461
  /**
23376
23462
  * The selected transcript genes as Xenium Explorer's tree: the gene groups, then the
23377
23463
  * ungrouped genes, each row with an eye that hides it without deselecting it, its marker
@@ -23380,6 +23466,11 @@ const ALL_GENES_ROWS = 200;
23380
23466
  * expands to every gene of the dataset, each with its eye when the dataset lists its genes
23381
23467
  * in code order (`transcriptTiles.genes`), filtered by name past a few hundred.
23382
23468
  *
23469
+ * Both lists are ordered by name, or by their transcripts in view, most first (the
23470
+ * sort switch, offered once the renderer reports counts). The all-genes list is sorted
23471
+ * before it is cut to its first rows, so by count those are the genes with the most
23472
+ * transcripts in view.
23473
+ *
23383
23474
  * Writes only through `controls.setViewState`.
23384
23475
  */
23385
23476
  class SpatialGeneTreeComponent {
@@ -23399,6 +23490,10 @@ class SpatialGeneTreeComponent {
23399
23490
  /** Swatches in the picker: the categorical palette genes are coloured from by default. */
23400
23491
  /** The tree, rebuilt when the view changes — never per change-detection pass. */
23401
23492
  this.geneTree = [];
23493
+ /** {@link geneTree} with each node's genes in {@link geneSort} order: what the template draws. */
23494
+ this.sortedTree = [];
23495
+ /** The rows' order. By count only while the renderer reports counts (see {@link sortShown}). */
23496
+ this.geneSort = 'name';
23402
23497
  /** Per gene: its eye, swatch, glyph and count — rebuilt with the inputs (and when the
23403
23498
  * cells' group colours arrive), so the template does no per-row lookups per check. */
23404
23499
  this.rows = new Map();
@@ -23460,8 +23555,30 @@ class SpatialGeneTreeComponent {
23460
23555
  .filter((node) => node.name)
23461
23556
  .map((node) => [node.name, this.areGenesShown(node.genes)]));
23462
23557
  this.rootShown = this.view.transcriptAllGenes ? this.anyOfAllGenesShown() : this.areGenesShown(genes);
23558
+ this.sortedTree = this.geneTree.map((node) => ({ ...node, genes: this.sorted(node.genes) }));
23463
23559
  this.rebuildAllRows();
23464
23560
  }
23561
+ /** Whether the rows are ordered by count: chosen, and counts to order by. */
23562
+ get byCount() {
23563
+ return this.geneSort === 'count' && !!this.geneCounts;
23564
+ }
23565
+ /** The sort switch is offered once there are counts to sort by. */
23566
+ get sortShown() {
23567
+ return !!this.geneCounts;
23568
+ }
23569
+ onGeneSort(sort) {
23570
+ if (sort === this.geneSort)
23571
+ return;
23572
+ this.geneSort = sort;
23573
+ this.rebuildRows();
23574
+ }
23575
+ /** `genes` in the rows' order: by count, most first (ties by name), else by name. */
23576
+ sorted(genes) {
23577
+ if (!this.byCount)
23578
+ return [...genes].sort(byName);
23579
+ const counts = this.geneCounts ?? {};
23580
+ return [...genes].sort((a, b) => (counts[b] ?? 0) - (counts[a] ?? 0) || byName(a, b));
23581
+ }
23465
23582
  /**
23466
23583
  * The all-genes list's rows: the genes matching the filter, up to the cap. Built only while
23467
23584
  * the list is open: the counts in view change with every pan, and the list can be thousands.
@@ -23475,24 +23592,25 @@ class SpatialGeneTreeComponent {
23475
23592
  }
23476
23593
  const needle = this.allGenesFilter.trim().toLowerCase();
23477
23594
  const shown = allGeneShownBy(this.view.transcriptAllGenesHidden);
23478
- const rows = [];
23479
- let matched = 0;
23595
+ // Every match, ordered, and only then cut: by count the first rows are the most abundant.
23596
+ const codes = new Map();
23480
23597
  list.forEach((gene, code) => {
23481
23598
  // An empty name is a code no transcript carries (a control probe).
23482
23599
  if (!gene || (needle && !gene.toLowerCase().includes(needle)))
23483
23600
  return;
23484
- matched++;
23485
- if (rows.length >= ALL_GENES_ROWS)
23486
- return;
23487
- rows.push({
23488
- gene,
23489
- shown: shown(gene),
23490
- swatch: this.canHideAllGenes ? this.allGeneColorOf(gene, code) : null,
23491
- count: this.geneCountOf(gene),
23492
- });
23601
+ if (!codes.has(gene))
23602
+ codes.set(gene, code);
23493
23603
  });
23494
- this.allRows = rows;
23495
- this.allGenesMatched = matched;
23604
+ this.allRows = this.sorted([...codes.keys()])
23605
+ .slice(0, ALL_GENES_ROWS)
23606
+ .map((gene) => ({
23607
+ gene,
23608
+ shown: shown(gene),
23609
+ swatch: this.canHideAllGenes ? this.allGeneColorOf(gene, codes.get(gene)) : null,
23610
+ code: codes.get(gene),
23611
+ count: this.geneCountOf(gene),
23612
+ }));
23613
+ this.allGenesMatched = codes.size;
23496
23614
  }
23497
23615
  /** Whether "All genes" draws anything: not switched off whole, nor every listed gene hidden. */
23498
23616
  anyOfAllGenesShown() {
@@ -23594,6 +23712,18 @@ class SpatialGeneTreeComponent {
23594
23712
  geneColorOf(gene) {
23595
23713
  return this.geneColor(Math.max(0, this.view.transcriptGenes.indexOf(gene)));
23596
23714
  }
23715
+ /**
23716
+ * The colour the open picker shows: with "All genes", the gene's colour among every gene (its
23717
+ * chosen one, else the palette's by its code), as its row and its markers have it.
23718
+ */
23719
+ styleColorOf(gene) {
23720
+ if (this.view.transcriptAllGenes) {
23721
+ const row = this.allRows.find((r) => r.gene === gene);
23722
+ if (row)
23723
+ return this.allGeneColorOf(gene, row.code);
23724
+ }
23725
+ return this.geneColorOf(gene);
23726
+ }
23597
23727
  /** In Cluster colouring, a gene's swatch is its cluster's colour, as its markers are. */
23598
23728
  geneSwatchOf(gene) {
23599
23729
  if (this.view.transcriptColorBy !== 'cluster')
@@ -23619,9 +23749,11 @@ class SpatialGeneTreeComponent {
23619
23749
  }
23620
23750
  async refreshCellGroupColors() {
23621
23751
  const column = this.dataset ? cellTypeColumnFor(this.dataset, this.view) : null;
23622
- if (column === this.cellGroupColorsFor)
23752
+ // The user's colours for the grouping are part of what the cluster swatches show.
23753
+ const key = column === null ? null : `${column}\u0001${groupColorsKey(column, this.view.groupColors)}`;
23754
+ if (key === this.cellGroupColorsFor)
23623
23755
  return;
23624
- this.cellGroupColorsFor = column;
23756
+ this.cellGroupColorsFor = key;
23625
23757
  const task = this.cellGroupColorsLoad.next();
23626
23758
  const map = new Map();
23627
23759
  const meta = column ? this.dataset?.columns.find((c) => c.name === column) : null;
@@ -23647,7 +23779,7 @@ class SpatialGeneTreeComponent {
23647
23779
  });
23648
23780
  }
23649
23781
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneTreeComponent, deps: [{ token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
23650
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGeneTreeComponent, isStandalone: true, selector: "spatial-gene-tree", inputs: { controls: "controls", dataset: "dataset", view: "view", geneCounts: "geneCounts", geneTotal: "geneTotal" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"sc-subhead\">Selected genes</div>\n<div class=\"sc-gene-tree\">\n <div class=\"sc-gene-row sc-gene-root\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n (click)=\"\n view.transcriptAllGenes ? onAllGenesShown(!rootShown) : onGenesShown(view.transcriptGenes, !rootShown)\n \"\n >\n <i class=\"pi\" [class.pi-eye]=\"rootShown\" [class.pi-eye-slash]=\"!rootShown\"></i>\n </button>\n <span class=\"sc-group-label\">Genes</span>\n <span class=\"sc-count\"\n >({{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{\n geneTotal | number\n }})</span\n >\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"geneTreeOpen = !geneTreeOpen\"\n *ngIf=\"!view.transcriptAllGenes && view.transcriptGenes.length\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"geneTreeOpen\" [class.pi-chevron-right]=\"!geneTreeOpen\"></i>\n </button>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"toggleAllGenesOpen()\"\n *ngIf=\"view.transcriptAllGenes && allGeneList?.length\"\n [attr.aria-label]=\"allGenesOpen ? 'Hide the gene list' : 'Show every gene'\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"allGenesOpen\" [class.pi-chevron-right]=\"!allGenesOpen\"></i>\n </button>\n </div>\n <!-- \"All genes\": every gene of the dataset, each with its eye when the dataset says which\n transcripts are whose (its gene list in code order). -->\n <ng-container *ngIf=\"allGenesOpen && view.transcriptAllGenes && allGeneList?.length\">\n <input\n pInputText\n class=\"sc-gene-filter\"\n placeholder=\"Filter genes\u2026\"\n aria-label=\"Filter genes\"\n [value]=\"allGenesFilter\"\n (input)=\"onAllGenesFilter($any($event.target).value)\"\n />\n <small class=\"sc-hint\" *ngIf=\"!canHideAllGenes\">\n This dataset does not say which transcripts are whose, so its genes cannot be hidden one by one.\n </small>\n <small class=\"sc-hint\" *ngIf=\"canHideAllGenes && view.transcriptMode === 'density'\">\n The density map draws every gene as one raster: genes are hidden one by one as circles or icons.\n </small>\n <div class=\"sc-gene-row sc-gene-leaf nested\" *ngFor=\"let row of allRows; trackBy: trackByAllRow\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n [disabled]=\"!canHideAllGenes || view.transcriptMode === 'density'\"\n (click)=\"onAllGeneShown(row.gene, !row.shown)\"\n >\n <i class=\"pi\" [class.pi-eye]=\"row.shown\" [class.pi-eye-slash]=\"!row.shown\"></i>\n </button>\n <span class=\"sc-gene-icon\" *ngIf=\"row.swatch\" [style.color]=\"row.swatch\" aria-hidden=\"true\">\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\"><circle r=\"0.75\"></circle></svg>\n </span>\n <span class=\"sc-group-label\" [title]=\"row.gene\">{{ row.gene }}</span>\n <span class=\"sc-gene-count\" *ngIf=\"row.count !== null\" title=\"Transcripts in view\">{{\n row.count | number\n }}</span>\n </div>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched > allRows.length\">\n Showing {{ allRows.length | number }} of {{ allGenesMatched | number }} genes: filter to find the others.\n </small>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched === 0\">No gene matches.</small>\n </ng-container>\n <ng-container *ngIf=\"geneTreeOpen && !view.transcriptAllGenes\">\n <ng-container *ngFor=\"let node of geneTree; trackBy: trackByNode\">\n <div class=\"sc-gene-row sc-gene-group\" *ngIf=\"node.name\">\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown(node.genes, !groupShown.get(node.name))\">\n <i\n class=\"pi\"\n [class.pi-eye]=\"groupShown.get(node.name)\"\n [class.pi-eye-slash]=\"!groupShown.get(node.name)\"\n ></i>\n </button>\n <span class=\"sc-group-label\" [title]=\"node.name\">{{ node.name }}</span>\n <span class=\"sc-count\">({{ node.genes.length }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"toggleGeneGroup(node.name)\">\n <i\n class=\"pi\"\n [class.pi-chevron-down]=\"!collapsedGeneGroups.has(node.name)\"\n [class.pi-chevron-right]=\"collapsedGeneGroups.has(node.name)\"\n ></i>\n </button>\n </div>\n <ng-container *ngIf=\"!node.name || !collapsedGeneGroups.has(node.name)\">\n <div\n class=\"sc-gene-row sc-gene-leaf\"\n [class.nested]=\"!!node.name\"\n *ngFor=\"let gene of node.genes; trackBy: trackByLabelString\"\n >\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown([gene], !rows.get(gene)?.shown)\">\n <i class=\"pi\" [class.pi-eye]=\"rows.get(gene)?.shown\" [class.pi-eye-slash]=\"!rows.get(gene)?.shown\"></i>\n </button>\n <!-- The gene's marker, in its colour: its icon in Icons mode, a dot for circles.\n Clicking it opens the icon and colour picker (as Xenium Explorer does). -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"rows.get(gene)?.swatch\"\n [attr.aria-label]=\"'Icon and colour for ' + gene\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon\n *ngIf=\"view.transcriptMode === 'glyphs'; else dotIcon\"\n [attr.points]=\"rows.get(gene)?.points\"\n ></polygon>\n <ng-template #dotIcon><circle r=\"0.75\"></circle></ng-template>\n </svg>\n </button>\n <span class=\"sc-group-label\">{{ gene }}</span>\n <span\n class=\"sc-gene-count\"\n *ngIf=\"rows.get(gene)?.count as n; else zeroCount\"\n title=\"Transcripts in view\"\n >{{ n | number }}</span\n >\n <ng-template #zeroCount>\n <span class=\"sc-gene-count\" *ngIf=\"geneCounts\" title=\"Transcripts in view\">0</span>\n </ng-template>\n </div>\n </ng-container>\n </ng-container>\n </ng-container>\n <!-- One picker for every gene row, filled for the gene that opened it. -->\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as gene\"\n [gene]=\"gene\"\n what=\"Transcript\"\n [color]=\"geneColorOf(gene)\"\n [glyph]=\"styleGlyph ?? 'circle'\"\n [iconHint]=\"\n view.transcriptMode !== 'glyphs' ? 'Icons show when the transcript point style is Icons.' : null\n \"\n (colorChange)=\"onGeneColor(gene, $event)\"\n (glyphChange)=\"onGlyph(gene, $event)\"\n (resetStyle)=\"resetGeneStyle(gene)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n <div class=\"sc-gene-footer\">\n {{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{ geneTotal | number }}\n genes selected\n </div>\n</div>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-gene-group,.sc-gene-leaf{padding-left:1rem}.sc-gene-leaf.nested{padding-left:2rem}.sc-eye{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:0;color:var(--text-color-secondary, #6c757d)}.sc-eye .pi-eye{color:var(--text-color, #333)}.sc-eye:disabled{cursor:default;opacity:.5}.sc-gene-filter{width:calc(100% - 1rem);margin:.2rem 0 .2rem 1rem;font-size:.8rem;padding:.2rem .4rem}.sc-gene-footer{text-align:right;font-size:.78rem;padding:.2rem 0;color:var(--text-color-secondary, #6c757d)}.sc-gene-count{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.8;padding-left:.5rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialGeneStyleComponent, selector: "spatial-gene-style", inputs: ["gene", "what", "color", "glyph", "iconHint"], outputs: ["colorChange", "glyphChange", "resetStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23782
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGeneTreeComponent, isStandalone: true, selector: "spatial-gene-tree", inputs: { controls: "controls", dataset: "dataset", view: "view", geneCounts: "geneCounts", geneTotal: "geneTotal" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"sc-subhead\">Selected genes</div>\n<div class=\"sc-gene-sort\" *ngIf=\"sortShown\" role=\"group\" aria-label=\"Sort genes\">\n <span class=\"sc-lbl\">Sort by</span>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"!byCount\"\n [attr.aria-pressed]=\"!byCount\"\n (click)=\"onGeneSort('name')\"\n >\n Name\n </button>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"byCount\"\n [attr.aria-pressed]=\"byCount\"\n title=\"Transcripts in view, most first\"\n (click)=\"onGeneSort('count')\"\n >\n Count\n </button>\n</div>\n<div class=\"sc-gene-tree\">\n <div class=\"sc-gene-row sc-gene-root\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n (click)=\"\n view.transcriptAllGenes ? onAllGenesShown(!rootShown) : onGenesShown(view.transcriptGenes, !rootShown)\n \"\n >\n <i class=\"pi\" [class.pi-eye]=\"rootShown\" [class.pi-eye-slash]=\"!rootShown\"></i>\n </button>\n <span class=\"sc-group-label\">Genes</span>\n <span class=\"sc-count\"\n >({{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{\n geneTotal | number\n }})</span\n >\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"geneTreeOpen = !geneTreeOpen\"\n *ngIf=\"!view.transcriptAllGenes && view.transcriptGenes.length\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"geneTreeOpen\" [class.pi-chevron-right]=\"!geneTreeOpen\"></i>\n </button>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"toggleAllGenesOpen()\"\n *ngIf=\"view.transcriptAllGenes && allGeneList?.length\"\n [attr.aria-label]=\"allGenesOpen ? 'Hide the gene list' : 'Show every gene'\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"allGenesOpen\" [class.pi-chevron-right]=\"!allGenesOpen\"></i>\n </button>\n </div>\n <!-- \"All genes\": every gene of the dataset, each with its eye when the dataset says which\n transcripts are whose (its gene list in code order). -->\n <ng-container *ngIf=\"allGenesOpen && view.transcriptAllGenes && allGeneList?.length\">\n <input\n pInputText\n class=\"sc-gene-filter\"\n placeholder=\"Filter genes\u2026\"\n aria-label=\"Filter genes\"\n [value]=\"allGenesFilter\"\n (input)=\"onAllGenesFilter($any($event.target).value)\"\n />\n <small class=\"sc-hint\" *ngIf=\"!canHideAllGenes\">\n This dataset does not say which transcripts are whose, so its genes cannot be hidden one by one.\n </small>\n <small class=\"sc-hint\" *ngIf=\"canHideAllGenes && view.transcriptMode === 'density'\">\n The density map draws every gene as one raster: genes are hidden one by one as circles or icons.\n </small>\n <div class=\"sc-gene-row sc-gene-leaf nested\" *ngFor=\"let row of allRows; trackBy: trackByAllRow\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n [disabled]=\"!canHideAllGenes || view.transcriptMode === 'density'\"\n (click)=\"onAllGeneShown(row.gene, !row.shown)\"\n >\n <i class=\"pi\" [class.pi-eye]=\"row.shown\" [class.pi-eye-slash]=\"!row.shown\"></i>\n </button>\n <!-- The gene's colour among every gene; clicking it opens the colour picker. -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n *ngIf=\"row.swatch\"\n [style.color]=\"row.swatch\"\n [attr.aria-label]=\"'Colour for ' + row.gene\"\n pTooltip=\"Colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, row.gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\"><circle r=\"0.75\"></circle></svg>\n </button>\n <span class=\"sc-group-label\" [title]=\"row.gene\">{{ row.gene }}</span>\n <span class=\"sc-gene-count\" *ngIf=\"row.count !== null\" title=\"Transcripts in view\">{{\n row.count | number\n }}</span>\n </div>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched > allRows.length\">\n Showing {{ allRows.length | number }} of {{ allGenesMatched | number }} genes: filter to find the others.\n </small>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched === 0\">No gene matches.</small>\n </ng-container>\n <ng-container *ngIf=\"geneTreeOpen && !view.transcriptAllGenes\">\n <ng-container *ngFor=\"let node of sortedTree; trackBy: trackByNode\">\n <div class=\"sc-gene-row sc-gene-group\" *ngIf=\"node.name\">\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown(node.genes, !groupShown.get(node.name))\">\n <i\n class=\"pi\"\n [class.pi-eye]=\"groupShown.get(node.name)\"\n [class.pi-eye-slash]=\"!groupShown.get(node.name)\"\n ></i>\n </button>\n <span class=\"sc-group-label\" [title]=\"node.name\">{{ node.name }}</span>\n <span class=\"sc-count\">({{ node.genes.length }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"toggleGeneGroup(node.name)\">\n <i\n class=\"pi\"\n [class.pi-chevron-down]=\"!collapsedGeneGroups.has(node.name)\"\n [class.pi-chevron-right]=\"collapsedGeneGroups.has(node.name)\"\n ></i>\n </button>\n </div>\n <ng-container *ngIf=\"!node.name || !collapsedGeneGroups.has(node.name)\">\n <div\n class=\"sc-gene-row sc-gene-leaf\"\n [class.nested]=\"!!node.name\"\n *ngFor=\"let gene of node.genes; trackBy: trackByLabelString\"\n >\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown([gene], !rows.get(gene)?.shown)\">\n <i class=\"pi\" [class.pi-eye]=\"rows.get(gene)?.shown\" [class.pi-eye-slash]=\"!rows.get(gene)?.shown\"></i>\n </button>\n <!-- The gene's marker, in its colour: its icon in Icons mode, a dot for circles.\n Clicking it opens the icon and colour picker (as Xenium Explorer does). -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"rows.get(gene)?.swatch\"\n [attr.aria-label]=\"'Icon and colour for ' + gene\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon\n *ngIf=\"view.transcriptMode === 'glyphs'; else dotIcon\"\n [attr.points]=\"rows.get(gene)?.points\"\n ></polygon>\n <ng-template #dotIcon><circle r=\"0.75\"></circle></ng-template>\n </svg>\n </button>\n <span class=\"sc-group-label\">{{ gene }}</span>\n <span\n class=\"sc-gene-count\"\n *ngIf=\"rows.get(gene)?.count as n; else zeroCount\"\n title=\"Transcripts in view\"\n >{{ n | number }}</span\n >\n <ng-template #zeroCount>\n <span class=\"sc-gene-count\" *ngIf=\"geneCounts\" title=\"Transcripts in view\">0</span>\n </ng-template>\n </div>\n </ng-container>\n </ng-container>\n </ng-container>\n <!-- One picker for every gene row, filled for the gene that opened it. -->\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as gene\"\n [gene]=\"gene\"\n what=\"Transcript\"\n [color]=\"styleColorOf(gene)\"\n [glyph]=\"styleGlyph ?? 'circle'\"\n [showIcons]=\"!view.transcriptAllGenes\"\n [iconHint]=\"\n view.transcriptMode !== 'glyphs' ? 'Icons show when the transcript point style is Icons.' : null\n \"\n (colorChange)=\"onGeneColor(gene, $event)\"\n (glyphChange)=\"onGlyph(gene, $event)\"\n (resetStyle)=\"resetGeneStyle(gene)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n <div class=\"sc-gene-footer\">\n {{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{ geneTotal | number }}\n genes selected\n </div>\n</div>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-gene-group,.sc-gene-leaf{padding-left:1rem}.sc-gene-leaf.nested{padding-left:2rem}.sc-eye{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:0;color:var(--text-color-secondary, #6c757d)}.sc-eye .pi-eye{color:var(--text-color, #333)}.sc-eye:disabled{cursor:default;opacity:.5}.sc-gene-sort{display:flex;align-items:center;gap:.25rem;font-size:.8rem}.sc-gene-sort .sc-lbl{margin-right:auto}.sc-sort-btn{border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);color:var(--text-color, #333);border-radius:4px;padding:.1rem .5rem;font:inherit;cursor:pointer}.sc-sort-btn.active{border-color:var(--primary-color, #2196f3);color:var(--primary-color, #2196f3);font-weight:600}.sc-gene-filter{width:calc(100% - 1rem);margin:.2rem 0 .2rem 1rem;font-size:.8rem;padding:.2rem .4rem}.sc-gene-footer{text-align:right;font-size:.78rem;padding:.2rem 0;color:var(--text-color-secondary, #6c757d)}.sc-gene-count{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.8;padding-left:.5rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialGeneStyleComponent, selector: "spatial-gene-style", inputs: ["gene", "what", "color", "glyph", "iconHint", "showIcons"], outputs: ["colorChange", "glyphChange", "resetStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23651
23783
  }
23652
23784
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneTreeComponent, decorators: [{
23653
23785
  type: Component,
@@ -23658,7 +23790,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
23658
23790
  OverlayPanelModule,
23659
23791
  TooltipModule,
23660
23792
  SpatialGeneStyleComponent,
23661
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"sc-subhead\">Selected genes</div>\n<div class=\"sc-gene-tree\">\n <div class=\"sc-gene-row sc-gene-root\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n (click)=\"\n view.transcriptAllGenes ? onAllGenesShown(!rootShown) : onGenesShown(view.transcriptGenes, !rootShown)\n \"\n >\n <i class=\"pi\" [class.pi-eye]=\"rootShown\" [class.pi-eye-slash]=\"!rootShown\"></i>\n </button>\n <span class=\"sc-group-label\">Genes</span>\n <span class=\"sc-count\"\n >({{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{\n geneTotal | number\n }})</span\n >\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"geneTreeOpen = !geneTreeOpen\"\n *ngIf=\"!view.transcriptAllGenes && view.transcriptGenes.length\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"geneTreeOpen\" [class.pi-chevron-right]=\"!geneTreeOpen\"></i>\n </button>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"toggleAllGenesOpen()\"\n *ngIf=\"view.transcriptAllGenes && allGeneList?.length\"\n [attr.aria-label]=\"allGenesOpen ? 'Hide the gene list' : 'Show every gene'\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"allGenesOpen\" [class.pi-chevron-right]=\"!allGenesOpen\"></i>\n </button>\n </div>\n <!-- \"All genes\": every gene of the dataset, each with its eye when the dataset says which\n transcripts are whose (its gene list in code order). -->\n <ng-container *ngIf=\"allGenesOpen && view.transcriptAllGenes && allGeneList?.length\">\n <input\n pInputText\n class=\"sc-gene-filter\"\n placeholder=\"Filter genes\u2026\"\n aria-label=\"Filter genes\"\n [value]=\"allGenesFilter\"\n (input)=\"onAllGenesFilter($any($event.target).value)\"\n />\n <small class=\"sc-hint\" *ngIf=\"!canHideAllGenes\">\n This dataset does not say which transcripts are whose, so its genes cannot be hidden one by one.\n </small>\n <small class=\"sc-hint\" *ngIf=\"canHideAllGenes && view.transcriptMode === 'density'\">\n The density map draws every gene as one raster: genes are hidden one by one as circles or icons.\n </small>\n <div class=\"sc-gene-row sc-gene-leaf nested\" *ngFor=\"let row of allRows; trackBy: trackByAllRow\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n [disabled]=\"!canHideAllGenes || view.transcriptMode === 'density'\"\n (click)=\"onAllGeneShown(row.gene, !row.shown)\"\n >\n <i class=\"pi\" [class.pi-eye]=\"row.shown\" [class.pi-eye-slash]=\"!row.shown\"></i>\n </button>\n <span class=\"sc-gene-icon\" *ngIf=\"row.swatch\" [style.color]=\"row.swatch\" aria-hidden=\"true\">\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\"><circle r=\"0.75\"></circle></svg>\n </span>\n <span class=\"sc-group-label\" [title]=\"row.gene\">{{ row.gene }}</span>\n <span class=\"sc-gene-count\" *ngIf=\"row.count !== null\" title=\"Transcripts in view\">{{\n row.count | number\n }}</span>\n </div>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched > allRows.length\">\n Showing {{ allRows.length | number }} of {{ allGenesMatched | number }} genes: filter to find the others.\n </small>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched === 0\">No gene matches.</small>\n </ng-container>\n <ng-container *ngIf=\"geneTreeOpen && !view.transcriptAllGenes\">\n <ng-container *ngFor=\"let node of geneTree; trackBy: trackByNode\">\n <div class=\"sc-gene-row sc-gene-group\" *ngIf=\"node.name\">\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown(node.genes, !groupShown.get(node.name))\">\n <i\n class=\"pi\"\n [class.pi-eye]=\"groupShown.get(node.name)\"\n [class.pi-eye-slash]=\"!groupShown.get(node.name)\"\n ></i>\n </button>\n <span class=\"sc-group-label\" [title]=\"node.name\">{{ node.name }}</span>\n <span class=\"sc-count\">({{ node.genes.length }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"toggleGeneGroup(node.name)\">\n <i\n class=\"pi\"\n [class.pi-chevron-down]=\"!collapsedGeneGroups.has(node.name)\"\n [class.pi-chevron-right]=\"collapsedGeneGroups.has(node.name)\"\n ></i>\n </button>\n </div>\n <ng-container *ngIf=\"!node.name || !collapsedGeneGroups.has(node.name)\">\n <div\n class=\"sc-gene-row sc-gene-leaf\"\n [class.nested]=\"!!node.name\"\n *ngFor=\"let gene of node.genes; trackBy: trackByLabelString\"\n >\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown([gene], !rows.get(gene)?.shown)\">\n <i class=\"pi\" [class.pi-eye]=\"rows.get(gene)?.shown\" [class.pi-eye-slash]=\"!rows.get(gene)?.shown\"></i>\n </button>\n <!-- The gene's marker, in its colour: its icon in Icons mode, a dot for circles.\n Clicking it opens the icon and colour picker (as Xenium Explorer does). -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"rows.get(gene)?.swatch\"\n [attr.aria-label]=\"'Icon and colour for ' + gene\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon\n *ngIf=\"view.transcriptMode === 'glyphs'; else dotIcon\"\n [attr.points]=\"rows.get(gene)?.points\"\n ></polygon>\n <ng-template #dotIcon><circle r=\"0.75\"></circle></ng-template>\n </svg>\n </button>\n <span class=\"sc-group-label\">{{ gene }}</span>\n <span\n class=\"sc-gene-count\"\n *ngIf=\"rows.get(gene)?.count as n; else zeroCount\"\n title=\"Transcripts in view\"\n >{{ n | number }}</span\n >\n <ng-template #zeroCount>\n <span class=\"sc-gene-count\" *ngIf=\"geneCounts\" title=\"Transcripts in view\">0</span>\n </ng-template>\n </div>\n </ng-container>\n </ng-container>\n </ng-container>\n <!-- One picker for every gene row, filled for the gene that opened it. -->\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as gene\"\n [gene]=\"gene\"\n what=\"Transcript\"\n [color]=\"geneColorOf(gene)\"\n [glyph]=\"styleGlyph ?? 'circle'\"\n [iconHint]=\"\n view.transcriptMode !== 'glyphs' ? 'Icons show when the transcript point style is Icons.' : null\n \"\n (colorChange)=\"onGeneColor(gene, $event)\"\n (glyphChange)=\"onGlyph(gene, $event)\"\n (resetStyle)=\"resetGeneStyle(gene)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n <div class=\"sc-gene-footer\">\n {{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{ geneTotal | number }}\n genes selected\n </div>\n</div>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-gene-group,.sc-gene-leaf{padding-left:1rem}.sc-gene-leaf.nested{padding-left:2rem}.sc-eye{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:0;color:var(--text-color-secondary, #6c757d)}.sc-eye .pi-eye{color:var(--text-color, #333)}.sc-eye:disabled{cursor:default;opacity:.5}.sc-gene-filter{width:calc(100% - 1rem);margin:.2rem 0 .2rem 1rem;font-size:.8rem;padding:.2rem .4rem}.sc-gene-footer{text-align:right;font-size:.78rem;padding:.2rem 0;color:var(--text-color-secondary, #6c757d)}.sc-gene-count{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.8;padding-left:.5rem}\n"] }]
23793
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"sc-subhead\">Selected genes</div>\n<div class=\"sc-gene-sort\" *ngIf=\"sortShown\" role=\"group\" aria-label=\"Sort genes\">\n <span class=\"sc-lbl\">Sort by</span>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"!byCount\"\n [attr.aria-pressed]=\"!byCount\"\n (click)=\"onGeneSort('name')\"\n >\n Name\n </button>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"byCount\"\n [attr.aria-pressed]=\"byCount\"\n title=\"Transcripts in view, most first\"\n (click)=\"onGeneSort('count')\"\n >\n Count\n </button>\n</div>\n<div class=\"sc-gene-tree\">\n <div class=\"sc-gene-row sc-gene-root\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n (click)=\"\n view.transcriptAllGenes ? onAllGenesShown(!rootShown) : onGenesShown(view.transcriptGenes, !rootShown)\n \"\n >\n <i class=\"pi\" [class.pi-eye]=\"rootShown\" [class.pi-eye-slash]=\"!rootShown\"></i>\n </button>\n <span class=\"sc-group-label\">Genes</span>\n <span class=\"sc-count\"\n >({{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{\n geneTotal | number\n }})</span\n >\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"geneTreeOpen = !geneTreeOpen\"\n *ngIf=\"!view.transcriptAllGenes && view.transcriptGenes.length\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"geneTreeOpen\" [class.pi-chevron-right]=\"!geneTreeOpen\"></i>\n </button>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"toggleAllGenesOpen()\"\n *ngIf=\"view.transcriptAllGenes && allGeneList?.length\"\n [attr.aria-label]=\"allGenesOpen ? 'Hide the gene list' : 'Show every gene'\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"allGenesOpen\" [class.pi-chevron-right]=\"!allGenesOpen\"></i>\n </button>\n </div>\n <!-- \"All genes\": every gene of the dataset, each with its eye when the dataset says which\n transcripts are whose (its gene list in code order). -->\n <ng-container *ngIf=\"allGenesOpen && view.transcriptAllGenes && allGeneList?.length\">\n <input\n pInputText\n class=\"sc-gene-filter\"\n placeholder=\"Filter genes\u2026\"\n aria-label=\"Filter genes\"\n [value]=\"allGenesFilter\"\n (input)=\"onAllGenesFilter($any($event.target).value)\"\n />\n <small class=\"sc-hint\" *ngIf=\"!canHideAllGenes\">\n This dataset does not say which transcripts are whose, so its genes cannot be hidden one by one.\n </small>\n <small class=\"sc-hint\" *ngIf=\"canHideAllGenes && view.transcriptMode === 'density'\">\n The density map draws every gene as one raster: genes are hidden one by one as circles or icons.\n </small>\n <div class=\"sc-gene-row sc-gene-leaf nested\" *ngFor=\"let row of allRows; trackBy: trackByAllRow\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n [disabled]=\"!canHideAllGenes || view.transcriptMode === 'density'\"\n (click)=\"onAllGeneShown(row.gene, !row.shown)\"\n >\n <i class=\"pi\" [class.pi-eye]=\"row.shown\" [class.pi-eye-slash]=\"!row.shown\"></i>\n </button>\n <!-- The gene's colour among every gene; clicking it opens the colour picker. -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n *ngIf=\"row.swatch\"\n [style.color]=\"row.swatch\"\n [attr.aria-label]=\"'Colour for ' + row.gene\"\n pTooltip=\"Colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, row.gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\"><circle r=\"0.75\"></circle></svg>\n </button>\n <span class=\"sc-group-label\" [title]=\"row.gene\">{{ row.gene }}</span>\n <span class=\"sc-gene-count\" *ngIf=\"row.count !== null\" title=\"Transcripts in view\">{{\n row.count | number\n }}</span>\n </div>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched > allRows.length\">\n Showing {{ allRows.length | number }} of {{ allGenesMatched | number }} genes: filter to find the others.\n </small>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched === 0\">No gene matches.</small>\n </ng-container>\n <ng-container *ngIf=\"geneTreeOpen && !view.transcriptAllGenes\">\n <ng-container *ngFor=\"let node of sortedTree; trackBy: trackByNode\">\n <div class=\"sc-gene-row sc-gene-group\" *ngIf=\"node.name\">\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown(node.genes, !groupShown.get(node.name))\">\n <i\n class=\"pi\"\n [class.pi-eye]=\"groupShown.get(node.name)\"\n [class.pi-eye-slash]=\"!groupShown.get(node.name)\"\n ></i>\n </button>\n <span class=\"sc-group-label\" [title]=\"node.name\">{{ node.name }}</span>\n <span class=\"sc-count\">({{ node.genes.length }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"toggleGeneGroup(node.name)\">\n <i\n class=\"pi\"\n [class.pi-chevron-down]=\"!collapsedGeneGroups.has(node.name)\"\n [class.pi-chevron-right]=\"collapsedGeneGroups.has(node.name)\"\n ></i>\n </button>\n </div>\n <ng-container *ngIf=\"!node.name || !collapsedGeneGroups.has(node.name)\">\n <div\n class=\"sc-gene-row sc-gene-leaf\"\n [class.nested]=\"!!node.name\"\n *ngFor=\"let gene of node.genes; trackBy: trackByLabelString\"\n >\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown([gene], !rows.get(gene)?.shown)\">\n <i class=\"pi\" [class.pi-eye]=\"rows.get(gene)?.shown\" [class.pi-eye-slash]=\"!rows.get(gene)?.shown\"></i>\n </button>\n <!-- The gene's marker, in its colour: its icon in Icons mode, a dot for circles.\n Clicking it opens the icon and colour picker (as Xenium Explorer does). -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"rows.get(gene)?.swatch\"\n [attr.aria-label]=\"'Icon and colour for ' + gene\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon\n *ngIf=\"view.transcriptMode === 'glyphs'; else dotIcon\"\n [attr.points]=\"rows.get(gene)?.points\"\n ></polygon>\n <ng-template #dotIcon><circle r=\"0.75\"></circle></ng-template>\n </svg>\n </button>\n <span class=\"sc-group-label\">{{ gene }}</span>\n <span\n class=\"sc-gene-count\"\n *ngIf=\"rows.get(gene)?.count as n; else zeroCount\"\n title=\"Transcripts in view\"\n >{{ n | number }}</span\n >\n <ng-template #zeroCount>\n <span class=\"sc-gene-count\" *ngIf=\"geneCounts\" title=\"Transcripts in view\">0</span>\n </ng-template>\n </div>\n </ng-container>\n </ng-container>\n </ng-container>\n <!-- One picker for every gene row, filled for the gene that opened it. -->\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as gene\"\n [gene]=\"gene\"\n what=\"Transcript\"\n [color]=\"styleColorOf(gene)\"\n [glyph]=\"styleGlyph ?? 'circle'\"\n [showIcons]=\"!view.transcriptAllGenes\"\n [iconHint]=\"\n view.transcriptMode !== 'glyphs' ? 'Icons show when the transcript point style is Icons.' : null\n \"\n (colorChange)=\"onGeneColor(gene, $event)\"\n (glyphChange)=\"onGlyph(gene, $event)\"\n (resetStyle)=\"resetGeneStyle(gene)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n <div class=\"sc-gene-footer\">\n {{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{ geneTotal | number }}\n genes selected\n </div>\n</div>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-gene-group,.sc-gene-leaf{padding-left:1rem}.sc-gene-leaf.nested{padding-left:2rem}.sc-eye{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:0;color:var(--text-color-secondary, #6c757d)}.sc-eye .pi-eye{color:var(--text-color, #333)}.sc-eye:disabled{cursor:default;opacity:.5}.sc-gene-sort{display:flex;align-items:center;gap:.25rem;font-size:.8rem}.sc-gene-sort .sc-lbl{margin-right:auto}.sc-sort-btn{border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);color:var(--text-color, #333);border-radius:4px;padding:.1rem .5rem;font:inherit;cursor:pointer}.sc-sort-btn.active{border-color:var(--primary-color, #2196f3);color:var(--primary-color, #2196f3);font-weight:600}.sc-gene-filter{width:calc(100% - 1rem);margin:.2rem 0 .2rem 1rem;font-size:.8rem;padding:.2rem .4rem}.sc-gene-footer{text-align:right;font-size:.78rem;padding:.2rem 0;color:var(--text-color-secondary, #6c757d)}.sc-gene-count{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.8;padding-left:.5rem}\n"] }]
23662
23794
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { controls: [{
23663
23795
  type: Input
23664
23796
  }], dataset: [{
@@ -24635,7 +24767,7 @@ class SpatialObservationsPanelComponent {
24635
24767
  this.controls?.setViewState({ percentileClip: value });
24636
24768
  }
24637
24769
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialObservationsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
24638
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialObservationsPanelComponent, isStandalone: true, selector: "spatial-observations-panel", inputs: { controls: "controls", dataset: "dataset", view: "view", genes: "genes", legend: "legend", colorBarCss: "colorBarCss", selection: "selection", selectedCategory: "selectedCategory", colormapOptions: "colormapOptions", is3d: "is3d", open: "open" }, outputs: { openChange: "openChange", categoryClicked: "categoryClicked" }, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"dataset as ds\">\n <!-- Observations: one marker per observation (cell centroid, Visium spot), and what\n colours them. The lead section for datasets without cell outlines. -->\n <div class=\"sc-panel\" [class.open]=\"open\">\n <div class=\"sc-panel-head\" (click)=\"openChange.emit(!open)\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowPoints($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Observations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open\" [class.pi-chevron-right]=\"!open\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open\">\n <small class=\"sc-hint\" *ngIf=\"hasCells && !is3d\">\n One circle per cell. With cell outlines on, circles are only drawn where cells are too small on screen to\n outline.\n </small>\n <!-- Colour source. Column and gene are mutually exclusive: picking one\n clears the other, so what drives the colours is never ambiguous. -->\n <div class=\"sc-section\">\n <label class=\"sc-lbl\" for=\"sc-column\">Colour by column</label>\n <p-dropdown\n inputId=\"sc-column\"\n [options]=\"columnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedColumn\"\n (ngModelChange)=\"onColumn($event)\"\n placeholder=\"None (flat colour)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n </div>\n\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-gene\">Colour by gene</label>\n <!-- A filterable dropdown, not a free-text box: the genes are visible before\n anything is typed, and each keystroke narrows a list the user can see.\n Virtual-scrolled because a targeted panel runs to a few thousand names. -->\n <p-dropdown\n inputId=\"sc-gene\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedGene\"\n (ngModelChange)=\"onGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [showClear]=\"true\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Search {{ ds.features.count | number }} genes\u2026\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n <small class=\"sc-hint\" *ngIf=\"gene.failed\">Gene lookup failed.</small>\n </div>\n\n <!-- Which observations are drawn at all, by what they express. Several genes at\n once, unlike the colour, which reads one: picking genes here hides every\n observation that expresses none of them (or not all of them), and by default\n colours (and, in 2D, marks) each drawn one by its gene. -->\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-show-genes\">Show observations expressing</label>\n <p-multiSelect\n inputId=\"sc-show-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenes\"\n (onChange)=\"onObservationGenes($event.value)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onPanelShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [showToggleAll]=\"false\"\n [showClear]=\"true\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Any gene (every observation)\"\n [selectedItemsLabel]=\"'{0} genes'\"\n [maxSelectedLabels]=\"3\"\n styleClass=\"sc-full\"\n >\n <!-- A picked gene shows its icon in its colour; clicking it edits them. -->\n <ng-template let-option pTemplate=\"item\">\n <div class=\"sc-obs-gene-option\">\n <span class=\"sc-obs-gene-name\">{{ option.label }}</span>\n <ng-container *ngIf=\"isObservationGene(option.value)\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: option.value }\"></ng-container>\n </ng-container>\n </div>\n </ng-template>\n </p-multiSelect>\n <p-selectButton\n *ngIf=\"view.observationGenes.length > 1\"\n [options]=\"matchOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenesMatch\"\n (ngModelChange)=\"onObservationGenesMatch($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <!-- The picked genes: each one's icon and colour (click to edit), and a way out. -->\n <div class=\"sc-obs-genes\" *ngIf=\"view.observationGenes.length\">\n <div class=\"sc-obs-gene-row\" *ngFor=\"let g of view.observationGenes; trackBy: trackByGene\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: g }\"></ng-container>\n <span class=\"sc-obs-gene-name\">{{ g }}</span>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n [attr.aria-label]=\"'Stop showing ' + g\"\n pTooltip=\"Remove\"\n tooltipPosition=\"top\"\n (click)=\"removeObservationGene(g)\"\n >\n <i class=\"pi pi-times\"></i>\n </button>\n </div>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.observationGenes.length\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.observationGenesColorBy\"\n (onChange)=\"onObservationGenesColorBy($event.checked)\"\n label=\"Colour by these genes\"\n pTooltip=\"Each observation in the colour (and, in 2D, the icon) of the picked gene it expresses most, relative to that gene's highest value. Off, Colour by column or gene applies.\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.observationGenes.length\">{{ geneFilterHint }}</small>\n <!-- The filter is never dropped silently: a failed expression read keeps the observations\n as they were drawn, and says so. -->\n <small class=\"sc-hint sc-error\" *ngIf=\"view.observationGenes.length && (geneFilterFailed$ | async)\">\n The expression of the picked genes could not be read, so the observations still show the previous\n filter.\n <a href=\"\" (click)=\"$event.preventDefault(); retryGeneFilter()\">Try again</a>\n </small>\n </div>\n </ng-container>\n\n <spatial-key\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n (categoryClicked)=\"categoryClicked.emit($event)\"\n ></spatial-key>\n\n <!-- Display -->\n <div class=\"sc-section\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Point size</span>\n <p-slider\n [ngModel]=\"view.pointScale\"\n [min]=\"0.25\"\n [max]=\"5\"\n [step]=\"0.25\"\n (onChange)=\"onPointScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.pointScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.opacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.opacity }}</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Only for a gene: the map draws that gene's expression as a field. In 2D\n it goes under the cells of the displayed plane; in 3D it is one sheet per\n imaged section, optionally smoothed along z into a volume. -->\n <div class=\"sc-section\" *ngIf=\"canMapGene\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMap\"\n (onChange)=\"onGeneMap($event.checked)\"\n label=\"Gene map (expression field)\"\n [pTooltip]=\"\n is3d\n ? 'Draw this gene\\'s expression as one field per imaged section \u2014 an estimate between the cells, empty where nothing was measured'\n : 'Draw this gene\\'s expression as a smooth field UNDER the cells \u2014 an estimate between them, transparent where nothing was measured'\n \"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- 3D only: interpolating along z turns the measured sheets into an\n estimate, so it is its own decision and its own label. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMapVolume\"\n (onChange)=\"onGeneMapVolume($event.checked)\"\n label=\"Volume rendering (interpolate along z)\"\n pTooltip=\"Smooth the per-section fields into a continuous volume \u2014 an ESTIMATE between the imaged sections, not measured tissue\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Meaningless while interpolating: a volume built from one section would\n smear that slide through the specimen's whole depth. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"geneMapOneSection\"\n (onChange)=\"onGeneMapOneSection($event.checked)\"\n label=\"One map section at a time\"\n pTooltip=\"Draw a single imaged section's field instead of the whole stack of sheets\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume && geneMapOneSection\">\n <span class=\"sc-lbl\">Map section</span>\n <p-slider\n [ngModel]=\"view.geneMapSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onGeneMapSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ geneMapSectionLabel }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.geneMapSmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onGeneMapSmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapSmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Map opacity</span>\n <p-slider\n [ngModel]=\"view.geneMapOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onGeneMapOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapOpacity }}</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && !is3d\">\n Kernel-weighted mean per cell, not a sum \u2014 so a crowded region does not glow for being crowded. Transparent\n where no cell was measured. Turn the cells' own Opacity down to read the field under them.\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && is3d\">\n {{ geneMapVolumeNote }}\n </div>\n </div>\n\n <!-- 3D only: the volume, the cloud and the density volumes occupy the same\n space, so each hides the others. Independent toggles, because the useful\n views are the combinations \u2014 the field alone, one section's cells over it,\n the anatomy alone. -->\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <span class=\"sc-lbl\">Show</span>\n <div class=\"sc-row\" *ngIf=\"ds.volume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showVolume\"\n (onChange)=\"onShowVolume($event.checked)\"\n label=\"Reference volume\"\n pTooltip=\"The registered anatomy the observations sit in\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- The backdrop's own opacity: reading the cloud or a density field\n through the anatomy means turning the anatomy down, not the data. -->\n <div class=\"sc-row\" *ngIf=\"ds.volume && view.showVolume\">\n <span class=\"sc-lbl\">Volume opacity</span>\n <p-slider\n [ngModel]=\"view.volumeOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onVolumeOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.volumeOpacity }}</span>\n </div>\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (onChange)=\"onShowPoints($event.checked)\"\n label=\"Observations\"\n pTooltip=\"The measured cells. Hide them to read the density volumes underneath\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Only for data that really is sectioned; a continuous z has no sections\n to step through. -->\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"oneSection\"\n (onChange)=\"onOneSection($event.checked)\"\n label=\"One section at a time\"\n pTooltip=\"Draw a single imaged section's cells, so the density volumes between the sections stay visible\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned && oneSection\">\n <span class=\"sc-lbl\">Section</span>\n <p-slider\n [ngModel]=\"view.pointSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onPointSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ sectionLabel }}</span>\n </div>\n </div>\n\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.densityVolume\"\n (onChange)=\"onDensityVolume($event.checked)\"\n label=\"Cluster density volumes\"\n pTooltip=\"Raymarch each cluster as a smooth density field \u2014 an ESTIMATE between the imaged sections, not measured cells\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.densityVolume\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.densitySmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onDensitySmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.densitySmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.densityVolume\">{{ densityNote }}</div>\n </div>\n\n <!-- Continuous-only knobs: meaningless for a categorical palette. -->\n <div class=\"sc-section\" *ngIf=\"isContinuous\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.logScale\"\n (onChange)=\"onLogScale($event.checked)\"\n label=\"Log scale\"\n pTooltip=\"Counts collapse against their outliers on a linear ramp\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clip outliers</span>\n <p-dropdown\n [options]=\"clipOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.percentileClip\"\n (ngModelChange)=\"onClip($event)\"\n appendTo=\"body\"\n pTooltip=\"Percentile the contrast window is taken at\"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- Reserves the readout column this row has no readout for, so the\n control's right edge lines up with every slider's. -->\n <span class=\"sc-val\"></span>\n </div>\n </div>\n\n <!-- A gene's icon in its colour: the button that opens its icon and colour picker. -->\n <ng-template #geneMark let-g>\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"geneColorOf(g)\"\n [attr.aria-label]=\"'Icon and colour for ' + g\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, g, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"glyphPointsOf(g)\"></polygon>\n </svg>\n </button>\n </ng-template>\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as g\"\n [gene]=\"g\"\n what=\"Observation\"\n [color]=\"geneColorOf(g)\"\n [glyph]=\"geneGlyphOf(g)\"\n iconHint=\"Icons show on the 2D observation markers.\"\n (colorChange)=\"onGeneColor(g, $event)\"\n (glyphChange)=\"onGeneGlyph(g, $event)\"\n (resetStyle)=\"resetGeneStyle(g)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-obs-genes{display:flex;flex-direction:column;gap:.15rem;margin-top:.35rem}.sc-obs-gene-row,.sc-obs-gene-option{display:flex;align-items:center;gap:.4rem;min-width:0}.sc-obs-gene-option{flex:1 1 auto;justify-content:space-between}.sc-obs-gene-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i5$2.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i3$2.MultiSelect, selector: "p-multiSelect", inputs: ["id", "ariaLabel", "style", "styleClass", "panelStyle", "panelStyleClass", "inputId", "disabled", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "variant", "appendTo", "dataKey", "name", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "defaultLabel", "placeholder", "options", "filterValue", "itemSize", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i8$2.SelectButton, selector: "p-selectButton", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "unselectable", "tabindex", "multiple", "allowEmpty", "style", "styleClass", "ariaLabelledBy", "disabled", "dataKey", "autofocus"], outputs: ["onOptionClick", "onChange"] }, { kind: "ngmodule", type: SliderModule }, { kind: "component", type: i8.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialKeyComponent, selector: "spatial-key", inputs: ["controls", "dataset", "view", "legend", "colorBarCss", "selectedCategory", "colormapOptions"], outputs: ["categoryClicked"] }, { kind: "component", type: SpatialGeneStyleComponent, selector: "spatial-gene-style", inputs: ["gene", "what", "color", "glyph", "iconHint"], outputs: ["colorChange", "glyphChange", "resetStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
24770
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialObservationsPanelComponent, isStandalone: true, selector: "spatial-observations-panel", inputs: { controls: "controls", dataset: "dataset", view: "view", genes: "genes", legend: "legend", colorBarCss: "colorBarCss", selection: "selection", selectedCategory: "selectedCategory", colormapOptions: "colormapOptions", is3d: "is3d", open: "open" }, outputs: { openChange: "openChange", categoryClicked: "categoryClicked" }, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"dataset as ds\">\n <!-- Observations: one marker per observation (cell centroid, Visium spot), and what\n colours them. The lead section for datasets without cell outlines. -->\n <div class=\"sc-panel\" [class.open]=\"open\">\n <div class=\"sc-panel-head\" (click)=\"openChange.emit(!open)\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowPoints($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Observations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open\" [class.pi-chevron-right]=\"!open\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open\">\n <small class=\"sc-hint\" *ngIf=\"hasCells && !is3d\">\n One circle per cell, at every zoom. Cell outlines are drawn over the circles once cells are big enough on\n screen to outline.\n </small>\n <!-- Colour source. Column and gene are mutually exclusive: picking one\n clears the other, so what drives the colours is never ambiguous. -->\n <div class=\"sc-section\">\n <label class=\"sc-lbl\" for=\"sc-column\">Colour by column</label>\n <p-dropdown\n inputId=\"sc-column\"\n [options]=\"columnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedColumn\"\n (ngModelChange)=\"onColumn($event)\"\n placeholder=\"None (flat colour)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n </div>\n\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-gene\">Colour by gene</label>\n <!-- A filterable dropdown, not a free-text box: the genes are visible before\n anything is typed, and each keystroke narrows a list the user can see.\n Virtual-scrolled because a targeted panel runs to a few thousand names. -->\n <p-dropdown\n inputId=\"sc-gene\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedGene\"\n (ngModelChange)=\"onGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [showClear]=\"true\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Search {{ ds.features.count | number }} genes\u2026\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n <small class=\"sc-hint\" *ngIf=\"gene.failed\">Gene lookup failed.</small>\n </div>\n\n <!-- Which observations are drawn at all, by what they express. Several genes at\n once, unlike the colour, which reads one: picking genes here hides every\n observation that expresses none of them (or not all of them), and by default\n colours (and, in 2D, marks) each drawn one by its gene. -->\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-show-genes\">Show observations expressing</label>\n <p-multiSelect\n inputId=\"sc-show-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenes\"\n (onChange)=\"onObservationGenes($event.value)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onPanelShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [showToggleAll]=\"false\"\n [showClear]=\"true\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Any gene (every observation)\"\n [selectedItemsLabel]=\"'{0} genes'\"\n [maxSelectedLabels]=\"3\"\n styleClass=\"sc-full\"\n >\n <!-- A picked gene shows its icon in its colour; clicking it edits them. -->\n <ng-template let-option pTemplate=\"item\">\n <div class=\"sc-obs-gene-option\">\n <span class=\"sc-obs-gene-name\">{{ option.label }}</span>\n <ng-container *ngIf=\"isObservationGene(option.value)\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: option.value }\"></ng-container>\n </ng-container>\n </div>\n </ng-template>\n </p-multiSelect>\n <p-selectButton\n *ngIf=\"view.observationGenes.length > 1\"\n [options]=\"matchOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenesMatch\"\n (ngModelChange)=\"onObservationGenesMatch($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <!-- The picked genes: each one's icon and colour (click to edit), and a way out. -->\n <div class=\"sc-obs-genes\" *ngIf=\"view.observationGenes.length\">\n <div class=\"sc-obs-gene-row\" *ngFor=\"let g of view.observationGenes; trackBy: trackByGene\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: g }\"></ng-container>\n <span class=\"sc-obs-gene-name\">{{ g }}</span>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n [attr.aria-label]=\"'Stop showing ' + g\"\n pTooltip=\"Remove\"\n tooltipPosition=\"top\"\n (click)=\"removeObservationGene(g)\"\n >\n <i class=\"pi pi-times\"></i>\n </button>\n </div>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.observationGenes.length\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.observationGenesColorBy\"\n (onChange)=\"onObservationGenesColorBy($event.checked)\"\n label=\"Colour by these genes\"\n pTooltip=\"Each observation in the colour (and, in 2D, the icon) of the picked gene it expresses most, relative to that gene's highest value. Off, Colour by column or gene applies.\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.observationGenes.length\">{{ geneFilterHint }}</small>\n <!-- The filter is never dropped silently: a failed expression read keeps the observations\n as they were drawn, and says so. -->\n <small class=\"sc-hint sc-error\" *ngIf=\"view.observationGenes.length && (geneFilterFailed$ | async)\">\n The expression of the picked genes could not be read, so the observations still show the previous\n filter.\n <a href=\"\" (click)=\"$event.preventDefault(); retryGeneFilter()\">Try again</a>\n </small>\n </div>\n </ng-container>\n\n <spatial-key\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n (categoryClicked)=\"categoryClicked.emit($event)\"\n ></spatial-key>\n\n <!-- Display -->\n <div class=\"sc-section\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Point size</span>\n <p-slider\n [ngModel]=\"view.pointScale\"\n [min]=\"0.25\"\n [max]=\"5\"\n [step]=\"0.25\"\n (onChange)=\"onPointScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.pointScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.opacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.opacity }}</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Only for a gene: the map draws that gene's expression as a field. In 2D\n it goes under the cells of the displayed plane; in 3D it is one sheet per\n imaged section, optionally smoothed along z into a volume. -->\n <div class=\"sc-section\" *ngIf=\"canMapGene\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMap\"\n (onChange)=\"onGeneMap($event.checked)\"\n label=\"Gene map (expression field)\"\n [pTooltip]=\"\n is3d\n ? 'Draw this gene\\'s expression as one field per imaged section \u2014 an estimate between the cells, empty where nothing was measured'\n : 'Draw this gene\\'s expression as a smooth field UNDER the cells \u2014 an estimate between them, transparent where nothing was measured'\n \"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- 3D only: interpolating along z turns the measured sheets into an\n estimate, so it is its own decision and its own label. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMapVolume\"\n (onChange)=\"onGeneMapVolume($event.checked)\"\n label=\"Volume rendering (interpolate along z)\"\n pTooltip=\"Smooth the per-section fields into a continuous volume \u2014 an ESTIMATE between the imaged sections, not measured tissue\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Meaningless while interpolating: a volume built from one section would\n smear that slide through the specimen's whole depth. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"geneMapOneSection\"\n (onChange)=\"onGeneMapOneSection($event.checked)\"\n label=\"One map section at a time\"\n pTooltip=\"Draw a single imaged section's field instead of the whole stack of sheets\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume && geneMapOneSection\">\n <span class=\"sc-lbl\">Map section</span>\n <p-slider\n [ngModel]=\"view.geneMapSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onGeneMapSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ geneMapSectionLabel }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.geneMapSmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onGeneMapSmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapSmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Map opacity</span>\n <p-slider\n [ngModel]=\"view.geneMapOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onGeneMapOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapOpacity }}</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && !is3d\">\n Kernel-weighted mean per cell, not a sum \u2014 so a crowded region does not glow for being crowded. Transparent\n where no cell was measured. Turn the cells' own Opacity down to read the field under them.\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && is3d\">\n {{ geneMapVolumeNote }}\n </div>\n </div>\n\n <!-- 3D only: the volume, the cloud and the density volumes occupy the same\n space, so each hides the others. Independent toggles, because the useful\n views are the combinations \u2014 the field alone, one section's cells over it,\n the anatomy alone. -->\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <span class=\"sc-lbl\">Show</span>\n <div class=\"sc-row\" *ngIf=\"ds.volume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showVolume\"\n (onChange)=\"onShowVolume($event.checked)\"\n label=\"Reference volume\"\n pTooltip=\"The registered anatomy the observations sit in\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- The backdrop's own opacity: reading the cloud or a density field\n through the anatomy means turning the anatomy down, not the data. -->\n <div class=\"sc-row\" *ngIf=\"ds.volume && view.showVolume\">\n <span class=\"sc-lbl\">Volume opacity</span>\n <p-slider\n [ngModel]=\"view.volumeOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onVolumeOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.volumeOpacity }}</span>\n </div>\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (onChange)=\"onShowPoints($event.checked)\"\n label=\"Observations\"\n pTooltip=\"The measured cells. Hide them to read the density volumes underneath\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Only for data that really is sectioned; a continuous z has no sections\n to step through. -->\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"oneSection\"\n (onChange)=\"onOneSection($event.checked)\"\n label=\"One section at a time\"\n pTooltip=\"Draw a single imaged section's cells, so the density volumes between the sections stay visible\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned && oneSection\">\n <span class=\"sc-lbl\">Section</span>\n <p-slider\n [ngModel]=\"view.pointSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onPointSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ sectionLabel }}</span>\n </div>\n </div>\n\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.densityVolume\"\n (onChange)=\"onDensityVolume($event.checked)\"\n label=\"Cluster density volumes\"\n pTooltip=\"Raymarch each cluster as a smooth density field \u2014 an ESTIMATE between the imaged sections, not measured cells\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.densityVolume\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.densitySmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onDensitySmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.densitySmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.densityVolume\">{{ densityNote }}</div>\n </div>\n\n <!-- Continuous-only knobs: meaningless for a categorical palette. -->\n <div class=\"sc-section\" *ngIf=\"isContinuous\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.logScale\"\n (onChange)=\"onLogScale($event.checked)\"\n label=\"Log scale\"\n pTooltip=\"Counts collapse against their outliers on a linear ramp\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clip outliers</span>\n <p-dropdown\n [options]=\"clipOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.percentileClip\"\n (ngModelChange)=\"onClip($event)\"\n appendTo=\"body\"\n pTooltip=\"Percentile the contrast window is taken at\"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- Reserves the readout column this row has no readout for, so the\n control's right edge lines up with every slider's. -->\n <span class=\"sc-val\"></span>\n </div>\n </div>\n\n <!-- A gene's icon in its colour: the button that opens its icon and colour picker. -->\n <ng-template #geneMark let-g>\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"geneColorOf(g)\"\n [attr.aria-label]=\"'Icon and colour for ' + g\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, g, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"glyphPointsOf(g)\"></polygon>\n </svg>\n </button>\n </ng-template>\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as g\"\n [gene]=\"g\"\n what=\"Observation\"\n [color]=\"geneColorOf(g)\"\n [glyph]=\"geneGlyphOf(g)\"\n iconHint=\"Icons show on the 2D observation markers.\"\n (colorChange)=\"onGeneColor(g, $event)\"\n (glyphChange)=\"onGeneGlyph(g, $event)\"\n (resetStyle)=\"resetGeneStyle(g)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-obs-genes{display:flex;flex-direction:column;gap:.15rem;margin-top:.35rem}.sc-obs-gene-row,.sc-obs-gene-option{display:flex;align-items:center;gap:.4rem;min-width:0}.sc-obs-gene-option{flex:1 1 auto;justify-content:space-between}.sc-obs-gene-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i5$2.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i3$2.MultiSelect, selector: "p-multiSelect", inputs: ["id", "ariaLabel", "style", "styleClass", "panelStyle", "panelStyleClass", "inputId", "disabled", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "variant", "appendTo", "dataKey", "name", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "defaultLabel", "placeholder", "options", "filterValue", "itemSize", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i8$2.SelectButton, selector: "p-selectButton", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "unselectable", "tabindex", "multiple", "allowEmpty", "style", "styleClass", "ariaLabelledBy", "disabled", "dataKey", "autofocus"], outputs: ["onOptionClick", "onChange"] }, { kind: "ngmodule", type: SliderModule }, { kind: "component", type: i8.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialKeyComponent, selector: "spatial-key", inputs: ["controls", "dataset", "view", "legend", "colorBarCss", "selectedCategory", "colormapOptions"], outputs: ["categoryClicked"] }, { kind: "component", type: SpatialGeneStyleComponent, selector: "spatial-gene-style", inputs: ["gene", "what", "color", "glyph", "iconHint", "showIcons"], outputs: ["colorChange", "glyphChange", "resetStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
24639
24771
  }
24640
24772
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialObservationsPanelComponent, decorators: [{
24641
24773
  type: Component,
@@ -24651,7 +24783,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
24651
24783
  TooltipModule,
24652
24784
  SpatialKeyComponent,
24653
24785
  SpatialGeneStyleComponent,
24654
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"dataset as ds\">\n <!-- Observations: one marker per observation (cell centroid, Visium spot), and what\n colours them. The lead section for datasets without cell outlines. -->\n <div class=\"sc-panel\" [class.open]=\"open\">\n <div class=\"sc-panel-head\" (click)=\"openChange.emit(!open)\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowPoints($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Observations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open\" [class.pi-chevron-right]=\"!open\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open\">\n <small class=\"sc-hint\" *ngIf=\"hasCells && !is3d\">\n One circle per cell. With cell outlines on, circles are only drawn where cells are too small on screen to\n outline.\n </small>\n <!-- Colour source. Column and gene are mutually exclusive: picking one\n clears the other, so what drives the colours is never ambiguous. -->\n <div class=\"sc-section\">\n <label class=\"sc-lbl\" for=\"sc-column\">Colour by column</label>\n <p-dropdown\n inputId=\"sc-column\"\n [options]=\"columnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedColumn\"\n (ngModelChange)=\"onColumn($event)\"\n placeholder=\"None (flat colour)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n </div>\n\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-gene\">Colour by gene</label>\n <!-- A filterable dropdown, not a free-text box: the genes are visible before\n anything is typed, and each keystroke narrows a list the user can see.\n Virtual-scrolled because a targeted panel runs to a few thousand names. -->\n <p-dropdown\n inputId=\"sc-gene\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedGene\"\n (ngModelChange)=\"onGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [showClear]=\"true\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Search {{ ds.features.count | number }} genes\u2026\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n <small class=\"sc-hint\" *ngIf=\"gene.failed\">Gene lookup failed.</small>\n </div>\n\n <!-- Which observations are drawn at all, by what they express. Several genes at\n once, unlike the colour, which reads one: picking genes here hides every\n observation that expresses none of them (or not all of them), and by default\n colours (and, in 2D, marks) each drawn one by its gene. -->\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-show-genes\">Show observations expressing</label>\n <p-multiSelect\n inputId=\"sc-show-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenes\"\n (onChange)=\"onObservationGenes($event.value)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onPanelShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [showToggleAll]=\"false\"\n [showClear]=\"true\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Any gene (every observation)\"\n [selectedItemsLabel]=\"'{0} genes'\"\n [maxSelectedLabels]=\"3\"\n styleClass=\"sc-full\"\n >\n <!-- A picked gene shows its icon in its colour; clicking it edits them. -->\n <ng-template let-option pTemplate=\"item\">\n <div class=\"sc-obs-gene-option\">\n <span class=\"sc-obs-gene-name\">{{ option.label }}</span>\n <ng-container *ngIf=\"isObservationGene(option.value)\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: option.value }\"></ng-container>\n </ng-container>\n </div>\n </ng-template>\n </p-multiSelect>\n <p-selectButton\n *ngIf=\"view.observationGenes.length > 1\"\n [options]=\"matchOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenesMatch\"\n (ngModelChange)=\"onObservationGenesMatch($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <!-- The picked genes: each one's icon and colour (click to edit), and a way out. -->\n <div class=\"sc-obs-genes\" *ngIf=\"view.observationGenes.length\">\n <div class=\"sc-obs-gene-row\" *ngFor=\"let g of view.observationGenes; trackBy: trackByGene\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: g }\"></ng-container>\n <span class=\"sc-obs-gene-name\">{{ g }}</span>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n [attr.aria-label]=\"'Stop showing ' + g\"\n pTooltip=\"Remove\"\n tooltipPosition=\"top\"\n (click)=\"removeObservationGene(g)\"\n >\n <i class=\"pi pi-times\"></i>\n </button>\n </div>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.observationGenes.length\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.observationGenesColorBy\"\n (onChange)=\"onObservationGenesColorBy($event.checked)\"\n label=\"Colour by these genes\"\n pTooltip=\"Each observation in the colour (and, in 2D, the icon) of the picked gene it expresses most, relative to that gene's highest value. Off, Colour by column or gene applies.\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.observationGenes.length\">{{ geneFilterHint }}</small>\n <!-- The filter is never dropped silently: a failed expression read keeps the observations\n as they were drawn, and says so. -->\n <small class=\"sc-hint sc-error\" *ngIf=\"view.observationGenes.length && (geneFilterFailed$ | async)\">\n The expression of the picked genes could not be read, so the observations still show the previous\n filter.\n <a href=\"\" (click)=\"$event.preventDefault(); retryGeneFilter()\">Try again</a>\n </small>\n </div>\n </ng-container>\n\n <spatial-key\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n (categoryClicked)=\"categoryClicked.emit($event)\"\n ></spatial-key>\n\n <!-- Display -->\n <div class=\"sc-section\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Point size</span>\n <p-slider\n [ngModel]=\"view.pointScale\"\n [min]=\"0.25\"\n [max]=\"5\"\n [step]=\"0.25\"\n (onChange)=\"onPointScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.pointScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.opacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.opacity }}</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Only for a gene: the map draws that gene's expression as a field. In 2D\n it goes under the cells of the displayed plane; in 3D it is one sheet per\n imaged section, optionally smoothed along z into a volume. -->\n <div class=\"sc-section\" *ngIf=\"canMapGene\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMap\"\n (onChange)=\"onGeneMap($event.checked)\"\n label=\"Gene map (expression field)\"\n [pTooltip]=\"\n is3d\n ? 'Draw this gene\\'s expression as one field per imaged section \u2014 an estimate between the cells, empty where nothing was measured'\n : 'Draw this gene\\'s expression as a smooth field UNDER the cells \u2014 an estimate between them, transparent where nothing was measured'\n \"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- 3D only: interpolating along z turns the measured sheets into an\n estimate, so it is its own decision and its own label. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMapVolume\"\n (onChange)=\"onGeneMapVolume($event.checked)\"\n label=\"Volume rendering (interpolate along z)\"\n pTooltip=\"Smooth the per-section fields into a continuous volume \u2014 an ESTIMATE between the imaged sections, not measured tissue\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Meaningless while interpolating: a volume built from one section would\n smear that slide through the specimen's whole depth. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"geneMapOneSection\"\n (onChange)=\"onGeneMapOneSection($event.checked)\"\n label=\"One map section at a time\"\n pTooltip=\"Draw a single imaged section's field instead of the whole stack of sheets\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume && geneMapOneSection\">\n <span class=\"sc-lbl\">Map section</span>\n <p-slider\n [ngModel]=\"view.geneMapSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onGeneMapSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ geneMapSectionLabel }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.geneMapSmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onGeneMapSmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapSmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Map opacity</span>\n <p-slider\n [ngModel]=\"view.geneMapOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onGeneMapOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapOpacity }}</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && !is3d\">\n Kernel-weighted mean per cell, not a sum \u2014 so a crowded region does not glow for being crowded. Transparent\n where no cell was measured. Turn the cells' own Opacity down to read the field under them.\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && is3d\">\n {{ geneMapVolumeNote }}\n </div>\n </div>\n\n <!-- 3D only: the volume, the cloud and the density volumes occupy the same\n space, so each hides the others. Independent toggles, because the useful\n views are the combinations \u2014 the field alone, one section's cells over it,\n the anatomy alone. -->\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <span class=\"sc-lbl\">Show</span>\n <div class=\"sc-row\" *ngIf=\"ds.volume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showVolume\"\n (onChange)=\"onShowVolume($event.checked)\"\n label=\"Reference volume\"\n pTooltip=\"The registered anatomy the observations sit in\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- The backdrop's own opacity: reading the cloud or a density field\n through the anatomy means turning the anatomy down, not the data. -->\n <div class=\"sc-row\" *ngIf=\"ds.volume && view.showVolume\">\n <span class=\"sc-lbl\">Volume opacity</span>\n <p-slider\n [ngModel]=\"view.volumeOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onVolumeOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.volumeOpacity }}</span>\n </div>\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (onChange)=\"onShowPoints($event.checked)\"\n label=\"Observations\"\n pTooltip=\"The measured cells. Hide them to read the density volumes underneath\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Only for data that really is sectioned; a continuous z has no sections\n to step through. -->\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"oneSection\"\n (onChange)=\"onOneSection($event.checked)\"\n label=\"One section at a time\"\n pTooltip=\"Draw a single imaged section's cells, so the density volumes between the sections stay visible\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned && oneSection\">\n <span class=\"sc-lbl\">Section</span>\n <p-slider\n [ngModel]=\"view.pointSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onPointSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ sectionLabel }}</span>\n </div>\n </div>\n\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.densityVolume\"\n (onChange)=\"onDensityVolume($event.checked)\"\n label=\"Cluster density volumes\"\n pTooltip=\"Raymarch each cluster as a smooth density field \u2014 an ESTIMATE between the imaged sections, not measured cells\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.densityVolume\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.densitySmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onDensitySmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.densitySmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.densityVolume\">{{ densityNote }}</div>\n </div>\n\n <!-- Continuous-only knobs: meaningless for a categorical palette. -->\n <div class=\"sc-section\" *ngIf=\"isContinuous\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.logScale\"\n (onChange)=\"onLogScale($event.checked)\"\n label=\"Log scale\"\n pTooltip=\"Counts collapse against their outliers on a linear ramp\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clip outliers</span>\n <p-dropdown\n [options]=\"clipOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.percentileClip\"\n (ngModelChange)=\"onClip($event)\"\n appendTo=\"body\"\n pTooltip=\"Percentile the contrast window is taken at\"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- Reserves the readout column this row has no readout for, so the\n control's right edge lines up with every slider's. -->\n <span class=\"sc-val\"></span>\n </div>\n </div>\n\n <!-- A gene's icon in its colour: the button that opens its icon and colour picker. -->\n <ng-template #geneMark let-g>\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"geneColorOf(g)\"\n [attr.aria-label]=\"'Icon and colour for ' + g\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, g, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"glyphPointsOf(g)\"></polygon>\n </svg>\n </button>\n </ng-template>\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as g\"\n [gene]=\"g\"\n what=\"Observation\"\n [color]=\"geneColorOf(g)\"\n [glyph]=\"geneGlyphOf(g)\"\n iconHint=\"Icons show on the 2D observation markers.\"\n (colorChange)=\"onGeneColor(g, $event)\"\n (glyphChange)=\"onGeneGlyph(g, $event)\"\n (resetStyle)=\"resetGeneStyle(g)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-obs-genes{display:flex;flex-direction:column;gap:.15rem;margin-top:.35rem}.sc-obs-gene-row,.sc-obs-gene-option{display:flex;align-items:center;gap:.4rem;min-width:0}.sc-obs-gene-option{flex:1 1 auto;justify-content:space-between}.sc-obs-gene-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
24786
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"dataset as ds\">\n <!-- Observations: one marker per observation (cell centroid, Visium spot), and what\n colours them. The lead section for datasets without cell outlines. -->\n <div class=\"sc-panel\" [class.open]=\"open\">\n <div class=\"sc-panel-head\" (click)=\"openChange.emit(!open)\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowPoints($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Observations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open\" [class.pi-chevron-right]=\"!open\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open\">\n <small class=\"sc-hint\" *ngIf=\"hasCells && !is3d\">\n One circle per cell, at every zoom. Cell outlines are drawn over the circles once cells are big enough on\n screen to outline.\n </small>\n <!-- Colour source. Column and gene are mutually exclusive: picking one\n clears the other, so what drives the colours is never ambiguous. -->\n <div class=\"sc-section\">\n <label class=\"sc-lbl\" for=\"sc-column\">Colour by column</label>\n <p-dropdown\n inputId=\"sc-column\"\n [options]=\"columnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedColumn\"\n (ngModelChange)=\"onColumn($event)\"\n placeholder=\"None (flat colour)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n </div>\n\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-gene\">Colour by gene</label>\n <!-- A filterable dropdown, not a free-text box: the genes are visible before\n anything is typed, and each keystroke narrows a list the user can see.\n Virtual-scrolled because a targeted panel runs to a few thousand names. -->\n <p-dropdown\n inputId=\"sc-gene\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedGene\"\n (ngModelChange)=\"onGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [showClear]=\"true\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Search {{ ds.features.count | number }} genes\u2026\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n <small class=\"sc-hint\" *ngIf=\"gene.failed\">Gene lookup failed.</small>\n </div>\n\n <!-- Which observations are drawn at all, by what they express. Several genes at\n once, unlike the colour, which reads one: picking genes here hides every\n observation that expresses none of them (or not all of them), and by default\n colours (and, in 2D, marks) each drawn one by its gene. -->\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-show-genes\">Show observations expressing</label>\n <p-multiSelect\n inputId=\"sc-show-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenes\"\n (onChange)=\"onObservationGenes($event.value)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onPanelShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [showToggleAll]=\"false\"\n [showClear]=\"true\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Any gene (every observation)\"\n [selectedItemsLabel]=\"'{0} genes'\"\n [maxSelectedLabels]=\"3\"\n styleClass=\"sc-full\"\n >\n <!-- A picked gene shows its icon in its colour; clicking it edits them. -->\n <ng-template let-option pTemplate=\"item\">\n <div class=\"sc-obs-gene-option\">\n <span class=\"sc-obs-gene-name\">{{ option.label }}</span>\n <ng-container *ngIf=\"isObservationGene(option.value)\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: option.value }\"></ng-container>\n </ng-container>\n </div>\n </ng-template>\n </p-multiSelect>\n <p-selectButton\n *ngIf=\"view.observationGenes.length > 1\"\n [options]=\"matchOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenesMatch\"\n (ngModelChange)=\"onObservationGenesMatch($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <!-- The picked genes: each one's icon and colour (click to edit), and a way out. -->\n <div class=\"sc-obs-genes\" *ngIf=\"view.observationGenes.length\">\n <div class=\"sc-obs-gene-row\" *ngFor=\"let g of view.observationGenes; trackBy: trackByGene\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: g }\"></ng-container>\n <span class=\"sc-obs-gene-name\">{{ g }}</span>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n [attr.aria-label]=\"'Stop showing ' + g\"\n pTooltip=\"Remove\"\n tooltipPosition=\"top\"\n (click)=\"removeObservationGene(g)\"\n >\n <i class=\"pi pi-times\"></i>\n </button>\n </div>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.observationGenes.length\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.observationGenesColorBy\"\n (onChange)=\"onObservationGenesColorBy($event.checked)\"\n label=\"Colour by these genes\"\n pTooltip=\"Each observation in the colour (and, in 2D, the icon) of the picked gene it expresses most, relative to that gene's highest value. Off, Colour by column or gene applies.\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.observationGenes.length\">{{ geneFilterHint }}</small>\n <!-- The filter is never dropped silently: a failed expression read keeps the observations\n as they were drawn, and says so. -->\n <small class=\"sc-hint sc-error\" *ngIf=\"view.observationGenes.length && (geneFilterFailed$ | async)\">\n The expression of the picked genes could not be read, so the observations still show the previous\n filter.\n <a href=\"\" (click)=\"$event.preventDefault(); retryGeneFilter()\">Try again</a>\n </small>\n </div>\n </ng-container>\n\n <spatial-key\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n (categoryClicked)=\"categoryClicked.emit($event)\"\n ></spatial-key>\n\n <!-- Display -->\n <div class=\"sc-section\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Point size</span>\n <p-slider\n [ngModel]=\"view.pointScale\"\n [min]=\"0.25\"\n [max]=\"5\"\n [step]=\"0.25\"\n (onChange)=\"onPointScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.pointScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.opacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.opacity }}</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Only for a gene: the map draws that gene's expression as a field. In 2D\n it goes under the cells of the displayed plane; in 3D it is one sheet per\n imaged section, optionally smoothed along z into a volume. -->\n <div class=\"sc-section\" *ngIf=\"canMapGene\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMap\"\n (onChange)=\"onGeneMap($event.checked)\"\n label=\"Gene map (expression field)\"\n [pTooltip]=\"\n is3d\n ? 'Draw this gene\\'s expression as one field per imaged section \u2014 an estimate between the cells, empty where nothing was measured'\n : 'Draw this gene\\'s expression as a smooth field UNDER the cells \u2014 an estimate between them, transparent where nothing was measured'\n \"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- 3D only: interpolating along z turns the measured sheets into an\n estimate, so it is its own decision and its own label. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMapVolume\"\n (onChange)=\"onGeneMapVolume($event.checked)\"\n label=\"Volume rendering (interpolate along z)\"\n pTooltip=\"Smooth the per-section fields into a continuous volume \u2014 an ESTIMATE between the imaged sections, not measured tissue\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Meaningless while interpolating: a volume built from one section would\n smear that slide through the specimen's whole depth. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"geneMapOneSection\"\n (onChange)=\"onGeneMapOneSection($event.checked)\"\n label=\"One map section at a time\"\n pTooltip=\"Draw a single imaged section's field instead of the whole stack of sheets\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume && geneMapOneSection\">\n <span class=\"sc-lbl\">Map section</span>\n <p-slider\n [ngModel]=\"view.geneMapSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onGeneMapSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ geneMapSectionLabel }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.geneMapSmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onGeneMapSmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapSmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Map opacity</span>\n <p-slider\n [ngModel]=\"view.geneMapOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onGeneMapOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapOpacity }}</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && !is3d\">\n Kernel-weighted mean per cell, not a sum \u2014 so a crowded region does not glow for being crowded. Transparent\n where no cell was measured. Turn the cells' own Opacity down to read the field under them.\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && is3d\">\n {{ geneMapVolumeNote }}\n </div>\n </div>\n\n <!-- 3D only: the volume, the cloud and the density volumes occupy the same\n space, so each hides the others. Independent toggles, because the useful\n views are the combinations \u2014 the field alone, one section's cells over it,\n the anatomy alone. -->\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <span class=\"sc-lbl\">Show</span>\n <div class=\"sc-row\" *ngIf=\"ds.volume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showVolume\"\n (onChange)=\"onShowVolume($event.checked)\"\n label=\"Reference volume\"\n pTooltip=\"The registered anatomy the observations sit in\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- The backdrop's own opacity: reading the cloud or a density field\n through the anatomy means turning the anatomy down, not the data. -->\n <div class=\"sc-row\" *ngIf=\"ds.volume && view.showVolume\">\n <span class=\"sc-lbl\">Volume opacity</span>\n <p-slider\n [ngModel]=\"view.volumeOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onVolumeOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.volumeOpacity }}</span>\n </div>\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (onChange)=\"onShowPoints($event.checked)\"\n label=\"Observations\"\n pTooltip=\"The measured cells. Hide them to read the density volumes underneath\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Only for data that really is sectioned; a continuous z has no sections\n to step through. -->\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"oneSection\"\n (onChange)=\"onOneSection($event.checked)\"\n label=\"One section at a time\"\n pTooltip=\"Draw a single imaged section's cells, so the density volumes between the sections stay visible\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned && oneSection\">\n <span class=\"sc-lbl\">Section</span>\n <p-slider\n [ngModel]=\"view.pointSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onPointSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ sectionLabel }}</span>\n </div>\n </div>\n\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.densityVolume\"\n (onChange)=\"onDensityVolume($event.checked)\"\n label=\"Cluster density volumes\"\n pTooltip=\"Raymarch each cluster as a smooth density field \u2014 an ESTIMATE between the imaged sections, not measured cells\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.densityVolume\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.densitySmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onDensitySmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.densitySmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.densityVolume\">{{ densityNote }}</div>\n </div>\n\n <!-- Continuous-only knobs: meaningless for a categorical palette. -->\n <div class=\"sc-section\" *ngIf=\"isContinuous\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.logScale\"\n (onChange)=\"onLogScale($event.checked)\"\n label=\"Log scale\"\n pTooltip=\"Counts collapse against their outliers on a linear ramp\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clip outliers</span>\n <p-dropdown\n [options]=\"clipOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.percentileClip\"\n (ngModelChange)=\"onClip($event)\"\n appendTo=\"body\"\n pTooltip=\"Percentile the contrast window is taken at\"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- Reserves the readout column this row has no readout for, so the\n control's right edge lines up with every slider's. -->\n <span class=\"sc-val\"></span>\n </div>\n </div>\n\n <!-- A gene's icon in its colour: the button that opens its icon and colour picker. -->\n <ng-template #geneMark let-g>\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"geneColorOf(g)\"\n [attr.aria-label]=\"'Icon and colour for ' + g\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, g, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"glyphPointsOf(g)\"></polygon>\n </svg>\n </button>\n </ng-template>\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as g\"\n [gene]=\"g\"\n what=\"Observation\"\n [color]=\"geneColorOf(g)\"\n [glyph]=\"geneGlyphOf(g)\"\n iconHint=\"Icons show on the 2D observation markers.\"\n (colorChange)=\"onGeneColor(g, $event)\"\n (glyphChange)=\"onGeneGlyph(g, $event)\"\n (resetStyle)=\"resetGeneStyle(g)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-obs-genes{display:flex;flex-direction:column;gap:.15rem;margin-top:.35rem}.sc-obs-gene-row,.sc-obs-gene-option{display:flex;align-items:center;gap:.4rem;min-width:0}.sc-obs-gene-option{flex:1 1 auto;justify-content:space-between}.sc-obs-gene-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
24655
24787
  }], propDecorators: { controls: [{
24656
24788
  type: Input
24657
24789
  }], dataset: [{
@@ -24984,6 +25116,10 @@ class SpatialControlsComponent {
24984
25116
  get hasTranscripts() {
24985
25117
  return !!this.dataset?.transcriptTiles || !!this.dataset?.density;
24986
25118
  }
25119
+ /** Whether the 3D cloud can draw the transcripts too: they carry z (a MERFISH volume's do). */
25120
+ get hasTranscripts3d() {
25121
+ return !!this.dataset?.transcriptTiles?.hasZ;
25122
+ }
24987
25123
  onShowImage(on) {
24988
25124
  this.controls?.setViewState({ showImage: on });
24989
25125
  }
@@ -25019,7 +25155,7 @@ class SpatialControlsComponent {
25019
25155
  return this.key.refresh(this.controls, this.dataset, this.view, this.colormap, this.reverse);
25020
25156
  }
25021
25157
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialControlsComponent, deps: [{ token: VISUALIZER }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); }
25022
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialControlsComponent, isStandalone: true, selector: "spatial-controls", inputs: { visible: "visible", is3d: "is3d" }, outputs: { visibleChange: "visibleChange" }, viewQueries: [{ propertyName: "charts", first: true, predicate: SpatialChartsComponent, descendants: true }], ngImport: i0, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n [loading]=\"selectingFromRegions\"\n (onClick)=\"selectFromRegions()\"\n [pTooltip]=\"\n selectsClassVolumes\n ? 'Select every observation inside the volumes of the classes picked in the Region Editor'\n : 'Select every observation inside the drawn regions'\n \"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\">\n No observations fell inside the\n {{ missedMode === 'class-volumes' ? 'picked classes' : 'drawn regions' }}.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectsClassVolumes\">\n Selects inside the volume of {{ pickedClasses[0]\n }}{{ pickedClasses.length > 1 ? ' and ' + (pickedClasses.length - 1 | number) + ' more classes' : '' }},\n picked in the Region Editor. Draw a region to select by it instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes && pickedClasses.length\">\n The classes picked in the Region Editor are not on the reference volume shown here, so this selects by\n the regions drawn over the cloud instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n <ng-container *ngIf=\"!pickedClasses.length\">\n Pick classes in the Region Editor's class tree to select inside their volumes.\n </ng-container>\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i5.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialCellsPanelComponent, selector: "spatial-cells-panel", inputs: ["controls", "dataset", "view", "genes", "colorBarCss", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialTranscriptsPanelComponent, selector: "spatial-transcripts-panel", inputs: ["controls", "dataset", "view", "genes", "estimate", "densityStats", "geneCounts", "colormapOptions", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialObservationsPanelComponent, selector: "spatial-observations-panel", inputs: ["controls", "dataset", "view", "genes", "legend", "colorBarCss", "selection", "selectedCategory", "colormapOptions", "is3d", "open"], outputs: ["openChange", "categoryClicked"] }, { kind: "component", type: SpatialChartsComponent, selector: "spatial-charts", inputs: ["active"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
25158
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialControlsComponent, isStandalone: true, selector: "spatial-controls", inputs: { visible: "visible", is3d: "is3d" }, outputs: { visibleChange: "visibleChange" }, viewQueries: [{ propertyName: "charts", first: true, predicate: SpatialChartsComponent, descendants: true }], ngImport: i0, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && (!is3d || hasTranscripts3d)\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n [loading]=\"selectingFromRegions\"\n (onClick)=\"selectFromRegions()\"\n [pTooltip]=\"\n selectsClassVolumes\n ? 'Select every observation inside the volumes of the classes picked in the Region Editor'\n : 'Select every observation inside the drawn regions'\n \"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\">\n No observations fell inside the\n {{ missedMode === 'class-volumes' ? 'picked classes' : 'drawn regions' }}.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectsClassVolumes\">\n Selects inside the volume of {{ pickedClasses[0]\n }}{{ pickedClasses.length > 1 ? ' and ' + (pickedClasses.length - 1 | number) + ' more classes' : '' }},\n picked in the Region Editor. Draw a region to select by it instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes && pickedClasses.length\">\n The classes picked in the Region Editor are not on the reference volume shown here, so this selects by\n the regions drawn over the cloud instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n <ng-container *ngIf=\"!pickedClasses.length\">\n Pick classes in the Region Editor's class tree to select inside their volumes.\n </ng-container>\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i5.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialCellsPanelComponent, selector: "spatial-cells-panel", inputs: ["controls", "dataset", "view", "genes", "colorBarCss", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialTranscriptsPanelComponent, selector: "spatial-transcripts-panel", inputs: ["controls", "dataset", "view", "genes", "estimate", "densityStats", "geneCounts", "colormapOptions", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialObservationsPanelComponent, selector: "spatial-observations-panel", inputs: ["controls", "dataset", "view", "genes", "legend", "colorBarCss", "selection", "selectedCategory", "colormapOptions", "is3d", "open"], outputs: ["openChange", "categoryClicked"] }, { kind: "component", type: SpatialChartsComponent, selector: "spatial-charts", inputs: ["active"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
25023
25159
  }
25024
25160
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialControlsComponent, decorators: [{
25025
25161
  type: Component,
@@ -25034,7 +25170,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
25034
25170
  SpatialTranscriptsPanelComponent,
25035
25171
  SpatialObservationsPanelComponent,
25036
25172
  SpatialChartsComponent,
25037
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n [loading]=\"selectingFromRegions\"\n (onClick)=\"selectFromRegions()\"\n [pTooltip]=\"\n selectsClassVolumes\n ? 'Select every observation inside the volumes of the classes picked in the Region Editor'\n : 'Select every observation inside the drawn regions'\n \"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\">\n No observations fell inside the\n {{ missedMode === 'class-volumes' ? 'picked classes' : 'drawn regions' }}.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectsClassVolumes\">\n Selects inside the volume of {{ pickedClasses[0]\n }}{{ pickedClasses.length > 1 ? ' and ' + (pickedClasses.length - 1 | number) + ' more classes' : '' }},\n picked in the Region Editor. Draw a region to select by it instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes && pickedClasses.length\">\n The classes picked in the Region Editor are not on the reference volume shown here, so this selects by\n the regions drawn over the cloud instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n <ng-container *ngIf=\"!pickedClasses.length\">\n Pick classes in the Region Editor's class tree to select inside their volumes.\n </ng-container>\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"] }]
25173
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && (!is3d || hasTranscripts3d)\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n [loading]=\"selectingFromRegions\"\n (onClick)=\"selectFromRegions()\"\n [pTooltip]=\"\n selectsClassVolumes\n ? 'Select every observation inside the volumes of the classes picked in the Region Editor'\n : 'Select every observation inside the drawn regions'\n \"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\">\n No observations fell inside the\n {{ missedMode === 'class-volumes' ? 'picked classes' : 'drawn regions' }}.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectsClassVolumes\">\n Selects inside the volume of {{ pickedClasses[0]\n }}{{ pickedClasses.length > 1 ? ' and ' + (pickedClasses.length - 1 | number) + ' more classes' : '' }},\n picked in the Region Editor. Draw a region to select by it instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes && pickedClasses.length\">\n The classes picked in the Region Editor are not on the reference volume shown here, so this selects by\n the regions drawn over the cloud instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n <ng-container *ngIf=\"!pickedClasses.length\">\n Pick classes in the Region Editor's class tree to select inside their volumes.\n </ng-container>\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"] }]
25038
25174
  }], ctorParameters: () => [{ type: undefined, decorators: [{
25039
25175
  type: Inject,
25040
25176
  args: [VISUALIZER]
@@ -39247,6 +39383,9 @@ class SpatialControlsFacade {
39247
39383
  // The gene filter names the old dataset's genes: kept, it could hide every observation.
39248
39384
  if (current.observationGenes?.length)
39249
39385
  ctx.store.setSpatialView({ observationGenes: [] });
39386
+ // Group colours are chosen for this dataset's groupings; another's same-named column is not them.
39387
+ if (Object.keys(current.groupColors ?? {}).length)
39388
+ ctx.store.setSpatialView({ groupColors: {} });
39250
39389
  });
39251
39390
  this.controls = this.build();
39252
39391
  }
@@ -39329,7 +39468,7 @@ class SpatialControlsFacade {
39329
39468
  }
39330
39469
  // Resolved by the SAME function the renderer uses, so a legend swatch
39331
39470
  // can never disagree with the colour on screen.
39332
- return resolveCategoryColors(column.meta);
39471
+ return categoryColorsFor(name, column.meta, this.ctx.store.currentSpatialView().groupColors);
39333
39472
  },
39334
39473
  continuousValues: async (source) => {
39335
39474
  if (source.kind === 'feature')
@@ -39348,7 +39487,7 @@ class SpatialControlsFacade {
39348
39487
  return {
39349
39488
  name,
39350
39489
  categories: column.meta.categories,
39351
- colors: resolveCategoryColors(column.meta),
39490
+ colors: categoryColorsFor(name, column.meta, this.ctx.store.currentSpatialView().groupColors),
39352
39491
  codes: column.codes,
39353
39492
  };
39354
39493
  },
@@ -42440,8 +42579,10 @@ const REGION_MESH_DIMMED_OPACITY = 0.12;
42440
42579
  /**
42441
42580
  * The region reconstructions shown in a volume view: one translucent surface per region, in its
42442
42581
  * colour, placed in the volume's world box (pixel x, y and slice z mapped onto it the way the
42443
- * volume maps its voxels) and re-placed when the volume's Z is stretched. A surface is updated in
42444
- * place (`setData`, `color`) rather than added again, and picked by napari-js (`pickSurface`).
42582
+ * volume maps its voxels). The box's Z stretch is the surfaces' `scale3d` (a uniform), so a stretch
42583
+ * alone changes no geometry; a new box re-places them. A surface is updated in place (`setData`,
42584
+ * `color`, `scale3d`) rather than added again, and picked by napari-js (`pickSurface`), which
42585
+ * picks it where it is drawn.
42445
42586
  *
42446
42587
  * Highlighting is focus + context: the highlighted surfaces turn near opaque and the others fade
42447
42588
  * to a ghost, so the selection stands out while staying in place among its neighbours.
@@ -42461,13 +42602,30 @@ class VolumeRegionMeshes {
42461
42602
  this.wanted = meshes;
42462
42603
  this.sync();
42463
42604
  }
42464
- /** Follow the volume's frame (it opened, or its Z was stretched): re-place every surface. */
42605
+ /**
42606
+ * Follow the volume's frame. A new box (it opened, another volume) re-places every surface; a Z
42607
+ * stretch of the same box only sets their `scale3d`, so nothing is re-uploaded while dragging.
42608
+ */
42465
42609
  setFrame(frame) {
42610
+ const placed = this.frame ? baseFrame(this.frame) : null;
42611
+ const next = baseFrame(frame);
42612
+ const sameBox = !!placed &&
42613
+ placed.size.every((v, i) => v === next.size[i]) &&
42614
+ placed.world.every((v, i) => Math.abs(v - next.world[i]) <= 1e-9 * Math.max(1, Math.abs(v)));
42466
42615
  this.frame = frame;
42467
- for (const e of this.layers.values())
42468
- e.layer.setData(toWorld(e.mesh.vertices, frame), e.mesh.faces);
42616
+ const scale = this.scale3d();
42617
+ for (const e of this.layers.values()) {
42618
+ if (!sameBox)
42619
+ e.layer.setData(toWorld(e.mesh.vertices, next), e.mesh.faces);
42620
+ e.layer.scale3d = scale;
42621
+ }
42469
42622
  this.sync();
42470
42623
  }
42624
+ /** The surfaces' GPU scale: the frame's Z stretch. */
42625
+ scale3d() {
42626
+ const s = this.frame?.zScale;
42627
+ return [1, 1, s && s > 0 ? s : 1];
42628
+ }
42471
42629
  /** Highlight the surfaces of these keys (none: every surface as usual). */
42472
42630
  setHighlight(keys) {
42473
42631
  this.highlighted = new Set(keys);
@@ -42519,7 +42677,7 @@ class VolumeRegionMeshes {
42519
42677
  }
42520
42678
  // Rebuilt or recoloured: the same surface, updated in place.
42521
42679
  if (want.vertices !== e.mesh.vertices && this.frame) {
42522
- e.layer.setData(toWorld(want.vertices, this.frame), want.faces);
42680
+ e.layer.setData(toWorld(want.vertices, baseFrame(this.frame)), want.faces);
42523
42681
  }
42524
42682
  if (want.color !== e.mesh.color)
42525
42683
  e.layer.color = want.color;
@@ -42534,8 +42692,9 @@ class VolumeRegionMeshes {
42534
42692
  }
42535
42693
  add(mesh) {
42536
42694
  const frame = this.frame;
42537
- const layer = this.viewer.addSurface(toWorld(mesh.vertices, frame), mesh.faces, undefined, {
42695
+ const layer = this.viewer.addSurface(toWorld(mesh.vertices, baseFrame(frame)), mesh.faces, undefined, {
42538
42696
  name: `region ${mesh.key}`,
42697
+ scale3d: this.scale3d(),
42539
42698
  color: mesh.color,
42540
42699
  opacity: this.opacityOf(mesh.key),
42541
42700
  blending: 'translucent',
@@ -42554,6 +42713,11 @@ class VolumeRegionMeshes {
42554
42713
  this.layers.delete(key);
42555
42714
  }
42556
42715
  }
42716
+ /** `frame` without its Z stretch: the box the surfaces' geometry is placed in. */
42717
+ function baseFrame(frame) {
42718
+ const s = frame.zScale && frame.zScale > 0 ? frame.zScale : 1;
42719
+ return s === 1 ? frame : { size: frame.size, world: [frame.world[0], frame.world[1], frame.world[2] / s] };
42720
+ }
42557
42721
  /**
42558
42722
  * Image px / slice coordinates → the volume's centred world box: pixel x of a `w`-wide image
42559
42723
  * spans the box's width, slice k sits at the middle of its share of the box's depth.
@@ -42800,7 +42964,11 @@ class VolumeScene {
42800
42964
  if (!base || !this.fullSize)
42801
42965
  return;
42802
42966
  const depth = base.depth * this.ctx.settings.volumeZScale;
42803
- const frame = { size: this.fullSize, world: [base.width, base.height, depth] };
42967
+ const frame = {
42968
+ size: this.fullSize,
42969
+ world: [base.width, base.height, depth],
42970
+ zScale: this.ctx.settings.volumeZScale,
42971
+ };
42804
42972
  this.regionMeshes.setFrame(frame);
42805
42973
  this.slicePlanes.setFrame(frame);
42806
42974
  }
@@ -43491,6 +43659,13 @@ const SPATIAL_FALLBACK_RADIUS = 4;
43491
43659
  * 40 µm/px template, so drawing them to scale would put every cell a fifth of a pixel wide
43492
43660
  * and the section would come up empty. The point-size control scales up from this floor. */
43493
43661
  const SPATIAL_SLICE_MIN_DIAMETER_PX = 1.5;
43662
+ /**
43663
+ * Smallest drawn marker diameter in the 2D view, in SCREEN pixels, at any zoom. A marker's size is
43664
+ * the cell's own (data units), so zoomed out over a whole Cytos plate or Xenium section every cell
43665
+ * fell below a pixel and the observations vanished; with the floor they stay a dense field of
43666
+ * dots, coloured as they are, at every zoom (the GPU applies it per frame: no rebuild on zoom).
43667
+ */
43668
+ const SPATIAL_MARKER_MIN_SCREEN_PX = 2;
43494
43669
  /** Longest side of the gene-map raster, in field pixels. A gene map is a smooth
43495
43670
  * field read as territory, so it gains nothing from matching a 2 Gpx slide's
43496
43671
  * resolution — and the estimate costs one pass over this many pixels. */
@@ -43658,9 +43833,9 @@ function totalDensityGroup(selection) {
43658
43833
  * {@link DENSITY_MAX_CLUSTERS}, each with its legend colour — restricted to the selection when
43659
43834
  * there is one, so "select a region, check the box" answers which clusters live there.
43660
43835
  */
43661
- function rankDensityGroups(columnName, column, count, selection) {
43836
+ function rankDensityGroups(columnName, column, count, selection, groupColors) {
43662
43837
  const inSelection = selection.count > 0 ? selection.mask : null;
43663
- const colors = resolveCategoryColors(column.meta);
43838
+ const colors = categoryColorsFor(columnName, column.meta, groupColors);
43664
43839
  const counts = new Uint32Array(column.meta.categories.length);
43665
43840
  for (let i = 0; i < count; i++) {
43666
43841
  if (inSelection && !inSelection[i])
@@ -44187,7 +44362,7 @@ class CellLayers {
44187
44362
  * value, and a selection or opacity change recolours with the vector unchanged (SPATIAL-12). */
44188
44363
  this.contrastWindows = new ContrastWindowCache();
44189
44364
  }
44190
- /** True while outlines are on screen — the service hides its dots then. */
44365
+ /** True while outlines are on screen. */
44191
44366
  get outlinesShown() {
44192
44367
  return this.polygonsShown;
44193
44368
  }
@@ -44237,6 +44412,8 @@ class CellLayers {
44237
44412
  JSON.stringify(view.continuousColormap),
44238
44413
  view.cellDraw,
44239
44414
  view.cellOpacity,
44415
+ // Any grouping's: the cells may be coloured by segmentation method rather than the cell type.
44416
+ JSON.stringify(view.groupColors),
44240
44417
  ].join('|');
44241
44418
  const present = ['cellFill', 'cellOutline', 'nucleusOutline'].some((g) => this.groups.shown(g));
44242
44419
  if (!geometryChanged && styleKey === this.cellStyleKey && present) {
@@ -44334,8 +44511,8 @@ class CellLayers {
44334
44511
  ? 'segmentation_method'
44335
44512
  : cellTypeColumnFor(dataset, view);
44336
44513
  const codes = name ? await this.lookup.codes(name) : null;
44337
- const palette = categoryRgba(codes?.meta ?? null);
44338
- const missing = rgba01(MISSING_COLOR);
44514
+ const palette = categoryRgba(name, codes?.meta ?? null, view.groupColors);
44515
+ const missing = rgba01$1(MISSING_COLOR);
44339
44516
  // One RGBA per shape (napari Shapes' `face_color`): any number of categories keeps its own
44340
44517
  // colour, where a colormap band per category ran out of LUT entries past ~127.
44341
44518
  const faceColor = new Float32Array(owners.length * 4);
@@ -44415,13 +44592,15 @@ class CellLayers {
44415
44592
  }
44416
44593
  }
44417
44594
  /** An 8-bit RGB as an opaque 0..1 RGBA. */
44418
- function rgba01([r, g, b]) {
44595
+ function rgba01$1([r, g, b]) {
44419
44596
  return [r / 255, g / 255, b / 255, 1];
44420
44597
  }
44421
44598
  /** A grouping's category colours, as 0..1 RGBA by category code (one default without a grouping). */
44422
- function categoryRgba(meta) {
44423
- const hex = meta && meta.kind === 'categorical' ? resolveCategoryColors(meta) : [DEFAULT_CATEGORICAL_PALETTE[0]];
44424
- return hex.map((h) => rgba01(parseHex(h)));
44599
+ function categoryRgba(name, meta, overrides) {
44600
+ const hex = name && meta && meta.kind === 'categorical'
44601
+ ? categoryColorsFor(name, meta, overrides)
44602
+ : [DEFAULT_CATEGORICAL_PALETTE[0]];
44603
+ return hex.map((h) => rgba01$1(parseHex(h)));
44425
44604
  }
44426
44605
 
44427
44606
  /**
@@ -45416,6 +45595,7 @@ function transcriptPlanKey(dataset, view, job, pxPerUnit) {
45416
45595
  view.transcriptAllGenesHidden.join(','),
45417
45596
  JSON.stringify(view.transcriptGeneColors),
45418
45597
  JSON.stringify(view.transcriptGeneGroups),
45598
+ groupColorsKey(cellTypeColumnFor(dataset, view), view.groupColors),
45419
45599
  ].join('|');
45420
45600
  }
45421
45601
  /** The view the markers are coloured by, for what was loaded. */
@@ -45434,7 +45614,7 @@ function colorViewFor(view, kind, clustered, hasCellTypes) {
45434
45614
  }
45435
45615
  return view;
45436
45616
  }
45437
- const UNASSIGNED_RGBA = [0.62, 0.62, 0.62, 0.55];
45617
+ const UNASSIGNED_RGBA$1 = [0.62, 0.62, 0.62, 0.55];
45438
45618
  /**
45439
45619
  * The transcript markers of the 2D spatial view: runs the planner's job, drops hidden groups
45440
45620
  * and genes, groups a selection under the marker budget, colours the entries and draws them as
@@ -45636,8 +45816,8 @@ class TranscriptLayers {
45636
45816
  const name = cellTypeColumnFor(dataset, view);
45637
45817
  const codes = name ? await this.lookup.codes(name).catch(() => null) : null;
45638
45818
  const cellColor = new Map();
45639
- if (codes?.meta.kind === 'categorical') {
45640
- const colors = resolveCategoryColors(codes.meta);
45819
+ if (name && codes?.meta.kind === 'categorical') {
45820
+ const colors = categoryColorsFor(name, codes.meta, view.groupColors);
45641
45821
  codes.meta.categories.forEach((c, k) => cellColor.set(c, colors[k]));
45642
45822
  }
45643
45823
  const colors = clusterColorMap(view.transcriptGenes, view.transcriptGeneGroups, cellColor, DEFAULT_CATEGORICAL_PALETTE);
@@ -45672,7 +45852,10 @@ class TranscriptLayers {
45672
45852
  else {
45673
45853
  const name = cellTypeColumnFor(dataset, view);
45674
45854
  const codes = name ? await this.lookup.codes(name) : null;
45675
- rgb = codes?.meta.kind === 'categorical' ? resolveCategoryColors(codes.meta).map(parseHex) : [];
45855
+ rgb =
45856
+ name && codes?.meta.kind === 'categorical'
45857
+ ? categoryColorsFor(name, codes.meta, view.groupColors).map(parseHex)
45858
+ : [];
45676
45859
  codeOf = (i) => {
45677
45860
  const o = t.observation[i];
45678
45861
  if (!codes || o === NO_OBSERVATION)
@@ -45685,7 +45868,7 @@ class TranscriptLayers {
45685
45868
  const tuples = rgb.map(([r, g, b]) => [r / 255, g / 255, b / 255, 1]);
45686
45869
  for (let i = 0; i < t.count; i++) {
45687
45870
  const c = codeOf(i);
45688
- rgba.set(c < 0 ? UNASSIGNED_RGBA : tuples[c], i * 4);
45871
+ rgba.set(c < 0 ? UNASSIGNED_RGBA$1 : tuples[c], i * 4);
45689
45872
  }
45690
45873
  return rgba;
45691
45874
  }
@@ -45718,11 +45901,10 @@ const MAX_TILE_RETRIES = 3;
45718
45901
  * -----------
45719
45902
  * Cell fill, cell outline, nucleus outline, the transcript density, then transcripts on top
45720
45903
  * ({@link TILE_LAYER_ORDER}); a plan restores the order once, after all its groups are in
45721
- * place. The service's observation markers sit under all of them; while outlines are drawn the
45722
- * markers are hidden (a cell is its outline then, not a dot), so the cells never have to
45723
- * be ordered against them. The density goes OVER the markers on purpose: under 10^5
45724
- * dots it would only show in the gaps between cells. {@link afterObservations} restores
45725
- * the order after the service re-adds its markers.
45904
+ * place. The service's observation markers sit under all of them, drawn at every zoom: once
45905
+ * cells are big enough to outline, their outlines go over their markers. The density goes
45906
+ * OVER the markers on purpose: under 10^5 dots it would only show in the gaps between cells.
45907
+ * {@link afterObservations} restores the order after the service re-adds its markers.
45726
45908
  *
45727
45909
  * ONE LAYER PER GROUP
45728
45910
  * -------------------
@@ -45794,7 +45976,7 @@ class NapariSpatialTileLayers {
45794
45976
  owns(layer) {
45795
45977
  return this.groups.owns(layer);
45796
45978
  }
45797
- /** True while outlines are on screen — the service hides its dots then. */
45979
+ /** True while outlines are on screen. */
45798
45980
  get outlinesShown() {
45799
45981
  return this.cells.outlinesShown;
45800
45982
  }
@@ -46200,13 +46382,13 @@ class Spatial2dScene extends SpatialSceneBase {
46200
46382
  return this.drawn ? this.drawn[k] : k;
46201
46383
  }
46202
46384
  /**
46203
- * Whether the markers are drawn: when the user wants them, and not while cell outlines are on
46204
- * screen — then a cell IS its outline, and a circle on top of it is noise. Zoomed out past the
46205
- * outline threshold, the circles stand in for the cells.
46385
+ * Whether the markers are drawn: whenever the user wants them, at every zoom. Cell outlines come
46386
+ * and go with the zoom (their level of detail follows it) and are drawn over the markers; the
46387
+ * observations themselves never disappear with them.
46206
46388
  */
46207
46389
  pointsVisible() {
46208
46390
  const view = this.session.latest?.[1];
46209
- return (view?.showPoints ?? true) && !this.session.tiles?.outlinesShown;
46391
+ return view?.showPoints ?? true;
46210
46392
  }
46211
46393
  /**
46212
46394
  * Redraw the markers over a freshly rendered image. Two reasons, both invisible from the
@@ -46355,6 +46537,8 @@ class Spatial2dScene extends SpatialSceneBase {
46355
46537
  scale: ref?.scale ?? [1, 1],
46356
46538
  translate: ref?.translate ?? [0, 0],
46357
46539
  visible: this.pointsVisible(),
46540
+ // Never under a couple of screen pixels, so the observations stay visible zoomed out.
46541
+ minScreenSize: SPATIAL_MARKER_MIN_SCREEN_PX,
46358
46542
  });
46359
46543
  // Outlines, density and transcripts go back over the markers just added.
46360
46544
  this.session.tiles?.afterObservations();
@@ -46674,7 +46858,7 @@ class Spatial2dScene extends SpatialSceneBase {
46674
46858
  const column = await port.getColumn(colorBy.name);
46675
46859
  if (isCategoricalColumn(column)) {
46676
46860
  const rgba = encodeCategorical(column.codes, {
46677
- colors: resolveCategoryColors(column.meta),
46861
+ colors: categoryColorsFor(colorBy.name, column.meta, view.groupColors),
46678
46862
  opacity: view.opacity,
46679
46863
  muted,
46680
46864
  });
@@ -46759,11 +46943,317 @@ function gatherSymbols(symbols, indices) {
46759
46943
  return Uint8Array.from(indices, (i) => symbols[i]);
46760
46944
  }
46761
46945
 
46946
+ /** Most transcripts drawn in the 3D view: past it, every k-th one is drawn (and the console says so). */
46947
+ const TRANSCRIPTS_3D_MAX = 2_000_000;
46948
+ /** Most level-0 tiles read for the 3D view: a dataset needing more is too large to draw whole. */
46949
+ const TRANSCRIPTS_3D_MAX_TILES = 4096;
46950
+ /** Tile reads in flight at once. */
46951
+ const CONCURRENCY = 8;
46952
+ /** A transcript's marker in the 3D view, in screen pixels at a transcript scale of 1. */
46953
+ const TRANSCRIPT_3D_BASE_SIZE = 2;
46954
+ /** A transcript with no colour by cell type (in no cell, or a cell in no group), as the 2D markers. */
46955
+ const UNASSIGNED_RGBA = [0.62, 0.62, 0.62, 0.55];
46956
+ /** The genes a view draws in 3D, as the tile query takes them, or null when it draws none. */
46957
+ function transcripts3dGenes(dataset, view) {
46958
+ if (view.transcriptMode === 'off')
46959
+ return null;
46960
+ if (view.transcriptAllGenes) {
46961
+ if (allGenesHidden(view))
46962
+ return null;
46963
+ return { query: [ALL_GENES], allGenes: true };
46964
+ }
46965
+ const hidden = new Set(view.transcriptHiddenGenes);
46966
+ const genes = view.transcriptGenes.filter((g) => !hidden.has(g));
46967
+ return genes.length ? { query: genes, allGenes: false } : null;
46968
+ }
46969
+ /**
46970
+ * Each loaded transcript's colour, packed RGBA, as the 2D markers colour them by gene: a gene's
46971
+ * chosen colour, else the palette's by its slot (the selection's order, or with "All genes" the
46972
+ * dataset's gene code).
46973
+ */
46974
+ function transcripts3dColors(view, names, gene, count) {
46975
+ const n = names ? names.length : DEFAULT_CATEGORICAL_PALETTE.length;
46976
+ const table = Array.from({ length: n }, (_g, i) => rgba01((names && view.transcriptGeneColors[names[i]]) ||
46977
+ DEFAULT_CATEGORICAL_PALETTE[i % DEFAULT_CATEGORICAL_PALETTE.length]));
46978
+ return packed(count, (i) => table[names ? gene[i] : gene[i] % n]);
46979
+ }
46980
+ /**
46981
+ * How the panel's "Colour by" applies in 3D, as it does to the 2D markers (`colorViewFor`): by
46982
+ * cell type when the dataset has one (else by gene), by cluster for the selected genes (with "All
46983
+ * genes" a cluster is its gene), by gene otherwise.
46984
+ */
46985
+ function transcripts3dColorBy(dataset, view, allGenes) {
46986
+ const by = view.transcriptColorBy;
46987
+ if (by === 'cellType')
46988
+ return cellTypeColumnFor(dataset, view) ? 'cellType' : 'gene';
46989
+ if (by === 'cluster' && !allGenes)
46990
+ return 'cluster';
46991
+ return 'gene';
46992
+ }
46993
+ /** The colours the cells' grouping gives its groups, by name (none without a grouping). */
46994
+ async function cellGroupColors(dataset, view, port) {
46995
+ const name = cellTypeColumnFor(dataset, view);
46996
+ if (!name)
46997
+ return null;
46998
+ const column = await port.getColumn(name);
46999
+ if (!isCategoricalColumn(column))
47000
+ return null;
47001
+ const hex = categoryColorsFor(name, column.meta, view.groupColors);
47002
+ const byName = new Map(column.meta.categories.map((c, k) => [c, hex[k]]));
47003
+ return { codes: column.codes, colors: hex.map(rgba01), byName };
47004
+ }
47005
+ /** `#rrggbb` as an opaque 0..1 RGBA. */
47006
+ function rgba01(hex) {
47007
+ const [r, g, b] = parseHex(hex);
47008
+ return [r / 255, g / 255, b / 255, 1];
47009
+ }
47010
+ /** `count` colours packed RGBA, unassigned where `at` has none. */
47011
+ function packed(count, at) {
47012
+ const out = new Float32Array(count * 4);
47013
+ for (let i = 0; i < count; i++)
47014
+ out.set(at(i) ?? UNASSIGNED_RGBA, i * 4);
47015
+ return out;
47016
+ }
47017
+ /**
47018
+ * The transcripts of the 3D spatial view: every molecule of the genes the Transcripts panel draws
47019
+ * (its selected genes, or "All genes"), at its own x, y and z, in the cloud's box and Z stretch.
47020
+ *
47021
+ * The 2D view reads transcripts by level of detail for the camera; a 3D orbit has no such window,
47022
+ * so this reads the whole level 0 once per gene selection (a few hundred thousand molecules for a
47023
+ * MERFISH section) and only restyles after that. Drawn only for a dataset whose transcripts carry z.
47024
+ */
47025
+ class Spatial3dTranscripts {
47026
+ constructor(viewer, begin) {
47027
+ this.viewer = viewer;
47028
+ this.begin = begin;
47029
+ this.layer = null;
47030
+ /** What {@link layer} was loaded from (dataset, genes, quality), and styled with. */
47031
+ this.loadKey = null;
47032
+ this.styleKey = null;
47033
+ this.loaded = null;
47034
+ this.loads = new Supersede();
47035
+ }
47036
+ /** The layer drawn, if any (for tests and the scene's Z stretch). */
47037
+ get drawn() {
47038
+ return this.layer;
47039
+ }
47040
+ /** Draw what `view` asks for, loading the molecules when the genes changed. */
47041
+ async sync(dataset, view, port, frame) {
47042
+ const meta = dataset.transcriptTiles;
47043
+ const genes = meta?.hasZ && port?.getTranscriptTile ? transcripts3dGenes(dataset, view) : null;
47044
+ if (!meta || !genes || !port) {
47045
+ this.remove();
47046
+ return;
47047
+ }
47048
+ if (genes.allGenes && (meta.count ?? 0) > TRANSCRIPTS_3D_MAX) {
47049
+ console.warn(`[napari-js] ${meta.count} transcripts are too many to draw in 3D: select genes to see theirs`);
47050
+ this.remove();
47051
+ return;
47052
+ }
47053
+ const quality = view.transcriptQuality === 'all' ? 'all' : 'high';
47054
+ const loadKey = `${dataset.id}|${genes.query.join('\u0001')}|${quality}`;
47055
+ if (loadKey !== this.loadKey) {
47056
+ const task = this.loads.next();
47057
+ const [x0, y0, x1, y1] = meta.bounds;
47058
+ const keys = tilesInRect({ x0, y0, x1: x1 + 1e-6, y1: y1 + 1e-6 }, 0, meta.levels, meta.bounds);
47059
+ if (keys.length > TRANSCRIPTS_3D_MAX_TILES) {
47060
+ console.warn(`[napari-js] ${keys.length} transcript tiles are too many to read for the 3D view`);
47061
+ this.remove();
47062
+ return;
47063
+ }
47064
+ const end = this.begin('Transcripts');
47065
+ let tiles;
47066
+ try {
47067
+ tiles = await pooled(keys, CONCURRENCY, (k) => port.getTranscriptTile(0, k.gx, k.gy, { genes: genes.query, quality }));
47068
+ }
47069
+ catch (err) {
47070
+ console.warn('[napari-js] transcripts for the 3D view could not be read', err);
47071
+ // The panel asked for other transcripts than those drawn: take them down rather than leave
47072
+ // the old selection up as if it were the new one; the next change reads again. Only the
47073
+ // current load may: a superseded one must not tear down what a newer one drew.
47074
+ if (task())
47075
+ this.clearLayer();
47076
+ return;
47077
+ }
47078
+ finally {
47079
+ end();
47080
+ }
47081
+ if (!task())
47082
+ return;
47083
+ this.loadKey = loadKey;
47084
+ this.styleKey = null;
47085
+ this.loaded = { tiles, names: genes.allGenes ? (meta.genes ?? null) : genes.query };
47086
+ }
47087
+ await this.draw(dataset, view, port, frame, genes.allGenes);
47088
+ }
47089
+ /** Follow the scene's Z stretch (nothing is re-uploaded). */
47090
+ setZScale(s) {
47091
+ if (this.layer)
47092
+ this.layer.scale3d = [1, 1, s];
47093
+ }
47094
+ /** Take the layer down and forget what it showed. */
47095
+ remove() {
47096
+ this.loads.cancel();
47097
+ this.clearLayer();
47098
+ }
47099
+ clearLayer() {
47100
+ if (this.layer)
47101
+ this.viewer.layers.remove(this.layer);
47102
+ this.layer = null;
47103
+ this.loadKey = null;
47104
+ this.styleKey = null;
47105
+ this.loaded = null;
47106
+ }
47107
+ async draw(dataset, view, port, frame, allGenes) {
47108
+ const loaded = this.loaded;
47109
+ if (!loaded)
47110
+ return;
47111
+ const by = transcripts3dColorBy(dataset, view, allGenes);
47112
+ const styleKey = [
47113
+ frame.origin.join(','),
47114
+ by,
47115
+ by === 'gene' ? JSON.stringify(view.transcriptGeneColors) : '',
47116
+ by !== 'gene' ? `${cellTypeColumnFor(dataset, view)}|${JSON.stringify(view.groupColors)}` : '',
47117
+ by === 'cluster' ? JSON.stringify(view.transcriptGeneGroups) : '',
47118
+ allGenes ? view.transcriptAllGenesHidden.join(',') : '',
47119
+ ].join('|');
47120
+ const size = TRANSCRIPT_3D_BASE_SIZE * (view.transcriptScale > 0 ? view.transcriptScale : 1);
47121
+ if (this.layer && styleKey === this.styleKey) {
47122
+ this.layer.size = size;
47123
+ this.layer.opacity = view.transcriptOpacity;
47124
+ this.layer.scale3d = [1, 1, frame.zScale];
47125
+ return;
47126
+ }
47127
+ const { names } = loaded;
47128
+ const shown = allGenes && names ? allGeneShownBy(view.transcriptAllGenesHidden) : null;
47129
+ const keep = shown && names ? (g) => !!names[g] && shown(names[g]) : () => true;
47130
+ const total = loaded.tiles.reduce((n, t) => n + t.count, 0);
47131
+ const stride = Math.max(1, Math.ceil(total / TRANSCRIPTS_3D_MAX));
47132
+ if (stride > 1)
47133
+ console.warn(`[napari-js] drawing every ${stride}th of ${total} transcripts in 3D`);
47134
+ const [ox, oy, oz] = frame.origin;
47135
+ const positions = [];
47136
+ const genes = [];
47137
+ const cells = [];
47138
+ let seen = 0;
47139
+ for (const t of loaded.tiles) {
47140
+ for (let i = 0; i < t.count; i++) {
47141
+ if (!keep(t.gene[i]) || seen++ % stride)
47142
+ continue;
47143
+ positions.push(t.x[i] + ox, t.y[i] + oy, t.z[i] + oz);
47144
+ genes.push(t.gene[i]);
47145
+ cells.push(t.observation[i]);
47146
+ }
47147
+ }
47148
+ const count = genes.length;
47149
+ let colors;
47150
+ if (by === 'gene') {
47151
+ colors = transcripts3dColors(view, names, Uint16Array.from(genes), count);
47152
+ }
47153
+ else {
47154
+ const groups = await cellGroupColors(dataset, view, port).catch((err) => {
47155
+ console.warn('[napari-js] the cells grouping for the 3D transcripts could not be read', err);
47156
+ return null;
47157
+ });
47158
+ // A newer draw (another style, or the layer taken down) won while the column loaded.
47159
+ if (this.loaded !== loaded)
47160
+ return;
47161
+ if (by === 'cellType') {
47162
+ colors = packed(count, (i) => {
47163
+ const o = cells[i];
47164
+ const code = !groups || o === NO_OBSERVATION ? NO_CATEGORY : groups.codes[o];
47165
+ return code === NO_CATEGORY ? undefined : groups.colors[code];
47166
+ });
47167
+ }
47168
+ else {
47169
+ // A cluster named like a group of the cells' grouping takes that group's colour, as in 2D.
47170
+ const byCluster = clusterColorMap(view.transcriptGenes, view.transcriptGeneGroups, groups?.byName ?? new Map(), DEFAULT_CATEGORICAL_PALETTE);
47171
+ const table = (names ?? []).map((gene) => {
47172
+ const hex = byCluster.get(clusterOfGene(gene, view.transcriptGeneGroups));
47173
+ return hex ? rgba01(hex) : undefined;
47174
+ });
47175
+ colors = packed(count, (i) => table[genes[i]]);
47176
+ }
47177
+ }
47178
+ if (this.layer)
47179
+ this.viewer.layers.remove(this.layer);
47180
+ this.layer = null;
47181
+ this.styleKey = styleKey;
47182
+ if (!count)
47183
+ return;
47184
+ this.layer = this.viewer.addPoints3D(Float32Array.from(positions), new Float32Array(count), {
47185
+ name: 'transcripts',
47186
+ size,
47187
+ opacity: view.transcriptOpacity,
47188
+ scale3d: [1, 1, frame.zScale],
47189
+ colors,
47190
+ });
47191
+ }
47192
+ }
47193
+ /** `work` over `items`, at most `limit` at once, results in order. */
47194
+ async function pooled(items, limit, work) {
47195
+ const out = new Array(items.length);
47196
+ let next = 0;
47197
+ const run = async () => {
47198
+ while (next < items.length) {
47199
+ const i = next++;
47200
+ out[i] = await work(items[i]);
47201
+ }
47202
+ };
47203
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, run));
47204
+ return out;
47205
+ }
47206
+
47207
+ /** The least Z stretch: a tenth of the true depth. */
47208
+ const SPATIAL_Z_SCALE_MIN = 0.1;
47209
+ /** The most Z stretch a cloud allows, at least: the Volume view's. */
47210
+ const SPATIAL_Z_SCALE_MAX = 10;
47211
+ /** Nor more than this, however thin the cloud is. */
47212
+ const SPATIAL_Z_SCALE_CEILING = 1000;
47213
+ /**
47214
+ * The most a box's Z may be stretched: until it is twice as deep as it is wide, so even a section
47215
+ * microns thick and millimetres wide can be stood up into a block — but never less than the
47216
+ * Volume view's 10x, nor more than {@link SPATIAL_Z_SCALE_CEILING}.
47217
+ */
47218
+ function spatialZScaleMax(box) {
47219
+ const flat = (2 * Math.max(box.width, box.height)) / Math.max(box.depth, 1e-9);
47220
+ return Math.min(SPATIAL_Z_SCALE_CEILING, Math.max(SPATIAL_Z_SCALE_MAX, flat));
47221
+ }
47222
+ /**
47223
+ * The Z handle's drag (px) per doubling: the Volume view's 180 px, less when the range is wider,
47224
+ * so going from the true depth to the most stretch is about the same drag (~540 px) either way.
47225
+ */
47226
+ function spatialZPixelsPerDouble(maxScale) {
47227
+ return Math.max(60, Math.min(180, 540 / Math.log2(Math.max(2, maxScale))));
47228
+ }
47229
+ /**
47230
+ * The X/Y/Z axis-end labels for a box (as drawn: Z stretched by `zScale`), its true lengths in µm
47231
+ * when the dataset declares `micronsPerUnit`, else in its own units. A stretched Z says by how much,
47232
+ * so its length is never read off the drawing.
47233
+ */
47234
+ function spatialAxesLabels(drawn, zScale, micronsPerUnit) {
47235
+ const hx = drawn.width / 2;
47236
+ const hy = drawn.height / 2;
47237
+ const hz = drawn.depth / 2;
47238
+ const mpu = micronsPerUnit && micronsPerUnit > 0 ? micronsPerUnit : 0;
47239
+ const len = (n) => (mpu ? formatLength(n * mpu, 'µm') : `${+n.toPrecision(3)} units`);
47240
+ const stretch = Math.abs(zScale - 1) > 1e-3 ? ` (×${+zScale.toPrecision(3)})` : '';
47241
+ return [
47242
+ { anchor: [hx, -hy, -hz], text: `X · ${len(drawn.width)}`, color: '#ed4545' },
47243
+ { anchor: [-hx, hy, -hz], text: `Y · ${len(drawn.height)}`, color: '#4dd959' },
47244
+ { anchor: [-hx, -hy, hz], text: `Z · ${len(drawn.depth / zScale)}${stretch}`, color: '#668cff' },
47245
+ ];
47246
+ }
46762
47247
  /**
46763
47248
  * SPATIAL_OMICS_3D (review Appendix B, cluster K): observations as a 3D point cloud under the
46764
47249
  * orbit camera, with the dataset's reference volume around it, per-cluster density volumes and the
46765
47250
  * 3D gene map.
46766
47251
  *
47252
+ * Everything sits in one box centred on the world origin (see {@link ensureFrame}): the reference
47253
+ * volume's, else the density grid's. The Volume view's axes gizmo and Z-height handle are drawn on
47254
+ * it, and the handle stretches Z: the cloud through its `scale3d`, every volume through its voxel
47255
+ * size, so nothing is re-uploaded while dragging (see {@link setZScale}).
47256
+ *
46767
47257
  * Thinner than the 2D scene: there is no tissue image to render (a registered volume like the
46768
47258
  * Allen CCF has no single reference plane), so no readback and no navigator; the 3D scale bar
46769
47259
  * follows the dataset (see {@link installScaleBar}). The region tools work in SCREEN space through
@@ -46804,6 +47294,14 @@ class Spatial3dScene extends SpatialSceneBase {
46804
47294
  this.geneMapKey = null;
46805
47295
  /** Offset applied to observation coordinates to sit them in the volume's box. */
46806
47296
  this.origin = [0, 0, 0];
47297
+ /** The box everything sits in, before the Z stretch, centred on the origin (see ensureFrame). */
47298
+ this.box = null;
47299
+ this.boxKey = null;
47300
+ /** The axes gizmo on {@link box} and the handle that stretches its Z (see setZScale). */
47301
+ this.gizmo = null;
47302
+ this.zHandle = null;
47303
+ /** Each volume layer's own voxel size, before the Z stretch. */
47304
+ this.baseVoxels = new WeakMap();
46807
47305
  /**
46808
47306
  * napari-js's depth-aware pick over the cloud, and the projection the ROI selection reads. It
46809
47307
  * owns the caching policy: re-project lazily, on the first pick after the camera, the canvas or
@@ -46825,6 +47323,7 @@ class Spatial3dScene extends SpatialSceneBase {
46825
47323
  * this scene carries it). */
46826
47324
  this.invertSub = null;
46827
47325
  this.regionMeshes = new VolumeRegionMeshes(ctx.viewer);
47326
+ this.transcripts3d = new Spatial3dTranscripts(ctx.viewer, (label) => ctx.badge.begin(label));
46828
47327
  }
46829
47328
  /** Show these region reconstructions with the cloud (replacing the ones shown). */
46830
47329
  setRegionMeshes(meshes) {
@@ -46860,7 +47359,8 @@ class Spatial3dScene extends SpatialSceneBase {
46860
47359
  const [vx, vy, vz] = meta.voxelSize;
46861
47360
  this.regionMeshes.setFrame({
46862
47361
  size: [meta.width, meta.height, meta.depth],
46863
- world: [meta.width * vx, meta.height * vy, meta.depth * vz],
47362
+ world: [meta.width * vx, meta.height * vy, meta.depth * vz * this.zScale()],
47363
+ zScale: this.zScale(),
46864
47364
  });
46865
47365
  }
46866
47366
  /**
@@ -46898,6 +47398,64 @@ class Spatial3dScene extends SpatialSceneBase {
46898
47398
  histogram() {
46899
47399
  return null;
46900
47400
  }
47401
+ /** The toolbar's 3D controls, as the Volume view's: drag mode, re-framing and the axes. */
47402
+ surface3dControls() {
47403
+ return surface3dControls(this.ctx, () => this.gizmo);
47404
+ }
47405
+ /**
47406
+ * Stretch Z by `factor` (1 = the data's true proportions), as the Z handle drags it: the cloud
47407
+ * through its `scale3d`, each volume through its voxel size, the axes and the region surfaces
47408
+ * with them — all about the world origin, which the box is centred on, so they stay aligned.
47409
+ * Nothing is re-uploaded. Kept in the settings, so a re-plot keeps it.
47410
+ */
47411
+ setZScale(factor) {
47412
+ const max = this.box ? spatialZScaleMax(this.box) : SPATIAL_Z_SCALE_MAX;
47413
+ this.ctx.settings.spatialZScale = Math.min(max, Math.max(SPATIAL_Z_SCALE_MIN, factor));
47414
+ this.applyZScale();
47415
+ }
47416
+ /** The Z stretch in effect: the setting, within what this box allows. */
47417
+ zScale() {
47418
+ const max = this.box ? spatialZScaleMax(this.box) : SPATIAL_Z_SCALE_MAX;
47419
+ const s = this.ctx.settings.spatialZScale;
47420
+ return Math.min(max, Math.max(SPATIAL_Z_SCALE_MIN, s > 0 ? s : 1));
47421
+ }
47422
+ applyZScale() {
47423
+ const s = this.zScale();
47424
+ if (this.points)
47425
+ this.points.scale3d = [1, 1, s];
47426
+ this.transcripts3d.setZScale(s);
47427
+ for (const layer of [this.volume, this.geneMapLayer, ...this.densityLayers]) {
47428
+ const base = layer && this.baseVoxels.get(layer);
47429
+ if (layer && base)
47430
+ layer.voxelSize = [base[0], base[1], base[2] * s];
47431
+ }
47432
+ const dataset = this.session.latest?.[0];
47433
+ if (this.gizmo && this.box) {
47434
+ const drawn = this.drawnBox(this.box);
47435
+ this.gizmo.setDepth(drawn.depth, spatialAxesLabels(drawn, s, dataset?.micronsPerUnit));
47436
+ }
47437
+ this.zHandle?.reposition();
47438
+ if (dataset)
47439
+ this.placeRegionMeshes(dataset);
47440
+ this.ctx.viewer.requestRender();
47441
+ }
47442
+ /** `box` with its Z stretched as drawn. */
47443
+ drawnBox(box) {
47444
+ return { width: box.width, height: box.height, depth: box.depth * this.zScale() };
47445
+ }
47446
+ /**
47447
+ * Add a volume layer with its Z stretched as the rest of the scene is, remembering its own
47448
+ * voxel size so a later stretch can be applied to it.
47449
+ */
47450
+ addVolume(viewer, data, width, height, depth, opts) {
47451
+ const [vx, vy, vz] = opts.voxelSize;
47452
+ const layer = viewer.addVolume(data, width, height, depth, {
47453
+ ...opts,
47454
+ voxelSize: [vx, vy, vz * this.zScale()],
47455
+ });
47456
+ this.baseVoxels.set(layer, [vx, vy, vz]);
47457
+ return layer;
47458
+ }
46901
47459
  /**
46902
47460
  * Every observation's position on the canvas under the current 3D camera (for the ROI
46903
47461
  * selection), indexed by observation with NaN for anything not drawn. Null before the cloud
@@ -46928,6 +47486,7 @@ class Spatial3dScene extends SpatialSceneBase {
46928
47486
  }
46929
47487
  dispose() {
46930
47488
  this.regionMeshes.clear();
47489
+ this.transcripts3d.remove();
46931
47490
  this.disposeSpatial();
46932
47491
  this.invertSub?.unsubscribe();
46933
47492
  this.invertSub = null;
@@ -46935,6 +47494,7 @@ class Spatial3dScene extends SpatialSceneBase {
46935
47494
  this.geneMapLoads.cancel();
46936
47495
  this.scaleBar?.dispose();
46937
47496
  this.scaleBar = null;
47497
+ this.removeAxes();
46938
47498
  // Drop the cached interleaved coordinates too: holding 3.7M x 3 floats after
46939
47499
  // a teardown is ~45MB of retained heap for a scene that no longer exists.
46940
47500
  this.positions = null;
@@ -47047,8 +47607,9 @@ class Spatial3dScene extends SpatialSceneBase {
47047
47607
  await this.ensureVolume(viewer, dataset, view);
47048
47608
  if (!task())
47049
47609
  return;
47050
- // Then the cluster density volumes, which can set the centring offset when
47051
- // there is no reference volume — so before any position is computed from it.
47610
+ // The box everything sits in, before any position is computed from its centring offset.
47611
+ this.ensureFrame(dataset);
47612
+ // Then the cluster density volumes.
47052
47613
  await this.ensureDensityVolumes(viewer, dataset, view, selection);
47053
47614
  if (!task())
47054
47615
  return;
@@ -47071,6 +47632,11 @@ class Spatial3dScene extends SpatialSceneBase {
47071
47632
  this.volume.opacity = view.volumeOpacity;
47072
47633
  }
47073
47634
  this.drawCloud(viewer, dataset, view, selection, enc, geneShown);
47635
+ // The transcripts last: they sit in the box the cloud fixed, and their first read can take a while.
47636
+ await this.transcripts3d.sync(dataset, view, this.session.port, {
47637
+ origin: this.origin,
47638
+ zScale: this.zScale(),
47639
+ });
47074
47640
  }
47075
47641
  /**
47076
47642
  * Draw the point cloud: a new layer for new geometry (a dataset or a section), the colour
@@ -47107,6 +47673,7 @@ class Spatial3dScene extends SpatialSceneBase {
47107
47673
  this.scalarKey = scalarKey;
47108
47674
  this.points = viewer.addPoints3D(this.positions, valuesFor(), {
47109
47675
  name: 'observations',
47676
+ scale3d: [1, 1, this.zScale()],
47110
47677
  colormap,
47111
47678
  contrastLimits,
47112
47679
  size,
@@ -47315,7 +47882,7 @@ class Spatial3dScene extends SpatialSceneBase {
47315
47882
  const lo = 0;
47316
47883
  const data = encodeExpressionVolume(field, [lo, hi], { log: view.logScale });
47317
47884
  const lut = this.ctx.display.spatialLut(view);
47318
- this.geneMapLayer = viewer.addVolume(data, field.width, field.height, field.depth, {
47885
+ this.geneMapLayer = this.addVolume(viewer, data, field.width, field.height, field.depth, {
47319
47886
  name: `gene map · ${gene}${interpolate ? ' · volume' : ''}`,
47320
47887
  colormap: colormapFromLut(`gene-map-${gene}`, lut),
47321
47888
  // The encoding already applied the window, so the layer must not apply a
@@ -47360,7 +47927,13 @@ class Spatial3dScene extends SpatialSceneBase {
47360
47927
  // holding the same number of cells would otherwise look like the same key and
47361
47928
  // leave the previous ROI's fields on screen.
47362
47929
  const key = on
47363
- ? [dataset.id, column ?? 'all', smoothing, this.session.selectionRev(selection)].join('|')
47930
+ ? [
47931
+ dataset.id,
47932
+ column ?? 'all',
47933
+ smoothing,
47934
+ this.session.selectionRev(selection),
47935
+ groupColorsKey(column, view.groupColors),
47936
+ ].join('|')
47364
47937
  : null;
47365
47938
  if (key === this.densityKey)
47366
47939
  return;
@@ -47371,24 +47944,13 @@ class Spatial3dScene extends SpatialSceneBase {
47371
47944
  const load = this.densityLoads.next();
47372
47945
  if (!key)
47373
47946
  return;
47947
+ // The density box is the scene's box (ensureFrame), so the cloud already sits in it.
47374
47948
  const grid = densityGrid(dataset);
47375
47949
  if (!grid)
47376
47950
  return;
47377
- // With no reference volume there is no offset yet, and a VolumeLayer has no
47378
- // translate — napari centres its box on the world origin. So the POINTS move by
47379
- // half the density box, exactly as they do for a reference volume, and the
47380
- // cached geometry is invalidated because that offset just changed.
47381
- if (!this.volume) {
47382
- this.origin = [
47383
- -(grid.width * grid.voxelSize[0]) / 2,
47384
- -(grid.height * grid.voxelSize[1]) / 2,
47385
- -(grid.depth * grid.voxelSize[2]) / 2,
47386
- ];
47387
- this.layerKey = null;
47388
- }
47389
47951
  let groups;
47390
47952
  try {
47391
- groups = await this.densityGroups(dataset, column, selection);
47953
+ groups = await this.densityGroups(dataset, column, selection, view);
47392
47954
  }
47393
47955
  catch (err) {
47394
47956
  console.warn('[napari-js] density volumes: column unavailable', err);
@@ -47424,7 +47986,7 @@ class Spatial3dScene extends SpatialSceneBase {
47424
47986
  // A cluster with nothing on the grid draws no layer, rather than an empty box.
47425
47987
  if (!data)
47426
47988
  return;
47427
- this.densityLayers.push(viewer.addVolume(data, grid.width, grid.height, grid.depth, {
47989
+ this.densityLayers.push(this.addVolume(viewer, data, grid.width, grid.height, grid.depth, {
47428
47990
  name: `density · ${group.name}`,
47429
47991
  colormap: this.ctx.display.channelTintColormap(group.color),
47430
47992
  invert: this.ctx.display.invert,
@@ -47447,7 +48009,7 @@ class Spatial3dScene extends SpatialSceneBase {
47447
48009
  * which is a real question on its own ("where is the tissue dense?") and the honest thing to
47448
48010
  * show when the view is not encoding a taxonomy.
47449
48011
  */
47450
- async densityGroups(dataset, column, selection) {
48012
+ async densityGroups(dataset, column, selection, view) {
47451
48013
  const port = this.session.port;
47452
48014
  const meta = column ? findColumnMeta(dataset, column) : undefined;
47453
48015
  if (!port || !column || !meta || meta.kind !== 'categorical')
@@ -47455,19 +48017,14 @@ class Spatial3dScene extends SpatialSceneBase {
47455
48017
  const loaded = await port.getColumn(column);
47456
48018
  if (!isCategoricalColumn(loaded))
47457
48019
  return [];
47458
- return rankDensityGroups(column, loaded, dataset.observations.count, selection);
48020
+ return rankDensityGroups(column, loaded, dataset.observations.count, selection, view.groupColors);
47459
48021
  }
47460
48022
  /**
47461
- * Add (or keep) the dataset's reference volume, and derive the offset that sits the
47462
- * observations inside it.
47463
- *
47464
- * `VolumeLayer` has no translate: napari-js maps the volume's unit cube to a world box
47465
- * **centred on the origin**, sized `dims x voxelSize`. The observations, by contract, are in
47466
- * the volume's own frame with its near corner at the coordinate origin. So the two only line
47467
- * up if the POINTS move — by half the box — which is what {@link origin} is.
48023
+ * Add (or keep) the dataset's reference volume. The offset that sits the observations inside
48024
+ * it is {@link ensureFrame}'s.
47468
48025
  *
47469
- * A failed or absent volume is not fatal: the cloud renders on its own, at its own
47470
- * coordinates, and the camera frames the points instead.
48026
+ * A failed or absent volume is not fatal: the cloud renders on its own, in the same box, and
48027
+ * the camera frames the points instead.
47471
48028
  */
47472
48029
  async ensureVolume(viewer, dataset, view) {
47473
48030
  const meta = dataset.volume;
@@ -47482,7 +48039,6 @@ class Spatial3dScene extends SpatialSceneBase {
47482
48039
  }
47483
48040
  // The surfaces sat in the volume just removed (or there was none): down until one is placed.
47484
48041
  this.regionMeshes.clearFrame();
47485
- this.origin = [0, 0, 0];
47486
48042
  if (!meta || !port?.getVolume)
47487
48043
  return;
47488
48044
  let voxels;
@@ -47497,7 +48053,7 @@ class Spatial3dScene extends SpatialSceneBase {
47497
48053
  return;
47498
48054
  const [vx, vy, vz] = meta.voxelSize;
47499
48055
  this.volumeKey = key;
47500
- this.volume = viewer.addVolume(voxels, meta.width, meta.height, meta.depth, {
48056
+ this.volume = this.addVolume(viewer, voxels, meta.width, meta.height, meta.depth, {
47501
48057
  name: 'reference volume',
47502
48058
  colormap: 'gray',
47503
48059
  // MIP would draw the brightest voxel along each ray, which for an averaged
@@ -47510,16 +48066,85 @@ class Spatial3dScene extends SpatialSceneBase {
47510
48066
  });
47511
48067
  if (this.regionsThrough)
47512
48068
  this.volume.blending = 'translucent_no_depth';
47513
- // Half the box, negated: the observations' origin is the box's near corner,
47514
- // and the box is centred on the world origin.
47515
- this.origin = [-(meta.width * vx) / 2, -(meta.height * vy) / 2, -(meta.depth * vz) / 2];
47516
48069
  this.placeRegionMeshes(dataset);
48070
+ }
48071
+ /**
48072
+ * The box everything sits in, centred on the world origin, and the offset that sits the
48073
+ * observations in it; with the axes gizmo and the Z handle on it.
48074
+ *
48075
+ * `VolumeLayer` has no translate: napari-js maps a volume's unit cube to a world box centred on
48076
+ * the origin, sized `dims x voxelSize`. The observations, by contract, are in the reference
48077
+ * volume's frame with its near corner at the coordinate origin, and the density grid is measured
48078
+ * from the coordinate origin the same way (`densityGrid`), so its box is the reference volume's
48079
+ * when there is one. So the POINTS move by half that box — {@link origin} — and every volume, the
48080
+ * reference, the densities and the gene map, lines up with them and with each other. Without a
48081
+ * reference volume this centres the cloud too, which the axes and the Z stretch need: both work
48082
+ * about the origin.
48083
+ */
48084
+ ensureFrame(dataset) {
48085
+ const meta = dataset.volume;
48086
+ const key = `${dataset.id}:${dataset.observations.count}:${meta ? `${meta.width}x${meta.height}x${meta.depth}` : '-'}`;
48087
+ if (key === this.boxKey)
48088
+ return;
48089
+ this.boxKey = key;
48090
+ const grid = densityGrid(dataset);
48091
+ this.box = grid
48092
+ ? {
48093
+ width: grid.width * grid.voxelSize[0],
48094
+ height: grid.height * grid.voxelSize[1],
48095
+ depth: grid.depth * grid.voxelSize[2],
48096
+ }
48097
+ : null;
48098
+ const box = this.box;
48099
+ this.origin = box ? [-box.width / 2, -box.height / 2, -box.depth / 2] : [0, 0, 0];
47517
48100
  // Force a geometry rebuild: the offset changed, so cached positions are stale.
47518
48101
  this.layerKey = null;
48102
+ this.installAxes(dataset);
48103
+ // A stretch chosen for another box may be past what this one allows: draw what is in effect.
48104
+ this.applyZScale();
48105
+ }
48106
+ /**
48107
+ * The Volume view's axes gizmo and Z-height handle, on {@link box}. The handle floats past the
48108
+ * top of the Z axis (the box's min-XY corner, where the blue axis and its label are), by a share
48109
+ * of the box's largest side rather than of its depth, so it clears the label even while a thin
48110
+ * section is drawn flat.
48111
+ */
48112
+ installAxes(dataset) {
48113
+ this.removeAxes();
48114
+ const box = this.box;
48115
+ if (!box)
48116
+ return;
48117
+ const { viewer, host, settings } = this.ctx;
48118
+ const drawn = this.drawnBox(box);
48119
+ this.gizmo = new Axes3dGizmo(viewer, host, drawn, spatialAxesLabels(drawn, this.zScale(), dataset.micronsPerUnit), {
48120
+ visible: settings.axesVisible,
48121
+ });
48122
+ const maxScale = spatialZScaleMax(box);
48123
+ this.zHandle = new NapariVolumeZHandle(host, viewer.camera3d, {
48124
+ topAnchor: () => {
48125
+ const d = this.drawnBox(box);
48126
+ return [-d.width / 2, -d.height / 2, d.depth / 2 + 0.08 * Math.max(d.width, d.height, d.depth)];
48127
+ },
48128
+ getScale: () => this.zScale(),
48129
+ setScale: (s) => this.setZScale(s),
48130
+ minScale: SPATIAL_Z_SCALE_MIN,
48131
+ maxScale,
48132
+ pixelsPerDouble: spatialZPixelsPerDouble(maxScale),
48133
+ });
48134
+ }
48135
+ removeAxes() {
48136
+ if (this.gizmo) {
48137
+ this.ctx.viewer.layers.remove(this.gizmo.layer);
48138
+ this.gizmo.destroy();
48139
+ this.gizmo = null;
48140
+ }
48141
+ this.zHandle?.destroy();
48142
+ this.zHandle = null;
47519
48143
  }
47520
48144
  /** Drop every layer of the cloud (the dataset went away, or has no z). */
47521
48145
  removeLayers(viewer) {
47522
48146
  this.regionMeshes.clearFrame();
48147
+ this.transcripts3d.remove();
47523
48148
  for (const layer of [this.points, this.volume, this.geneMapLayer, ...this.densityLayers]) {
47524
48149
  if (layer)
47525
48150
  viewer.layers.remove(layer);
@@ -47537,6 +48162,9 @@ class Spatial3dScene extends SpatialSceneBase {
47537
48162
  this.volume = null;
47538
48163
  this.volumeKey = null;
47539
48164
  this.origin = [0, 0, 0];
48165
+ this.box = null;
48166
+ this.boxKey = null;
48167
+ this.removeAxes();
47540
48168
  this.points = null;
47541
48169
  this.setPicker(null);
47542
48170
  this.layerKey = null;
@@ -47569,7 +48197,7 @@ class Spatial3dScene extends SpatialSceneBase {
47569
48197
  if (colorBy.kind === 'column') {
47570
48198
  const column = await port.getColumn(colorBy.name);
47571
48199
  if (isCategoricalColumn(column)) {
47572
- return encodeSpatial3dCategorical(column.codes, resolveCategoryColors(column.meta));
48200
+ return encodeSpatial3dCategorical(column.codes, categoryColorsFor(colorBy.name, column.meta, view.groupColors));
47573
48201
  }
47574
48202
  return this.encodeContinuous(column.values, view);
47575
48203
  }
@@ -47606,6 +48234,7 @@ function cloudSubset(dataset, view, geneShown = null) {
47606
48234
  const scalarKey = [
47607
48235
  key,
47608
48236
  view.colorBy ? `${view.colorBy.kind}:${view.colorBy.name}` : 'flat',
48237
+ groupColorsKey(view.colorBy?.kind === 'column' ? view.colorBy.name : null, view.groupColors),
47609
48238
  observationGeneStyleKey(view),
47610
48239
  view.logScale ? 'log' : 'lin',
47611
48240
  clip.join(','),
@@ -47712,6 +48341,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
47712
48341
  axesVisible: true,
47713
48342
  surfaceWireframe: false,
47714
48343
  volumeZScale: 1,
48344
+ spatialZScale: 1,
47715
48345
  };
47716
48346
  /**
47717
48347
  * Frame loading (volume assembly, surface preload): aborted by {@link reset} AND by
@@ -50212,4 +50842,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
50212
50842
  */
50213
50843
 
50214
50844
  export { SupersededError as $, ALL_GENES as A, Polygon as B, CANVAS_TOOL_IDS as C, DEFAULT_CATEGORICAL_PALETTE as D, ProcessingImage as E, REGION_IO_PORT as F, REGION_SOURCE_MIN_CHARS as G, HexColorPickerComponent as H, IMAGE_STATE_PORT as I, Rectangle as J, Region as K, LUT_COLORS as L, MISSING_COLOR as M, NAPARI_DECIMATE_OPTIONS as N, RegionEditorComponent as O, PLOT_MODE_CONTEXT as P, RoutingVisualizerService as Q, REGION_EDITOR_API as R, SAM_MODELS as S, SEMANTIC_SEGMENTER as T, SPATIAL_DATA_PORT as U, SPATIAL_WIRE_VERSION as V, ShapeSelection as W, SpatialChartsComponent as X, SpatialControlsComponent as Y, SpatialDataHttpService as Z, SpatialSelectionStore as _, runDecoder as a, omicsLayout as a$, TILE_ACCESS_PORT as a0, TOOLBAR_TOOLS as a1, VISUALIZER as a2, VIZ_ALERT_TOAST_KEY as a3, VIZ_CONFIG as a4, VIZ_TOAST_KEY as a5, ViewerFeature as a6, VisualizationModule as a7, VisualizerComponent as a8, assertManifestVersion as a9, hydrateBounds as aA, importFile as aB, isBuiltinPlotType as aC, isCanvasToolId as aD, isCategoricalColumn as aE, isContinuousColumn as aF, isDialogToolContribution as aG, isEmptyPresetSet as aH, isLabelVolumeName as aI, isLittleEndian as aJ, isNapari3d as aK, isNapariIsosurface as aL, isNapariScatter as aM, isNapariScatter3d as aN, isNapariSurface as aO, isNapariVolume as aP, isSamModelReady as aQ, isSpatialOmics as aR, isSpatialOmics3d as aS, isThreeDimensional as aT, isZipName as aU, labelClasses as aV, labelSliceGeoJson as aW, lutFor as aX, markerDiameters as aY, maskToIndices as aZ, mutedFromSelection as a_, benefitsFromGrouping as aa, buildOmicsTraces as ab, capabilitiesOf as ac, clearSamModelCache as ad, contrastWindow as ae, contributedPlotTypeOption as af, countMask as ag, cropImage as ah, datasetFromManifest as ai, decodeColumn as aj, decodeCoords as ak, decodeFeatureVector as al, decodePolygons as am, decodeRadius as an, defaultChannelVisible as ao, defaultPresetSet as ap, dialogToolContributions as aq, emptySelection as ar, encodeCategorical as as, encodeContinuous as at, expandZips as au, findColumnMeta as av, getDefaultSamModelId as aw, getPlotTypeDescriptor as ax, getSamModel as ay, hasZarrMetadata as az, ALL_TOOLBAR_TOOLS as b, openAnnotations as b0, openGeoJsonRegionSource as b1, openHttpRegionSource as b2, parsePresetSet as b3, planRegionImport as b4, pointInRing as b5, provideVisualization as b6, readAnnotationSlices as b7, readLabelVolume as b8, readOmeZarrLabels as b9, readZip as ba, rendererOwnsWheel as bb, resolveCategoryColors as bc, selectByCategory as bd, selectInRegions as be, setDefaultSamModel as bf, setOrtWasmBase as bg, setSamModelUrls as bh, sortToolContributions as bi, visibleToolContributions as bj, ANNOTATION_IMAGE_MAX_DIM as c, CELL_SEGMENTER as d, CHANNEL_HISTOGRAM_API as e, fetchModel as f, getOrtWasmBase as g, CellposeSegmenterService as h, ChannelHistogramComponent as i, DEFAULT_FALLBACK_PALETTE as j, DEFAULT_MUTED_OPACITY as k, DEFAULT_SAM_MODEL_ID as l, DEFAULT_SPATIAL_VIEW as m, DEFAULT_VISIBLE_CHANNELS as n, INSTANCE_SEGMENTER as o, ImageConverterService as p, MultiPolygon as q, runEncoder as r, NAPARI_DEFAULT_DECIMATE as s, NO_CATEGORY as t, NO_OBSERVATION as u, PLOT_MODE_SESSION as v, PLOT_TYPE_CONTRIBUTIONS as w, PLOT_TYPE_DESCRIPTORS as x, PREFERENCES_PORT as y, PlotType as z };
50215
- //# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-qUeMuBb0.mjs.map
50845
+ //# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DHrHO2ay.mjs.map