@jax-data-science/sci-image-visualizer 0.13.0 → 0.14.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 (61) hide show
  1. package/esm2022/lib/contracts/channel-histogram-api.contract.mjs +19 -1
  2. package/esm2022/lib/contracts/display-types.mjs +7 -1
  3. package/esm2022/lib/contracts/image.contract.mjs +1 -1
  4. package/esm2022/lib/contracts/spatial-dataset.contract.mjs +1 -1
  5. package/esm2022/lib/contracts/visualizer.contract.mjs +1 -1
  6. package/esm2022/lib/implementations/napari-js/napari-image-2d-scene.mjs +17 -2
  7. package/esm2022/lib/implementations/napari-js/napari-spatial-2d-scene.mjs +88 -10
  8. package/esm2022/lib/implementations/napari-js/napari-spatial-3d-scene.mjs +62 -13
  9. package/esm2022/lib/implementations/napari-js/napari-spatial-encoding.mjs +21 -1
  10. package/esm2022/lib/implementations/napari-js/napari-spatial-scene.mjs +5 -1
  11. package/esm2022/lib/implementations/napari-js/napari-visualizer.service.mjs +4 -1
  12. package/esm2022/lib/implementations/napari-js/spatial-tiles/categorical-lookup.mjs +26 -3
  13. package/esm2022/lib/implementations/napari-js/spatial-tiles/cell-layers.mjs +3 -2
  14. package/esm2022/lib/implementations/napari-js/spatial-tiles/density-layer.mjs +20 -4
  15. package/esm2022/lib/implementations/napari-js/spatial-tiles/transcript-hover.mjs +4 -2
  16. package/esm2022/lib/implementations/napari-js/spatial-tiles/transcript-jobs.mjs +4 -2
  17. package/esm2022/lib/implementations/napari-js/spatial-tiles/transcript-layers.mjs +42 -15
  18. package/esm2022/lib/implementations/osd/openseadragon-visualizer.service.mjs +3 -1
  19. package/esm2022/lib/spatial/lod.mjs +6 -3
  20. package/esm2022/lib/spatial/spatial-controls.facade.mjs +8 -3
  21. package/esm2022/lib/spatial/spatial-gene-filter.mjs +138 -0
  22. package/esm2022/lib/spatial/spatial-tile-merge.mjs +42 -1
  23. package/esm2022/lib/spatial-controls/spatial-cells-panel/spatial-cells-panel.component.mjs +3 -3
  24. package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +4 -3
  25. package/esm2022/lib/spatial-controls/spatial-gene-style/spatial-gene-style.component.mjs +66 -0
  26. package/esm2022/lib/spatial-controls/spatial-gene-tree/spatial-gene-tree.component.mjs +106 -26
  27. package/esm2022/lib/spatial-controls/spatial-groups-panel/spatial-groups-panel.component.mjs +3 -3
  28. package/esm2022/lib/spatial-controls/spatial-key/spatial-key.component.mjs +3 -3
  29. package/esm2022/lib/spatial-controls/spatial-marker-genes-form/spatial-marker-genes-form.component.mjs +3 -3
  30. package/esm2022/lib/spatial-controls/spatial-observations-panel/spatial-observations-panel.component.mjs +110 -8
  31. package/esm2022/lib/spatial-controls/spatial-transcripts-panel/spatial-transcripts-panel.component.mjs +41 -9
  32. package/esm2022/lib/store/visualizer-store.service.mjs +50 -1
  33. package/fesm2022/{jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DURPqs5a.mjs → jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DvFp1jNH.mjs} +1064 -318
  34. package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DvFp1jNH.mjs.map +1 -0
  35. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-D0IQ6PjZ.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-D7lSUAM1.mjs} +2 -2
  36. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-D0IQ6PjZ.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-D7lSUAM1.mjs.map} +1 -1
  37. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +1 -1
  38. package/lib/contracts/channel-histogram-api.contract.d.ts +14 -0
  39. package/lib/contracts/display-types.d.ts +27 -0
  40. package/lib/contracts/image.contract.d.ts +2 -0
  41. package/lib/contracts/spatial-dataset.contract.d.ts +7 -0
  42. package/lib/contracts/visualizer.contract.d.ts +3 -0
  43. package/lib/implementations/napari-js/napari-image-2d-scene.d.ts +7 -0
  44. package/lib/implementations/napari-js/napari-spatial-2d-scene.d.ts +13 -2
  45. package/lib/implementations/napari-js/napari-spatial-encoding.d.ts +5 -0
  46. package/lib/implementations/napari-js/napari-spatial-scene.d.ts +4 -0
  47. package/lib/implementations/napari-js/napari-visualizer.service.d.ts +2 -0
  48. package/lib/implementations/napari-js/spatial-tiles/categorical-lookup.d.ts +6 -1
  49. package/lib/implementations/napari-js/spatial-tiles/transcript-hover.d.ts +4 -0
  50. package/lib/implementations/napari-js/spatial-tiles/transcript-layers.d.ts +3 -1
  51. package/lib/spatial/lod.d.ts +3 -1
  52. package/lib/spatial/spatial-controls.facade.d.ts +3 -0
  53. package/lib/spatial/spatial-gene-filter.d.ts +44 -0
  54. package/lib/spatial/spatial-tile-merge.d.ts +17 -0
  55. package/lib/spatial-controls/spatial-gene-style/spatial-gene-style.component.d.ts +30 -0
  56. package/lib/spatial-controls/spatial-gene-tree/spatial-gene-tree.component.d.ts +39 -7
  57. package/lib/spatial-controls/spatial-observations-panel/spatial-observations-panel.component.d.ts +33 -1
  58. package/lib/spatial-controls/spatial-transcripts-panel/spatial-transcripts-panel.component.d.ts +13 -1
  59. package/lib/store/visualizer-store.service.d.ts +13 -0
  60. package/package.json +1 -1
  61. package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DURPqs5a.mjs.map +0 -1
@@ -18,13 +18,13 @@ import * as i1$2 from 'primeng/api';
18
18
  import { SharedModule } from 'primeng/api';
19
19
  import * as i3 from 'primeng/button';
20
20
  import { ButtonModule } from 'primeng/button';
21
- import * as i4$3 from 'primeng/checkbox';
21
+ import * as i4$2 from 'primeng/checkbox';
22
22
  import { CheckboxModule } from 'primeng/checkbox';
23
23
  import * as i11$1 from 'primeng/contextmenu';
24
24
  import { ContextMenuModule } from 'primeng/contextmenu';
25
25
  import * as i5 from 'primeng/dialog';
26
26
  import { DialogModule } from 'primeng/dialog';
27
- import * as i4 from 'primeng/dropdown';
27
+ import * as i5$2 from 'primeng/dropdown';
28
28
  import { DropdownModule } from 'primeng/dropdown';
29
29
  import * as i7$3 from 'primeng/inputnumber';
30
30
  import { InputNumberModule } from 'primeng/inputnumber';
@@ -42,9 +42,9 @@ import { saveAs } from 'file-saver';
42
42
  import { SlicePlanesWidget, scaleBarFor, formatLength, AxesLabelsOverlay, ScaleBarOverlay, NavigatorOverlay } from 'napari-js/overlays';
43
43
  import * as i3$1 from 'primeng/ripple';
44
44
  import { RippleModule } from 'primeng/ripple';
45
- import * as i4$2 from 'primeng/toolbar';
45
+ import * as i4$1 from 'primeng/toolbar';
46
46
  import { ToolbarModule } from 'primeng/toolbar';
47
- import * as i4$1 from 'primeng/menu';
47
+ import * as i4 from 'primeng/menu';
48
48
  import { MenuModule } from 'primeng/menu';
49
49
  import * as i7$2 from 'primeng/confirmdialog';
50
50
  import { ConfirmDialogModule } from 'primeng/confirmdialog';
@@ -155,6 +155,24 @@ function findColumnMeta(dataset, name) {
155
155
  return dataset.columns.find((c) => c.name === name);
156
156
  }
157
157
 
158
+ /**
159
+ * How many channels of a multichannel image start visible. Each visible channel is drawn
160
+ * from tiles of its own, so a 22-channel plate showed every channel at once and asked the
161
+ * server for 22 tiles per position on every pan and zoom. Channels past these start hidden
162
+ * (one click shows them), and a hidden channel fetches no tiles.
163
+ */
164
+ const DEFAULT_VISIBLE_CHANNELS = 4;
165
+ /**
166
+ * Whether channel `index` of `count` starts visible: as the image says when its
167
+ * `channelInfo` does, else every channel of an image with at most
168
+ * {@link DEFAULT_VISIBLE_CHANNELS}, and the first {@link DEFAULT_VISIBLE_CHANNELS} of one with more.
169
+ * Only a boolean says: a descriptor's `null` (a JSON field left empty) leaves the default.
170
+ */
171
+ function defaultChannelVisible(index, count, specified) {
172
+ if (typeof specified === 'boolean')
173
+ return specified;
174
+ return count <= DEFAULT_VISIBLE_CHANNELS || index < DEFAULT_VISIBLE_CHANNELS;
175
+ }
158
176
  /** The standard LUT pseudo-colours (mirrors Fiji's Merge Channels palette) for
159
177
  * quick per-channel assignment. */
160
178
  const LUT_COLORS = [
@@ -711,6 +729,7 @@ const DEFAULT_SPATIAL_VIEW = {
711
729
  transcriptGeneColors: {},
712
730
  transcriptGeneGroups: [],
713
731
  transcriptAllGenes: false,
732
+ transcriptAllGenesHidden: [],
714
733
  transcriptBudget: 100_000,
715
734
  transcriptMode: 'off',
716
735
  transcriptColorBy: 'cellType',
@@ -726,6 +745,11 @@ const DEFAULT_SPATIAL_VIEW = {
726
745
  cellColorGene: null,
727
746
  cellSingleColor: '#4fc3f7',
728
747
  hiddenGroups: [],
748
+ observationGenes: [],
749
+ observationGenesMatch: 'any',
750
+ observationGenesColorBy: true,
751
+ observationGeneColors: {},
752
+ observationGeneGlyphs: {},
729
753
  showImage: true,
730
754
  showAnnotations: true,
731
755
  };
@@ -8968,6 +8992,52 @@ class VisualizerStore {
8968
8992
  meta[0].mppY = mppY;
8969
8993
  this.imageMeta$.next(meta);
8970
8994
  }
8995
+ /**
8996
+ * Fill in the channel names and tints the host's image meta left out, from the image's tile
8997
+ * descriptor: the server reads them from the file (an OME-TIFF's channel names, such as Cytos's
8998
+ * "Actin" or "Cell-Membrane"), where a host's metadata (jit-ui's `/metadata`) carries none, so
8999
+ * the Channels pane showed "Channel 1", "Channel 2"… The host's own names and tints win; only a
9000
+ * channel still showing its derived default takes the descriptor's, so an edit made in the pane
9001
+ * stays. A no-op when the channel counts differ, or for RGB.
9002
+ */
9003
+ adoptChannelInfo(info) {
9004
+ const meta = this.imageMeta$.value;
9005
+ const m0 = meta?.[0];
9006
+ const current = this.channelStates$.value;
9007
+ if (!info?.length || !m0 || (m0.rgbChannels ?? 1) >= 3)
9008
+ return;
9009
+ if ((m0.channelCount ?? 1) !== info.length || current.length !== info.length)
9010
+ return;
9011
+ const host = m0.channelInfo ?? [];
9012
+ const merged = info.map((d, i) => ({
9013
+ ...(host[i] ?? {}),
9014
+ name: host[i]?.name || d.name || '',
9015
+ color: host[i]?.color || d.color || '',
9016
+ }));
9017
+ const before = this.defaultChannelStates;
9018
+ const nextMeta = meta.map((e, k) => (k === 0 ? { ...e, channelInfo: merged } : e));
9019
+ this.imageMeta$.next(nextMeta);
9020
+ const next = this.deriveChannels(nextMeta);
9021
+ this.defaultChannelStates = next.map((c) => ({ ...c }));
9022
+ let changed = false;
9023
+ const states = current.map((st, i) => {
9024
+ const was = before[i];
9025
+ const now = next[i];
9026
+ if (!was || !now)
9027
+ return st;
9028
+ const patch = {};
9029
+ if (st.name === was.name && now.name !== was.name)
9030
+ patch.name = now.name;
9031
+ if (st.color === was.color && now.color !== was.color)
9032
+ patch.color = now.color;
9033
+ if (!Object.keys(patch).length)
9034
+ return st;
9035
+ changed = true;
9036
+ return { ...st, ...patch };
9037
+ });
9038
+ if (changed)
9039
+ this.channelStates$.next(states);
9040
+ }
8971
9041
  /**
8972
9042
  * Publish the current image's metadata and derive its channels.
8973
9043
  *
@@ -8995,6 +9065,7 @@ class VisualizerStore {
8995
9065
  * - RGB (rgbChannels ≥ 3) → Red/Green/Blue;
8996
9066
  * - multichannel fluorescence (channelCount > 1) → one channel per band,
8997
9067
  * named/tinted from the server `channelInfo` when present, else a palette;
9068
+ * only the first `DEFAULT_VISIBLE_CHANNELS` start visible (see `defaultChannelVisible`);
8998
9069
  * - otherwise a single grayscale "Intensity" channel. */
8999
9070
  deriveChannels(meta) {
9000
9071
  const base = { min: 0, max: 255, gamma: 1, visible: true };
@@ -9016,6 +9087,7 @@ class VisualizerStore {
9016
9087
  name: info?.[i]?.name || `Channel ${i + 1}`,
9017
9088
  color: info?.[i]?.color || palette[i % palette.length],
9018
9089
  ...base,
9090
+ visible: defaultChannelVisible(i, count, info?.[i]?.visible),
9019
9091
  }));
9020
9092
  }
9021
9093
  return [{ index: 0, name: 'Intensity', color: '#ffffff', ...base }];
@@ -10489,7 +10561,7 @@ function frameToRgba(cached, frameIndex) {
10489
10561
  /** Lazy-import so onnxruntime-web is never pulled into unit tests or the
10490
10562
  * initial bundle — only when segmentation actually runs. */
10491
10563
  const onnxSessionFactory = async () => {
10492
- const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-D0IQ6PjZ.mjs');
10564
+ const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-D7lSUAM1.mjs');
10493
10565
  return new OnnxSamSession();
10494
10566
  };
10495
10567
  /** Thrown inside a load that a model switch superseded. */
@@ -13100,7 +13172,7 @@ class PlotTypeSelectorComponent {
13100
13172
  this.isoRangeChange.emit(Array.isArray(value) ? value : undefined);
13101
13173
  }
13102
13174
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: PlotTypeSelectorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
13103
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: PlotTypeSelectorComponent, isStandalone: true, selector: "toolbar-plot-type-selector", inputs: { options: "options", selected: "selected", isosurface: "isosurface", isoRange: "isoRange", intensityLines: "intensityLines" }, outputs: { selectPlotType: "selectPlotType", isoRangeChange: "isoRangeChange", addProfileLine: "addProfileLine" }, ngImport: i0, template: "<span class=\"toolbar-separator\"></span>\n<p-dropdown\n [options]=\"options\"\n optionLabel=\"label\"\n optionValue=\"type\"\n [ngModel]=\"selected\"\n ariaLabel=\"Plot type\"\n appendTo=\"body\"\n pTooltip=\"Select plot type\"\n tooltipPosition=\"bottom\"\n [style]=\"{ 'min-width': '11rem' }\"\n styleClass=\"plotmode-dropdown\"\n panelStyleClass=\"plotmode-panel\"\n (onChange)=\"selectPlotType.emit($event.value)\"\n>\n <ng-template let-opt pTemplate=\"selectedItem\">\n <span class=\"plotmode-item\" *ngIf=\"opt\">\n <i *ngIf=\"isPiIcon(opt.icon)\" [class]=\"opt.icon\"></i>\n <img *ngIf=\"opt.icon && !isPiIcon(opt.icon)\" class=\"plotmode-icon-img\" [src]=\"opt.icon\" alt=\"\" />\n <span>{{ opt.label }}</span>\n </span>\n </ng-template>\n <ng-template let-opt pTemplate=\"item\">\n <span class=\"plotmode-item\">\n <i *ngIf=\"isPiIcon(opt.icon)\" [class]=\"opt.icon\"></i>\n <img *ngIf=\"opt.icon && !isPiIcon(opt.icon)\" class=\"plotmode-icon-img\" [src]=\"opt.icon\" alt=\"\" />\n <span>{{ opt.label }}</span>\n </span>\n </ng-template>\n</p-dropdown>\n<ng-container *ngIf=\"isosurface\">\n <toolbar-tool-slider\n label=\"Iso\"\n [value]=\"isoRange\"\n [range]=\"true\"\n [min]=\"isoValueMin\"\n [max]=\"isoValueMax\"\n [step]=\"isoValueStep\"\n tooltip=\"Isosurface band (0\u2013255), mapped onto the volume's\n actual intensity range. Surfaces are drawn for voxels whose\n intensity falls in the selected band \u2014 narrow it to isolate\n features, widen it to see the whole structure.\"\n [valueText]=\"isoRange[0] + '\u2013' + isoRange[1]\"\n (valueChange)=\"onIsoRange($event)\"\n >\n </toolbar-tool-slider>\n</ng-container>\n<!-- Intensity tools (Heatmap / Image mode): add another colored line ROI.\n Each added line gets the next bright palette colour and its own inset\n trace; select a line on the image and use the trash tool to remove it\n (and its trace). The lines are Region-based but excluded from the\n Region Editor and exports. -->\n<ng-container *ngIf=\"intensityLines\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Add an intensity line\"\n pTooltip=\"Add an intensity line (new colour). Click a line to select it,\n then use the trash tool to remove it.\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"addProfileLine.emit()\"\n >\n <img src=\"assets/plotting/spline-chart.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" alt=\"\" />\n </p-button>\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}::ng-deep .plotmode-item{display:inline-flex;align-items:center;gap:.5rem;color:#000}::ng-deep .plotmode-item i{font-size:1rem;color:var(--primary-color)}::ng-deep .plotmode-item .plotmode-icon-img{width:1.1rem;height:1.1rem;object-fit:contain;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}\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: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { 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: i4.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"] }, { kind: "component", type: ToolSliderComponent, selector: "toolbar-tool-slider", inputs: ["label", "value", "min", "max", "step", "range", "tooltip", "valueText"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13175
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: PlotTypeSelectorComponent, isStandalone: true, selector: "toolbar-plot-type-selector", inputs: { options: "options", selected: "selected", isosurface: "isosurface", isoRange: "isoRange", intensityLines: "intensityLines" }, outputs: { selectPlotType: "selectPlotType", isoRangeChange: "isoRangeChange", addProfileLine: "addProfileLine" }, ngImport: i0, template: "<span class=\"toolbar-separator\"></span>\n<p-dropdown\n [options]=\"options\"\n optionLabel=\"label\"\n optionValue=\"type\"\n [ngModel]=\"selected\"\n ariaLabel=\"Plot type\"\n appendTo=\"body\"\n pTooltip=\"Select plot type\"\n tooltipPosition=\"bottom\"\n [style]=\"{ 'min-width': '11rem' }\"\n styleClass=\"plotmode-dropdown\"\n panelStyleClass=\"plotmode-panel\"\n (onChange)=\"selectPlotType.emit($event.value)\"\n>\n <ng-template let-opt pTemplate=\"selectedItem\">\n <span class=\"plotmode-item\" *ngIf=\"opt\">\n <i *ngIf=\"isPiIcon(opt.icon)\" [class]=\"opt.icon\"></i>\n <img *ngIf=\"opt.icon && !isPiIcon(opt.icon)\" class=\"plotmode-icon-img\" [src]=\"opt.icon\" alt=\"\" />\n <span>{{ opt.label }}</span>\n </span>\n </ng-template>\n <ng-template let-opt pTemplate=\"item\">\n <span class=\"plotmode-item\">\n <i *ngIf=\"isPiIcon(opt.icon)\" [class]=\"opt.icon\"></i>\n <img *ngIf=\"opt.icon && !isPiIcon(opt.icon)\" class=\"plotmode-icon-img\" [src]=\"opt.icon\" alt=\"\" />\n <span>{{ opt.label }}</span>\n </span>\n </ng-template>\n</p-dropdown>\n<ng-container *ngIf=\"isosurface\">\n <toolbar-tool-slider\n label=\"Iso\"\n [value]=\"isoRange\"\n [range]=\"true\"\n [min]=\"isoValueMin\"\n [max]=\"isoValueMax\"\n [step]=\"isoValueStep\"\n tooltip=\"Isosurface band (0\u2013255), mapped onto the volume's\n actual intensity range. Surfaces are drawn for voxels whose\n intensity falls in the selected band \u2014 narrow it to isolate\n features, widen it to see the whole structure.\"\n [valueText]=\"isoRange[0] + '\u2013' + isoRange[1]\"\n (valueChange)=\"onIsoRange($event)\"\n >\n </toolbar-tool-slider>\n</ng-container>\n<!-- Intensity tools (Heatmap / Image mode): add another colored line ROI.\n Each added line gets the next bright palette colour and its own inset\n trace; select a line on the image and use the trash tool to remove it\n (and its trace). The lines are Region-based but excluded from the\n Region Editor and exports. -->\n<ng-container *ngIf=\"intensityLines\">\n <span class=\"toolbar-separator\"></span>\n <p-button\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Add an intensity line\"\n pTooltip=\"Add an intensity line (new colour). Click a line to select it,\n then use the trash tool to remove it.\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"addProfileLine.emit()\"\n >\n <img src=\"assets/plotting/spline-chart.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" alt=\"\" />\n </p-button>\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}::ng-deep .plotmode-item{display:inline-flex;align-items:center;gap:.5rem;color:#000}::ng-deep .plotmode-item i{font-size:1rem;color:var(--primary-color)}::ng-deep .plotmode-item .plotmode-icon-img{width:1.1rem;height:1.1rem;object-fit:contain;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}\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: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { 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"] }, { kind: "component", type: ToolSliderComponent, selector: "toolbar-tool-slider", inputs: ["label", "value", "min", "max", "step", "range", "tooltip", "valueText"], outputs: ["valueChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13104
13176
  }
13105
13177
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: PlotTypeSelectorComponent, decorators: [{
13106
13178
  type: Component,
@@ -13289,7 +13361,7 @@ class ViewControlsComponent {
13289
13361
  this.decimateOptions = NAPARI_DECIMATE_OPTIONS;
13290
13362
  }
13291
13363
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ViewControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
13292
- 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: i4.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 }); }
13364
+ 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 }); }
13293
13365
  }
13294
13366
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ViewControlsComponent, decorators: [{
13295
13367
  type: Component,
@@ -13661,7 +13733,7 @@ class SegmentationToolsComponent {
13661
13733
  return tool.id;
13662
13734
  }
13663
13735
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SegmentationToolsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
13664
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SegmentationToolsComponent, isStandalone: true, selector: "toolbar-segmentation-tools", inputs: { activeDragMode: "activeDragMode", samModels: "samModels", samModelId: "samModelId", contributedTools: "contributedTools", toolModelIds: "toolModelIds" }, outputs: { segmentRegions: "segmentRegions", segmentCellpose: "segmentCellpose", toggleDragMode: "toggleDragMode", samModelChange: "samModelChange", runTool: "runTool", toolModelChange: "toolModelChange", openToolParams: "openToolParams" }, usesOnChanges: true, ngImport: i0, template: "<!-- SAM (jit-ui#90): box-prompt segment, interactive point prompts, and a\n model picker. -->\n<span class=\"toolbar-separator\"></span>\n<!-- SAM controls grouped: box-prompt Segment, then the point-prompt tool,\n then the model-picker dropdown (the chevron) which governs BOTH tools.\n Falls back to Segment + point with no model picker when none are set. -->\n<span *ngIf=\"samModels.length; else segmentPlain\" class=\"p-buttonset segment-split\">\n <p-button\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Segment with SAM (box prompt)\"\n pTooltip=\"Segment the drawn rectangles with SAM (box prompt)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentRegions.emit()\"\n >\n <img src=\"assets/plotting/sam-box-prompt.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n <ng-container [ngTemplateOutlet]=\"samPointBtn\"></ng-container>\n <p-button\n icon=\"pi pi-chevron-down\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Pick the SAM model\"\n pTooltip=\"Pick the SAM model (used by both segment tools)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"samModelMenu.toggle($event)\"\n >\n </p-button>\n</span>\n<ng-template #segmentPlain>\n <span class=\"p-buttonset segment-split\">\n <p-button\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Segment with SAM (box prompt)\"\n pTooltip=\"Segment the drawn rectangles with SAM (box prompt)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentRegions.emit()\"\n >\n <img src=\"assets/plotting/sam-box-prompt.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n <ng-container [ngTemplateOutlet]=\"samPointBtn\"></ng-container>\n </span>\n</ng-template>\n<!-- Point-prompt tool \u2014 shared between the models / no-models layouts so it\n always sits just left of the model dropdown. -->\n<ng-template #samPointBtn>\n <p-button\n size=\"small\"\n ariaLabel=\"SAM point prompts\"\n [text]=\"activeDragMode !== 'samPoint'\"\n pTooltip=\"SAM point prompts: click a fiber to segment it as a new region;\n Shift/Alt-click to refine (exclude) the current one.<br>Esc undoes it.\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('samPoint')\"\n >\n <img\n src=\"assets/plotting/sam-point-prompt.svg\"\n [class]=\"activeDragMode === 'samPoint' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"\n />\n </p-button>\n</ng-template>\n<p-menu #samModelMenu styleClass=\"model-menu\" [model]=\"samMenuItems\" [popup]=\"true\" appendTo=\"body\">\n <ng-template pTemplate=\"item\" let-item>\n <ng-container\n [ngTemplateOutlet]=\"modelMenuItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n ></ng-container>\n </ng-template>\n</p-menu>\n\n<!-- Shared item body for all three model pickers (SAM / YOLO / retinal):\n the standard check-or-blank icon and label, plus an info icon carrying\n the model's description as a hover tooltip. This replaced a single\n \"About the SAM models\" overlay panel \u2014 per-model text sits next to the\n model it describes, and YOLO/retinal get the same affordance.\n\n p-menu puts its click handler on the wrapping `.p-menuitem-content`,\n outside this template, so `command` still fires and the whole row stays\n clickable \u2014 which is also why the info icon has to stop propagation, or\n reading about a model would switch to it. -->\n<ng-template #modelMenuItem let-item>\n <a class=\"p-menuitem-link model-menu-item\">\n <span class=\"p-menuitem-icon\" [ngClass]=\"item.icon\"></span>\n <span class=\"p-menuitem-text\">{{ item.label }}</span>\n <!-- `bottom` like every other tooltip in this toolbar, and for room:\n these menus open under buttons that can sit near the right edge,\n where a `right` tooltip has a few hundred px and squeezes its text\n into a narrow column instead of using its max-width. -->\n <!-- A focusable <button>, not a bare <i>. As an <i> the description\n was reachable only by hovering a mouse: not tabbable, no\n accessible name, nothing for a screen reader to announce \u2014 so\n keyboard and AT users could pick a checkpoint but never read what\n it was. PrimeNG's tooltip shows on focus as well as hover, so\n making it a button restores it for free.\n\n `aria-describedby` is deliberately not used: the tooltip is\n rendered into `body` on demand and has no stable id to point at.\n The description is carried on `aria-label` instead, which is\n announced whether or not the tooltip is showing. It is stripped of\n markup first, since the copy contains `<b>`/`<br>` for the visual\n tooltip and those would otherwise be read out literally. -->\n <button\n *ngIf=\"item.tooltip\"\n type=\"button\"\n class=\"pi pi-info-circle model-info-icon\"\n [attr.aria-label]=\"item.infoLabel\"\n [pTooltip]=\"item.tooltip\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n appendTo=\"body\"\n tooltipStyleClass=\"model-info-tooltip\"\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n ></button>\n </a>\n</ng-template>\n<p-button\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Segment cells (cellpose-SAM)\"\n pTooltip=\"Auto-segment cells inside each drawn rectangle (cellpose-SAM)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentCellpose.emit()\"\n>\n <img src=\"assets/plotting/cells.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n</p-button>\n\n<!-- Contributed no-prompt tools (TOOLBAR_TOOLS). Unlike the SAM and\n cellpose tools these take no prompt \u2014 they sweep the whole current\n view rather than being pointed at something \u2014 so there is nothing to\n draw first. Each renders the same split button: run, pick checkpoint,\n open parameters.\n\n Nothing here names a specific tool. The library ships none; a host\n registers them, which is what lets this package be published without\n the closed model dependencies. -->\n<ng-container *ngFor=\"let tool of contributedTools; trackBy: trackToolById\">\n <span class=\"p-buttonset segment-split\">\n <p-button\n size=\"small\"\n [text]=\"true\"\n [ariaLabel]=\"tool.label\"\n [pTooltip]=\"tool.runTooltip\"\n tooltipPosition=\"bottom\"\n (onClick)=\"runTool.emit(tool.id)\"\n >\n <!-- `toolbar-icon-dark` is hardcoded rather than bound, unlike the\n toggle tools: these fire a run and have no active state. -->\n <img\n *ngIf=\"tool.icon.src\"\n [src]=\"tool.icon.src\"\n class=\"toolbar-icon-dark\"\n style=\"width: 1rem\"\n [alt]=\"tool.label\"\n />\n <i *ngIf=\"tool.icon.pi\" class=\"pi {{ tool.icon.pi }}\"></i>\n </p-button>\n <p-button\n icon=\"pi pi-chevron-down\"\n size=\"small\"\n [text]=\"true\"\n [ariaLabel]=\"'Pick the model for ' + tool.label\"\n [pTooltip]=\"tool.modelTooltip || 'Pick the model'\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toolMenu.toggle($event)\"\n >\n </p-button>\n <p-button\n *ngIf=\"tool.params.length\"\n icon=\"pi pi-sliders-h\"\n size=\"small\"\n [text]=\"true\"\n [ariaLabel]=\"'Parameters for ' + tool.label\"\n [pTooltip]=\"tool.paramsTooltip || 'Parameters'\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openToolParams.emit(tool.id)\"\n >\n </p-button>\n </span>\n <p-menu #toolMenu styleClass=\"model-menu\" [model]=\"toolMenuItems[tool.id] || []\" [popup]=\"true\" appendTo=\"body\">\n <ng-template pTemplate=\"item\" let-item>\n <ng-container\n [ngTemplateOutlet]=\"modelMenuItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n ></ng-container>\n </ng-template>\n </p-menu>\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.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { 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: MenuModule }, { kind: "component", type: i4$1.Menu, selector: "p-menu", inputs: ["model", "popup", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex"], outputs: ["onShow", "onHide", "onBlur", "onFocus"] }, { 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: "ngmodule", type: TooltipModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13736
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SegmentationToolsComponent, isStandalone: true, selector: "toolbar-segmentation-tools", inputs: { activeDragMode: "activeDragMode", samModels: "samModels", samModelId: "samModelId", contributedTools: "contributedTools", toolModelIds: "toolModelIds" }, outputs: { segmentRegions: "segmentRegions", segmentCellpose: "segmentCellpose", toggleDragMode: "toggleDragMode", samModelChange: "samModelChange", runTool: "runTool", toolModelChange: "toolModelChange", openToolParams: "openToolParams" }, usesOnChanges: true, ngImport: i0, template: "<!-- SAM (jit-ui#90): box-prompt segment, interactive point prompts, and a\n model picker. -->\n<span class=\"toolbar-separator\"></span>\n<!-- SAM controls grouped: box-prompt Segment, then the point-prompt tool,\n then the model-picker dropdown (the chevron) which governs BOTH tools.\n Falls back to Segment + point with no model picker when none are set. -->\n<span *ngIf=\"samModels.length; else segmentPlain\" class=\"p-buttonset segment-split\">\n <p-button\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Segment with SAM (box prompt)\"\n pTooltip=\"Segment the drawn rectangles with SAM (box prompt)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentRegions.emit()\"\n >\n <img src=\"assets/plotting/sam-box-prompt.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n <ng-container [ngTemplateOutlet]=\"samPointBtn\"></ng-container>\n <p-button\n icon=\"pi pi-chevron-down\"\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Pick the SAM model\"\n pTooltip=\"Pick the SAM model (used by both segment tools)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"samModelMenu.toggle($event)\"\n >\n </p-button>\n</span>\n<ng-template #segmentPlain>\n <span class=\"p-buttonset segment-split\">\n <p-button\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Segment with SAM (box prompt)\"\n pTooltip=\"Segment the drawn rectangles with SAM (box prompt)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentRegions.emit()\"\n >\n <img src=\"assets/plotting/sam-box-prompt.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n </p-button>\n <ng-container [ngTemplateOutlet]=\"samPointBtn\"></ng-container>\n </span>\n</ng-template>\n<!-- Point-prompt tool \u2014 shared between the models / no-models layouts so it\n always sits just left of the model dropdown. -->\n<ng-template #samPointBtn>\n <p-button\n size=\"small\"\n ariaLabel=\"SAM point prompts\"\n [text]=\"activeDragMode !== 'samPoint'\"\n pTooltip=\"SAM point prompts: click a fiber to segment it as a new region;\n Shift/Alt-click to refine (exclude) the current one.<br>Esc undoes it.\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toggleDragMode.emit('samPoint')\"\n >\n <img\n src=\"assets/plotting/sam-point-prompt.svg\"\n [class]=\"activeDragMode === 'samPoint' ? 'toolbar-icon-active' : 'toolbar-icon-dark'\"\n style=\"width: 1rem\"\n />\n </p-button>\n</ng-template>\n<p-menu #samModelMenu styleClass=\"model-menu\" [model]=\"samMenuItems\" [popup]=\"true\" appendTo=\"body\">\n <ng-template pTemplate=\"item\" let-item>\n <ng-container\n [ngTemplateOutlet]=\"modelMenuItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n ></ng-container>\n </ng-template>\n</p-menu>\n\n<!-- Shared item body for all three model pickers (SAM / YOLO / retinal):\n the standard check-or-blank icon and label, plus an info icon carrying\n the model's description as a hover tooltip. This replaced a single\n \"About the SAM models\" overlay panel \u2014 per-model text sits next to the\n model it describes, and YOLO/retinal get the same affordance.\n\n p-menu puts its click handler on the wrapping `.p-menuitem-content`,\n outside this template, so `command` still fires and the whole row stays\n clickable \u2014 which is also why the info icon has to stop propagation, or\n reading about a model would switch to it. -->\n<ng-template #modelMenuItem let-item>\n <a class=\"p-menuitem-link model-menu-item\">\n <span class=\"p-menuitem-icon\" [ngClass]=\"item.icon\"></span>\n <span class=\"p-menuitem-text\">{{ item.label }}</span>\n <!-- `bottom` like every other tooltip in this toolbar, and for room:\n these menus open under buttons that can sit near the right edge,\n where a `right` tooltip has a few hundred px and squeezes its text\n into a narrow column instead of using its max-width. -->\n <!-- A focusable <button>, not a bare <i>. As an <i> the description\n was reachable only by hovering a mouse: not tabbable, no\n accessible name, nothing for a screen reader to announce \u2014 so\n keyboard and AT users could pick a checkpoint but never read what\n it was. PrimeNG's tooltip shows on focus as well as hover, so\n making it a button restores it for free.\n\n `aria-describedby` is deliberately not used: the tooltip is\n rendered into `body` on demand and has no stable id to point at.\n The description is carried on `aria-label` instead, which is\n announced whether or not the tooltip is showing. It is stripped of\n markup first, since the copy contains `<b>`/`<br>` for the visual\n tooltip and those would otherwise be read out literally. -->\n <button\n *ngIf=\"item.tooltip\"\n type=\"button\"\n class=\"pi pi-info-circle model-info-icon\"\n [attr.aria-label]=\"item.infoLabel\"\n [pTooltip]=\"item.tooltip\"\n [escape]=\"false\"\n tooltipPosition=\"bottom\"\n appendTo=\"body\"\n tooltipStyleClass=\"model-info-tooltip\"\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"$event.stopPropagation()\"\n (keydown.space)=\"$event.stopPropagation()\"\n ></button>\n </a>\n</ng-template>\n<p-button\n size=\"small\"\n [text]=\"true\"\n ariaLabel=\"Segment cells (cellpose-SAM)\"\n pTooltip=\"Auto-segment cells inside each drawn rectangle (cellpose-SAM)\"\n tooltipPosition=\"bottom\"\n (onClick)=\"segmentCellpose.emit()\"\n>\n <img src=\"assets/plotting/cells.svg\" class=\"toolbar-icon-dark\" style=\"width: 1rem\" />\n</p-button>\n\n<!-- Contributed no-prompt tools (TOOLBAR_TOOLS). Unlike the SAM and\n cellpose tools these take no prompt \u2014 they sweep the whole current\n view rather than being pointed at something \u2014 so there is nothing to\n draw first. Each renders the same split button: run, pick checkpoint,\n open parameters.\n\n Nothing here names a specific tool. The library ships none; a host\n registers them, which is what lets this package be published without\n the closed model dependencies. -->\n<ng-container *ngFor=\"let tool of contributedTools; trackBy: trackToolById\">\n <span class=\"p-buttonset segment-split\">\n <p-button\n size=\"small\"\n [text]=\"true\"\n [ariaLabel]=\"tool.label\"\n [pTooltip]=\"tool.runTooltip\"\n tooltipPosition=\"bottom\"\n (onClick)=\"runTool.emit(tool.id)\"\n >\n <!-- `toolbar-icon-dark` is hardcoded rather than bound, unlike the\n toggle tools: these fire a run and have no active state. -->\n <img\n *ngIf=\"tool.icon.src\"\n [src]=\"tool.icon.src\"\n class=\"toolbar-icon-dark\"\n style=\"width: 1rem\"\n [alt]=\"tool.label\"\n />\n <i *ngIf=\"tool.icon.pi\" class=\"pi {{ tool.icon.pi }}\"></i>\n </p-button>\n <p-button\n icon=\"pi pi-chevron-down\"\n size=\"small\"\n [text]=\"true\"\n [ariaLabel]=\"'Pick the model for ' + tool.label\"\n [pTooltip]=\"tool.modelTooltip || 'Pick the model'\"\n tooltipPosition=\"bottom\"\n (onClick)=\"toolMenu.toggle($event)\"\n >\n </p-button>\n <p-button\n *ngIf=\"tool.params.length\"\n icon=\"pi pi-sliders-h\"\n size=\"small\"\n [text]=\"true\"\n [ariaLabel]=\"'Parameters for ' + tool.label\"\n [pTooltip]=\"tool.paramsTooltip || 'Parameters'\"\n tooltipPosition=\"bottom\"\n (onClick)=\"openToolParams.emit(tool.id)\"\n >\n </p-button>\n </span>\n <p-menu #toolMenu styleClass=\"model-menu\" [model]=\"toolMenuItems[tool.id] || []\" [popup]=\"true\" appendTo=\"body\">\n <ng-template pTemplate=\"item\" let-item>\n <ng-container\n [ngTemplateOutlet]=\"modelMenuItem\"\n [ngTemplateOutletContext]=\"{ $implicit: item }\"\n ></ng-container>\n </ng-template>\n </p-menu>\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.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { 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: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { 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: MenuModule }, { kind: "component", type: i4.Menu, selector: "p-menu", inputs: ["model", "popup", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex"], outputs: ["onShow", "onHide", "onBlur", "onFocus"] }, { 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: "ngmodule", type: TooltipModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
13665
13737
  }
13666
13738
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SegmentationToolsComponent, decorators: [{
13667
13739
  type: Component,
@@ -13938,7 +14010,7 @@ class ToolbarComponent {
13938
14010
  this.displayHelpDialog = true;
13939
14011
  }
13940
14012
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
13941
- 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$2.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 }); }
14013
+ 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 }); }
13942
14014
  }
13943
14015
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ToolbarComponent, decorators: [{
13944
14016
  type: Component,
@@ -15247,7 +15319,7 @@ class RegionTableComponent {
15247
15319
  this.pageChange.emit({ first: event.first ?? 0, rows: event.rows ?? this.rows });
15248
15320
  }
15249
15321
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: RegionTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
15250
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: RegionTableComponent, isStandalone: true, selector: "region-table", inputs: { page: "page", total: "total", first: "first", rows: "rows", rowsPerPageOptions: "rowsPerPageOptions", metaKey: "metaKey", selection: "selection", presetSet: "presetSet", defaultColor: "defaultColor", mpp: "mpp", volumes: "volumes", labelDrafts: "labelDrafts" }, outputs: { selectionChange: "selectionChange", rowSelect: "rowSelect", labelEditStart: "labelEditStart", labelDraftChange: "labelDraftChange", labelEditStop: "labelEditStop", classPick: "classPick", deleteRow: "deleteRow", pageChange: "pageChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"region-editor-content\">\n <p-table\n [value]=\"page\"\n [tableStyle]=\"{ width: '100%' }\"\n [selection]=\"selection\"\n (selectionChange)=\"selectionChange.emit($event)\"\n selectionMode=\"multiple\"\n [metaKeySelection]=\"metaKey\"\n (onRowSelect)=\"rowSelect.emit()\"\n (onRowUnselect)=\"rowSelect.emit()\"\n [resizableColumns]=\"true\"\n [rowHover]=\"true\"\n dataKey=\"id\"\n styleClass=\"p-datatable-sm region-editor-table\"\n [scrollable]=\"true\"\n scrollHeight=\"100%\"\n >\n <ng-template pTemplate=\"header\">\n <!-- Hide the column header when there are no regions to show. -->\n <tr *ngIf=\"total > 0\">\n <!-- Class column: shrinks to fit \"Region\" by default, grows with longer labels. -->\n <th class=\"class-col-header\" pResizableColumn>Class <p-sortIcon field=\"label\"></p-sortIcon></th>\n <th\n *ngIf=\"!volumes\"\n class=\"surface-col-header\"\n pResizableColumn\n pTooltip=\"The region's area on this slice\"\n tooltipPosition=\"top\"\n >\n Surface\n </th>\n <th\n *ngIf=\"volumes\"\n class=\"surface-col-header\"\n pResizableColumn\n pTooltip=\"The volume of the region's class over the whole stack: its area on every slice, one slice thick\"\n tooltipPosition=\"top\"\n >\n Volume\n </th>\n <!-- Trash column: just enough for the icon. -->\n <th style=\"width: 2rem\"></th>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"body\" let-region let-rowIdx=\"rowIndex\">\n <tr [pSelectableRow]=\"region\" [pSelectableRowIndex]=\"rowIdx\">\n <td class=\"class-cell\">\n <ng-container *ngIf=\"!labelDrafts.has(region); else editLabelTpl\">\n <span class=\"class-cell-display\">\n <!-- Quick-pick a preset class (its colour follows); the pencil is for\n a free-form name / rename (jit-ui#70). -->\n <p-dropdown\n class=\"class-dropdown\"\n appendTo=\"body\"\n [options]=\"presetSet.classes\"\n optionLabel=\"name\"\n optionValue=\"name\"\n [ngModel]=\"region.label\"\n [autoDisplayFirst]=\"false\"\n ariaLabel=\"Class\"\n (onChange)=\"classPick.emit({ region: region, name: $event.value })\"\n >\n <ng-template pTemplate=\"selectedItem\">\n <span class=\"dd-value\">\n <span class=\"chip-swatch\" [style.background]=\"rowView.get(region)?.color\"></span>\n <span class=\"class-name\" [pTooltip]=\"region.label\" tooltipPosition=\"bottom\">{{\n region.label || '\u2014'\n }}</span>\n </span>\n </ng-template>\n <ng-template let-c pTemplate=\"item\">\n <span class=\"dd-value\">\n <span class=\"chip-swatch\" [style.background]=\"c.color\"></span>\n <span>{{ c.name }}</span>\n </span>\n </ng-template>\n </p-dropdown>\n <i\n class=\"pi pi-pencil icon-hover ml-2\"\n pTooltip=\"Rename / free-form label\"\n tooltipPosition=\"bottom\"\n role=\"button\"\n aria-label=\"Rename\"\n (click)=\"startEdit(region, $event)\"\n ></i>\n </span>\n </ng-container>\n <ng-template #editLabelTpl>\n <span class=\"p-input-icon-right class-cell-edit\">\n <i\n class=\"pi pi-times icon-hover\"\n pTooltip=\"Close\"\n tooltipPosition=\"bottom\"\n role=\"button\"\n aria-label=\"Cancel rename\"\n (click)=\"stopEdit(region, false, $event)\"\n ></i>\n <input\n type=\"text\"\n pInputText\n class=\"p-inputtext-sm w-full\"\n aria-label=\"Class label\"\n [ngModel]=\"labelDrafts.get(region)\"\n (ngModelChange)=\"labelDraftChange.emit({ region: region, value: $event })\"\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"stopEdit(region, true, $event)\"\n (keydown.escape)=\"stopEdit(region, false, $event)\"\n />\n </span>\n </ng-template>\n </td>\n <!-- Surface area (\u00B5m\u00B2/mm\u00B2 when the image is scaled, else px\u00B2). -->\n <td class=\"surface-cell\">{{ rowView.get(region)?.measure || '\u2014' }}</td>\n <td>\n <button\n pButton\n pRipple\n icon=\"pi pi-trash\"\n aria-label=\"Delete region\"\n class=\"p-button-sm p-button-rounded p-button-text p-button-warning\"\n (click)=\"deleteRow.emit(first + rowIdx)\"\n ></button>\n </td>\n </tr>\n </ng-template>\n </p-table>\n</div>\n\n<p-paginator\n *ngIf=\"total > 0\"\n [rows]=\"rows\"\n [totalRecords]=\"total\"\n [first]=\"first\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [showCurrentPageReport]=\"true\"\n currentPageReportTemplate=\"{first} to {last} of {totalRecords}\"\n (onPageChange)=\"onPage($event)\"\n styleClass=\"region-editor-paginator\"\n>\n</p-paginator>\n", styles: ["@charset \"UTF-8\";.chip-swatch{display:inline-block;width:.75rem;height:.75rem;border-radius:50%;border:1px solid rgba(0,0,0,.2);flex:0 0 auto}.dd-value{display:inline-flex;align-items:center;gap:.4rem;min-width:0}.dd-value .class-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}.region-editor-content{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}::ng-deep .region-editor-content .p-3{padding:0!important}::ng-deep .region-editor-content>p-table{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}::ng-deep .region-editor-content .p-datatable.region-editor-table{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}::ng-deep .region-editor-content .p-datatable.region-editor-table>.p-datatable-header{flex:0 0 auto}::ng-deep .region-editor-content .p-datatable.region-editor-table>.p-datatable-wrapper,::ng-deep .region-editor-content .p-datatable.region-editor-table>.p-datatable-table-container,::ng-deep .region-editor-content .p-datatable.region-editor-table .p-scroller-viewport{flex:1 1 0;min-height:0;height:100%;max-height:100%;overflow:auto}:host>p-paginator{display:block;flex:0 0 auto;border-top:1px solid var(--surface-border);padding:.25rem 0}.class-col-header,.class-cell{white-space:nowrap;max-width:16rem}.class-cell .class-cell-display{display:flex;align-items:center;min-width:0}.class-cell .class-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.class-cell .icon-hover{flex:0 0 auto}.class-cell .icon-hover{cursor:pointer;color:var(--text-color-secondary);transition:color .15s ease}.class-cell .icon-hover:hover{color:var(--primary-color)}.class-cell .class-cell-edit{display:inline-block;width:100%}::ng-deep .p-datatable-sm .p-datatable-tbody>tr>td .p-button.p-button-rounded,::ng-deep .p-datatable-sm .p-datatable-tbody>tr>td .p-button.p-button-icon-only{width:1.5rem;height:1.5rem;padding:0}::ng-deep .p-datatable-sm .p-datatable-tbody>tr>td .p-button.p-button-rounded .p-button-icon,::ng-deep .p-datatable-sm .p-datatable-tbody>tr>td .p-button.p-button-icon-only .p-button-icon{font-size:.75rem}::ng-deep .p-datatable-sm .p-datatable-tbody>tr>td .p-inputtext{padding:.25rem .5rem;font-size:.875rem}::ng-deep .p-datatable-sm .p-paginator{padding:.15rem .25rem!important;min-height:unset!important}::ng-deep .p-datatable-sm .p-paginator,::ng-deep .p-datatable-sm .p-paginator *{font-size:.75rem!important}::ng-deep .p-datatable-sm .p-paginator .p-paginator-element{min-width:1.5rem!important;height:1.5rem!important}::ng-deep .p-datatable-sm .p-paginator .p-paginator-current{font-size:.75rem!important;height:1.5rem!important;min-width:unset!important}::ng-deep .p-datatable-sm .p-paginator .p-dropdown{height:1.5rem!important}::ng-deep .p-datatable-sm .p-paginator .p-dropdown .p-dropdown-label{padding:.1rem .25rem!important;line-height:1.2!important}::ng-deep .p-datatable-sm .p-paginator .p-dropdown .p-dropdown-trigger{width:1.25rem!important}::ng-deep .p-paginator-rpp-options .p-dropdown-panel .p-dropdown-items .p-dropdown-item{padding:.25rem .5rem!important;font-size:.75rem!important}.class-cell .class-dropdown{min-width:0;flex:1 1 auto}::ng-deep .class-cell .class-dropdown.p-dropdown{width:100%}::ng-deep .class-cell .class-dropdown .p-dropdown-label{padding:.15rem .5rem;display:flex;align-items:center}\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.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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: ButtonModule }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i4.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: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }, { kind: "ngmodule", type: PaginatorModule }, { kind: "component", type: i7.Paginator, selector: "p-paginator", inputs: ["pageLinkSize", "style", "styleClass", "alwaysShow", "dropdownAppendTo", "templateLeft", "templateRight", "appendTo", "dropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showFirstLastIcon", "totalRecords", "rows", "rowsPerPageOptions", "showJumpToPageDropdown", "showJumpToPageInput", "jumpToPageItemTemplate", "showPageLinks", "locale", "dropdownItemTemplate", "first"], outputs: ["onPageChange"] }, { kind: "ngmodule", type: RippleModule }, { kind: "directive", type: i3$1.Ripple, selector: "[pRipple]" }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i9.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "style", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "scrollDirection", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "responsive", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "autoLayout", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll", "virtualRowHeight"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i9.SelectableRow, selector: "[pSelectableRow]", inputs: ["pSelectableRow", "pSelectableRowIndex", "pSelectableRowDisabled"] }, { kind: "directive", type: i9.ResizableColumn, selector: "[pResizableColumn]", inputs: ["pResizableColumnDisabled"] }, { kind: "component", type: i9.SortIcon, selector: "p-sortIcon", inputs: ["field"] }, { 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 }); }
15322
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: RegionTableComponent, isStandalone: true, selector: "region-table", inputs: { page: "page", total: "total", first: "first", rows: "rows", rowsPerPageOptions: "rowsPerPageOptions", metaKey: "metaKey", selection: "selection", presetSet: "presetSet", defaultColor: "defaultColor", mpp: "mpp", volumes: "volumes", labelDrafts: "labelDrafts" }, outputs: { selectionChange: "selectionChange", rowSelect: "rowSelect", labelEditStart: "labelEditStart", labelDraftChange: "labelDraftChange", labelEditStop: "labelEditStop", classPick: "classPick", deleteRow: "deleteRow", pageChange: "pageChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"region-editor-content\">\n <p-table\n [value]=\"page\"\n [tableStyle]=\"{ width: '100%' }\"\n [selection]=\"selection\"\n (selectionChange)=\"selectionChange.emit($event)\"\n selectionMode=\"multiple\"\n [metaKeySelection]=\"metaKey\"\n (onRowSelect)=\"rowSelect.emit()\"\n (onRowUnselect)=\"rowSelect.emit()\"\n [resizableColumns]=\"true\"\n [rowHover]=\"true\"\n dataKey=\"id\"\n styleClass=\"p-datatable-sm region-editor-table\"\n [scrollable]=\"true\"\n scrollHeight=\"100%\"\n >\n <ng-template pTemplate=\"header\">\n <!-- Hide the column header when there are no regions to show. -->\n <tr *ngIf=\"total > 0\">\n <!-- Class column: shrinks to fit \"Region\" by default, grows with longer labels. -->\n <th class=\"class-col-header\" pResizableColumn>Class <p-sortIcon field=\"label\"></p-sortIcon></th>\n <th\n *ngIf=\"!volumes\"\n class=\"surface-col-header\"\n pResizableColumn\n pTooltip=\"The region's area on this slice\"\n tooltipPosition=\"top\"\n >\n Surface\n </th>\n <th\n *ngIf=\"volumes\"\n class=\"surface-col-header\"\n pResizableColumn\n pTooltip=\"The volume of the region's class over the whole stack: its area on every slice, one slice thick\"\n tooltipPosition=\"top\"\n >\n Volume\n </th>\n <!-- Trash column: just enough for the icon. -->\n <th style=\"width: 2rem\"></th>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"body\" let-region let-rowIdx=\"rowIndex\">\n <tr [pSelectableRow]=\"region\" [pSelectableRowIndex]=\"rowIdx\">\n <td class=\"class-cell\">\n <ng-container *ngIf=\"!labelDrafts.has(region); else editLabelTpl\">\n <span class=\"class-cell-display\">\n <!-- Quick-pick a preset class (its colour follows); the pencil is for\n a free-form name / rename (jit-ui#70). -->\n <p-dropdown\n class=\"class-dropdown\"\n appendTo=\"body\"\n [options]=\"presetSet.classes\"\n optionLabel=\"name\"\n optionValue=\"name\"\n [ngModel]=\"region.label\"\n [autoDisplayFirst]=\"false\"\n ariaLabel=\"Class\"\n (onChange)=\"classPick.emit({ region: region, name: $event.value })\"\n >\n <ng-template pTemplate=\"selectedItem\">\n <span class=\"dd-value\">\n <span class=\"chip-swatch\" [style.background]=\"rowView.get(region)?.color\"></span>\n <span class=\"class-name\" [pTooltip]=\"region.label\" tooltipPosition=\"bottom\">{{\n region.label || '\u2014'\n }}</span>\n </span>\n </ng-template>\n <ng-template let-c pTemplate=\"item\">\n <span class=\"dd-value\">\n <span class=\"chip-swatch\" [style.background]=\"c.color\"></span>\n <span>{{ c.name }}</span>\n </span>\n </ng-template>\n </p-dropdown>\n <i\n class=\"pi pi-pencil icon-hover ml-2\"\n pTooltip=\"Rename / free-form label\"\n tooltipPosition=\"bottom\"\n role=\"button\"\n aria-label=\"Rename\"\n (click)=\"startEdit(region, $event)\"\n ></i>\n </span>\n </ng-container>\n <ng-template #editLabelTpl>\n <span class=\"p-input-icon-right class-cell-edit\">\n <i\n class=\"pi pi-times icon-hover\"\n pTooltip=\"Close\"\n tooltipPosition=\"bottom\"\n role=\"button\"\n aria-label=\"Cancel rename\"\n (click)=\"stopEdit(region, false, $event)\"\n ></i>\n <input\n type=\"text\"\n pInputText\n class=\"p-inputtext-sm w-full\"\n aria-label=\"Class label\"\n [ngModel]=\"labelDrafts.get(region)\"\n (ngModelChange)=\"labelDraftChange.emit({ region: region, value: $event })\"\n (click)=\"$event.stopPropagation()\"\n (keydown.enter)=\"stopEdit(region, true, $event)\"\n (keydown.escape)=\"stopEdit(region, false, $event)\"\n />\n </span>\n </ng-template>\n </td>\n <!-- Surface area (\u00B5m\u00B2/mm\u00B2 when the image is scaled, else px\u00B2). -->\n <td class=\"surface-cell\">{{ rowView.get(region)?.measure || '\u2014' }}</td>\n <td>\n <button\n pButton\n pRipple\n icon=\"pi pi-trash\"\n aria-label=\"Delete region\"\n class=\"p-button-sm p-button-rounded p-button-text p-button-warning\"\n (click)=\"deleteRow.emit(first + rowIdx)\"\n ></button>\n </td>\n </tr>\n </ng-template>\n </p-table>\n</div>\n\n<p-paginator\n *ngIf=\"total > 0\"\n [rows]=\"rows\"\n [totalRecords]=\"total\"\n [first]=\"first\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [showCurrentPageReport]=\"true\"\n currentPageReportTemplate=\"{first} to {last} of {totalRecords}\"\n (onPageChange)=\"onPage($event)\"\n styleClass=\"region-editor-paginator\"\n>\n</p-paginator>\n", styles: ["@charset \"UTF-8\";.chip-swatch{display:inline-block;width:.75rem;height:.75rem;border-radius:50%;border:1px solid rgba(0,0,0,.2);flex:0 0 auto}.dd-value{display:inline-flex;align-items:center;gap:.4rem;min-width:0}.dd-value .class-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}.region-editor-content{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}::ng-deep .region-editor-content .p-3{padding:0!important}::ng-deep .region-editor-content>p-table{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}::ng-deep .region-editor-content .p-datatable.region-editor-table{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;overflow:hidden}::ng-deep .region-editor-content .p-datatable.region-editor-table>.p-datatable-header{flex:0 0 auto}::ng-deep .region-editor-content .p-datatable.region-editor-table>.p-datatable-wrapper,::ng-deep .region-editor-content .p-datatable.region-editor-table>.p-datatable-table-container,::ng-deep .region-editor-content .p-datatable.region-editor-table .p-scroller-viewport{flex:1 1 0;min-height:0;height:100%;max-height:100%;overflow:auto}:host>p-paginator{display:block;flex:0 0 auto;border-top:1px solid var(--surface-border);padding:.25rem 0}.class-col-header,.class-cell{white-space:nowrap;max-width:16rem}.class-cell .class-cell-display{display:flex;align-items:center;min-width:0}.class-cell .class-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.class-cell .icon-hover{flex:0 0 auto}.class-cell .icon-hover{cursor:pointer;color:var(--text-color-secondary);transition:color .15s ease}.class-cell .icon-hover:hover{color:var(--primary-color)}.class-cell .class-cell-edit{display:inline-block;width:100%}::ng-deep .p-datatable-sm .p-datatable-tbody>tr>td .p-button.p-button-rounded,::ng-deep .p-datatable-sm .p-datatable-tbody>tr>td .p-button.p-button-icon-only{width:1.5rem;height:1.5rem;padding:0}::ng-deep .p-datatable-sm .p-datatable-tbody>tr>td .p-button.p-button-rounded .p-button-icon,::ng-deep .p-datatable-sm .p-datatable-tbody>tr>td .p-button.p-button-icon-only .p-button-icon{font-size:.75rem}::ng-deep .p-datatable-sm .p-datatable-tbody>tr>td .p-inputtext{padding:.25rem .5rem;font-size:.875rem}::ng-deep .p-datatable-sm .p-paginator{padding:.15rem .25rem!important;min-height:unset!important}::ng-deep .p-datatable-sm .p-paginator,::ng-deep .p-datatable-sm .p-paginator *{font-size:.75rem!important}::ng-deep .p-datatable-sm .p-paginator .p-paginator-element{min-width:1.5rem!important;height:1.5rem!important}::ng-deep .p-datatable-sm .p-paginator .p-paginator-current{font-size:.75rem!important;height:1.5rem!important;min-width:unset!important}::ng-deep .p-datatable-sm .p-paginator .p-dropdown{height:1.5rem!important}::ng-deep .p-datatable-sm .p-paginator .p-dropdown .p-dropdown-label{padding:.1rem .25rem!important;line-height:1.2!important}::ng-deep .p-datatable-sm .p-paginator .p-dropdown .p-dropdown-trigger{width:1.25rem!important}::ng-deep .p-paginator-rpp-options .p-dropdown-panel .p-dropdown-items .p-dropdown-item{padding:.25rem .5rem!important;font-size:.75rem!important}.class-cell .class-dropdown{min-width:0;flex:1 1 auto}::ng-deep .class-cell .class-dropdown.p-dropdown{width:100%}::ng-deep .class-cell .class-dropdown .p-dropdown-label{padding:.15rem .5rem;display:flex;align-items:center}\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.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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: ButtonModule }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain"] }, { 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: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }, { kind: "ngmodule", type: PaginatorModule }, { kind: "component", type: i7.Paginator, selector: "p-paginator", inputs: ["pageLinkSize", "style", "styleClass", "alwaysShow", "dropdownAppendTo", "templateLeft", "templateRight", "appendTo", "dropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showFirstLastIcon", "totalRecords", "rows", "rowsPerPageOptions", "showJumpToPageDropdown", "showJumpToPageInput", "jumpToPageItemTemplate", "showPageLinks", "locale", "dropdownItemTemplate", "first"], outputs: ["onPageChange"] }, { kind: "ngmodule", type: RippleModule }, { kind: "directive", type: i3$1.Ripple, selector: "[pRipple]" }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i9.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "style", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "scrollDirection", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "responsive", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "autoLayout", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll", "virtualRowHeight"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i9.SelectableRow, selector: "[pSelectableRow]", inputs: ["pSelectableRow", "pSelectableRowIndex", "pSelectableRowDisabled"] }, { kind: "directive", type: i9.ResizableColumn, selector: "[pResizableColumn]", inputs: ["pResizableColumnDisabled"] }, { kind: "component", type: i9.SortIcon, selector: "p-sortIcon", inputs: ["field"] }, { 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 }); }
15251
15323
  }
15252
15324
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: RegionTableComponent, decorators: [{
15253
15325
  type: Component,
@@ -15517,7 +15589,7 @@ class ManageClassesDialogComponent {
15517
15589
  this.draftChange.emit(this.draft);
15518
15590
  }
15519
15591
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ManageClassesDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
15520
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ManageClassesDialogComponent, isStandalone: true, selector: "region-manage-classes-dialog", inputs: { visible: "visible", draft: "draft" }, outputs: { visibleChange: "visibleChange", draftChange: "draftChange", apply: "apply", resetDefaults: "resetDefaults", importFile: "importFile", export: "export" }, ngImport: i0, template: "<p-dialog\n header=\"Manage annotation classes\"\n [visible]=\"visible\"\n (visibleChange)=\"visibleChange.emit($event)\"\n [modal]=\"true\"\n appendTo=\"body\"\n [style]=\"{ width: '34rem' }\"\n [draggable]=\"true\"\n [resizable]=\"false\"\n>\n <div *ngIf=\"draft\" class=\"manage-dialog flex flex-column gap-3 pt-2\">\n <div class=\"manage-classes flex flex-column gap-2\">\n <div\n *ngFor=\"let c of draft.classes; let i = index; trackBy: trackByIndex\"\n class=\"manage-class-row flex align-items-center gap-2\"\n >\n <hex-color-picker [color]=\"c.color\" (colorChange)=\"patchClass(i, { color: $event })\"></hex-color-picker>\n <input\n type=\"text\"\n pInputText\n class=\"p-inputtext-sm flex-1\"\n [ngModel]=\"c.name\"\n (ngModelChange)=\"patchClass(i, { name: $event })\"\n placeholder=\"Class name\"\n aria-label=\"Class name\"\n />\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-trash\"\n class=\"p-button-sm p-button-text p-button-warning\"\n pTooltip=\"Remove class\"\n tooltipPosition=\"bottom\"\n aria-label=\"Remove class\"\n (click)=\"removeClass(i)\"\n ></button>\n </div>\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-plus\"\n label=\"Add class\"\n class=\"p-button-sm p-button-text align-self-start\"\n (click)=\"addClass()\"\n ></button>\n </div>\n\n <div class=\"manage-section flex flex-column gap-2\">\n <span class=\"section-title font-medium\"\n >Fallback palette <small class=\"text-color-secondary\">\u2014 colours for classes not listed</small></span\n >\n <div class=\"fallback-palette flex align-items-center flex-wrap gap-2\">\n <span\n *ngFor=\"let col of draft.fallbackPalette; let i = index; trackBy: trackByIndex\"\n class=\"fallback-swatch flex align-items-center\"\n >\n <hex-color-picker [color]=\"col\" (colorChange)=\"setFallbackColor(i, $event)\"></hex-color-picker>\n <i\n class=\"pi pi-times icon-hover ml-1\"\n pTooltip=\"Remove\"\n tooltipPosition=\"bottom\"\n (click)=\"removeFallbackColor(i)\"\n ></i>\n </span>\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-plus\"\n class=\"p-button-sm p-button-text\"\n pTooltip=\"Add fallback colour\"\n tooltipPosition=\"bottom\"\n aria-label=\"Add fallback colour\"\n (click)=\"addFallbackColor()\"\n ></button>\n </div>\n </div>\n\n <div class=\"flex align-items-center gap-2\">\n <p-checkbox\n [ngModel]=\"draft.autoPromote\"\n (ngModelChange)=\"setAutoPromote($event)\"\n [binary]=\"true\"\n inputId=\"autoPromote\"\n ></p-checkbox>\n <label for=\"autoPromote\">Auto-add detected classes to this list as they appear</label>\n </div>\n\n <div class=\"flex align-items-center gap-3\">\n <span class=\"font-medium\">Label matching</span>\n <p-selectButton\n [options]=\"matchModeOptions\"\n [ngModel]=\"draft.matchMode\"\n (ngModelChange)=\"setMatchMode($event)\"\n optionLabel=\"label\"\n optionValue=\"value\"\n ></p-selectButton>\n </div>\n </div>\n\n <ng-template pTemplate=\"footer\">\n <div class=\"manage-footer flex align-items-center flex-wrap gap-2 w-full\">\n <span class=\"flex align-items-center flex-wrap gap-1\">\n <p-button label=\"Reset\" icon=\"pi pi-refresh\" [text]=\"true\" (onClick)=\"resetDefaults.emit()\"></p-button>\n <p-button label=\"Import\" icon=\"pi pi-upload\" [text]=\"true\" (onClick)=\"presetFileInput.click()\"></p-button>\n <input\n #presetFileInput\n type=\"file\"\n accept=\".json,application/json\"\n hidden\n (change)=\"importFile.emit($event); presetFileInput.value = ''\"\n />\n <p-button label=\"Export\" icon=\"pi pi-download\" [text]=\"true\" (onClick)=\"export.emit()\"></p-button>\n </span>\n <span class=\"flex-1\"></span>\n <!-- Apply persists to the server and closes. -->\n <p-button label=\"Apply\" icon=\"pi pi-check\" (onClick)=\"apply.emit()\"></p-button>\n </div>\n </ng-template>\n</p-dialog>\n", styles: [".manage-dialog .manage-class-row hex-color-picker,.manage-dialog .fallback-palette hex-color-picker{flex:0 0 auto}.manage-dialog .fallback-palette .icon-hover{cursor:pointer;color:var(--text-color-secondary)}.manage-dialog .fallback-palette .icon-hover:hover{color:var(--primary-color)}\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: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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: ButtonModule }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain"] }, { 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$3.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: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i8$1.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: 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: HexColorPickerComponent, selector: "hex-color-picker", inputs: ["color"], outputs: ["colorChange", "colorInput"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15592
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ManageClassesDialogComponent, isStandalone: true, selector: "region-manage-classes-dialog", inputs: { visible: "visible", draft: "draft" }, outputs: { visibleChange: "visibleChange", draftChange: "draftChange", apply: "apply", resetDefaults: "resetDefaults", importFile: "importFile", export: "export" }, ngImport: i0, template: "<p-dialog\n header=\"Manage annotation classes\"\n [visible]=\"visible\"\n (visibleChange)=\"visibleChange.emit($event)\"\n [modal]=\"true\"\n appendTo=\"body\"\n [style]=\"{ width: '34rem' }\"\n [draggable]=\"true\"\n [resizable]=\"false\"\n>\n <div *ngIf=\"draft\" class=\"manage-dialog flex flex-column gap-3 pt-2\">\n <div class=\"manage-classes flex flex-column gap-2\">\n <div\n *ngFor=\"let c of draft.classes; let i = index; trackBy: trackByIndex\"\n class=\"manage-class-row flex align-items-center gap-2\"\n >\n <hex-color-picker [color]=\"c.color\" (colorChange)=\"patchClass(i, { color: $event })\"></hex-color-picker>\n <input\n type=\"text\"\n pInputText\n class=\"p-inputtext-sm flex-1\"\n [ngModel]=\"c.name\"\n (ngModelChange)=\"patchClass(i, { name: $event })\"\n placeholder=\"Class name\"\n aria-label=\"Class name\"\n />\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-trash\"\n class=\"p-button-sm p-button-text p-button-warning\"\n pTooltip=\"Remove class\"\n tooltipPosition=\"bottom\"\n aria-label=\"Remove class\"\n (click)=\"removeClass(i)\"\n ></button>\n </div>\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-plus\"\n label=\"Add class\"\n class=\"p-button-sm p-button-text align-self-start\"\n (click)=\"addClass()\"\n ></button>\n </div>\n\n <div class=\"manage-section flex flex-column gap-2\">\n <span class=\"section-title font-medium\"\n >Fallback palette <small class=\"text-color-secondary\">\u2014 colours for classes not listed</small></span\n >\n <div class=\"fallback-palette flex align-items-center flex-wrap gap-2\">\n <span\n *ngFor=\"let col of draft.fallbackPalette; let i = index; trackBy: trackByIndex\"\n class=\"fallback-swatch flex align-items-center\"\n >\n <hex-color-picker [color]=\"col\" (colorChange)=\"setFallbackColor(i, $event)\"></hex-color-picker>\n <i\n class=\"pi pi-times icon-hover ml-1\"\n pTooltip=\"Remove\"\n tooltipPosition=\"bottom\"\n (click)=\"removeFallbackColor(i)\"\n ></i>\n </span>\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-plus\"\n class=\"p-button-sm p-button-text\"\n pTooltip=\"Add fallback colour\"\n tooltipPosition=\"bottom\"\n aria-label=\"Add fallback colour\"\n (click)=\"addFallbackColor()\"\n ></button>\n </div>\n </div>\n\n <div class=\"flex align-items-center gap-2\">\n <p-checkbox\n [ngModel]=\"draft.autoPromote\"\n (ngModelChange)=\"setAutoPromote($event)\"\n [binary]=\"true\"\n inputId=\"autoPromote\"\n ></p-checkbox>\n <label for=\"autoPromote\">Auto-add detected classes to this list as they appear</label>\n </div>\n\n <div class=\"flex align-items-center gap-3\">\n <span class=\"font-medium\">Label matching</span>\n <p-selectButton\n [options]=\"matchModeOptions\"\n [ngModel]=\"draft.matchMode\"\n (ngModelChange)=\"setMatchMode($event)\"\n optionLabel=\"label\"\n optionValue=\"value\"\n ></p-selectButton>\n </div>\n </div>\n\n <ng-template pTemplate=\"footer\">\n <div class=\"manage-footer flex align-items-center flex-wrap gap-2 w-full\">\n <span class=\"flex align-items-center flex-wrap gap-1\">\n <p-button label=\"Reset\" icon=\"pi pi-refresh\" [text]=\"true\" (onClick)=\"resetDefaults.emit()\"></p-button>\n <p-button label=\"Import\" icon=\"pi pi-upload\" [text]=\"true\" (onClick)=\"presetFileInput.click()\"></p-button>\n <input\n #presetFileInput\n type=\"file\"\n accept=\".json,application/json\"\n hidden\n (change)=\"importFile.emit($event); presetFileInput.value = ''\"\n />\n <p-button label=\"Export\" icon=\"pi pi-download\" [text]=\"true\" (onClick)=\"export.emit()\"></p-button>\n </span>\n <span class=\"flex-1\"></span>\n <!-- Apply persists to the server and closes. -->\n <p-button label=\"Apply\" icon=\"pi pi-check\" (onClick)=\"apply.emit()\"></p-button>\n </div>\n </ng-template>\n</p-dialog>\n", styles: [".manage-dialog .manage-class-row hex-color-picker,.manage-dialog .fallback-palette hex-color-picker{flex:0 0 auto}.manage-dialog .fallback-palette .icon-hover{cursor:pointer;color:var(--text-color-secondary)}.manage-dialog .fallback-palette .icon-hover:hover{color:var(--primary-color)}\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: FormsModule }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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: ButtonModule }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain"] }, { 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: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i8$1.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: 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: HexColorPickerComponent, selector: "hex-color-picker", inputs: ["color"], outputs: ["colorChange", "colorInput"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
15521
15593
  }
15522
15594
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ManageClassesDialogComponent, decorators: [{
15523
15595
  type: Component,
@@ -17256,7 +17328,7 @@ class RegionEditorComponent {
17256
17328
  this.messageService.add({ key: VIZ_TOAST_KEY, severity, summary, detail });
17257
17329
  }
17258
17330
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: RegionEditorComponent, deps: [{ token: REGION_EDITOR_API }, { token: i1$2.MessageService }, { token: i1$2.ConfirmationService }, { token: RegionPersistenceService }, { token: MaskExportService }], target: i0.ɵɵFactoryTarget.Component }); }
17259
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: RegionEditorComponent, isStandalone: true, selector: "region-editor", inputs: { active: "active" }, providers: [RegionPersistenceService], viewQueries: [{ propertyName: "overlayPanel", first: true, predicate: ["op"], descendants: true }], ngImport: i0, template: "<p-toolbar styleClass=\"region-toolbar\">\n <div class=\"p-toolbar-group-start align-items-center flex-wrap gap-1\">\n <p-button\n icon=\"pi pi-tag\"\n size=\"small\"\n [text]=\"!showShapeLabel\"\n pTooltip=\"Show region labels.\"\n tooltipPosition=\"bottom\"\n (onClick)=\"showShapeLabel = !showShapeLabel; changeShowShapeLabel(showShapeLabel)\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-stop-circle\"\n size=\"small\"\n [text]=\"!regionsFilled\"\n pTooltip=\"Fill regions in their colour (an atlas's look); labels sit inside.\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Fill regions\"\n (onClick)=\"toggleFill()\"\n >\n </p-button>\n <p-button\n *ngIf=\"volumeView\"\n icon=\"pi pi-eye\"\n size=\"small\"\n [text]=\"!regionsThroughVolume\"\n [attr.aria-pressed]=\"regionsThroughVolume\"\n pTooltip=\"See the regions through the volume. Off, the volume hides the parts of them behind what it shows; on, none (layers traced inside bright structures, such as an OCT's).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Regions through volume\"\n (onClick)=\"toggleThroughVolume()\"\n >\n </p-button>\n <span class=\"toolbar-separator\"></span>\n <!-- New regions are now created from the diagram toolbar\n (rectangle, polyline, freeform, wand). -->\n <p-button\n icon=\"pi pi-upload\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import regions: a GeoJSON file, or a label volume (.nii, .nii.gz, .nrrd) chosen with its ontology JSON for the class tree.\"\n tooltipPosition=\"bottom\"\n (click)=\"roiFileInput.click()\"\n >\n </p-button>\n <input\n #roiFileInput\n type=\"file\"\n accept=\".geojson,.json,.nii,.gz,.nrrd\"\n multiple\n hidden\n (change)=\"importRois($event); roiFileInput.value = ''\"\n />\n <p-button\n icon=\"pi pi-folder-open\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import a folder of per-slice GeoJSON files (one per slice, in name order, with its regionset.json class tree).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Import a folder of regions\"\n (click)=\"roiFolderInput.click()\"\n >\n </p-button>\n <input\n #roiFolderInput\n type=\"file\"\n webkitdirectory\n multiple\n hidden\n (change)=\"importRois($event); roiFolderInput.value = ''\"\n />\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download regions as geojson file.\"\n tooltipPosition=\"bottom\"\n icon=\"pi pi-download\"\n (click)=\"exportRois()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-save\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Save regions to server.\"\n tooltipPosition=\"bottom\"\n (click)=\"persistRegions()\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n >\n </p-button>\n <p-button\n *ngIf=\"canImportFromServer\"\n icon=\"pi pi-cloud-download\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import regions from server: a GeoJSON file, a folder of per-slice GeoJSON files or a label volume (.nii, .nii.gz, .nrrd).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Import regions from server\"\n [loading]=\"importing\"\n (click)=\"importFromServer()\"\n >\n </p-button>\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download mask of the regions as PNG\"\n tooltipPosition=\"bottom\"\n (click)=\"openSaveMaskDialog()\"\n [disabled]=\"!regions || !regions.length\"\n >\n <!-- References the file so Inkscape edits to mask.svg show up directly\n (the lib's assets/** are served at assets/plotting/). The\n toolbar-icon-dark filter recolours the black SVG to the theme blue,\n same as region-editor.svg and the other SVG toolbar icons. -->\n <img\n src=\"assets/plotting/mask.svg\"\n class=\"toolbar-icon-dark\"\n style=\"width: 1rem; height: 1rem; vertical-align: middle\"\n alt=\"Download mask\"\n />\n </p-button>\n <p-button\n label=\"x512\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Round all rectangle regions\n to width and height that are multiple of 512\"\n tooltipPosition=\"bottom\"\n (click)=\"roundRectangleLengths()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-check-square\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Select all regions\"\n tooltipPosition=\"bottom\"\n (click)=\"selectAllRegions()\"\n [disabled]=\"!regions || !regions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-tags\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit class on selected rows\"\n tooltipPosition=\"bottom\"\n (click)=\"!selectedRegions || !selectedRegions.length ? op.hide() : op.toggle($event)\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-palette\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit colour of selected regions\"\n tooltipPosition=\"bottom\"\n (click)=\"openColorDialog()\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-overlayPanel #op (onHide)=\"bulkClass = ''; newBulkClass = ''\">\n <div class=\"flex flex-column gap-2\" style=\"width: 18rem\">\n <span class=\"font-medium text-900\"\n >Set class on {{ selectedRegions?.length || 0 }} selected region(s)</span\n >\n <!-- Selecting a class applies it immediately (choosing an option closes the\n overlay, so there's no separate Apply button). -->\n <p-dropdown\n styleClass=\"w-full\"\n appendTo=\"body\"\n [options]=\"presetSet.classes\"\n optionLabel=\"name\"\n optionValue=\"name\"\n [(ngModel)]=\"bulkClass\"\n placeholder=\"Choose a class\u2026\"\n [filter]=\"presetSet.classes.length > 8\"\n filterBy=\"name\"\n (onChange)=\"applyBulkClass()\"\n >\n <ng-template let-c pTemplate=\"item\">\n <span class=\"dd-value\"\n ><span class=\"chip-swatch\" [style.background]=\"c.color\"></span>{{ c.name }}</span\n >\n </ng-template>\n </p-dropdown>\n <div class=\"p-inputgroup\">\n <input\n type=\"text\"\n pInputText\n placeholder=\"\u2026or a new class\"\n [(ngModel)]=\"newBulkClass\"\n (keydown.enter)=\"addAndApplyBulkClass()\"\n />\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-plus\"\n pTooltip=\"Add class &amp; set it\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!newBulkClass.trim()\"\n (click)=\"addAndApplyBulkClass()\"\n ></button>\n </div>\n </div>\n </p-overlayPanel>\n <p-button\n icon=\"pi pi-trash\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"deleteSelectedRegions()\"\n pTooltip=\"Delete selected regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n ></p-button>\n <p-button\n icon=\"pi pi-times-circle\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"clearAllRegions()\"\n pTooltip=\"Delete all regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n ></p-button>\n <p-button\n icon=\"pi pi-question\"\n size=\"small\"\n [text]=\"true\"\n (click)=\"showHelp()\"\n pTooltip=\"Help\"\n tooltipPosition=\"bottom\"\n ></p-button>\n </div>\n</p-toolbar>\n\n<region-classes-panel\n [classes]=\"displayClasses\"\n [counts]=\"classCounts\"\n [matchMode]=\"presetSet.matchMode\"\n [activeClass]=\"activeClass\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n [defaultClassName]=\"defaultClassName\"\n (pick)=\"selectActiveClass($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (remove)=\"deleteClass($event)\"\n (manage)=\"openManageDialog()\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (toggle3d)=\"toggleClass3d($event)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden([$event], !hiddenClasses.has($event))\"\n [treeView]=\"classHierarchy ? classTreeView : null\"\n (treeViewChange)=\"classTreeView = $event\"\n>\n <!-- A region set with a class hierarchy (an atlas ontology) can show its classes as a tree. -->\n <region-class-tree\n *ngIf=\"classHierarchy as hierarchy\"\n [nodes]=\"hierarchy\"\n [counts]=\"treeCounts\"\n [colorOf]=\"treeColor\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (pick)=\"pickClasses($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (toggle3d)=\"toggleClass3d($event.name, $event.classes)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden($event.classes, $event.hidden)\"\n >\n </region-class-tree>\n</region-classes-panel>\n\n<region-table\n [page]=\"pagedRegions\"\n [total]=\"tableTotal\"\n [first]=\"paginatorFirst\"\n [rows]=\"paginatorRows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [metaKey]=\"metaKey\"\n [selection]=\"selectedRegions\"\n (selectionChange)=\"selectedRegions = $event\"\n (rowSelect)=\"onSelectionChanged()\"\n [presetSet]=\"tablePresetSet\"\n [defaultColor]=\"shapeColor\"\n [mpp]=\"mpp\"\n [volumes]=\"tableVolumes\"\n [labelDrafts]=\"editingLabelRegions\"\n (labelEditStart)=\"startEditLabel($event)\"\n (labelDraftChange)=\"setLabelDraft($event.region, $event.value)\"\n (labelEditStop)=\"stopEditLabel($event.region, $event.commit)\"\n (classPick)=\"applyPresetToRegion($event.region, $event.name)\"\n (deleteRow)=\"deleteRegion($event)\"\n (pageChange)=\"onPageChange($event)\"\n>\n</region-table>\n\n<!-- Live-edit mode: no Save/Cancel \u2014 every edit commits immediately to the\n active image's region cache. -->\n\n<p-confirmDialog\n #cd\n [style]=\"{ width: '24rem', height: '14rem' }\"\n key=\"positionDialog\"\n [baseZIndex]=\"10000\"\n rejectButtonStyleClass=\"p-button-outlined\"\n>\n <ng-template pTemplate=\"footer\">\n <button type=\"button\" pButton icon=\"pi pi-check\" label=\"Yes\" (click)=\"cd.accept()\"></button>\n <button\n type=\"button\"\n class=\"p-button-outlined\"\n pButton\n icon=\"pi pi-times\"\n label=\"No\"\n (click)=\"cd.reject()\"\n ></button>\n </ng-template>\n</p-confirmDialog>\n\n<region-save-regions-dialog\n [(visible)]=\"showSaveAsDialog\"\n [filename]=\"saveAsFilename\"\n (filenameChange)=\"saveAsFilename = $event; checkSaveAsFileExists()\"\n [fileExists]=\"saveAsFileExists\"\n [busy]=\"saveAsBusy\"\n (confirm)=\"confirmSaveAs()\"\n (cancelSave)=\"cancelSaveAs()\"\n>\n</region-save-regions-dialog>\n\n<region-save-regions-dialog\n header=\"Export Regions\"\n confirmLabel=\"Export\"\n confirmIcon=\"pi pi-download\"\n inputId=\"export-filename\"\n [(visible)]=\"showExportDialog\"\n [(filename)]=\"exportFilename\"\n (confirm)=\"confirmExport()\"\n>\n</region-save-regions-dialog>\n\n<region-save-mask-dialog\n [(visible)]=\"showSaveMaskDialog\"\n [(filename)]=\"saveMaskFilename\"\n [(mode)]=\"maskMode\"\n [busy]=\"maskBusy\"\n [progress]=\"maskProgress\"\n [encoding]=\"maskEncoding\"\n (confirm)=\"confirmSaveMask()\"\n (cancelExport)=\"cancelSaveMask()\"\n>\n</region-save-mask-dialog>\n\n<region-color-dialog\n [(visible)]=\"showColorDialog\"\n [edits]=\"classColorEdits\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n (apply)=\"applyColorToSelected($event)\"\n>\n</region-color-dialog>\n\n<!-- Manage annotation classes (jit-ui#70): edits a working copy, committed on Apply. -->\n<region-manage-classes-dialog\n [(visible)]=\"showManageDialog\"\n [(draft)]=\"presetDraft\"\n (apply)=\"applyManageDialog(true)\"\n (resetDefaults)=\"resetPresetsToDefaults()\"\n (importFile)=\"importPresets($event)\"\n (export)=\"exportPresets()\"\n>\n</region-manage-classes-dialog>\n\n<region-editor-help [(visible)]=\"displayHelpDialog\"></region-editor-help>\n", styles: ["@charset \"UTF-8\";.chip-swatch{display:inline-block;width:.75rem;height:.75rem;border-radius:50%;border:1px solid rgba(0,0,0,.2);flex:0 0 auto}.dd-value{display:inline-flex;align-items:center;gap:.4rem;min-width:0}.dd-value .class-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}::ng-deep region-editor .p-toolbar.region-toolbar{padding:.25rem .5rem;border-radius:0;border-left:none;border-right:none;border-top:none}::ng-deep region-editor .p-toolbar.region-toolbar .p-button.p-button-icon-only{width:auto;padding:.25rem .5rem}::ng-deep region-editor .p-toolbar.region-toolbar .p-button{padding:.25rem .5rem}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}\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.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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: ButtonModule }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain"] }, { 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: ConfirmDialogModule }, { kind: "component", type: i7$2.ConfirmDialog, selector: "p-confirmDialog", inputs: ["header", "icon", "message", "style", "styleClass", "maskStyleClass", "acceptIcon", "acceptLabel", "closeAriaLabel", "acceptAriaLabel", "acceptVisible", "rejectIcon", "rejectLabel", "rejectAriaLabel", "rejectVisible", "acceptButtonStyleClass", "rejectButtonStyleClass", "closeOnEscape", "dismissableMask", "blockScroll", "rtl", "closable", "appendTo", "key", "autoZIndex", "baseZIndex", "transitionOptions", "focusTrap", "defaultFocus", "breakpoints", "visible", "position"], outputs: ["onHide"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i4.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: 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: ToolbarModule }, { kind: "component", type: i4$2.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: ClassesPanelComponent, selector: "region-classes-panel", inputs: ["classes", "counts", "matchMode", "activeClass", "picked", "marked", "selectedCount", "defaultClassName", "volume", "shown3d", "hidden", "treeView"], outputs: ["toggleHidden", "toggle3d", "treeViewChange", "pick", "recolor", "remove", "manage"] }, { kind: "component", type: RegionTableComponent, selector: "region-table", inputs: ["page", "total", "first", "rows", "rowsPerPageOptions", "metaKey", "selection", "presetSet", "defaultColor", "mpp", "volumes", "labelDrafts"], outputs: ["selectionChange", "rowSelect", "labelEditStart", "labelDraftChange", "labelEditStop", "classPick", "deleteRow", "pageChange"] }, { kind: "component", type: ClassTreeComponent, selector: "region-class-tree", inputs: ["nodes", "counts", "colorOf", "picked", "marked", "volume", "shown3d", "hidden"], outputs: ["pick", "recolor", "toggle3d", "toggleHidden"] }, { kind: "component", type: SaveRegionsDialogComponent, selector: "region-save-regions-dialog", inputs: ["header", "confirmLabel", "confirmIcon", "inputId", "visible", "filename", "fileExists", "busy"], outputs: ["visibleChange", "filenameChange", "confirm", "cancelSave"] }, { kind: "component", type: SaveMaskDialogComponent, selector: "region-save-mask-dialog", inputs: ["visible", "filename", "mode", "busy", "progress", "encoding"], outputs: ["visibleChange", "filenameChange", "modeChange", "confirm", "cancelExport"] }, { kind: "component", type: RegionColorDialogComponent, selector: "region-color-dialog", inputs: ["visible", "selectedCount", "edits"], outputs: ["visibleChange", "apply"] }, { kind: "component", type: ManageClassesDialogComponent, selector: "region-manage-classes-dialog", inputs: ["visible", "draft"], outputs: ["visibleChange", "draftChange", "apply", "resetDefaults", "importFile", "export"] }, { kind: "component", type: RegionEditorHelpComponent, selector: "region-editor-help", inputs: ["visible"], outputs: ["visibleChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
17331
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: RegionEditorComponent, isStandalone: true, selector: "region-editor", inputs: { active: "active" }, providers: [RegionPersistenceService], viewQueries: [{ propertyName: "overlayPanel", first: true, predicate: ["op"], descendants: true }], ngImport: i0, template: "<p-toolbar styleClass=\"region-toolbar\">\n <div class=\"p-toolbar-group-start align-items-center flex-wrap gap-1\">\n <p-button\n icon=\"pi pi-tag\"\n size=\"small\"\n [text]=\"!showShapeLabel\"\n pTooltip=\"Show region labels.\"\n tooltipPosition=\"bottom\"\n (onClick)=\"showShapeLabel = !showShapeLabel; changeShowShapeLabel(showShapeLabel)\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-stop-circle\"\n size=\"small\"\n [text]=\"!regionsFilled\"\n pTooltip=\"Fill regions in their colour (an atlas's look); labels sit inside.\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Fill regions\"\n (onClick)=\"toggleFill()\"\n >\n </p-button>\n <p-button\n *ngIf=\"volumeView\"\n icon=\"pi pi-eye\"\n size=\"small\"\n [text]=\"!regionsThroughVolume\"\n [attr.aria-pressed]=\"regionsThroughVolume\"\n pTooltip=\"See the regions through the volume. Off, the volume hides the parts of them behind what it shows; on, none (layers traced inside bright structures, such as an OCT's).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Regions through volume\"\n (onClick)=\"toggleThroughVolume()\"\n >\n </p-button>\n <span class=\"toolbar-separator\"></span>\n <!-- New regions are now created from the diagram toolbar\n (rectangle, polyline, freeform, wand). -->\n <p-button\n icon=\"pi pi-upload\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import regions: a GeoJSON file, or a label volume (.nii, .nii.gz, .nrrd) chosen with its ontology JSON for the class tree.\"\n tooltipPosition=\"bottom\"\n (click)=\"roiFileInput.click()\"\n >\n </p-button>\n <input\n #roiFileInput\n type=\"file\"\n accept=\".geojson,.json,.nii,.gz,.nrrd\"\n multiple\n hidden\n (change)=\"importRois($event); roiFileInput.value = ''\"\n />\n <p-button\n icon=\"pi pi-folder-open\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import a folder of per-slice GeoJSON files (one per slice, in name order, with its regionset.json class tree).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Import a folder of regions\"\n (click)=\"roiFolderInput.click()\"\n >\n </p-button>\n <input\n #roiFolderInput\n type=\"file\"\n webkitdirectory\n multiple\n hidden\n (change)=\"importRois($event); roiFolderInput.value = ''\"\n />\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download regions as geojson file.\"\n tooltipPosition=\"bottom\"\n icon=\"pi pi-download\"\n (click)=\"exportRois()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-save\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Save regions to server.\"\n tooltipPosition=\"bottom\"\n (click)=\"persistRegions()\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n >\n </p-button>\n <p-button\n *ngIf=\"canImportFromServer\"\n icon=\"pi pi-cloud-download\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Import regions from server: a GeoJSON file, a folder of per-slice GeoJSON files or a label volume (.nii, .nii.gz, .nrrd).\"\n tooltipPosition=\"bottom\"\n ariaLabel=\"Import regions from server\"\n [loading]=\"importing\"\n (click)=\"importFromServer()\"\n >\n </p-button>\n <p-button\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Download mask of the regions as PNG\"\n tooltipPosition=\"bottom\"\n (click)=\"openSaveMaskDialog()\"\n [disabled]=\"!regions || !regions.length\"\n >\n <!-- References the file so Inkscape edits to mask.svg show up directly\n (the lib's assets/** are served at assets/plotting/). The\n toolbar-icon-dark filter recolours the black SVG to the theme blue,\n same as region-editor.svg and the other SVG toolbar icons. -->\n <img\n src=\"assets/plotting/mask.svg\"\n class=\"toolbar-icon-dark\"\n style=\"width: 1rem; height: 1rem; vertical-align: middle\"\n alt=\"Download mask\"\n />\n </p-button>\n <p-button\n label=\"x512\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Round all rectangle regions\n to width and height that are multiple of 512\"\n tooltipPosition=\"bottom\"\n (click)=\"roundRectangleLengths()\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-check-square\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Select all regions\"\n tooltipPosition=\"bottom\"\n (click)=\"selectAllRegions()\"\n [disabled]=\"!regions || !regions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-tags\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit class on selected rows\"\n tooltipPosition=\"bottom\"\n (click)=\"!selectedRegions || !selectedRegions.length ? op.hide() : op.toggle($event)\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-button\n icon=\"pi pi-palette\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n pTooltip=\"Edit colour of selected regions\"\n tooltipPosition=\"bottom\"\n (click)=\"openColorDialog()\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n >\n </p-button>\n <p-overlayPanel #op (onHide)=\"bulkClass = ''; newBulkClass = ''\">\n <div class=\"flex flex-column gap-2\" style=\"width: 18rem\">\n <span class=\"font-medium text-900\"\n >Set class on {{ selectedRegions?.length || 0 }} selected region(s)</span\n >\n <!-- Selecting a class applies it immediately (choosing an option closes the\n overlay, so there's no separate Apply button). -->\n <p-dropdown\n styleClass=\"w-full\"\n appendTo=\"body\"\n [options]=\"presetSet.classes\"\n optionLabel=\"name\"\n optionValue=\"name\"\n [(ngModel)]=\"bulkClass\"\n placeholder=\"Choose a class\u2026\"\n [filter]=\"presetSet.classes.length > 8\"\n filterBy=\"name\"\n (onChange)=\"applyBulkClass()\"\n >\n <ng-template let-c pTemplate=\"item\">\n <span class=\"dd-value\"\n ><span class=\"chip-swatch\" [style.background]=\"c.color\"></span>{{ c.name }}</span\n >\n </ng-template>\n </p-dropdown>\n <div class=\"p-inputgroup\">\n <input\n type=\"text\"\n pInputText\n placeholder=\"\u2026or a new class\"\n [(ngModel)]=\"newBulkClass\"\n (keydown.enter)=\"addAndApplyBulkClass()\"\n />\n <button\n pButton\n type=\"button\"\n icon=\"pi pi-plus\"\n pTooltip=\"Add class &amp; set it\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!newBulkClass.trim()\"\n (click)=\"addAndApplyBulkClass()\"\n ></button>\n </div>\n </div>\n </p-overlayPanel>\n <p-button\n icon=\"pi pi-trash\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"deleteSelectedRegions()\"\n pTooltip=\"Delete selected regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"!selectedRegions || !selectedRegions.length\"\n ></p-button>\n <p-button\n icon=\"pi pi-times-circle\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"mr-2\"\n (click)=\"clearAllRegions()\"\n pTooltip=\"Delete all regions\"\n tooltipPosition=\"bottom\"\n [disabled]=\"(!regions || !regions.length) && !hasRegionSource\"\n ></p-button>\n <p-button\n icon=\"pi pi-question\"\n size=\"small\"\n [text]=\"true\"\n (click)=\"showHelp()\"\n pTooltip=\"Help\"\n tooltipPosition=\"bottom\"\n ></p-button>\n </div>\n</p-toolbar>\n\n<region-classes-panel\n [classes]=\"displayClasses\"\n [counts]=\"classCounts\"\n [matchMode]=\"presetSet.matchMode\"\n [activeClass]=\"activeClass\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n [defaultClassName]=\"defaultClassName\"\n (pick)=\"selectActiveClass($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (remove)=\"deleteClass($event)\"\n (manage)=\"openManageDialog()\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (toggle3d)=\"toggleClass3d($event)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden([$event], !hiddenClasses.has($event))\"\n [treeView]=\"classHierarchy ? classTreeView : null\"\n (treeViewChange)=\"classTreeView = $event\"\n>\n <!-- A region set with a class hierarchy (an atlas ontology) can show its classes as a tree. -->\n <region-class-tree\n *ngIf=\"classHierarchy as hierarchy\"\n [nodes]=\"hierarchy\"\n [counts]=\"treeCounts\"\n [colorOf]=\"treeColor\"\n [picked]=\"pickedClass\"\n [marked]=\"markedClasses\"\n [volume]=\"volumeView\"\n [shown3d]=\"shown3dNames\"\n (pick)=\"pickClasses($event)\"\n (recolor)=\"setClassColor($event.name, $event.color)\"\n (toggle3d)=\"toggleClass3d($event.name, $event.classes)\"\n [hidden]=\"hiddenClasses\"\n (toggleHidden)=\"setClassesHidden($event.classes, $event.hidden)\"\n >\n </region-class-tree>\n</region-classes-panel>\n\n<region-table\n [page]=\"pagedRegions\"\n [total]=\"tableTotal\"\n [first]=\"paginatorFirst\"\n [rows]=\"paginatorRows\"\n [rowsPerPageOptions]=\"rowsPerPageOptions\"\n [metaKey]=\"metaKey\"\n [selection]=\"selectedRegions\"\n (selectionChange)=\"selectedRegions = $event\"\n (rowSelect)=\"onSelectionChanged()\"\n [presetSet]=\"tablePresetSet\"\n [defaultColor]=\"shapeColor\"\n [mpp]=\"mpp\"\n [volumes]=\"tableVolumes\"\n [labelDrafts]=\"editingLabelRegions\"\n (labelEditStart)=\"startEditLabel($event)\"\n (labelDraftChange)=\"setLabelDraft($event.region, $event.value)\"\n (labelEditStop)=\"stopEditLabel($event.region, $event.commit)\"\n (classPick)=\"applyPresetToRegion($event.region, $event.name)\"\n (deleteRow)=\"deleteRegion($event)\"\n (pageChange)=\"onPageChange($event)\"\n>\n</region-table>\n\n<!-- Live-edit mode: no Save/Cancel \u2014 every edit commits immediately to the\n active image's region cache. -->\n\n<p-confirmDialog\n #cd\n [style]=\"{ width: '24rem', height: '14rem' }\"\n key=\"positionDialog\"\n [baseZIndex]=\"10000\"\n rejectButtonStyleClass=\"p-button-outlined\"\n>\n <ng-template pTemplate=\"footer\">\n <button type=\"button\" pButton icon=\"pi pi-check\" label=\"Yes\" (click)=\"cd.accept()\"></button>\n <button\n type=\"button\"\n class=\"p-button-outlined\"\n pButton\n icon=\"pi pi-times\"\n label=\"No\"\n (click)=\"cd.reject()\"\n ></button>\n </ng-template>\n</p-confirmDialog>\n\n<region-save-regions-dialog\n [(visible)]=\"showSaveAsDialog\"\n [filename]=\"saveAsFilename\"\n (filenameChange)=\"saveAsFilename = $event; checkSaveAsFileExists()\"\n [fileExists]=\"saveAsFileExists\"\n [busy]=\"saveAsBusy\"\n (confirm)=\"confirmSaveAs()\"\n (cancelSave)=\"cancelSaveAs()\"\n>\n</region-save-regions-dialog>\n\n<region-save-regions-dialog\n header=\"Export Regions\"\n confirmLabel=\"Export\"\n confirmIcon=\"pi pi-download\"\n inputId=\"export-filename\"\n [(visible)]=\"showExportDialog\"\n [(filename)]=\"exportFilename\"\n (confirm)=\"confirmExport()\"\n>\n</region-save-regions-dialog>\n\n<region-save-mask-dialog\n [(visible)]=\"showSaveMaskDialog\"\n [(filename)]=\"saveMaskFilename\"\n [(mode)]=\"maskMode\"\n [busy]=\"maskBusy\"\n [progress]=\"maskProgress\"\n [encoding]=\"maskEncoding\"\n (confirm)=\"confirmSaveMask()\"\n (cancelExport)=\"cancelSaveMask()\"\n>\n</region-save-mask-dialog>\n\n<region-color-dialog\n [(visible)]=\"showColorDialog\"\n [edits]=\"classColorEdits\"\n [selectedCount]=\"selectedRegions?.length || 0\"\n (apply)=\"applyColorToSelected($event)\"\n>\n</region-color-dialog>\n\n<!-- Manage annotation classes (jit-ui#70): edits a working copy, committed on Apply. -->\n<region-manage-classes-dialog\n [(visible)]=\"showManageDialog\"\n [(draft)]=\"presetDraft\"\n (apply)=\"applyManageDialog(true)\"\n (resetDefaults)=\"resetPresetsToDefaults()\"\n (importFile)=\"importPresets($event)\"\n (export)=\"exportPresets()\"\n>\n</region-manage-classes-dialog>\n\n<region-editor-help [(visible)]=\"displayHelpDialog\"></region-editor-help>\n", styles: ["@charset \"UTF-8\";.chip-swatch{display:inline-block;width:.75rem;height:.75rem;border-radius:50%;border:1px solid rgba(0,0,0,.2);flex:0 0 auto}.dd-value{display:inline-flex;align-items:center;gap:.4rem;min-width:0}.dd-value .class-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}::ng-deep region-editor .p-toolbar.region-toolbar{padding:.25rem .5rem;border-radius:0;border-left:none;border-right:none;border-top:none}::ng-deep region-editor .p-toolbar.region-toolbar .p-button.p-button-icon-only{width:auto;padding:.25rem .5rem}::ng-deep region-editor .p-toolbar.region-toolbar .p-button{padding:.25rem .5rem}.toolbar-icon-dark{filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}\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.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { 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: ButtonModule }, { kind: "directive", type: i3.ButtonDirective, selector: "[pButton]", inputs: ["iconPos", "loadingIcon", "label", "icon", "loading", "severity", "raised", "rounded", "text", "outlined", "size", "plain"] }, { 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: ConfirmDialogModule }, { kind: "component", type: i7$2.ConfirmDialog, selector: "p-confirmDialog", inputs: ["header", "icon", "message", "style", "styleClass", "maskStyleClass", "acceptIcon", "acceptLabel", "closeAriaLabel", "acceptAriaLabel", "acceptVisible", "rejectIcon", "rejectLabel", "rejectAriaLabel", "rejectVisible", "acceptButtonStyleClass", "rejectButtonStyleClass", "closeOnEscape", "dismissableMask", "blockScroll", "rtl", "closable", "appendTo", "key", "autoZIndex", "baseZIndex", "transitionOptions", "focusTrap", "defaultFocus", "breakpoints", "visible", "position"], outputs: ["onHide"] }, { 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: 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: 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: ClassesPanelComponent, selector: "region-classes-panel", inputs: ["classes", "counts", "matchMode", "activeClass", "picked", "marked", "selectedCount", "defaultClassName", "volume", "shown3d", "hidden", "treeView"], outputs: ["toggleHidden", "toggle3d", "treeViewChange", "pick", "recolor", "remove", "manage"] }, { kind: "component", type: RegionTableComponent, selector: "region-table", inputs: ["page", "total", "first", "rows", "rowsPerPageOptions", "metaKey", "selection", "presetSet", "defaultColor", "mpp", "volumes", "labelDrafts"], outputs: ["selectionChange", "rowSelect", "labelEditStart", "labelDraftChange", "labelEditStop", "classPick", "deleteRow", "pageChange"] }, { kind: "component", type: ClassTreeComponent, selector: "region-class-tree", inputs: ["nodes", "counts", "colorOf", "picked", "marked", "volume", "shown3d", "hidden"], outputs: ["pick", "recolor", "toggle3d", "toggleHidden"] }, { kind: "component", type: SaveRegionsDialogComponent, selector: "region-save-regions-dialog", inputs: ["header", "confirmLabel", "confirmIcon", "inputId", "visible", "filename", "fileExists", "busy"], outputs: ["visibleChange", "filenameChange", "confirm", "cancelSave"] }, { kind: "component", type: SaveMaskDialogComponent, selector: "region-save-mask-dialog", inputs: ["visible", "filename", "mode", "busy", "progress", "encoding"], outputs: ["visibleChange", "filenameChange", "modeChange", "confirm", "cancelExport"] }, { kind: "component", type: RegionColorDialogComponent, selector: "region-color-dialog", inputs: ["visible", "selectedCount", "edits"], outputs: ["visibleChange", "apply"] }, { kind: "component", type: ManageClassesDialogComponent, selector: "region-manage-classes-dialog", inputs: ["visible", "draft"], outputs: ["visibleChange", "draftChange", "apply", "resetDefaults", "importFile", "export"] }, { kind: "component", type: RegionEditorHelpComponent, selector: "region-editor-help", inputs: ["visible"], outputs: ["visibleChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
17260
17332
  }
17261
17333
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: RegionEditorComponent, decorators: [{
17262
17334
  type: Component,
@@ -17786,7 +17858,7 @@ class ChannelHistogramComponent {
17786
17858
  };
17787
17859
  }
17788
17860
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ChannelHistogramComponent, deps: [{ token: CHANNEL_HISTOGRAM_API }], target: i0.ɵɵFactoryTarget.Component }); }
17789
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ChannelHistogramComponent, isStandalone: true, selector: "channel-histogram", inputs: { visible: "visible" }, outputs: { visibleChange: "visibleChange" }, ngImport: i0, template: "<p-dialog\n header=\"Channels &amp; Histogram\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"channel-histogram-dialog\"\n (onShow)=\"onShow()\"\n>\n <div class=\"ch-body\">\n <!-- Channels -->\n <p-table\n [value]=\"channels\"\n selectionMode=\"single\"\n [(selection)]=\"selected\"\n dataKey=\"index\"\n (onRowSelect)=\"selectChannel($event.data)\"\n styleClass=\"ch-table p-datatable-sm\"\n >\n <ng-template pTemplate=\"header\">\n <tr>\n <th style=\"width: 2.5rem\"></th>\n <th>Channel</th>\n <th style=\"width: 3.5rem; text-align: center\">Show</th>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"body\" let-ch>\n <tr [pSelectableRow]=\"ch\">\n <td>\n <hex-color-picker\n *ngIf=\"multichannel\"\n [color]=\"ch.color\"\n (colorChange)=\"onColorChange(ch, $event)\"\n ></hex-color-picker>\n </td>\n <td>{{ ch.name }}</td>\n <td style=\"text-align: center\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"ch.visible\"\n (onChange)=\"onVisibleToggle(ch, $event.checked)\"\n ></p-checkbox>\n </td>\n </tr>\n </ng-template>\n </p-table>\n\n <!-- Pseudo-color presets for the selected channel (multichannel only;\n grayscale uses the colormap below). -->\n <div class=\"ch-presets\" *ngIf=\"multichannel && selected\">\n <span class=\"ch-presets-lbl\">{{ selected.name }} LUT</span>\n <button\n *ngFor=\"let p of lutColors\"\n type=\"button\"\n class=\"ch-preset\"\n [style.background]=\"p.color\"\n [pTooltip]=\"p.name\"\n tooltipPosition=\"top\"\n (click)=\"setPreset(selected, p.color)\"\n ></button>\n </div>\n\n <!-- Histogram -->\n <div class=\"ch-hist-head\">\n <span>Histogram{{ selected ? ' \u2014 ' + selected.name : '' }}</span>\n <p-checkbox\n [binary]=\"true\"\n [(ngModel)]=\"logScale\"\n (onChange)=\"toggleLog($event.checked)\"\n label=\"Log\"\n ></p-checkbox>\n </div>\n <div [id]=\"histogramDiv\" class=\"ch-hist-plot\"></div>\n\n <!-- Display window / gamma -->\n <div class=\"ch-sliders\" *ngIf=\"selected\">\n <!-- Window endpoints are shown/edited in NATIVE units (0..255 for 8-bit,\n the channel's observed native range for 16-bit). One-way bound to the\n native getters; the handlers map back to the 8-bit display window. -->\n <div class=\"ch-row\">\n <span class=\"ch-lbl\">Min</span>\n <p-slider\n [ngModel]=\"minNative\"\n [min]=\"sliderMin\"\n [max]=\"sliderMax\"\n [step]=\"sliderStep\"\n (onChange)=\"onMinChange($event.value)\"\n styleClass=\"ch-slider\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"minNative\"\n [min]=\"sliderMin\"\n [max]=\"sliderMax\"\n [showButtons]=\"false\"\n (onInput)=\"onMinChange($event.value)\"\n styleClass=\"ch-num\"\n ></p-inputNumber>\n <span class=\"ch-busy\"\n ><i *ngIf=\"activeAdjust === 'min'\" class=\"pi pi-spin pi-spinner\" aria-label=\"updating\"></i\n ></span>\n </div>\n <div class=\"ch-row\">\n <span class=\"ch-lbl\">Max</span>\n <p-slider\n [ngModel]=\"maxNative\"\n [min]=\"sliderMin\"\n [max]=\"sliderMax\"\n [step]=\"sliderStep\"\n (onChange)=\"onMaxChange($event.value)\"\n styleClass=\"ch-slider\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"maxNative\"\n [min]=\"sliderMin\"\n [max]=\"sliderMax\"\n [showButtons]=\"false\"\n (onInput)=\"onMaxChange($event.value)\"\n styleClass=\"ch-num\"\n ></p-inputNumber>\n <span class=\"ch-busy\"\n ><i *ngIf=\"activeAdjust === 'max'\" class=\"pi pi-spin pi-spinner\" aria-label=\"updating\"></i\n ></span>\n </div>\n <div class=\"ch-row\">\n <span class=\"ch-lbl\">Gamma</span>\n <p-slider\n [(ngModel)]=\"selected.gamma\"\n [min]=\"0.1\"\n [max]=\"5\"\n [step]=\"0.01\"\n (onChange)=\"onGammaChange($event.value)\"\n styleClass=\"ch-slider\"\n ></p-slider>\n <p-inputNumber\n [(ngModel)]=\"selected.gamma\"\n [min]=\"0.1\"\n [max]=\"5\"\n [minFractionDigits]=\"2\"\n [maxFractionDigits]=\"2\"\n [showButtons]=\"false\"\n (onInput)=\"onGammaChange($event.value)\"\n styleClass=\"ch-num\"\n ></p-inputNumber>\n <span class=\"ch-busy\"\n ><i *ngIf=\"activeAdjust === 'gamma'\" class=\"pi pi-spin pi-spinner\" aria-label=\"updating\"></i\n ></span>\n </div>\n <div class=\"ch-btns\">\n <p-button label=\"Auto\" size=\"small\" (onClick)=\"auto()\"></p-button>\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n\n <!-- LUT / colormap controls. The colormap maps single-channel intensity \u2192\n colour, so it only applies to grayscale; RGB/multichannel use the\n per-channel pseudo-colour presets above. -->\n <div class=\"ch-display\">\n <p-treeSelect\n *ngIf=\"!multichannel\"\n [options]=\"colormapOptions\"\n [(ngModel)]=\"selectedColormap\"\n [selectionMode]=\"'single'\"\n placeholder=\"Colormap\"\n [scrollHeight]=\"'300px'\"\n pTooltip=\"Color Lookup Tables\"\n tooltipPosition=\"bottom\"\n (onNodeSelect)=\"onColormap($event.node)\"\n >\n <ng-template let-node pTemplate=\"default\">\n <div class=\"flex align-items-center gap-2\">\n <img *ngIf=\"!node.children\" [src]=\"node.data?.src\" class=\"colormap\" />\n <div>{{ node.label }}</div>\n </div>\n </ng-template>\n </p-treeSelect>\n <p-checkbox\n [binary]=\"true\"\n [(ngModel)]=\"invert\"\n (onChange)=\"onInvert($event.checked)\"\n label=\"Invert\"\n ></p-checkbox>\n </div>\n\n <!-- Exports: an 8-bit publication composite (always), plus a true-16-bit\n data-preserving TIFF when the image carries >8-bit pixels. -->\n <div class=\"ch-export\">\n <p-button\n label=\"Export composite (PNG)\"\n icon=\"pi pi-download\"\n size=\"small\"\n (onClick)=\"exportComposite()\"\n ></p-button>\n <p-button\n *ngIf=\"is16bit\"\n label=\"Export 16-bit TIFF\"\n icon=\"pi pi-download\"\n size=\"small\"\n [outlined]=\"true\"\n pTooltip=\"Data-preserving multi-band TIFF (visible channels, native bit depth)\"\n tooltipPosition=\"top\"\n (onClick)=\"exportData()\"\n ></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [":host ::ng-deep .channel-histogram-dialog.p-dialog{display:flex;flex-direction:column}:host ::ng-deep .channel-histogram-dialog .p-dialog-content{display:flex;flex-direction:column;flex:1 1 auto;overflow:auto}.ch-body{display:flex;flex-direction:column;gap:.5rem;flex:1 1 auto;min-height:0}.ch-table{flex:0 0 auto}.ch-presets{display:flex;align-items:center;gap:4px;margin:.25rem 0}.ch-presets .ch-presets-lbl{font-size:.8rem;margin-right:.25rem}.ch-presets .ch-preset{width:16px;height:16px;padding:0;border:1px solid rgba(0,0,0,.3);border-radius:2px;cursor:pointer}.ch-export{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.25rem}.ch-hist-head{display:flex;align-items:center;justify-content:space-between;font-size:.8rem;font-weight:600;margin-top:.25rem}.ch-hist-plot{flex:1 1 auto;min-height:120px;width:100%}.ch-sliders{display:flex;flex-direction:column;gap:.35rem}.ch-sliders .ch-row{display:flex;align-items:center;gap:.5rem}.ch-sliders .ch-row .ch-lbl{flex:0 0 3rem;font-size:.8rem}.ch-sliders .ch-row p-slider{flex:2 1 0;min-width:0}.ch-sliders .ch-row p-inputnumber{flex:1 1 0;min-width:0}.ch-sliders .ch-row .ch-busy{flex:0 0 1rem;display:inline-flex;align-items:center;justify-content:center;color:#4fa3ff;font-size:.8rem}.ch-sliders .ch-btns{display:flex;gap:.5rem;margin-top:.25rem}.ch-display{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-top:.25rem}.ch-display .colormap{width:60px;height:12px;object-fit:fill}:host ::ng-deep .ch-num .p-inputnumber-input{width:100%}:host ::ng-deep .ch-row p-slider .p-slider{width:100%}\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: 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: 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$3.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: InputNumberModule }, { kind: "component", type: i7$3.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "ngmodule", type: SliderModule }, { kind: "component", type: i2$1.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i9.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "style", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "scrollDirection", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "responsive", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "autoLayout", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll", "virtualRowHeight"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i9.SelectableRow, selector: "[pSelectableRow]", inputs: ["pSelectableRow", "pSelectableRowIndex", "pSelectableRowDisabled"] }, { 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: "ngmodule", type: TreeSelectModule }, { kind: "component", type: i11.TreeSelect, selector: "p-treeSelect", inputs: ["inputId", "scrollHeight", "disabled", "metaKeySelection", "variant", "display", "selectionMode", "tabindex", "ariaLabel", "ariaLabelledBy", "placeholder", "panelClass", "panelStyle", "panelStyleClass", "containerStyle", "containerStyleClass", "labelStyle", "labelStyleClass", "overlayOptions", "emptyMessage", "appendTo", "filter", "filterBy", "filterMode", "filterPlaceholder", "filterLocale", "filterInputAutoFocus", "propagateSelectionDown", "propagateSelectionUp", "showClear", "resetFilterOnHide", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autofocus", "options", "showTransitionOptions", "hideTransitionOptions", "loading"], outputs: ["onNodeExpand", "onNodeCollapse", "onShow", "onHide", "onClear", "onFilter", "onFocus", "onBlur", "onNodeUnselect", "onNodeSelect"] }, { kind: "component", type: HexColorPickerComponent, selector: "hex-color-picker", inputs: ["color"], outputs: ["colorChange", "colorInput"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
17861
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: ChannelHistogramComponent, isStandalone: true, selector: "channel-histogram", inputs: { visible: "visible" }, outputs: { visibleChange: "visibleChange" }, ngImport: i0, template: "<p-dialog\n header=\"Channels &amp; Histogram\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"channel-histogram-dialog\"\n (onShow)=\"onShow()\"\n>\n <div class=\"ch-body\">\n <!-- Channels -->\n <p-table\n [value]=\"channels\"\n selectionMode=\"single\"\n [(selection)]=\"selected\"\n dataKey=\"index\"\n (onRowSelect)=\"selectChannel($event.data)\"\n styleClass=\"ch-table p-datatable-sm\"\n >\n <ng-template pTemplate=\"header\">\n <tr>\n <th style=\"width: 2.5rem\"></th>\n <th>Channel</th>\n <th style=\"width: 3.5rem; text-align: center\">Show</th>\n </tr>\n </ng-template>\n <ng-template pTemplate=\"body\" let-ch>\n <tr [pSelectableRow]=\"ch\">\n <td>\n <hex-color-picker\n *ngIf=\"multichannel\"\n [color]=\"ch.color\"\n (colorChange)=\"onColorChange(ch, $event)\"\n ></hex-color-picker>\n </td>\n <td>{{ ch.name }}</td>\n <td style=\"text-align: center\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"ch.visible\"\n (onChange)=\"onVisibleToggle(ch, $event.checked)\"\n ></p-checkbox>\n </td>\n </tr>\n </ng-template>\n </p-table>\n\n <!-- Pseudo-color presets for the selected channel (multichannel only;\n grayscale uses the colormap below). -->\n <div class=\"ch-presets\" *ngIf=\"multichannel && selected\">\n <span class=\"ch-presets-lbl\">{{ selected.name }} LUT</span>\n <button\n *ngFor=\"let p of lutColors\"\n type=\"button\"\n class=\"ch-preset\"\n [style.background]=\"p.color\"\n [pTooltip]=\"p.name\"\n tooltipPosition=\"top\"\n (click)=\"setPreset(selected, p.color)\"\n ></button>\n </div>\n\n <!-- Histogram -->\n <div class=\"ch-hist-head\">\n <span>Histogram{{ selected ? ' \u2014 ' + selected.name : '' }}</span>\n <p-checkbox\n [binary]=\"true\"\n [(ngModel)]=\"logScale\"\n (onChange)=\"toggleLog($event.checked)\"\n label=\"Log\"\n ></p-checkbox>\n </div>\n <div [id]=\"histogramDiv\" class=\"ch-hist-plot\"></div>\n\n <!-- Display window / gamma -->\n <div class=\"ch-sliders\" *ngIf=\"selected\">\n <!-- Window endpoints are shown/edited in NATIVE units (0..255 for 8-bit,\n the channel's observed native range for 16-bit). One-way bound to the\n native getters; the handlers map back to the 8-bit display window. -->\n <div class=\"ch-row\">\n <span class=\"ch-lbl\">Min</span>\n <p-slider\n [ngModel]=\"minNative\"\n [min]=\"sliderMin\"\n [max]=\"sliderMax\"\n [step]=\"sliderStep\"\n (onChange)=\"onMinChange($event.value)\"\n styleClass=\"ch-slider\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"minNative\"\n [min]=\"sliderMin\"\n [max]=\"sliderMax\"\n [showButtons]=\"false\"\n (onInput)=\"onMinChange($event.value)\"\n styleClass=\"ch-num\"\n ></p-inputNumber>\n <span class=\"ch-busy\"\n ><i *ngIf=\"activeAdjust === 'min'\" class=\"pi pi-spin pi-spinner\" aria-label=\"updating\"></i\n ></span>\n </div>\n <div class=\"ch-row\">\n <span class=\"ch-lbl\">Max</span>\n <p-slider\n [ngModel]=\"maxNative\"\n [min]=\"sliderMin\"\n [max]=\"sliderMax\"\n [step]=\"sliderStep\"\n (onChange)=\"onMaxChange($event.value)\"\n styleClass=\"ch-slider\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"maxNative\"\n [min]=\"sliderMin\"\n [max]=\"sliderMax\"\n [showButtons]=\"false\"\n (onInput)=\"onMaxChange($event.value)\"\n styleClass=\"ch-num\"\n ></p-inputNumber>\n <span class=\"ch-busy\"\n ><i *ngIf=\"activeAdjust === 'max'\" class=\"pi pi-spin pi-spinner\" aria-label=\"updating\"></i\n ></span>\n </div>\n <div class=\"ch-row\">\n <span class=\"ch-lbl\">Gamma</span>\n <p-slider\n [(ngModel)]=\"selected.gamma\"\n [min]=\"0.1\"\n [max]=\"5\"\n [step]=\"0.01\"\n (onChange)=\"onGammaChange($event.value)\"\n styleClass=\"ch-slider\"\n ></p-slider>\n <p-inputNumber\n [(ngModel)]=\"selected.gamma\"\n [min]=\"0.1\"\n [max]=\"5\"\n [minFractionDigits]=\"2\"\n [maxFractionDigits]=\"2\"\n [showButtons]=\"false\"\n (onInput)=\"onGammaChange($event.value)\"\n styleClass=\"ch-num\"\n ></p-inputNumber>\n <span class=\"ch-busy\"\n ><i *ngIf=\"activeAdjust === 'gamma'\" class=\"pi pi-spin pi-spinner\" aria-label=\"updating\"></i\n ></span>\n </div>\n <div class=\"ch-btns\">\n <p-button label=\"Auto\" size=\"small\" (onClick)=\"auto()\"></p-button>\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n\n <!-- LUT / colormap controls. The colormap maps single-channel intensity \u2192\n colour, so it only applies to grayscale; RGB/multichannel use the\n per-channel pseudo-colour presets above. -->\n <div class=\"ch-display\">\n <p-treeSelect\n *ngIf=\"!multichannel\"\n [options]=\"colormapOptions\"\n [(ngModel)]=\"selectedColormap\"\n [selectionMode]=\"'single'\"\n placeholder=\"Colormap\"\n [scrollHeight]=\"'300px'\"\n pTooltip=\"Color Lookup Tables\"\n tooltipPosition=\"bottom\"\n (onNodeSelect)=\"onColormap($event.node)\"\n >\n <ng-template let-node pTemplate=\"default\">\n <div class=\"flex align-items-center gap-2\">\n <img *ngIf=\"!node.children\" [src]=\"node.data?.src\" class=\"colormap\" />\n <div>{{ node.label }}</div>\n </div>\n </ng-template>\n </p-treeSelect>\n <p-checkbox\n [binary]=\"true\"\n [(ngModel)]=\"invert\"\n (onChange)=\"onInvert($event.checked)\"\n label=\"Invert\"\n ></p-checkbox>\n </div>\n\n <!-- Exports: an 8-bit publication composite (always), plus a true-16-bit\n data-preserving TIFF when the image carries >8-bit pixels. -->\n <div class=\"ch-export\">\n <p-button\n label=\"Export composite (PNG)\"\n icon=\"pi pi-download\"\n size=\"small\"\n (onClick)=\"exportComposite()\"\n ></p-button>\n <p-button\n *ngIf=\"is16bit\"\n label=\"Export 16-bit TIFF\"\n icon=\"pi pi-download\"\n size=\"small\"\n [outlined]=\"true\"\n pTooltip=\"Data-preserving multi-band TIFF (visible channels, native bit depth)\"\n tooltipPosition=\"top\"\n (onClick)=\"exportData()\"\n ></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [":host ::ng-deep .channel-histogram-dialog.p-dialog{display:flex;flex-direction:column}:host ::ng-deep .channel-histogram-dialog .p-dialog-content{display:flex;flex-direction:column;flex:1 1 auto;overflow:auto}.ch-body{display:flex;flex-direction:column;gap:.5rem;flex:1 1 auto;min-height:0}.ch-table{flex:0 0 auto}.ch-presets{display:flex;align-items:center;gap:4px;margin:.25rem 0}.ch-presets .ch-presets-lbl{font-size:.8rem;margin-right:.25rem}.ch-presets .ch-preset{width:16px;height:16px;padding:0;border:1px solid rgba(0,0,0,.3);border-radius:2px;cursor:pointer}.ch-export{display:flex;justify-content:flex-end;gap:.5rem;margin-top:.25rem}.ch-hist-head{display:flex;align-items:center;justify-content:space-between;font-size:.8rem;font-weight:600;margin-top:.25rem}.ch-hist-plot{flex:1 1 auto;min-height:120px;width:100%}.ch-sliders{display:flex;flex-direction:column;gap:.35rem}.ch-sliders .ch-row{display:flex;align-items:center;gap:.5rem}.ch-sliders .ch-row .ch-lbl{flex:0 0 3rem;font-size:.8rem}.ch-sliders .ch-row p-slider{flex:2 1 0;min-width:0}.ch-sliders .ch-row p-inputnumber{flex:1 1 0;min-width:0}.ch-sliders .ch-row .ch-busy{flex:0 0 1rem;display:inline-flex;align-items:center;justify-content:center;color:#4fa3ff;font-size:.8rem}.ch-sliders .ch-btns{display:flex;gap:.5rem;margin-top:.25rem}.ch-display{display:flex;align-items:center;gap:.75rem;flex-wrap:wrap;margin-top:.25rem}.ch-display .colormap{width:60px;height:12px;object-fit:fill}:host ::ng-deep .ch-num .p-inputnumber-input{width:100%}:host ::ng-deep .ch-row p-slider .p-slider{width:100%}\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: 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: 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: InputNumberModule }, { kind: "component", type: i7$3.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "ngmodule", type: SliderModule }, { kind: "component", type: i2$1.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "ngmodule", type: TableModule }, { kind: "component", type: i9.Table, selector: "p-table", inputs: ["frozenColumns", "frozenValue", "style", "styleClass", "tableStyle", "tableStyleClass", "paginator", "pageLinks", "rowsPerPageOptions", "alwaysShowPaginator", "paginatorPosition", "paginatorStyleClass", "paginatorDropdownAppendTo", "paginatorDropdownScrollHeight", "currentPageReportTemplate", "showCurrentPageReport", "showJumpToPageDropdown", "showJumpToPageInput", "showFirstLastIcon", "showPageLinks", "defaultSortOrder", "sortMode", "resetPageOnSort", "selectionMode", "selectionPageOnly", "contextMenuSelection", "contextMenuSelectionMode", "dataKey", "metaKeySelection", "rowSelectable", "rowTrackBy", "lazy", "lazyLoadOnInit", "compareSelectionBy", "csvSeparator", "exportFilename", "filters", "globalFilterFields", "filterDelay", "filterLocale", "expandedRowKeys", "editingRowKeys", "rowExpandMode", "scrollable", "scrollDirection", "rowGroupMode", "scrollHeight", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "virtualScrollDelay", "frozenWidth", "responsive", "contextMenu", "resizableColumns", "columnResizeMode", "reorderableColumns", "loading", "loadingIcon", "showLoader", "rowHover", "customSort", "showInitialSortBadge", "autoLayout", "exportFunction", "exportHeader", "stateKey", "stateStorage", "editMode", "groupRowsBy", "groupRowsByOrder", "responsiveLayout", "breakpoint", "paginatorLocale", "value", "columns", "first", "rows", "totalRecords", "sortField", "sortOrder", "multiSortMeta", "selection", "selectAll", "virtualRowHeight"], outputs: ["contextMenuSelectionChange", "selectAllChange", "selectionChange", "onRowSelect", "onRowUnselect", "onPage", "onSort", "onFilter", "onLazyLoad", "onRowExpand", "onRowCollapse", "onContextMenuSelect", "onColResize", "onColReorder", "onRowReorder", "onEditInit", "onEditComplete", "onEditCancel", "onHeaderCheckboxToggle", "sortFunction", "firstChange", "rowsChange", "onStateSave", "onStateRestore"] }, { kind: "directive", type: i9.SelectableRow, selector: "[pSelectableRow]", inputs: ["pSelectableRow", "pSelectableRowIndex", "pSelectableRowDisabled"] }, { 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: "ngmodule", type: TreeSelectModule }, { kind: "component", type: i11.TreeSelect, selector: "p-treeSelect", inputs: ["inputId", "scrollHeight", "disabled", "metaKeySelection", "variant", "display", "selectionMode", "tabindex", "ariaLabel", "ariaLabelledBy", "placeholder", "panelClass", "panelStyle", "panelStyleClass", "containerStyle", "containerStyleClass", "labelStyle", "labelStyleClass", "overlayOptions", "emptyMessage", "appendTo", "filter", "filterBy", "filterMode", "filterPlaceholder", "filterLocale", "filterInputAutoFocus", "propagateSelectionDown", "propagateSelectionUp", "showClear", "resetFilterOnHide", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autofocus", "options", "showTransitionOptions", "hideTransitionOptions", "loading"], outputs: ["onNodeExpand", "onNodeCollapse", "onShow", "onHide", "onClear", "onFilter", "onFocus", "onBlur", "onNodeUnselect", "onNodeSelect"] }, { kind: "component", type: HexColorPickerComponent, selector: "hex-color-picker", inputs: ["color"], outputs: ["colorChange", "colorInput"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
17790
17862
  }
17791
17863
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: ChannelHistogramComponent, decorators: [{
17792
17864
  type: Component,
@@ -20401,7 +20473,7 @@ class SpatialHeatmapControlsComponent {
20401
20473
  this.zScoreChange = new EventEmitter();
20402
20474
  }
20403
20475
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialHeatmapControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
20404
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialHeatmapControlsComponent, isStandalone: true, selector: "spatial-heatmap-controls", inputs: { geneOptions: "geneOptions", genes: "genes", zScore: "zScore" }, outputs: { genesChange: "genesChange", filterChange: "filterChange", zScoreChange: "zScoreChange" }, ngImport: i0, template: "<!-- The heatmap's rows are a gene LIST, so it needs its own picker: the other\n kinds chart whatever single source the map is coloured by. -->\n<div class=\"sx-controls\">\n <p-multiSelect\n [options]=\"geneOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"genes\"\n (ngModelChange)=\"genesChange.emit($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n (onFilter)=\"filterChange.emit($event.filter)\"\n [virtualScroll]=\"true\"\n [virtualScrollItemSize]=\"30\"\n appendTo=\"body\"\n [showToggleAll]=\"false\"\n [maxSelectedLabels]=\"3\"\n selectedItemsLabel=\"{0} genes\"\n placeholder=\"Pick genes for the rows\"\n pTooltip=\"One row per gene, averaged within each group\"\n tooltipPosition=\"top\"\n styleClass=\"sx-genes\"\n ></p-multiSelect>\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"zScore\"\n (onChange)=\"zScoreChange.emit($event.checked)\"\n label=\"z-score\"\n pTooltip=\"Scale each gene across the groups. Without it one highly-expressed gene saturates the colour scale and the rest reads as blank\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n</div>\n", styles: [".sx-controls{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.sx-controls p-dropdown{flex:1 1 8rem;min-width:0}:host ::ng-deep .sx-controls p-dropdown .p-dropdown{width:100%}.sx-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d);line-height:1.35}.sx-warn{color:var(--orange-600, #c2410c)}:host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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$3.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: 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: 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 }); }
20476
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialHeatmapControlsComponent, isStandalone: true, selector: "spatial-heatmap-controls", inputs: { geneOptions: "geneOptions", genes: "genes", zScore: "zScore" }, outputs: { genesChange: "genesChange", filterChange: "filterChange", zScoreChange: "zScoreChange" }, ngImport: i0, template: "<!-- The heatmap's rows are a gene LIST, so it needs its own picker: the other\n kinds chart whatever single source the map is coloured by. -->\n<div class=\"sx-controls\">\n <p-multiSelect\n [options]=\"geneOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"genes\"\n (ngModelChange)=\"genesChange.emit($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n (onFilter)=\"filterChange.emit($event.filter)\"\n [virtualScroll]=\"true\"\n [virtualScrollItemSize]=\"30\"\n appendTo=\"body\"\n [showToggleAll]=\"false\"\n [maxSelectedLabels]=\"3\"\n selectedItemsLabel=\"{0} genes\"\n placeholder=\"Pick genes for the rows\"\n pTooltip=\"One row per gene, averaged within each group\"\n tooltipPosition=\"top\"\n styleClass=\"sx-genes\"\n ></p-multiSelect>\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"zScore\"\n (onChange)=\"zScoreChange.emit($event.checked)\"\n label=\"z-score\"\n pTooltip=\"Scale each gene across the groups. Without it one highly-expressed gene saturates the colour scale and the rest reads as blank\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n</div>\n", styles: [".sx-controls{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.sx-controls p-dropdown{flex:1 1 8rem;min-width:0}:host ::ng-deep .sx-controls p-dropdown .p-dropdown{width:100%}.sx-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d);line-height:1.35}.sx-warn{color:var(--orange-600, #c2410c)}:host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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: "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: 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 }); }
20405
20477
  }
20406
20478
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialHeatmapControlsComponent, decorators: [{
20407
20479
  type: Component,
@@ -20460,7 +20532,7 @@ class SpatialEmbeddingControlsComponent {
20460
20532
  this.detachedToggle = new EventEmitter();
20461
20533
  }
20462
20534
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialEmbeddingControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
20463
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialEmbeddingControlsComponent, isStandalone: true, selector: "spatial-embedding-controls", inputs: { isEmbedding: "isEmbedding", embeddings: "embeddings", embedding: "embedding", computable: "computable", computed: "computed", running: "running", state: "state", estimateLabel: "estimateLabel", tooLargeNote: "tooLargeNote", detached: "detached" }, outputs: { embeddingChange: "embeddingChange", compute: "compute", cancel: "cancel", detachedToggle: "detachedToggle" }, ngImport: i0, template: "<!-- Detach applies to EVERY kind: the dialog is only so wide, and a counts plot read\n against a heatmap is the same argument as a UMAP read against the map. -->\n<div class=\"sx-controls\">\n <!-- A picker only when the dataset publishes more than one embedding. -->\n <p-dropdown\n *ngIf=\"isEmbedding && embeddings.length > 1\"\n [options]=\"embeddings\"\n optionLabel=\"label\"\n optionValue=\"name\"\n [ngModel]=\"embedding?.name\"\n (ngModelChange)=\"embeddingChange.emit($event)\"\n appendTo=\"body\"\n pTooltip=\"Which embedding to draw\"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- Offered only for an embedding this dataset does not publish. It runs in a Web\n Worker on the GPU, so the panel stays usable while it goes. -->\n <p-button\n *ngIf=\"isEmbedding && computable && !computed && !running\"\n icon=\"pi pi-play\"\n size=\"small\"\n [text]=\"true\"\n [label]=\"'Compute (' + estimateLabel + ')'\"\n pTooltip=\"Run t-SNE here, in a background worker on the GPU, from this dataset's PCA. The panel stays usable, and it can be cancelled.\"\n tooltipPosition=\"top\"\n (onClick)=\"compute.emit()\"\n ></p-button>\n <p-button\n *ngIf=\"running\"\n icon=\"pi pi-times\"\n size=\"small\"\n [text]=\"true\"\n severity=\"secondary\"\n label=\"Cancel\"\n (onClick)=\"cancel.emit()\"\n ></p-button>\n <p-button\n [icon]=\"detached ? 'pi pi-window-minimize' : 'pi pi-external-link'\"\n size=\"small\"\n [text]=\"true\"\n [label]=\"detached ? 'Put back' : 'Detach'\"\n [pTooltip]=\"\n detached ? 'Show this chart in the panel again' : 'Open this chart in its own window, alongside this panel'\n \"\n tooltipPosition=\"top\"\n (onClick)=\"detachedToggle.emit()\"\n ></p-button>\n</div>\n\n<!-- Progress while a computation runs. `mode=\"indeterminate\"` until the first report,\n because the run starts with a neighbour graph and there is nothing honest to show\n as a percentage yet. -->\n<div class=\"sx-compute\" *ngIf=\"running\">\n <p-progressBar\n [mode]=\"state.fraction === null ? 'indeterminate' : 'determinate'\"\n [value]=\"state.fraction === null ? 0 : state.fraction * 100\"\n [showValue]=\"state.fraction !== null\"\n [style]=\"{ height: '0.6rem' }\"\n ></p-progressBar>\n <small class=\"sx-hint\">\n {{ state.fraction === null ? 'Building the neighbour graph\u2026' : 'Optimising\u2026' }}\n <ng-container *ngIf=\"state.backend\"> on {{ state.backend }}</ng-container>\n </small>\n <small class=\"sx-hint sx-warn\" *ngIf=\"state.message\">{{ state.message }}</small>\n</div>\n<small class=\"sx-hint sx-warn\" *ngIf=\"state.error\">{{ state.error }}</small>\n<!-- Withheld rather than offered with a warning: an hour-long job is not a choice a\n reader can meaningfully consent to from a dialog. -->\n<small class=\"sx-hint\" *ngIf=\"isEmbedding && tooLargeNote\">{{ tooLargeNote }}</small>\n", styles: [".sx-controls{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.sx-controls p-dropdown{flex:1 1 8rem;min-width:0}:host ::ng-deep .sx-controls p-dropdown .p-dropdown{width:100%}.sx-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d);line-height:1.35}.sx-warn{color:var(--orange-600, #c2410c)}:host{display:contents}.sx-compute{display:flex;flex-direction:column;gap:.3rem;padding:.5rem 0}\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: i4.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: ProgressBarModule }, { kind: "component", type: i7$1.ProgressBar, selector: "p-progressBar", inputs: ["value", "showValue", "styleClass", "style", "unit", "mode", "color"] }, { 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 }); }
20535
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialEmbeddingControlsComponent, isStandalone: true, selector: "spatial-embedding-controls", inputs: { isEmbedding: "isEmbedding", embeddings: "embeddings", embedding: "embedding", computable: "computable", computed: "computed", running: "running", state: "state", estimateLabel: "estimateLabel", tooLargeNote: "tooLargeNote", detached: "detached" }, outputs: { embeddingChange: "embeddingChange", compute: "compute", cancel: "cancel", detachedToggle: "detachedToggle" }, ngImport: i0, template: "<!-- Detach applies to EVERY kind: the dialog is only so wide, and a counts plot read\n against a heatmap is the same argument as a UMAP read against the map. -->\n<div class=\"sx-controls\">\n <!-- A picker only when the dataset publishes more than one embedding. -->\n <p-dropdown\n *ngIf=\"isEmbedding && embeddings.length > 1\"\n [options]=\"embeddings\"\n optionLabel=\"label\"\n optionValue=\"name\"\n [ngModel]=\"embedding?.name\"\n (ngModelChange)=\"embeddingChange.emit($event)\"\n appendTo=\"body\"\n pTooltip=\"Which embedding to draw\"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- Offered only for an embedding this dataset does not publish. It runs in a Web\n Worker on the GPU, so the panel stays usable while it goes. -->\n <p-button\n *ngIf=\"isEmbedding && computable && !computed && !running\"\n icon=\"pi pi-play\"\n size=\"small\"\n [text]=\"true\"\n [label]=\"'Compute (' + estimateLabel + ')'\"\n pTooltip=\"Run t-SNE here, in a background worker on the GPU, from this dataset's PCA. The panel stays usable, and it can be cancelled.\"\n tooltipPosition=\"top\"\n (onClick)=\"compute.emit()\"\n ></p-button>\n <p-button\n *ngIf=\"running\"\n icon=\"pi pi-times\"\n size=\"small\"\n [text]=\"true\"\n severity=\"secondary\"\n label=\"Cancel\"\n (onClick)=\"cancel.emit()\"\n ></p-button>\n <p-button\n [icon]=\"detached ? 'pi pi-window-minimize' : 'pi pi-external-link'\"\n size=\"small\"\n [text]=\"true\"\n [label]=\"detached ? 'Put back' : 'Detach'\"\n [pTooltip]=\"\n detached ? 'Show this chart in the panel again' : 'Open this chart in its own window, alongside this panel'\n \"\n tooltipPosition=\"top\"\n (onClick)=\"detachedToggle.emit()\"\n ></p-button>\n</div>\n\n<!-- Progress while a computation runs. `mode=\"indeterminate\"` until the first report,\n because the run starts with a neighbour graph and there is nothing honest to show\n as a percentage yet. -->\n<div class=\"sx-compute\" *ngIf=\"running\">\n <p-progressBar\n [mode]=\"state.fraction === null ? 'indeterminate' : 'determinate'\"\n [value]=\"state.fraction === null ? 0 : state.fraction * 100\"\n [showValue]=\"state.fraction !== null\"\n [style]=\"{ height: '0.6rem' }\"\n ></p-progressBar>\n <small class=\"sx-hint\">\n {{ state.fraction === null ? 'Building the neighbour graph\u2026' : 'Optimising\u2026' }}\n <ng-container *ngIf=\"state.backend\"> on {{ state.backend }}</ng-container>\n </small>\n <small class=\"sx-hint sx-warn\" *ngIf=\"state.message\">{{ state.message }}</small>\n</div>\n<small class=\"sx-hint sx-warn\" *ngIf=\"state.error\">{{ state.error }}</small>\n<!-- Withheld rather than offered with a warning: an hour-long job is not a choice a\n reader can meaningfully consent to from a dialog. -->\n<small class=\"sx-hint\" *ngIf=\"isEmbedding && tooLargeNote\">{{ tooLargeNote }}</small>\n", styles: [".sx-controls{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.sx-controls p-dropdown{flex:1 1 8rem;min-width:0}:host ::ng-deep .sx-controls p-dropdown .p-dropdown{width:100%}.sx-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d);line-height:1.35}.sx-warn{color:var(--orange-600, #c2410c)}:host{display:contents}.sx-compute{display:flex;flex-direction:column;gap:.3rem;padding:.5rem 0}\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: ProgressBarModule }, { kind: "component", type: i7$1.ProgressBar, selector: "p-progressBar", inputs: ["value", "showValue", "styleClass", "style", "unit", "mode", "color"] }, { 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 }); }
20464
20536
  }
20465
20537
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialEmbeddingControlsComponent, decorators: [{
20466
20538
  type: Component,
@@ -21261,7 +21333,7 @@ class SpatialChartsComponent {
21261
21333
  await this.draw(buildHeatmapTraces$1(input), heatmapLayout$1(input));
21262
21334
  }
21263
21335
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialChartsComponent, deps: [{ token: VISUALIZER }], target: i0.ɵɵFactoryTarget.Component }); }
21264
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialChartsComponent, isStandalone: true, selector: "spatial-charts", inputs: { active: "active" }, ngImport: i0, template: "<div class=\"sx-body\" *ngIf=\"controls\">\n <!-- The chart follows whatever the map is coloured by, so the two can never\n disagree about what is being shown. -->\n <div class=\"sx-head\">\n <span class=\"sx-subject\" *ngIf=\"subject\">{{ subject }}</span>\n <span class=\"sx-subject sx-muted\" *ngIf=\"!subject\">nothing selected to chart</span>\n <span class=\"sx-badge\" *ngIf=\"selectionCount\"> {{ selectionCount | number }} selected </span>\n <i class=\"pi pi-spin pi-spinner sx-busy\" *ngIf=\"busy\" aria-label=\"loading\"></i>\n </div>\n\n <div class=\"sx-controls\">\n <p-selectButton\n [options]=\"kindOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"kind\"\n (ngModelChange)=\"onKind($event)\"\n styleClass=\"sx-kind\"\n ></p-selectButton>\n <p-dropdown\n [options]=\"groupOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"groupBy\"\n (ngModelChange)=\"onGroupBy($event)\"\n placeholder=\"No grouping\"\n appendTo=\"body\"\n [disabled]=\"kind === 'histogram'\"\n [pTooltip]=\"\n kind === 'heatmap'\n ? 'The categorical column the heatmap\\'s columns come from'\n : 'Split a violin or box by a categorical column'\n \"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- What the chart on screen shows, and what it must NOT be read for. Each of these\n answers a different question and two are routinely over-read, so the caveat\n travels with the explanation. -->\n <i\n class=\"pi pi-question-circle sx-help\"\n [pTooltip]=\"kindHelp\"\n [escape]=\"false\"\n tooltipPosition=\"left\"\n tooltipStyleClass=\"sx-help-tip\"\n [attr.aria-label]=\"'About the ' + kind + ' chart'\"\n tabindex=\"0\"\n ></i>\n </div>\n\n <spatial-heatmap-controls\n *ngIf=\"kind === 'heatmap'\"\n [geneOptions]=\"geneOptions\"\n [genes]=\"heatmapGenes\"\n [zScore]=\"heatmapZScore\"\n (genesChange)=\"onHeatmapGenes($event)\"\n (filterChange)=\"onGeneFilter($event)\"\n (zScoreChange)=\"onHeatmapZScore($event)\"\n ></spatial-heatmap-controls>\n\n <spatial-embedding-controls\n [isEmbedding]=\"kind === 'embedding'\"\n [embeddings]=\"embeddings\"\n [embedding]=\"embedding\"\n [computable]=\"isComputable\"\n [computed]=\"isComputed\"\n [running]=\"isComputing\"\n [state]=\"computeState\"\n [estimateLabel]=\"computeEstimateLabel\"\n [tooLargeNote]=\"tsneTooLarge ? tsneTooLargeNote : null\"\n [detached]=\"detached\"\n (embeddingChange)=\"onEmbedding($event)\"\n (compute)=\"computeEmbedding()\"\n (cancel)=\"cancelCompute()\"\n (detachedToggle)=\"toggleDetached()\"\n >\n </spatial-embedding-controls>\n\n <div [id]=\"chartDiv\" class=\"sx-plot\" *ngIf=\"!detached\"></div>\n\n <!-- Detached: the chart keeps drawing, in its own window. Says so rather than\n leaving a blank panel that looks broken. -->\n <div class=\"sx-detached\" *ngIf=\"detached\">Shown in its own window.</div>\n\n <ng-container *ngTemplateOutlet=\"hints\"></ng-container>\n</div>\n\n<!-- Declared once and rendered in both the panel and the detached window: the ladder is\n deliberately not mutually exclusive (a heatmap still needs its grouping prompt), so\n collapsing it to a single expression would drop a hint rather than move it. -->\n<ng-template #hints>\n <small class=\"sx-hint\" *ngIf=\"notice\">{{ notice }}</small>\n <small class=\"sx-hint\" *ngIf=\"!notice && suggestsGrouping\"> Pick a grouping to compare categories. </small>\n <small class=\"sx-hint\" *ngIf=\"!notice && selectionCount && kind === 'histogram'\">\n The selection is overlaid on the full distribution.\n </small>\n <small class=\"sx-hint\" *ngIf=\"!notice && selectionCount && kind !== 'histogram' && kind !== 'heatmap'\">\n Showing the selection only.\n </small>\n <small class=\"sx-hint\" *ngIf=\"!notice && kind === 'heatmap'\">{{ heatmapNote }}</small>\n <small class=\"sx-hint\" *ngIf=\"!notice && kind === 'embedding'\">{{ embeddingNote }}</small>\n</ng-template>\n\n<spatial-chart-window\n *ngIf=\"detached\"\n [title]=\"detachedTitle\"\n [divId]=\"detachedDiv\"\n [fixedHeight]=\"hasFixedHeight\"\n (closed)=\"toggleDetached()\"\n (shown)=\"onDetachedWindowShown()\"\n (resizeEnd)=\"onDetachedResizeEnd()\"\n>\n <ng-container *ngTemplateOutlet=\"hints\"></ng-container>\n</spatial-chart-window>\n", styles: [".sx-controls{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.sx-controls p-dropdown{flex:1 1 8rem;min-width:0}:host ::ng-deep .sx-controls p-dropdown .p-dropdown{width:100%}.sx-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d);line-height:1.35}.sx-warn{color:var(--orange-600, #c2410c)}:host ::ng-deep .spatial-charts-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sx-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sx-body{display:flex;flex-direction:column;gap:.6rem}.sx-head{display:flex;align-items:center;gap:.5rem;padding-bottom:.4rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sx-subject{font-weight:600;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sx-muted{font-weight:400;color:var(--text-color-secondary, #6c757d)}.sx-badge{margin-left:auto;padding:.1rem .4rem;border-radius:10px;font-size:.7rem;font-variant-numeric:tabular-nums;background:var(--surface-hover, rgba(213, 94, 0, .12));color:#a8480a;white-space:nowrap}.sx-busy{font-size:.8rem;color:var(--text-color-secondary, #6c757d)}:host ::ng-deep .sx-kind .p-button{padding:.25rem .6rem;font-size:.78rem}.sx-plot{width:100%;min-height:15rem;flex:1}.sx-detached{flex:1;min-height:6rem;display:flex;align-items:center;justify-content:center;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sx-help{margin-left:auto;align-self:center;font-size:.95rem;color:var(--text-color-secondary, #6c757d);cursor:help}.sx-help:hover,.sx-help:focus-visible{color:var(--primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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.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: DropdownModule }, { kind: "component", type: i4.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: SelectButtonModule }, { kind: "component", type: i8$1.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: 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: SpatialHeatmapControlsComponent, selector: "spatial-heatmap-controls", inputs: ["geneOptions", "genes", "zScore"], outputs: ["genesChange", "filterChange", "zScoreChange"] }, { kind: "component", type: SpatialEmbeddingControlsComponent, selector: "spatial-embedding-controls", inputs: ["isEmbedding", "embeddings", "embedding", "computable", "computed", "running", "state", "estimateLabel", "tooLargeNote", "detached"], outputs: ["embeddingChange", "compute", "cancel", "detachedToggle"] }, { kind: "component", type: SpatialChartWindowComponent, selector: "spatial-chart-window", inputs: ["title", "divId", "fixedHeight"], outputs: ["closed", "shown", "resizeEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
21336
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialChartsComponent, isStandalone: true, selector: "spatial-charts", inputs: { active: "active" }, ngImport: i0, template: "<div class=\"sx-body\" *ngIf=\"controls\">\n <!-- The chart follows whatever the map is coloured by, so the two can never\n disagree about what is being shown. -->\n <div class=\"sx-head\">\n <span class=\"sx-subject\" *ngIf=\"subject\">{{ subject }}</span>\n <span class=\"sx-subject sx-muted\" *ngIf=\"!subject\">nothing selected to chart</span>\n <span class=\"sx-badge\" *ngIf=\"selectionCount\"> {{ selectionCount | number }} selected </span>\n <i class=\"pi pi-spin pi-spinner sx-busy\" *ngIf=\"busy\" aria-label=\"loading\"></i>\n </div>\n\n <div class=\"sx-controls\">\n <p-selectButton\n [options]=\"kindOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"kind\"\n (ngModelChange)=\"onKind($event)\"\n styleClass=\"sx-kind\"\n ></p-selectButton>\n <p-dropdown\n [options]=\"groupOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"groupBy\"\n (ngModelChange)=\"onGroupBy($event)\"\n placeholder=\"No grouping\"\n appendTo=\"body\"\n [disabled]=\"kind === 'histogram'\"\n [pTooltip]=\"\n kind === 'heatmap'\n ? 'The categorical column the heatmap\\'s columns come from'\n : 'Split a violin or box by a categorical column'\n \"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- What the chart on screen shows, and what it must NOT be read for. Each of these\n answers a different question and two are routinely over-read, so the caveat\n travels with the explanation. -->\n <i\n class=\"pi pi-question-circle sx-help\"\n [pTooltip]=\"kindHelp\"\n [escape]=\"false\"\n tooltipPosition=\"left\"\n tooltipStyleClass=\"sx-help-tip\"\n [attr.aria-label]=\"'About the ' + kind + ' chart'\"\n tabindex=\"0\"\n ></i>\n </div>\n\n <spatial-heatmap-controls\n *ngIf=\"kind === 'heatmap'\"\n [geneOptions]=\"geneOptions\"\n [genes]=\"heatmapGenes\"\n [zScore]=\"heatmapZScore\"\n (genesChange)=\"onHeatmapGenes($event)\"\n (filterChange)=\"onGeneFilter($event)\"\n (zScoreChange)=\"onHeatmapZScore($event)\"\n ></spatial-heatmap-controls>\n\n <spatial-embedding-controls\n [isEmbedding]=\"kind === 'embedding'\"\n [embeddings]=\"embeddings\"\n [embedding]=\"embedding\"\n [computable]=\"isComputable\"\n [computed]=\"isComputed\"\n [running]=\"isComputing\"\n [state]=\"computeState\"\n [estimateLabel]=\"computeEstimateLabel\"\n [tooLargeNote]=\"tsneTooLarge ? tsneTooLargeNote : null\"\n [detached]=\"detached\"\n (embeddingChange)=\"onEmbedding($event)\"\n (compute)=\"computeEmbedding()\"\n (cancel)=\"cancelCompute()\"\n (detachedToggle)=\"toggleDetached()\"\n >\n </spatial-embedding-controls>\n\n <div [id]=\"chartDiv\" class=\"sx-plot\" *ngIf=\"!detached\"></div>\n\n <!-- Detached: the chart keeps drawing, in its own window. Says so rather than\n leaving a blank panel that looks broken. -->\n <div class=\"sx-detached\" *ngIf=\"detached\">Shown in its own window.</div>\n\n <ng-container *ngTemplateOutlet=\"hints\"></ng-container>\n</div>\n\n<!-- Declared once and rendered in both the panel and the detached window: the ladder is\n deliberately not mutually exclusive (a heatmap still needs its grouping prompt), so\n collapsing it to a single expression would drop a hint rather than move it. -->\n<ng-template #hints>\n <small class=\"sx-hint\" *ngIf=\"notice\">{{ notice }}</small>\n <small class=\"sx-hint\" *ngIf=\"!notice && suggestsGrouping\"> Pick a grouping to compare categories. </small>\n <small class=\"sx-hint\" *ngIf=\"!notice && selectionCount && kind === 'histogram'\">\n The selection is overlaid on the full distribution.\n </small>\n <small class=\"sx-hint\" *ngIf=\"!notice && selectionCount && kind !== 'histogram' && kind !== 'heatmap'\">\n Showing the selection only.\n </small>\n <small class=\"sx-hint\" *ngIf=\"!notice && kind === 'heatmap'\">{{ heatmapNote }}</small>\n <small class=\"sx-hint\" *ngIf=\"!notice && kind === 'embedding'\">{{ embeddingNote }}</small>\n</ng-template>\n\n<spatial-chart-window\n *ngIf=\"detached\"\n [title]=\"detachedTitle\"\n [divId]=\"detachedDiv\"\n [fixedHeight]=\"hasFixedHeight\"\n (closed)=\"toggleDetached()\"\n (shown)=\"onDetachedWindowShown()\"\n (resizeEnd)=\"onDetachedResizeEnd()\"\n>\n <ng-container *ngTemplateOutlet=\"hints\"></ng-container>\n</spatial-chart-window>\n", styles: [".sx-controls{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap}.sx-controls p-dropdown{flex:1 1 8rem;min-width:0}:host ::ng-deep .sx-controls p-dropdown .p-dropdown{width:100%}.sx-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d);line-height:1.35}.sx-warn{color:var(--orange-600, #c2410c)}:host ::ng-deep .spatial-charts-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sx-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sx-body{display:flex;flex-direction:column;gap:.6rem}.sx-head{display:flex;align-items:center;gap:.5rem;padding-bottom:.4rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sx-subject{font-weight:600;font-size:.9rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sx-muted{font-weight:400;color:var(--text-color-secondary, #6c757d)}.sx-badge{margin-left:auto;padding:.1rem .4rem;border-radius:10px;font-size:.7rem;font-variant-numeric:tabular-nums;background:var(--surface-hover, rgba(213, 94, 0, .12));color:#a8480a;white-space:nowrap}.sx-busy{font-size:.8rem;color:var(--text-color-secondary, #6c757d)}:host ::ng-deep .sx-kind .p-button{padding:.25rem .6rem;font-size:.78rem}.sx-plot{width:100%;min-height:15rem;flex:1}.sx-detached{flex:1;min-height:6rem;display:flex;align-items:center;justify-content:center;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sx-help{margin-left:auto;align-self:center;font-size:.95rem;color:var(--text-color-secondary, #6c757d);cursor:help}.sx-help:hover,.sx-help:focus-visible{color:var(--primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { 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.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: 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: SelectButtonModule }, { kind: "component", type: i8$1.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: 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: SpatialHeatmapControlsComponent, selector: "spatial-heatmap-controls", inputs: ["geneOptions", "genes", "zScore"], outputs: ["genesChange", "filterChange", "zScoreChange"] }, { kind: "component", type: SpatialEmbeddingControlsComponent, selector: "spatial-embedding-controls", inputs: ["isEmbedding", "embeddings", "embedding", "computable", "computed", "running", "state", "estimateLabel", "tooLargeNote", "detached"], outputs: ["embeddingChange", "compute", "cancel", "detachedToggle"] }, { kind: "component", type: SpatialChartWindowComponent, selector: "spatial-chart-window", inputs: ["title", "divId", "fixedHeight"], outputs: ["closed", "shown", "resizeEnd"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
21265
21337
  }
21266
21338
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialChartsComponent, decorators: [{
21267
21339
  type: Component,
@@ -21310,14 +21382,17 @@ function isCuratedColumn(name) {
21310
21382
  * The categorical column that names each cell's type: the view's choice when it is a
21311
21383
  * column of this dataset, else the first categorical column that is not a curated
21312
21384
  * annotation (a pipeline's clustering is the default; curation is opted into), else
21313
- * any categorical column.
21385
+ * any categorical column. A column of one category, such as the `region` a SpatialData
21386
+ * table links its cells to their shapes with, names no type and is only used when chosen;
21387
+ * a dataset with nothing else has no cell types (null).
21314
21388
  */
21315
21389
  function cellTypeColumnFor(dataset, view) {
21316
21390
  const categorical = dataset.columns.filter((c) => c.kind === 'categorical');
21317
21391
  if (view.cellTypeColumn && categorical.some((c) => c.name === view.cellTypeColumn)) {
21318
21392
  return view.cellTypeColumn;
21319
21393
  }
21320
- return (categorical.find((c) => !isCuratedColumn(c.name)) ?? categorical[0])?.name ?? null;
21394
+ const typed = categorical.filter((c) => c.categories.length > 1);
21395
+ return (typed.find((c) => !isCuratedColumn(c.name)) ?? typed[0])?.name ?? null;
21321
21396
  }
21322
21397
  // ── Tiles in view ─────────────────────────────────────────────────────────────────────
21323
21398
  /** A stable string id for a tile key, `level/gx/gy` — usable as a Map key. */
@@ -22457,11 +22532,11 @@ class SpatialKeyComponent {
22457
22532
  this.controls?.setViewState({ continuousColormap: node?.data?.value ?? null });
22458
22533
  }
22459
22534
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialKeyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
22460
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialKeyComponent, isStandalone: true, selector: "spatial-key", inputs: { controls: "controls", dataset: "dataset", view: "view", legend: "legend", colorBarCss: "colorBarCss", selectedCategory: "selectedCategory", colormapOptions: "colormapOptions" }, outputs: { categoryClicked: "categoryClicked" }, usesOnChanges: true, ngImport: i0, template: "<!-- Key: categorical legend or a continuous colour bar, built from the same\n functions the renderer uses so it cannot disagree with the canvas. -->\n<div class=\"sc-section sc-key\" *ngIf=\"view.colorBy\">\n <span class=\"sc-lbl\">{{ colorByLabel }}</span>\n <!-- Derived columns say so, so they are not mistaken for measured data. -->\n <small class=\"sc-hint\" *ngIf=\"activeDescription\">{{ activeDescription }}</small>\n\n <!-- Legend rows are buttons: clicking one selects that category, clicking\n it again clears \u2014 so a click is always reversible. -->\n <ul class=\"sc-legend\" *ngIf=\"isCategorical\">\n <li *ngFor=\"let entry of legend; index as i; trackBy: trackByLabel\">\n <button\n type=\"button\"\n class=\"sc-legend-btn\"\n [class.selected]=\"selectedCategory === i\"\n (click)=\"categoryClicked.emit(i)\"\n [title]=\"'Select ' + entry.label\"\n >\n <span class=\"sc-swatch\" [style.background]=\"entry.color\"></span>\n <span class=\"sc-legend-label\">{{ entry.label }}</span>\n </button>\n </li>\n </ul>\n\n <div *ngIf=\"isContinuous\">\n <div class=\"sc-colorbar\" [style.background]=\"colorBarCss\"></div>\n <div class=\"sc-colorbar-ends\">\n <span>low</span>\n <span class=\"sc-hint\">window from percentiles</span>\n <span>high</span>\n </div>\n <!-- The gradient this bar and the map are drawn with. Sits under the bar\n it controls, with the same swatch previews the image's colormap picker\n uses: which gradient reads best is a visual judgement, so a list of\n names would be the wrong control. Cleared = follow the image's. -->\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Colormap</span>\n <p-treeSelect\n [options]=\"colormapOptions\"\n [ngModel]=\"selectedColormapNode\"\n selectionMode=\"single\"\n [showClear]=\"true\"\n placeholder=\"Match the image\"\n [scrollHeight]=\"'260px'\"\n appendTo=\"body\"\n pTooltip=\"Colour scale for gene and numeric values \u2014 the markers, the gene map and this bar. Clear it to follow the image's colormap\"\n tooltipPosition=\"top\"\n (onNodeSelect)=\"onContinuousColormap($event.node)\"\n (onClear)=\"onContinuousColormap(null)\"\n >\n <ng-template let-node pTemplate=\"default\">\n <span class=\"sc-cmap-option\">\n <img *ngIf=\"!node.children && node.data?.src\" [src]=\"node.data.src\" class=\"sc-cmap-swatch\" alt=\"\" />\n <span>{{ node.label }}</span>\n </span>\n </ng-template>\n </p-treeSelect>\n <span class=\"sc-val\"></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";.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}:host{display:contents}.sc-key{padding:.5rem 0;border-top:1px solid var(--surface-border, #dee2e6);border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-legend{list-style:none;margin:0;padding:0;max-height:11rem;overflow-y:auto;display:flex;flex-direction:column;gap:.2rem}.sc-legend li{display:flex}.sc-legend-btn{display:flex;align-items:center;gap:.45rem;width:100%;padding:.15rem .3rem;font:inherit;font-size:.8rem;text-align:left;background:none;border:1px solid transparent;border-radius:3px;cursor:pointer;color:inherit}.sc-legend-btn:hover{background:var(--surface-hover, rgba(0, 0, 0, .04))}.sc-legend-btn.selected{border-color:var(--primary-color, #3b82f6);background:var(--surface-hover, rgba(59, 130, 246, .08));font-weight:600}.sc-swatch{width:.85rem;height:.85rem;flex:0 0 auto;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-legend-label{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: "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: 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: "ngmodule", type: TreeSelectModule }, { kind: "component", type: i11.TreeSelect, selector: "p-treeSelect", inputs: ["inputId", "scrollHeight", "disabled", "metaKeySelection", "variant", "display", "selectionMode", "tabindex", "ariaLabel", "ariaLabelledBy", "placeholder", "panelClass", "panelStyle", "panelStyleClass", "containerStyle", "containerStyleClass", "labelStyle", "labelStyleClass", "overlayOptions", "emptyMessage", "appendTo", "filter", "filterBy", "filterMode", "filterPlaceholder", "filterLocale", "filterInputAutoFocus", "propagateSelectionDown", "propagateSelectionUp", "showClear", "resetFilterOnHide", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autofocus", "options", "showTransitionOptions", "hideTransitionOptions", "loading"], outputs: ["onNodeExpand", "onNodeCollapse", "onShow", "onHide", "onClear", "onFilter", "onFocus", "onBlur", "onNodeUnselect", "onNodeSelect"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
22535
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialKeyComponent, isStandalone: true, selector: "spatial-key", inputs: { controls: "controls", dataset: "dataset", view: "view", legend: "legend", colorBarCss: "colorBarCss", selectedCategory: "selectedCategory", colormapOptions: "colormapOptions" }, outputs: { categoryClicked: "categoryClicked" }, usesOnChanges: true, ngImport: i0, template: "<!-- Key: categorical legend or a continuous colour bar, built from the same\n functions the renderer uses so it cannot disagree with the canvas. -->\n<div class=\"sc-section sc-key\" *ngIf=\"view.colorBy\">\n <span class=\"sc-lbl\">{{ colorByLabel }}</span>\n <!-- Derived columns say so, so they are not mistaken for measured data. -->\n <small class=\"sc-hint\" *ngIf=\"activeDescription\">{{ activeDescription }}</small>\n\n <!-- Legend rows are buttons: clicking one selects that category, clicking\n it again clears \u2014 so a click is always reversible. -->\n <ul class=\"sc-legend\" *ngIf=\"isCategorical\">\n <li *ngFor=\"let entry of legend; index as i; trackBy: trackByLabel\">\n <button\n type=\"button\"\n class=\"sc-legend-btn\"\n [class.selected]=\"selectedCategory === i\"\n (click)=\"categoryClicked.emit(i)\"\n [title]=\"'Select ' + entry.label\"\n >\n <span class=\"sc-swatch\" [style.background]=\"entry.color\"></span>\n <span class=\"sc-legend-label\">{{ entry.label }}</span>\n </button>\n </li>\n </ul>\n\n <div *ngIf=\"isContinuous\">\n <div class=\"sc-colorbar\" [style.background]=\"colorBarCss\"></div>\n <div class=\"sc-colorbar-ends\">\n <span>low</span>\n <span class=\"sc-hint\">window from percentiles</span>\n <span>high</span>\n </div>\n <!-- The gradient this bar and the map are drawn with. Sits under the bar\n it controls, with the same swatch previews the image's colormap picker\n uses: which gradient reads best is a visual judgement, so a list of\n names would be the wrong control. Cleared = follow the image's. -->\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Colormap</span>\n <p-treeSelect\n [options]=\"colormapOptions\"\n [ngModel]=\"selectedColormapNode\"\n selectionMode=\"single\"\n [showClear]=\"true\"\n placeholder=\"Match the image\"\n [scrollHeight]=\"'260px'\"\n appendTo=\"body\"\n pTooltip=\"Colour scale for gene and numeric values \u2014 the markers, the gene map and this bar. Clear it to follow the image's colormap\"\n tooltipPosition=\"top\"\n (onNodeSelect)=\"onContinuousColormap($event.node)\"\n (onClear)=\"onContinuousColormap(null)\"\n >\n <ng-template let-node pTemplate=\"default\">\n <span class=\"sc-cmap-option\">\n <img *ngIf=\"!node.children && node.data?.src\" [src]=\"node.data.src\" class=\"sc-cmap-swatch\" alt=\"\" />\n <span>{{ node.label }}</span>\n </span>\n </ng-template>\n </p-treeSelect>\n <span class=\"sc-val\"></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";.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-key{padding:.5rem 0;border-top:1px solid var(--surface-border, #dee2e6);border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-legend{list-style:none;margin:0;padding:0;max-height:11rem;overflow-y:auto;display:flex;flex-direction:column;gap:.2rem}.sc-legend li{display:flex}.sc-legend-btn{display:flex;align-items:center;gap:.45rem;width:100%;padding:.15rem .3rem;font:inherit;font-size:.8rem;text-align:left;background:none;border:1px solid transparent;border-radius:3px;cursor:pointer;color:inherit}.sc-legend-btn:hover{background:var(--surface-hover, rgba(0, 0, 0, .04))}.sc-legend-btn.selected{border-color:var(--primary-color, #3b82f6);background:var(--surface-hover, rgba(59, 130, 246, .08));font-weight:600}.sc-swatch{width:.85rem;height:.85rem;flex:0 0 auto;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-legend-label{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: "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: 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: "ngmodule", type: TreeSelectModule }, { kind: "component", type: i11.TreeSelect, selector: "p-treeSelect", inputs: ["inputId", "scrollHeight", "disabled", "metaKeySelection", "variant", "display", "selectionMode", "tabindex", "ariaLabel", "ariaLabelledBy", "placeholder", "panelClass", "panelStyle", "panelStyleClass", "containerStyle", "containerStyleClass", "labelStyle", "labelStyleClass", "overlayOptions", "emptyMessage", "appendTo", "filter", "filterBy", "filterMode", "filterPlaceholder", "filterLocale", "filterInputAutoFocus", "propagateSelectionDown", "propagateSelectionUp", "showClear", "resetFilterOnHide", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autofocus", "options", "showTransitionOptions", "hideTransitionOptions", "loading"], outputs: ["onNodeExpand", "onNodeCollapse", "onShow", "onHide", "onClear", "onFilter", "onFocus", "onBlur", "onNodeUnselect", "onNodeSelect"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
22461
22536
  }
22462
22537
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialKeyComponent, decorators: [{
22463
22538
  type: Component,
22464
- args: [{ selector: 'spatial-key', standalone: true, imports: [CommonModule, FormsModule, SharedModule, TooltipModule, TreeSelectModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Key: categorical legend or a continuous colour bar, built from the same\n functions the renderer uses so it cannot disagree with the canvas. -->\n<div class=\"sc-section sc-key\" *ngIf=\"view.colorBy\">\n <span class=\"sc-lbl\">{{ colorByLabel }}</span>\n <!-- Derived columns say so, so they are not mistaken for measured data. -->\n <small class=\"sc-hint\" *ngIf=\"activeDescription\">{{ activeDescription }}</small>\n\n <!-- Legend rows are buttons: clicking one selects that category, clicking\n it again clears \u2014 so a click is always reversible. -->\n <ul class=\"sc-legend\" *ngIf=\"isCategorical\">\n <li *ngFor=\"let entry of legend; index as i; trackBy: trackByLabel\">\n <button\n type=\"button\"\n class=\"sc-legend-btn\"\n [class.selected]=\"selectedCategory === i\"\n (click)=\"categoryClicked.emit(i)\"\n [title]=\"'Select ' + entry.label\"\n >\n <span class=\"sc-swatch\" [style.background]=\"entry.color\"></span>\n <span class=\"sc-legend-label\">{{ entry.label }}</span>\n </button>\n </li>\n </ul>\n\n <div *ngIf=\"isContinuous\">\n <div class=\"sc-colorbar\" [style.background]=\"colorBarCss\"></div>\n <div class=\"sc-colorbar-ends\">\n <span>low</span>\n <span class=\"sc-hint\">window from percentiles</span>\n <span>high</span>\n </div>\n <!-- The gradient this bar and the map are drawn with. Sits under the bar\n it controls, with the same swatch previews the image's colormap picker\n uses: which gradient reads best is a visual judgement, so a list of\n names would be the wrong control. Cleared = follow the image's. -->\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Colormap</span>\n <p-treeSelect\n [options]=\"colormapOptions\"\n [ngModel]=\"selectedColormapNode\"\n selectionMode=\"single\"\n [showClear]=\"true\"\n placeholder=\"Match the image\"\n [scrollHeight]=\"'260px'\"\n appendTo=\"body\"\n pTooltip=\"Colour scale for gene and numeric values \u2014 the markers, the gene map and this bar. Clear it to follow the image's colormap\"\n tooltipPosition=\"top\"\n (onNodeSelect)=\"onContinuousColormap($event.node)\"\n (onClear)=\"onContinuousColormap(null)\"\n >\n <ng-template let-node pTemplate=\"default\">\n <span class=\"sc-cmap-option\">\n <img *ngIf=\"!node.children && node.data?.src\" [src]=\"node.data.src\" class=\"sc-cmap-swatch\" alt=\"\" />\n <span>{{ node.label }}</span>\n </span>\n </ng-template>\n </p-treeSelect>\n <span class=\"sc-val\"></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";.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}:host{display:contents}.sc-key{padding:.5rem 0;border-top:1px solid var(--surface-border, #dee2e6);border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-legend{list-style:none;margin:0;padding:0;max-height:11rem;overflow-y:auto;display:flex;flex-direction:column;gap:.2rem}.sc-legend li{display:flex}.sc-legend-btn{display:flex;align-items:center;gap:.45rem;width:100%;padding:.15rem .3rem;font:inherit;font-size:.8rem;text-align:left;background:none;border:1px solid transparent;border-radius:3px;cursor:pointer;color:inherit}.sc-legend-btn:hover{background:var(--surface-hover, rgba(0, 0, 0, .04))}.sc-legend-btn.selected{border-color:var(--primary-color, #3b82f6);background:var(--surface-hover, rgba(59, 130, 246, .08));font-weight:600}.sc-swatch{width:.85rem;height:.85rem;flex:0 0 auto;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-legend-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
22539
+ args: [{ selector: 'spatial-key', standalone: true, imports: [CommonModule, FormsModule, SharedModule, TooltipModule, TreeSelectModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Key: categorical legend or a continuous colour bar, built from the same\n functions the renderer uses so it cannot disagree with the canvas. -->\n<div class=\"sc-section sc-key\" *ngIf=\"view.colorBy\">\n <span class=\"sc-lbl\">{{ colorByLabel }}</span>\n <!-- Derived columns say so, so they are not mistaken for measured data. -->\n <small class=\"sc-hint\" *ngIf=\"activeDescription\">{{ activeDescription }}</small>\n\n <!-- Legend rows are buttons: clicking one selects that category, clicking\n it again clears \u2014 so a click is always reversible. -->\n <ul class=\"sc-legend\" *ngIf=\"isCategorical\">\n <li *ngFor=\"let entry of legend; index as i; trackBy: trackByLabel\">\n <button\n type=\"button\"\n class=\"sc-legend-btn\"\n [class.selected]=\"selectedCategory === i\"\n (click)=\"categoryClicked.emit(i)\"\n [title]=\"'Select ' + entry.label\"\n >\n <span class=\"sc-swatch\" [style.background]=\"entry.color\"></span>\n <span class=\"sc-legend-label\">{{ entry.label }}</span>\n </button>\n </li>\n </ul>\n\n <div *ngIf=\"isContinuous\">\n <div class=\"sc-colorbar\" [style.background]=\"colorBarCss\"></div>\n <div class=\"sc-colorbar-ends\">\n <span>low</span>\n <span class=\"sc-hint\">window from percentiles</span>\n <span>high</span>\n </div>\n <!-- The gradient this bar and the map are drawn with. Sits under the bar\n it controls, with the same swatch previews the image's colormap picker\n uses: which gradient reads best is a visual judgement, so a list of\n names would be the wrong control. Cleared = follow the image's. -->\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Colormap</span>\n <p-treeSelect\n [options]=\"colormapOptions\"\n [ngModel]=\"selectedColormapNode\"\n selectionMode=\"single\"\n [showClear]=\"true\"\n placeholder=\"Match the image\"\n [scrollHeight]=\"'260px'\"\n appendTo=\"body\"\n pTooltip=\"Colour scale for gene and numeric values \u2014 the markers, the gene map and this bar. Clear it to follow the image's colormap\"\n tooltipPosition=\"top\"\n (onNodeSelect)=\"onContinuousColormap($event.node)\"\n (onClear)=\"onContinuousColormap(null)\"\n >\n <ng-template let-node pTemplate=\"default\">\n <span class=\"sc-cmap-option\">\n <img *ngIf=\"!node.children && node.data?.src\" [src]=\"node.data.src\" class=\"sc-cmap-swatch\" alt=\"\" />\n <span>{{ node.label }}</span>\n </span>\n </ng-template>\n </p-treeSelect>\n <span class=\"sc-val\"></span>\n </div>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";.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-key{padding:.5rem 0;border-top:1px solid var(--surface-border, #dee2e6);border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-legend{list-style:none;margin:0;padding:0;max-height:11rem;overflow-y:auto;display:flex;flex-direction:column;gap:.2rem}.sc-legend li{display:flex}.sc-legend-btn{display:flex;align-items:center;gap:.45rem;width:100%;padding:.15rem .3rem;font:inherit;font-size:.8rem;text-align:left;background:none;border:1px solid transparent;border-radius:3px;cursor:pointer;color:inherit}.sc-legend-btn:hover{background:var(--surface-hover, rgba(0, 0, 0, .04))}.sc-legend-btn.selected{border-color:var(--primary-color, #3b82f6);background:var(--surface-hover, rgba(59, 130, 246, .08));font-weight:600}.sc-swatch{width:.85rem;height:.85rem;flex:0 0 auto;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-legend-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
22465
22540
  }], propDecorators: { controls: [{
22466
22541
  type: Input
22467
22542
  }], dataset: [{
@@ -22667,11 +22742,11 @@ class SpatialGroupsPanelComponent {
22667
22742
  });
22668
22743
  }
22669
22744
  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 }); }
22670
- 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}: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$3.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: i4.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 }); }
22745
+ 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 }); }
22671
22746
  }
22672
22747
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGroupsPanelComponent, decorators: [{
22673
22748
  type: Component,
22674
- 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}: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"] }]
22749
+ 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"] }]
22675
22750
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { controls: [{
22676
22751
  type: Input
22677
22752
  }], dataset: [{
@@ -22780,7 +22855,7 @@ class SpatialCellsPanelComponent {
22780
22855
  this.controls?.setViewState({ cellSingleColor: hex });
22781
22856
  }
22782
22857
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialCellsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
22783
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialCellsPanelComponent, isStandalone: true, selector: "spatial-cells-panel", inputs: { controls: "controls", dataset: "dataset", view: "view", genes: "genes", colorBarCss: "colorBarCss", open: "open" }, outputs: { openChange: "openChange" }, usesOnChanges: true, ngImport: i0, template: "<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]=\"cellsOn\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowCells($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Cells</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 <ng-container *ngIf=\"cellSetOptions.length > 1\">\n <div class=\"sc-subhead\">Boundaries</div>\n <p-selectButton\n [options]=\"cellSetOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"activeCellSet\"\n (ngModelChange)=\"onCellSet($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n </ng-container>\n\n <div class=\"sc-subhead\">Coloring</div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Cell Color</span>\n <p-dropdown\n [options]=\"cellColorOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellColorMode\"\n (ngModelChange)=\"onCellColorMode($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.cellColorMode === 'group'\">\n Adjust cell color in the groups section below.\n </small>\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-row\" *ngIf=\"view.cellColorMode === 'gene' && dataset?.features\">\n <span class=\"sc-lbl\">Gene</span>\n <p-dropdown\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellColorGene\"\n (ngModelChange)=\"onCellColorGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n appendTo=\"body\"\n placeholder=\"Search genes\u2026\"\n ></p-dropdown>\n </div>\n </ng-container>\n <div class=\"sc-row\" *ngIf=\"view.cellColorMode === 'single'\">\n <span class=\"sc-lbl\">Color</span>\n <input\n type=\"color\"\n class=\"sc-color-input\"\n [value]=\"view.cellSingleColor\"\n (change)=\"onCellSingleColor($any($event.target).value)\"\n />\n </div>\n <div *ngIf=\"view.cellColorMode === 'gene' || view.cellColorMode === 'transcriptDensity'\">\n <div class=\"sc-colorbar\" [style.background]=\"colorBarCss || densityColorBarCss\"></div>\n </div>\n\n <spatial-groups-panel [controls]=\"controls\" [dataset]=\"dataset\" [view]=\"view\"></spatial-groups-panel>\n\n <div class=\"sc-subhead\">Visibility</div>\n <span class=\"sc-lbl\">View cells as</span>\n <p-selectButton\n [options]=\"cellDrawOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellDraw\"\n (ngModelChange)=\"onCellDraw($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <ng-container *ngIf=\"view.cellDraw !== 'outline'\">\n <span class=\"sc-lbl\">Cell fill opacity</span>\n <div class=\"sc-row sc-row-number\">\n <p-slider\n [ngModel]=\"view.cellOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onCellOpacity($event.value)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"cellOpacityPercent\"\n (ngModelChange)=\"onCellOpacityPercent($event)\"\n [min]=\"5\"\n [max]=\"100\"\n [showButtons]=\"false\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n </ng-container>\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}:host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { 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$3.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: i4.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: InputNumberModule }, { kind: "component", type: i7$3.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i8$1.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: i2$1.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "component", type: SpatialGroupsPanelComponent, selector: "spatial-groups-panel", inputs: ["controls", "dataset", "view"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
22858
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialCellsPanelComponent, isStandalone: true, selector: "spatial-cells-panel", inputs: { controls: "controls", dataset: "dataset", view: "view", genes: "genes", colorBarCss: "colorBarCss", open: "open" }, outputs: { openChange: "openChange" }, usesOnChanges: true, ngImport: i0, template: "<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]=\"cellsOn\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowCells($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Cells</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 <ng-container *ngIf=\"cellSetOptions.length > 1\">\n <div class=\"sc-subhead\">Boundaries</div>\n <p-selectButton\n [options]=\"cellSetOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"activeCellSet\"\n (ngModelChange)=\"onCellSet($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n </ng-container>\n\n <div class=\"sc-subhead\">Coloring</div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Cell Color</span>\n <p-dropdown\n [options]=\"cellColorOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellColorMode\"\n (ngModelChange)=\"onCellColorMode($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.cellColorMode === 'group'\">\n Adjust cell color in the groups section below.\n </small>\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-row\" *ngIf=\"view.cellColorMode === 'gene' && dataset?.features\">\n <span class=\"sc-lbl\">Gene</span>\n <p-dropdown\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellColorGene\"\n (ngModelChange)=\"onCellColorGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n appendTo=\"body\"\n placeholder=\"Search genes\u2026\"\n ></p-dropdown>\n </div>\n </ng-container>\n <div class=\"sc-row\" *ngIf=\"view.cellColorMode === 'single'\">\n <span class=\"sc-lbl\">Color</span>\n <input\n type=\"color\"\n class=\"sc-color-input\"\n [value]=\"view.cellSingleColor\"\n (change)=\"onCellSingleColor($any($event.target).value)\"\n />\n </div>\n <div *ngIf=\"view.cellColorMode === 'gene' || view.cellColorMode === 'transcriptDensity'\">\n <div class=\"sc-colorbar\" [style.background]=\"colorBarCss || densityColorBarCss\"></div>\n </div>\n\n <spatial-groups-panel [controls]=\"controls\" [dataset]=\"dataset\" [view]=\"view\"></spatial-groups-panel>\n\n <div class=\"sc-subhead\">Visibility</div>\n <span class=\"sc-lbl\">View cells as</span>\n <p-selectButton\n [options]=\"cellDrawOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellDraw\"\n (ngModelChange)=\"onCellDraw($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <ng-container *ngIf=\"view.cellDraw !== 'outline'\">\n <span class=\"sc-lbl\">Cell fill opacity</span>\n <div class=\"sc-row sc-row-number\">\n <p-slider\n [ngModel]=\"view.cellOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onCellOpacity($event.value)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"cellOpacityPercent\"\n (ngModelChange)=\"onCellOpacityPercent($event)\"\n [min]=\"5\"\n [max]=\"100\"\n [showButtons]=\"false\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n </ng-container>\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}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { 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: "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: InputNumberModule }, { kind: "component", type: i7$3.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i8$1.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: i2$1.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "component", type: SpatialGroupsPanelComponent, selector: "spatial-groups-panel", inputs: ["controls", "dataset", "view"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
22784
22859
  }
22785
22860
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialCellsPanelComponent, decorators: [{
22786
22861
  type: Component,
@@ -22793,7 +22868,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
22793
22868
  SelectButtonModule,
22794
22869
  SliderModule,
22795
22870
  SpatialGroupsPanelComponent,
22796
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<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]=\"cellsOn\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowCells($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Cells</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 <ng-container *ngIf=\"cellSetOptions.length > 1\">\n <div class=\"sc-subhead\">Boundaries</div>\n <p-selectButton\n [options]=\"cellSetOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"activeCellSet\"\n (ngModelChange)=\"onCellSet($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n </ng-container>\n\n <div class=\"sc-subhead\">Coloring</div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Cell Color</span>\n <p-dropdown\n [options]=\"cellColorOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellColorMode\"\n (ngModelChange)=\"onCellColorMode($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.cellColorMode === 'group'\">\n Adjust cell color in the groups section below.\n </small>\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-row\" *ngIf=\"view.cellColorMode === 'gene' && dataset?.features\">\n <span class=\"sc-lbl\">Gene</span>\n <p-dropdown\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellColorGene\"\n (ngModelChange)=\"onCellColorGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n appendTo=\"body\"\n placeholder=\"Search genes\u2026\"\n ></p-dropdown>\n </div>\n </ng-container>\n <div class=\"sc-row\" *ngIf=\"view.cellColorMode === 'single'\">\n <span class=\"sc-lbl\">Color</span>\n <input\n type=\"color\"\n class=\"sc-color-input\"\n [value]=\"view.cellSingleColor\"\n (change)=\"onCellSingleColor($any($event.target).value)\"\n />\n </div>\n <div *ngIf=\"view.cellColorMode === 'gene' || view.cellColorMode === 'transcriptDensity'\">\n <div class=\"sc-colorbar\" [style.background]=\"colorBarCss || densityColorBarCss\"></div>\n </div>\n\n <spatial-groups-panel [controls]=\"controls\" [dataset]=\"dataset\" [view]=\"view\"></spatial-groups-panel>\n\n <div class=\"sc-subhead\">Visibility</div>\n <span class=\"sc-lbl\">View cells as</span>\n <p-selectButton\n [options]=\"cellDrawOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellDraw\"\n (ngModelChange)=\"onCellDraw($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <ng-container *ngIf=\"view.cellDraw !== 'outline'\">\n <span class=\"sc-lbl\">Cell fill opacity</span>\n <div class=\"sc-row sc-row-number\">\n <p-slider\n [ngModel]=\"view.cellOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onCellOpacity($event.value)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"cellOpacityPercent\"\n (ngModelChange)=\"onCellOpacityPercent($event)\"\n [min]=\"5\"\n [max]=\"100\"\n [showButtons]=\"false\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n </ng-container>\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}:host{display:contents}\n"] }]
22871
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<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]=\"cellsOn\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowCells($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Cells</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 <ng-container *ngIf=\"cellSetOptions.length > 1\">\n <div class=\"sc-subhead\">Boundaries</div>\n <p-selectButton\n [options]=\"cellSetOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"activeCellSet\"\n (ngModelChange)=\"onCellSet($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n </ng-container>\n\n <div class=\"sc-subhead\">Coloring</div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Cell Color</span>\n <p-dropdown\n [options]=\"cellColorOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellColorMode\"\n (ngModelChange)=\"onCellColorMode($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.cellColorMode === 'group'\">\n Adjust cell color in the groups section below.\n </small>\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-row\" *ngIf=\"view.cellColorMode === 'gene' && dataset?.features\">\n <span class=\"sc-lbl\">Gene</span>\n <p-dropdown\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellColorGene\"\n (ngModelChange)=\"onCellColorGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n appendTo=\"body\"\n placeholder=\"Search genes\u2026\"\n ></p-dropdown>\n </div>\n </ng-container>\n <div class=\"sc-row\" *ngIf=\"view.cellColorMode === 'single'\">\n <span class=\"sc-lbl\">Color</span>\n <input\n type=\"color\"\n class=\"sc-color-input\"\n [value]=\"view.cellSingleColor\"\n (change)=\"onCellSingleColor($any($event.target).value)\"\n />\n </div>\n <div *ngIf=\"view.cellColorMode === 'gene' || view.cellColorMode === 'transcriptDensity'\">\n <div class=\"sc-colorbar\" [style.background]=\"colorBarCss || densityColorBarCss\"></div>\n </div>\n\n <spatial-groups-panel [controls]=\"controls\" [dataset]=\"dataset\" [view]=\"view\"></spatial-groups-panel>\n\n <div class=\"sc-subhead\">Visibility</div>\n <span class=\"sc-lbl\">View cells as</span>\n <p-selectButton\n [options]=\"cellDrawOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.cellDraw\"\n (ngModelChange)=\"onCellDraw($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <ng-container *ngIf=\"view.cellDraw !== 'outline'\">\n <span class=\"sc-lbl\">Cell fill opacity</span>\n <div class=\"sc-row sc-row-number\">\n <p-slider\n [ngModel]=\"view.cellOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onCellOpacity($event.value)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"cellOpacityPercent\"\n (ngModelChange)=\"onCellOpacityPercent($event)\"\n [min]=\"5\"\n [max]=\"100\"\n [showButtons]=\"false\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n </ng-container>\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}\n"] }]
22797
22872
  }], propDecorators: { controls: [{
22798
22873
  type: Input
22799
22874
  }], dataset: [{
@@ -22810,11 +22885,328 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
22810
22885
  type: Output
22811
22886
  }] } });
22812
22887
 
22888
+ /**
22889
+ * Merging and filtering of the spatial-tile wire format (observation ids, NO_CATEGORY): the
22890
+ * pieces of the 2D view's tile loop that are pure — concatenating tiles, dropping hidden
22891
+ * groups and genes, and the density-grid markers of a zoomed-out gene selection.
22892
+ *
22893
+ * These work on SIV's own tile contract, so they stay in SIV rather than moving to napari-js.
22894
+ */
22895
+ /** Rings whose owning cell is in a switched-off group removed. */
22896
+ function filterRings(rings, hidden) {
22897
+ if (!hidden)
22898
+ return rings;
22899
+ const keep = [];
22900
+ let vertices = 0;
22901
+ for (let i = 0; i < rings.count; i++) {
22902
+ const code = hidden.codes[rings.observation[i]];
22903
+ if (code !== NO_CATEGORY && hidden.hidden[code])
22904
+ continue;
22905
+ keep.push(i);
22906
+ vertices += rings.offsets[i + 1] - rings.offsets[i];
22907
+ }
22908
+ if (keep.length === rings.count)
22909
+ return rings;
22910
+ const observation = new Uint32Array(keep.length);
22911
+ const offsets = new Uint32Array(keep.length + 1);
22912
+ const coords = new Float32Array(vertices * 2);
22913
+ let v = 0;
22914
+ keep.forEach((i, r) => {
22915
+ observation[r] = rings.observation[i];
22916
+ offsets[r] = v;
22917
+ const a = rings.offsets[i] * 2;
22918
+ const b = rings.offsets[i + 1] * 2;
22919
+ coords.set(rings.coords.subarray(a, b), v * 2);
22920
+ v += (b - a) / 2;
22921
+ });
22922
+ offsets[keep.length] = v;
22923
+ return { count: keep.length, observation, offsets, coords };
22924
+ }
22925
+ /** Slots (indices into the selected genes) switched off with their eye toggle. */
22926
+ function hiddenGeneSlots(view) {
22927
+ if (!view.transcriptHiddenGenes.length)
22928
+ return null;
22929
+ const off = new Set(view.transcriptHiddenGenes);
22930
+ return Uint8Array.from(view.transcriptGenes, (g) => (off.has(g) ? 1 : 0));
22931
+ }
22932
+ /**
22933
+ * Whether "All genes" is switched off as a whole with the gene tree's root eye: its hidden
22934
+ * list is `[ALL_GENES]` (every gene hidden, none excepted). Hiding every gene by name would
22935
+ * need the gene list, which a dataset need not have.
22936
+ */
22937
+ function allGenesHidden(view) {
22938
+ return view.transcriptAllGenes && everyGeneOff(view.transcriptAllGenesHidden);
22939
+ }
22940
+ /** Whether an "All genes" hidden list hides every gene, none excepted. */
22941
+ function everyGeneOff(off) {
22942
+ return off.length === 1 && off[0] === ALL_GENES;
22943
+ }
22944
+ /** Whether a gene of "All genes" is drawn, by its hidden list (see `transcriptAllGenesHidden`). */
22945
+ function allGeneShownBy(off) {
22946
+ const listed = new Set(off);
22947
+ const except = off[0] === ALL_GENES;
22948
+ return (gene) => listed.has(gene) === except;
22949
+ }
22950
+ /** "All genes"'s hidden list with `gene` shown (`on`) or hidden, in the list's own form. */
22951
+ function toggleAllGeneHidden(off, gene, on) {
22952
+ const except = off[0] === ALL_GENES;
22953
+ // Hidden by name: listed means off. Led by ALL_GENES: listed means on.
22954
+ const listed = new Set(except ? off.slice(1) : off);
22955
+ if (on === except)
22956
+ listed.add(gene);
22957
+ else
22958
+ listed.delete(gene);
22959
+ return except ? [ALL_GENES, ...listed] : [...listed];
22960
+ }
22961
+ /**
22962
+ * Codes (indices into `genes`, the dataset's all-gene code order) switched off with their eye
22963
+ * toggle while every gene is drawn; null when none is, or without the gene list.
22964
+ */
22965
+ function hiddenAllGeneCodes(view, genes) {
22966
+ if (!genes || !view.transcriptAllGenesHidden.length)
22967
+ return null;
22968
+ const shown = allGeneShownBy(view.transcriptAllGenesHidden);
22969
+ const codes = Uint8Array.from(genes, (g) => (shown(g) ? 0 : 1));
22970
+ return codes.includes(1) ? codes : null;
22971
+ }
22972
+ /** Entries in a hidden group's cell, or of a hidden gene, removed (with their sizes). */
22973
+ function filterTranscripts(t, px, hidden, hiddenGenes) {
22974
+ if (!hidden && !hiddenGenes)
22975
+ return { merged: t, px };
22976
+ const keep = [];
22977
+ for (let i = 0; i < t.count; i++) {
22978
+ if (hiddenGenes && hiddenGenes[t.gene[i]])
22979
+ continue;
22980
+ const o = t.observation[i];
22981
+ if (hidden && o !== NO_OBSERVATION) {
22982
+ const code = hidden.codes[o];
22983
+ if (code !== NO_CATEGORY && hidden.hidden[code])
22984
+ continue;
22985
+ }
22986
+ keep.push(i);
22987
+ }
22988
+ const pick = (a) => {
22989
+ const out = new a.constructor(keep.length);
22990
+ keep.forEach((i, k) => {
22991
+ out[k] = a[i];
22992
+ });
22993
+ return out;
22994
+ };
22995
+ return {
22996
+ merged: {
22997
+ count: keep.length,
22998
+ aggregated: t.aggregated,
22999
+ x: pick(t.x),
23000
+ y: pick(t.y),
23001
+ z: pick(t.z),
23002
+ weight: pick(t.weight),
23003
+ observation: pick(t.observation),
23004
+ gene: pick(t.gene),
23005
+ },
23006
+ px: pick(px),
23007
+ };
23008
+ }
23009
+ /** The median of `v`, from a sample of about 1024 values. */
23010
+ function median(v) {
23011
+ if (!v.length)
23012
+ return 0;
23013
+ const step = Math.max(1, Math.floor(v.length / 1024));
23014
+ const sample = [];
23015
+ for (let i = 0; i < v.length; i += step)
23016
+ sample.push(v[i]);
23017
+ sample.sort((a, b) => a - b);
23018
+ return sample[sample.length >> 1];
23019
+ }
23020
+ /** Concatenate tiles' rings into one set, dropping rings already seen (a cell straddling
23021
+ * two tiles may be listed by both). */
23022
+ function mergePolygonTiles(tiles) {
23023
+ const seen = new Set();
23024
+ let rings = 0;
23025
+ let vertices = 0;
23026
+ for (const t of tiles) {
23027
+ for (let i = 0; i < t.count; i++) {
23028
+ if (seen.has(t.observation[i]))
23029
+ continue;
23030
+ seen.add(t.observation[i]);
23031
+ rings++;
23032
+ vertices += t.offsets[i + 1] - t.offsets[i];
23033
+ }
23034
+ }
23035
+ seen.clear();
23036
+ const observation = new Uint32Array(rings);
23037
+ const offsets = new Uint32Array(rings + 1);
23038
+ const coords = new Float32Array(vertices * 2);
23039
+ let r = 0;
23040
+ let v = 0;
23041
+ for (const t of tiles) {
23042
+ for (let i = 0; i < t.count; i++) {
23043
+ const o = t.observation[i];
23044
+ if (seen.has(o))
23045
+ continue;
23046
+ seen.add(o);
23047
+ observation[r] = o;
23048
+ offsets[r] = v;
23049
+ const a = t.offsets[i] * 2;
23050
+ const b = t.offsets[i + 1] * 2;
23051
+ coords.set(t.coords.subarray(a, b), v * 2);
23052
+ v += (b - a) / 2;
23053
+ r++;
23054
+ }
23055
+ }
23056
+ offsets[rings] = v;
23057
+ return { count: rings, observation, offsets, coords };
23058
+ }
23059
+ /** Concatenate transcript tiles, stopping at `limit` entries. */
23060
+ function mergeTranscriptTiles(tiles, limit = Infinity) {
23061
+ let n = 0;
23062
+ for (const t of tiles)
23063
+ n += t.count;
23064
+ n = Math.min(n, limit);
23065
+ const out = {
23066
+ count: n,
23067
+ aggregated: tiles.some((t) => t.aggregated),
23068
+ x: new Float32Array(n),
23069
+ y: new Float32Array(n),
23070
+ z: new Float32Array(n),
23071
+ weight: new Uint32Array(n),
23072
+ observation: new Uint32Array(n),
23073
+ gene: new Uint16Array(n),
23074
+ };
23075
+ let o = 0;
23076
+ for (const t of tiles) {
23077
+ const k = Math.min(t.count, n - o);
23078
+ if (k <= 0)
23079
+ break;
23080
+ for (const f of ['x', 'y', 'z', 'weight', 'observation', 'gene']) {
23081
+ out[f].set(t[f].subarray(0, k), o);
23082
+ }
23083
+ o += k;
23084
+ }
23085
+ return out;
23086
+ }
23087
+ /**
23088
+ * One marker per (cluster, `bin` square) from per-cluster density grids: the counts of the grid
23089
+ * cells inside `area` summed per square, at their count-weighted centre. `slots[c]` is the gene
23090
+ * slot whose icon cluster c's markers take.
23091
+ */
23092
+ function clusterMarkers(rasters, slots, bin, area) {
23093
+ const x = [];
23094
+ const y = [];
23095
+ const w = [];
23096
+ const gene = [];
23097
+ const group = [];
23098
+ rasters.forEach((r, c) => {
23099
+ const [cw, ch] = r.meta.gridSize;
23100
+ const [ox, oy] = r.meta.origin;
23101
+ const { rows, cols } = r.meta;
23102
+ const c0 = Math.max(0, Math.floor((area.x0 - ox) / cw));
23103
+ const c1 = Math.min(cols - 1, Math.floor((area.x1 - ox) / cw));
23104
+ const r0 = Math.max(0, Math.floor((area.y0 - oy) / ch));
23105
+ const r1 = Math.min(rows - 1, Math.floor((area.y1 - oy) / ch));
23106
+ const squares = new Map();
23107
+ for (let row = r0; row <= r1; row++) {
23108
+ for (let col = c0; col <= c1; col++) {
23109
+ const v = r.values[row * cols + col];
23110
+ if (!(v > 0))
23111
+ continue;
23112
+ const cx = ox + (col + 0.5) * cw;
23113
+ const cy = oy + (row + 0.5) * ch;
23114
+ const key = Math.floor((cy - oy) / bin) * 1e6 + Math.floor((cx - ox) / bin);
23115
+ const s = squares.get(key) ?? { n: 0, sx: 0, sy: 0 };
23116
+ s.n += v;
23117
+ s.sx += v * cx;
23118
+ s.sy += v * cy;
23119
+ squares.set(key, s);
23120
+ }
23121
+ }
23122
+ for (const s of squares.values()) {
23123
+ x.push(s.sx / s.n);
23124
+ y.push(s.sy / s.n);
23125
+ w.push(Math.round(s.n));
23126
+ gene.push(slots[c]);
23127
+ group.push(c);
23128
+ }
23129
+ });
23130
+ const n = w.length;
23131
+ return {
23132
+ tile: {
23133
+ count: n,
23134
+ aggregated: true,
23135
+ x: Float32Array.from(x),
23136
+ y: Float32Array.from(y),
23137
+ z: new Float32Array(n),
23138
+ weight: Uint32Array.from(w),
23139
+ observation: new Uint32Array(n).fill(NO_OBSERVATION),
23140
+ gene: Uint16Array.from(gene),
23141
+ },
23142
+ group: Int32Array.from(group),
23143
+ };
23144
+ }
23145
+
23146
+ /**
23147
+ * One gene's icon and colour: the icon grid, a colour picker with its hex value, presets, and
23148
+ * a reset — the content of a popover its host opens on the gene's swatch. Writes nothing itself:
23149
+ * it reports the choice, and the host stores it where it belongs.
23150
+ */
23151
+ class SpatialGeneStyleComponent {
23152
+ constructor() {
23153
+ /** The gene, for the controls' labels. */
23154
+ this.gene = '';
23155
+ /** What the icons and colour are of ("Observation", "Transcript"), for the titles. */
23156
+ this.what = '';
23157
+ /** The gene's colour, `#rrggbb`. */
23158
+ this.color = '#ffffff';
23159
+ /** The gene's icon. */
23160
+ this.glyph = 'circle';
23161
+ /** A note under the icons, e.g. where they show; none when null. */
23162
+ this.iconHint = null;
23163
+ this.colorChange = new EventEmitter();
23164
+ this.glyphChange = new EventEmitter();
23165
+ /** Back to the gene's default icon and colour. */
23166
+ this.resetStyle = new EventEmitter();
23167
+ this.glyphOptions = GLYPH_OPTIONS;
23168
+ this.colorPresets = DEFAULT_CATEGORICAL_PALETTE.slice(0, 10).map((c) => c.toLowerCase());
23169
+ }
23170
+ /** A typed hex colour, accepted as `#rrggbb` or `rrggbb`; anything else is ignored. */
23171
+ onHex(text) {
23172
+ const t = text.trim();
23173
+ const rgb = /^#?[0-9a-f]{6}$/i.test(t) ? parseCssColor(t) : null;
23174
+ if (rgb)
23175
+ this.colorChange.emit(rgbToHex(rgb));
23176
+ }
23177
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneStyleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
23178
+ 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 }); }
23179
+ }
23180
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneStyleComponent, decorators: [{
23181
+ type: Component,
23182
+ 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"] }]
23183
+ }], propDecorators: { gene: [{
23184
+ type: Input
23185
+ }], what: [{
23186
+ type: Input
23187
+ }], color: [{
23188
+ type: Input
23189
+ }], glyph: [{
23190
+ type: Input
23191
+ }], iconHint: [{
23192
+ type: Input
23193
+ }], colorChange: [{
23194
+ type: Output
23195
+ }], glyphChange: [{
23196
+ type: Output
23197
+ }], resetStyle: [{
23198
+ type: Output
23199
+ }] } });
23200
+
23201
+ /** Rows of the all-genes list drawn at once; the filter narrows the rest. */
23202
+ const ALL_GENES_ROWS = 200;
22813
23203
  /**
22814
23204
  * The selected transcript genes as Xenium Explorer's tree: the gene groups, then the
22815
23205
  * ungrouped genes, each row with an eye that hides it without deselecting it, its marker
22816
23206
  * in its colour (opening one icon-and-colour picker per gene), and its transcript count in
22817
- * view.
23207
+ * view. With "All genes" on, the root eye hides or shows every transcript and the root
23208
+ * expands to every gene of the dataset, each with its eye when the dataset lists its genes
23209
+ * in code order (`transcriptTiles.genes`), filtered by name past a few hundred.
22818
23210
  *
22819
23211
  * Writes only through `controls.setViewState`.
22820
23212
  */
@@ -22832,9 +23224,7 @@ class SpatialGeneTreeComponent {
22832
23224
  this.geneCounts = null;
22833
23225
  /** Real genes in the panel — the tree's denominator. */
22834
23226
  this.geneTotal = 0;
22835
- this.glyphOptions = GLYPH_OPTIONS;
22836
23227
  /** Swatches in the picker: the categorical palette genes are coloured from by default. */
22837
- this.colorPresets = DEFAULT_CATEGORICAL_PALETTE.slice(0, 10).map((c) => c.toLowerCase());
22838
23228
  /** The tree, rebuilt when the view changes — never per change-detection pass. */
22839
23229
  this.geneTree = [];
22840
23230
  /** Per gene: its eye, swatch, glyph and count — rebuilt with the inputs (and when the
@@ -22845,6 +23235,18 @@ class SpatialGeneTreeComponent {
22845
23235
  this.rootShown = false;
22846
23236
  this.collapsedGeneGroups = new Set();
22847
23237
  this.geneTreeOpen = true;
23238
+ /** The all-genes list starts closed: it can run to thousands of genes. */
23239
+ this.allGenesOpen = false;
23240
+ /** The genes "All genes" lists, or null when the dataset names none. */
23241
+ this.allGeneList = null;
23242
+ /** Whether a gene of "All genes" can be hidden: the dataset lists its genes in code order. */
23243
+ this.canHideAllGenes = false;
23244
+ this.allGenesFilter = '';
23245
+ /** The list's rows matching the filter, at most {@link ALL_GENES_ROWS}. */
23246
+ this.allRows = [];
23247
+ /** How many genes match the filter, for the "showing n of m" line. */
23248
+ this.allGenesMatched = 0;
23249
+ this.allGenesRowCap = ALL_GENES_ROWS;
22848
23250
  /** The gene the open picker edits. */
22849
23251
  this.styleGene = null;
22850
23252
  /** Colours of the cells' groups, by name — what a cluster of the same name is drawn in. */
@@ -22855,14 +23257,19 @@ class SpatialGeneTreeComponent {
22855
23257
  this.cellGroupColorsLoad = new Supersede();
22856
23258
  this.trackByNode = (_i, node) => node.name ?? '';
22857
23259
  this.trackByLabelString = (_i, s) => s;
23260
+ this.trackByAllRow = (_i, row) => row.gene;
22858
23261
  this.clusterColorMemo = null;
22859
23262
  }
22860
23263
  ngOnChanges(changes) {
22861
23264
  if (changes['view'])
22862
23265
  this.geneTree = buildGeneTree(this.view.transcriptGenes, this.view.transcriptGeneGroups);
22863
23266
  // A same-named grouping of a new dataset is not the old one.
22864
- if (changes['dataset'])
23267
+ if (changes['dataset']) {
22865
23268
  this.cellGroupColorsFor = undefined;
23269
+ const coded = this.dataset?.transcriptTiles?.genes;
23270
+ this.canHideAllGenes = !!coded?.length;
23271
+ this.allGeneList = coded?.length ? coded : (this.dataset?.features?.names ?? null);
23272
+ }
22866
23273
  this.rebuildRows();
22867
23274
  void this.refreshCellGroupColors();
22868
23275
  }
@@ -22880,7 +23287,62 @@ class SpatialGeneTreeComponent {
22880
23287
  this.groupShown = new Map(this.geneTree
22881
23288
  .filter((node) => node.name)
22882
23289
  .map((node) => [node.name, this.areGenesShown(node.genes)]));
22883
- this.rootShown = this.areGenesShown(genes);
23290
+ this.rootShown = this.view.transcriptAllGenes ? this.anyOfAllGenesShown() : this.areGenesShown(genes);
23291
+ this.rebuildAllRows();
23292
+ }
23293
+ /**
23294
+ * The all-genes list's rows: the genes matching the filter, up to the cap. Built only while
23295
+ * the list is open: the counts in view change with every pan, and the list can be thousands.
23296
+ */
23297
+ rebuildAllRows() {
23298
+ const list = this.view.transcriptAllGenes && this.allGenesOpen ? this.allGeneList : null;
23299
+ if (!list) {
23300
+ this.allRows = [];
23301
+ this.allGenesMatched = 0;
23302
+ return;
23303
+ }
23304
+ const needle = this.allGenesFilter.trim().toLowerCase();
23305
+ const shown = allGeneShownBy(this.view.transcriptAllGenesHidden);
23306
+ const rows = [];
23307
+ let matched = 0;
23308
+ list.forEach((gene, code) => {
23309
+ // An empty name is a code no transcript carries (a control probe).
23310
+ if (!gene || (needle && !gene.toLowerCase().includes(needle)))
23311
+ return;
23312
+ matched++;
23313
+ if (rows.length >= ALL_GENES_ROWS)
23314
+ return;
23315
+ rows.push({
23316
+ gene,
23317
+ shown: shown(gene),
23318
+ swatch: this.canHideAllGenes ? this.allGeneColorOf(gene, code) : null,
23319
+ count: this.geneCountOf(gene),
23320
+ });
23321
+ });
23322
+ this.allRows = rows;
23323
+ this.allGenesMatched = matched;
23324
+ }
23325
+ /** Whether "All genes" draws anything: not switched off whole, nor every listed gene hidden. */
23326
+ anyOfAllGenesShown() {
23327
+ if (allGenesHidden(this.view))
23328
+ return false;
23329
+ if (!this.canHideAllGenes || !this.allGeneList)
23330
+ return true;
23331
+ const shown = allGeneShownBy(this.view.transcriptAllGenesHidden);
23332
+ return this.allGeneList.some((g) => g && shown(g));
23333
+ }
23334
+ /** A gene of "All genes" in its colour by gene: its chosen one, else the palette's by code. */
23335
+ allGeneColorOf(gene, code) {
23336
+ return (this.view.transcriptGeneColors[gene] ||
23337
+ DEFAULT_CATEGORICAL_PALETTE[code % DEFAULT_CATEGORICAL_PALETTE.length]);
23338
+ }
23339
+ toggleAllGenesOpen() {
23340
+ this.allGenesOpen = !this.allGenesOpen;
23341
+ this.rebuildAllRows();
23342
+ }
23343
+ onAllGenesFilter(text) {
23344
+ this.allGenesFilter = text;
23345
+ this.rebuildAllRows();
22884
23346
  }
22885
23347
  toggleGeneGroup(name) {
22886
23348
  const next = new Set(this.collapsedGeneGroups);
@@ -22902,11 +23364,17 @@ class SpatialGeneTreeComponent {
22902
23364
  transcriptHiddenGenes: toggleHidden(this.view.transcriptHiddenGenes, genes, on),
22903
23365
  });
22904
23366
  }
22905
- /** The root eye while every gene is shown: back to the chosen list. */
22906
- onToggleAllGenes() {
22907
- if (!this.dataset?.transcriptBins)
23367
+ /** The root eye while every gene is drawn: hide or show them all (the '±' leaves "All genes"). */
23368
+ onAllGenesShown(on) {
23369
+ this.controls?.setViewState({ transcriptAllGenesHidden: on ? [] : [ALL_GENES] });
23370
+ }
23371
+ /** One gene's eye in the all-genes list. Showing one while all are off shows it alone. */
23372
+ onAllGeneShown(gene, on) {
23373
+ if (!this.canHideAllGenes)
22908
23374
  return;
22909
- this.controls?.setViewState({ transcriptAllGenes: !this.view.transcriptAllGenes });
23375
+ this.controls?.setViewState({
23376
+ transcriptAllGenesHidden: toggleAllGeneHidden(this.view.transcriptAllGenesHidden, gene, on),
23377
+ });
22910
23378
  }
22911
23379
  onGeneColor(gene, hex) {
22912
23380
  this.controls?.setViewState({ transcriptGeneColors: { ...this.view.transcriptGeneColors, [gene]: hex } });
@@ -22936,13 +23404,6 @@ class SpatialGeneTreeComponent {
22936
23404
  onGlyph(gene, glyph) {
22937
23405
  this.controls?.setViewState({ transcriptGlyphs: { ...this.view.transcriptGlyphs, [gene]: glyph } });
22938
23406
  }
22939
- /** A typed hex colour, accepted as `#rrggbb` or `rrggbb`; anything else is ignored. */
22940
- onGeneHex(gene, text) {
22941
- const t = text.trim();
22942
- const rgb = /^#?[0-9a-f]{6}$/i.test(t) ? parseCssColor(t) : null;
22943
- if (rgb)
22944
- this.onGeneColor(gene, rgbToHex(rgb));
22945
- }
22946
23407
  /** Back to the gene's default icon and colour (by its position in the list). */
22947
23408
  resetGeneStyle(gene) {
22948
23409
  const colors = { ...this.view.transcriptGeneColors };
@@ -23014,11 +23475,18 @@ class SpatialGeneTreeComponent {
23014
23475
  });
23015
23476
  }
23016
23477
  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 }); }
23017
- 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)=\"view.transcriptAllGenes ? onToggleAllGenes() : onGenesShown(view.transcriptGenes, !rootShown)\"\n >\n <i\n class=\"pi\"\n [class.pi-eye]=\"view.transcriptAllGenes || rootShown\"\n [class.pi-eye-slash]=\"!view.transcriptAllGenes && !rootShown\"\n ></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 </div>\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 <div class=\"sc-style-pop\" *ngIf=\"styleGene as gene\">\n <div class=\"sc-style-title\">Transcript 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 === styleGlyph\"\n [class.active]=\"opt.value === styleGlyph\"\n [title]=\"opt.label\"\n (click)=\"onGlyph(gene, 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=\"view.transcriptMode !== 'glyphs'\">\n Icons show when the transcript point style is Icons.\n </small>\n <div class=\"sc-style-title\">Transcript Color</div>\n <div class=\"sc-style-color\">\n <label class=\"sc-style-swatch\" [style.background]=\"geneColorOf(gene)\" title=\"Pick a colour\">\n <input\n type=\"color\"\n [value]=\"geneColorOf(gene)\"\n (change)=\"onGeneColor(gene, $any($event.target).value)\"\n />\n </label>\n <input\n pInputText\n class=\"sc-style-hex\"\n [value]=\"geneColorOf(gene)\"\n maxlength=\"7\"\n [attr.aria-label]=\"'Hex colour for ' + gene\"\n (change)=\"onGeneHex(gene, $any($event.target).value)\"\n (keydown.enter)=\"onGeneHex(gene, $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 === geneColorOf(gene).toLowerCase()\"\n [title]=\"c\"\n [attr.aria-label]=\"'Colour ' + c\"\n (click)=\"onGeneColor(gene, 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)=\"resetGeneStyle(gene)\"\n ></p-button>\n </div>\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}: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-gene-footer{text-align:right;font-size:.78rem;padding:.2rem 0;color:var(--text-color-secondary, #6c757d)}.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))}::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)}.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: 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"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23478
+ 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 }); }
23018
23479
  }
23019
23480
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneTreeComponent, decorators: [{
23020
23481
  type: Component,
23021
- args: [{ selector: 'spatial-gene-tree', standalone: true, imports: [CommonModule, SharedModule, ButtonModule, InputTextModule, OverlayPanelModule, TooltipModule], 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)=\"view.transcriptAllGenes ? onToggleAllGenes() : onGenesShown(view.transcriptGenes, !rootShown)\"\n >\n <i\n class=\"pi\"\n [class.pi-eye]=\"view.transcriptAllGenes || rootShown\"\n [class.pi-eye-slash]=\"!view.transcriptAllGenes && !rootShown\"\n ></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 </div>\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 <div class=\"sc-style-pop\" *ngIf=\"styleGene as gene\">\n <div class=\"sc-style-title\">Transcript 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 === styleGlyph\"\n [class.active]=\"opt.value === styleGlyph\"\n [title]=\"opt.label\"\n (click)=\"onGlyph(gene, 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=\"view.transcriptMode !== 'glyphs'\">\n Icons show when the transcript point style is Icons.\n </small>\n <div class=\"sc-style-title\">Transcript Color</div>\n <div class=\"sc-style-color\">\n <label class=\"sc-style-swatch\" [style.background]=\"geneColorOf(gene)\" title=\"Pick a colour\">\n <input\n type=\"color\"\n [value]=\"geneColorOf(gene)\"\n (change)=\"onGeneColor(gene, $any($event.target).value)\"\n />\n </label>\n <input\n pInputText\n class=\"sc-style-hex\"\n [value]=\"geneColorOf(gene)\"\n maxlength=\"7\"\n [attr.aria-label]=\"'Hex colour for ' + gene\"\n (change)=\"onGeneHex(gene, $any($event.target).value)\"\n (keydown.enter)=\"onGeneHex(gene, $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 === geneColorOf(gene).toLowerCase()\"\n [title]=\"c\"\n [attr.aria-label]=\"'Colour ' + c\"\n (click)=\"onGeneColor(gene, 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)=\"resetGeneStyle(gene)\"\n ></p-button>\n </div>\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}: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-gene-footer{text-align:right;font-size:.78rem;padding:.2rem 0;color:var(--text-color-secondary, #6c757d)}.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))}::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)}.sc-gene-count{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.8;padding-left:.5rem}\n"] }]
23482
+ args: [{ selector: 'spatial-gene-tree', standalone: true, imports: [
23483
+ CommonModule,
23484
+ SharedModule,
23485
+ InputTextModule,
23486
+ OverlayPanelModule,
23487
+ TooltipModule,
23488
+ SpatialGeneStyleComponent,
23489
+ ], 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"] }]
23022
23490
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { controls: [{
23023
23491
  type: Input
23024
23492
  }], dataset: [{
@@ -23146,11 +23614,11 @@ class SpatialMarkerGenesFormComponent {
23146
23614
  });
23147
23615
  }
23148
23616
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialMarkerGenesFormComponent, deps: [{ token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
23149
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialMarkerGenesFormComponent, isStandalone: true, selector: "spatial-marker-genes-form", inputs: { controls: "controls", dataset: "dataset", view: "view", open: "open" }, outputs: { openChange: "openChange" }, usesOnChanges: true, ngImport: i0, template: "<!-- Marker genes of each cluster, added as gene groups named after the clusters. -->\n<div class=\"sc-markers\" *ngIf=\"open\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clusters from</span>\n <p-dropdown\n [options]=\"markerColumnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"markerColumn\"\n (ngModelChange)=\"onMarkerColumn($event)\"\n appendTo=\"body\"\n [filter]=\"markerColumnOptions.length > 8\"\n ></p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Per cluster</span>\n <p-dropdown\n [options]=\"markerPerGroupOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [(ngModel)]=\"markerPerGroup\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clusters</span>\n <p-multiSelect\n [options]=\"markerClusterOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [(ngModel)]=\"markerClusters\"\n appendTo=\"body\"\n [filter]=\"true\"\n selectedItemsLabel=\"{0} clusters\"\n [maxSelectedLabels]=\"2\"\n placeholder=\"Pick clusters\"\n ></p-multiSelect>\n </div>\n <small class=\"sc-hint\" *ngIf=\"markerLoading\">\n Finding marker genes\u2026 the first time for a grouping reads the whole expression matrix, which can take a minute.\n </small>\n <small class=\"sc-hint sc-error\" *ngIf=\"markerError\">{{ markerError }}</small>\n <div class=\"sc-row sc-markers-actions\">\n <p-button\n label=\"Add marker genes\"\n icon=\"pi pi-sitemap\"\n size=\"small\"\n [loading]=\"markerLoading\"\n [disabled]=\"!markerClusters.length\"\n (onClick)=\"addMarkerGenes()\"\n ></p-button>\n <p-button label=\"Cancel\" size=\"small\" [text]=\"true\" [disabled]=\"markerLoading\" (onClick)=\"close()\"></p-button>\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}:host{display:contents}.sc-markers{display:flex;flex-direction:column;gap:.4rem;padding:.5rem;margin:.25rem 0 .5rem;border:1px solid var(--surface-border, rgba(0, 0, 0, .15));border-radius:6px}.sc-markers-actions{gap:.5rem}\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: i4.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23617
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialMarkerGenesFormComponent, isStandalone: true, selector: "spatial-marker-genes-form", inputs: { controls: "controls", dataset: "dataset", view: "view", open: "open" }, outputs: { openChange: "openChange" }, usesOnChanges: true, ngImport: i0, template: "<!-- Marker genes of each cluster, added as gene groups named after the clusters. -->\n<div class=\"sc-markers\" *ngIf=\"open\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clusters from</span>\n <p-dropdown\n [options]=\"markerColumnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"markerColumn\"\n (ngModelChange)=\"onMarkerColumn($event)\"\n appendTo=\"body\"\n [filter]=\"markerColumnOptions.length > 8\"\n ></p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Per cluster</span>\n <p-dropdown\n [options]=\"markerPerGroupOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [(ngModel)]=\"markerPerGroup\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clusters</span>\n <p-multiSelect\n [options]=\"markerClusterOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [(ngModel)]=\"markerClusters\"\n appendTo=\"body\"\n [filter]=\"true\"\n selectedItemsLabel=\"{0} clusters\"\n [maxSelectedLabels]=\"2\"\n placeholder=\"Pick clusters\"\n ></p-multiSelect>\n </div>\n <small class=\"sc-hint\" *ngIf=\"markerLoading\">\n Finding marker genes\u2026 the first time for a grouping reads the whole expression matrix, which can take a minute.\n </small>\n <small class=\"sc-hint sc-error\" *ngIf=\"markerError\">{{ markerError }}</small>\n <div class=\"sc-row sc-markers-actions\">\n <p-button\n label=\"Add marker genes\"\n icon=\"pi pi-sitemap\"\n size=\"small\"\n [loading]=\"markerLoading\"\n [disabled]=\"!markerClusters.length\"\n (onClick)=\"addMarkerGenes()\"\n ></p-button>\n <p-button label=\"Cancel\" size=\"small\" [text]=\"true\" [disabled]=\"markerLoading\" (onClick)=\"close()\"></p-button>\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-markers{display:flex;flex-direction:column;gap:.4rem;padding:.5rem;margin:.25rem 0 .5rem;border:1px solid var(--surface-border, rgba(0, 0, 0, .15));border-radius:6px}.sc-markers-actions{gap:.5rem}\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: 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23150
23618
  }
23151
23619
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialMarkerGenesFormComponent, decorators: [{
23152
23620
  type: Component,
23153
- args: [{ selector: 'spatial-marker-genes-form', standalone: true, imports: [CommonModule, FormsModule, ButtonModule, DropdownModule, MultiSelectModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Marker genes of each cluster, added as gene groups named after the clusters. -->\n<div class=\"sc-markers\" *ngIf=\"open\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clusters from</span>\n <p-dropdown\n [options]=\"markerColumnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"markerColumn\"\n (ngModelChange)=\"onMarkerColumn($event)\"\n appendTo=\"body\"\n [filter]=\"markerColumnOptions.length > 8\"\n ></p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Per cluster</span>\n <p-dropdown\n [options]=\"markerPerGroupOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [(ngModel)]=\"markerPerGroup\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clusters</span>\n <p-multiSelect\n [options]=\"markerClusterOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [(ngModel)]=\"markerClusters\"\n appendTo=\"body\"\n [filter]=\"true\"\n selectedItemsLabel=\"{0} clusters\"\n [maxSelectedLabels]=\"2\"\n placeholder=\"Pick clusters\"\n ></p-multiSelect>\n </div>\n <small class=\"sc-hint\" *ngIf=\"markerLoading\">\n Finding marker genes\u2026 the first time for a grouping reads the whole expression matrix, which can take a minute.\n </small>\n <small class=\"sc-hint sc-error\" *ngIf=\"markerError\">{{ markerError }}</small>\n <div class=\"sc-row sc-markers-actions\">\n <p-button\n label=\"Add marker genes\"\n icon=\"pi pi-sitemap\"\n size=\"small\"\n [loading]=\"markerLoading\"\n [disabled]=\"!markerClusters.length\"\n (onClick)=\"addMarkerGenes()\"\n ></p-button>\n <p-button label=\"Cancel\" size=\"small\" [text]=\"true\" [disabled]=\"markerLoading\" (onClick)=\"close()\"></p-button>\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}:host{display:contents}.sc-markers{display:flex;flex-direction:column;gap:.4rem;padding:.5rem;margin:.25rem 0 .5rem;border:1px solid var(--surface-border, rgba(0, 0, 0, .15));border-radius:6px}.sc-markers-actions{gap:.5rem}\n"] }]
23621
+ args: [{ selector: 'spatial-marker-genes-form', standalone: true, imports: [CommonModule, FormsModule, ButtonModule, DropdownModule, MultiSelectModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Marker genes of each cluster, added as gene groups named after the clusters. -->\n<div class=\"sc-markers\" *ngIf=\"open\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clusters from</span>\n <p-dropdown\n [options]=\"markerColumnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"markerColumn\"\n (ngModelChange)=\"onMarkerColumn($event)\"\n appendTo=\"body\"\n [filter]=\"markerColumnOptions.length > 8\"\n ></p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Per cluster</span>\n <p-dropdown\n [options]=\"markerPerGroupOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [(ngModel)]=\"markerPerGroup\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clusters</span>\n <p-multiSelect\n [options]=\"markerClusterOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [(ngModel)]=\"markerClusters\"\n appendTo=\"body\"\n [filter]=\"true\"\n selectedItemsLabel=\"{0} clusters\"\n [maxSelectedLabels]=\"2\"\n placeholder=\"Pick clusters\"\n ></p-multiSelect>\n </div>\n <small class=\"sc-hint\" *ngIf=\"markerLoading\">\n Finding marker genes\u2026 the first time for a grouping reads the whole expression matrix, which can take a minute.\n </small>\n <small class=\"sc-hint sc-error\" *ngIf=\"markerError\">{{ markerError }}</small>\n <div class=\"sc-row sc-markers-actions\">\n <p-button\n label=\"Add marker genes\"\n icon=\"pi pi-sitemap\"\n size=\"small\"\n [loading]=\"markerLoading\"\n [disabled]=\"!markerClusters.length\"\n (onClick)=\"addMarkerGenes()\"\n ></p-button>\n <p-button label=\"Cancel\" size=\"small\" [text]=\"true\" [disabled]=\"markerLoading\" (onClick)=\"close()\"></p-button>\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-markers{display:flex;flex-direction:column;gap:.4rem;padding:.5rem;margin:.25rem 0 .5rem;border:1px solid var(--surface-border, rgba(0, 0, 0, .15));border-radius:6px}.sc-markers-actions{gap:.5rem}\n"] }]
23154
23622
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { controls: [{
23155
23623
  type: Input
23156
23624
  }], dataset: [{
@@ -23163,6 +23631,142 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
23163
23631
  type: Output
23164
23632
  }] } });
23165
23633
 
23634
+ /** Whether the view draws only the observations expressing some genes. */
23635
+ function hasGeneFilter(view) {
23636
+ return !!view.observationGenes?.length;
23637
+ }
23638
+ /** What decides the gene filter, for the layers' cache keys: '' when there is none. */
23639
+ function geneFilterKey(view) {
23640
+ if (!hasGeneFilter(view))
23641
+ return '';
23642
+ return `${view.observationGenesMatch ?? 'any'}:${view.observationGenes.join('\u0001')}`;
23643
+ }
23644
+ /**
23645
+ * 1 for each of `count` observations that expresses the genes — a value above 0 in any of
23646
+ * `vectors`, or in all of them — else 0. A missing value (NaN) is not expressed.
23647
+ */
23648
+ function expressingMask(vectors, count, match) {
23649
+ const mask = new Uint8Array(count);
23650
+ if (!vectors.length)
23651
+ return mask;
23652
+ if (match === 'all') {
23653
+ mask.fill(1);
23654
+ for (const v of vectors) {
23655
+ for (let i = 0; i < count; i++)
23656
+ if (!(v[i] > 0))
23657
+ mask[i] = 0;
23658
+ }
23659
+ }
23660
+ else {
23661
+ for (const v of vectors) {
23662
+ for (let i = 0; i < count; i++)
23663
+ if (v[i] > 0)
23664
+ mask[i] = 1;
23665
+ }
23666
+ }
23667
+ return mask;
23668
+ }
23669
+ /** The last mask built, reused while the same vectors (the port caches them) and rule ask again. */
23670
+ let last = null;
23671
+ /**
23672
+ * The view's gene filter as a per-observation mask (1 = drawn), or null when it has none. Every
23673
+ * layer asks on every rebuild, so the mask of the same vectors is built once.
23674
+ */
23675
+ async function observationGeneMask(view, count, read) {
23676
+ if (!hasGeneFilter(view))
23677
+ return null;
23678
+ const vectors = await Promise.all(view.observationGenes.map((g) => read(g)));
23679
+ const match = view.observationGenesMatch ?? 'any';
23680
+ if (last &&
23681
+ last.match === match &&
23682
+ last.count === count &&
23683
+ last.vectors.length === vectors.length &&
23684
+ last.vectors.every((v, i) => v === vectors[i])) {
23685
+ return last.mask;
23686
+ }
23687
+ const mask = expressingMask(vectors, count, match);
23688
+ last = { vectors, match, count, mask };
23689
+ return mask;
23690
+ }
23691
+ /** The colour a gene of the observation filter is drawn in: the view's own, else the palette's
23692
+ * for its place in the list. */
23693
+ function observationGeneColor(view, gene) {
23694
+ const own = view.observationGeneColors?.[gene];
23695
+ if (own)
23696
+ return own;
23697
+ const at = Math.max(0, view.observationGenes.indexOf(gene));
23698
+ return DEFAULT_CATEGORICAL_PALETTE[at % DEFAULT_CATEGORICAL_PALETTE.length];
23699
+ }
23700
+ /** The icon a gene of the observation filter is drawn as (2D): the view's own, else a circle. */
23701
+ function observationGeneGlyph(view, gene) {
23702
+ return view.observationGeneGlyphs?.[gene] ?? 'circle';
23703
+ }
23704
+ /** Whether the filtered observations are coloured by their gene (rather than by `colorBy`). */
23705
+ function colorsByObservationGene(view) {
23706
+ return hasGeneFilter(view) && view.observationGenesColorBy !== false;
23707
+ }
23708
+ /**
23709
+ * Each observation's gene among `vectors`: the index of the one it expresses most relative to
23710
+ * that gene's own highest value — so a gene expressed at low counts everywhere is not always
23711
+ * outvoted by a highly expressed one — or {@link NO_CATEGORY} where it expresses none.
23712
+ */
23713
+ function dominantGenes(vectors, count) {
23714
+ const out = new Uint16Array(count).fill(NO_CATEGORY);
23715
+ const best = new Float32Array(count);
23716
+ vectors.forEach((v, g) => {
23717
+ let max = 0;
23718
+ for (let i = 0; i < count; i++)
23719
+ if (v[i] > max)
23720
+ max = v[i];
23721
+ if (!(max > 0))
23722
+ return;
23723
+ for (let i = 0; i < count; i++) {
23724
+ const r = v[i] / max;
23725
+ if (r > 0 && r > best[i]) {
23726
+ best[i] = r;
23727
+ out[i] = g;
23728
+ }
23729
+ }
23730
+ });
23731
+ return out;
23732
+ }
23733
+ /** The last codes built, reused while the same vectors ask again (as for the mask). */
23734
+ let lastCodes = null;
23735
+ /**
23736
+ * How the filtered observations are drawn, by gene: each observation's gene (an index into the
23737
+ * view's genes, see {@link dominantGenes}) with the genes' colours and icons. Null when the view
23738
+ * does not colour by them.
23739
+ */
23740
+ async function observationGeneStyle(view, count, read) {
23741
+ if (!colorsByObservationGene(view))
23742
+ return null;
23743
+ const vectors = await Promise.all(view.observationGenes.map((g) => read(g)));
23744
+ let codes;
23745
+ if (lastCodes &&
23746
+ lastCodes.count === count &&
23747
+ lastCodes.vectors.length === vectors.length &&
23748
+ lastCodes.vectors.every((v, i) => v === vectors[i])) {
23749
+ codes = lastCodes.codes;
23750
+ }
23751
+ else {
23752
+ codes = dominantGenes(vectors, count);
23753
+ lastCodes = { vectors, count, codes };
23754
+ }
23755
+ return {
23756
+ codes,
23757
+ colors: view.observationGenes.map((g) => observationGeneColor(view, g)),
23758
+ glyphs: view.observationGenes.map((g) => observationGeneGlyph(view, g)),
23759
+ };
23760
+ }
23761
+ /** What decides how the filtered observations are drawn by gene, for cache keys. */
23762
+ function observationGeneStyleKey(view) {
23763
+ if (!colorsByObservationGene(view))
23764
+ return '';
23765
+ return view.observationGenes
23766
+ .map((g) => `${g}=${observationGeneColor(view, g)}/${observationGeneGlyph(view, g)}`)
23767
+ .join('\u0001');
23768
+ }
23769
+
23166
23770
  /**
23167
23771
  * The Transcripts section, as Xenium Explorer's: the on/off switch, the transcript genes
23168
23772
  * (with "All genes", the '⋮' menu and gene-group import), the
@@ -23199,11 +23803,13 @@ class SpatialTranscriptsPanelComponent {
23199
23803
  this.openChange = new EventEmitter();
23200
23804
  /** Virtual-scrolled only past this many options (see the cells panel's gene dropdown). */
23201
23805
  this.geneVirtualScrollFrom = 200;
23202
- this.transcriptColorOptions = [
23203
- { label: 'Cluster', value: 'cluster' },
23204
- { label: 'Cell type', value: 'cellType' },
23205
- { label: 'Gene', value: 'gene' },
23206
- ];
23806
+ /**
23807
+ * Colourings on offer: "Cell type" only for a dataset with cell types. Rebuilt only when that
23808
+ * changes, for the same reason as {@link transcriptModeOptions}.
23809
+ */
23810
+ this.transcriptColorOptions = [];
23811
+ /** Whether the dataset has a column naming each cell's type (see `cellTypeColumnFor`). */
23812
+ this.hasCellTypes = false;
23207
23813
  this.glyphOptions = GLYPH_OPTIONS;
23208
23814
  this.budgetOptions = [25_000, 50_000, 100_000, 200_000, 400_000].map((n) => ({
23209
23815
  label: n.toLocaleString(),
@@ -23247,6 +23853,17 @@ class SpatialTranscriptsPanelComponent {
23247
23853
  }
23248
23854
  if (changes['view'] || changes['dataset'] || changes['controls'])
23249
23855
  this.geneMenu = this.buildGeneMenu();
23856
+ if (changes['view'] || changes['dataset']) {
23857
+ const hasCellTypes = !!this.dataset && cellTypeColumnFor(this.dataset, this.view) !== null;
23858
+ if (hasCellTypes !== this.hasCellTypes || !this.transcriptColorOptions.length) {
23859
+ this.hasCellTypes = hasCellTypes;
23860
+ this.transcriptColorOptions = [
23861
+ { label: 'Cluster', value: 'cluster' },
23862
+ ...(hasCellTypes ? [{ label: 'Cell type', value: 'cellType' }] : []),
23863
+ { label: 'Gene', value: 'gene' },
23864
+ ];
23865
+ }
23866
+ }
23250
23867
  if (changes['view'] || changes['colormapOptions']) {
23251
23868
  this.selectedDensityColormapNode = colormapNodeFor(this.colormapOptions, this.view.densityColormap);
23252
23869
  }
@@ -23293,7 +23910,10 @@ class SpatialTranscriptsPanelComponent {
23293
23910
  return this.canShowAllGenes && this.view.transcriptAllGenes && this.view.transcriptMode !== 'density';
23294
23911
  }
23295
23912
  onTranscriptAllGenes(on) {
23296
- this.controls?.setViewState({ transcriptAllGenes: on });
23913
+ // "All genes" switched off whole with the tree's root eye stays a property of that one visit;
23914
+ // genes hidden one by one are kept for the next.
23915
+ const wholeOff = everyGeneOff(this.view.transcriptAllGenesHidden);
23916
+ this.controls?.setViewState({ transcriptAllGenes: on, ...(wholeOff ? { transcriptAllGenesHidden: [] } : {}) });
23297
23917
  }
23298
23918
  /** '±': every gene, or back to the chosen list. */
23299
23919
  onToggleAllGenes() {
@@ -23343,12 +23963,25 @@ class SpatialTranscriptsPanelComponent {
23343
23963
  const e = this.estimate;
23344
23964
  return e && e.max > 0 ? Math.min(100, (100 * e.points) / e.max) : 0;
23345
23965
  }
23966
+ /** Whether the Observations gene filter is on, which limits the transcripts drawn. */
23967
+ get observationsFiltered() {
23968
+ return hasGeneFilter(this.view);
23969
+ }
23346
23970
  get estimateOverMax() {
23347
23971
  return !!this.estimate && this.estimate.points > this.estimate.max;
23348
23972
  }
23349
23973
  onTranscriptBudget(n) {
23350
23974
  this.controls?.setViewState({ transcriptBudget: n });
23351
23975
  }
23976
+ /** The colouring in use: "Cell type" without cell types colours by gene (as the markers do). */
23977
+ get transcriptColorShown() {
23978
+ return this.view.transcriptColorBy === 'cellType' && !this.hasCellTypes ? 'gene' : this.view.transcriptColorBy;
23979
+ }
23980
+ /** Genes of "All genes" hidden one by one (not the whole set switched off). */
23981
+ get allGenesSomeHidden() {
23982
+ const off = this.view.transcriptAllGenesHidden;
23983
+ return this.view.transcriptAllGenes && off.length > 0 && !everyGeneOff(off);
23984
+ }
23352
23985
  onTranscriptColorBy(by) {
23353
23986
  this.controls?.setViewState({ transcriptColorBy: by });
23354
23987
  }
@@ -23456,7 +24089,7 @@ class SpatialTranscriptsPanelComponent {
23456
24089
  ];
23457
24090
  }
23458
24091
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialTranscriptsPanelComponent, deps: [{ token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
23459
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialTranscriptsPanelComponent, isStandalone: true, selector: "spatial-transcripts-panel", inputs: { controls: "controls", dataset: "dataset", view: "view", genes: "genes", estimate: "estimate", densityStats: "densityStats", geneCounts: "geneCounts", colormapOptions: "colormapOptions", open: "open" }, outputs: { openChange: "openChange" }, viewQueries: [{ propertyName: "geneGroupFileRef", first: true, predicate: ["geneGroupFile"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<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.transcriptMode !== 'off'\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onTranscriptsOn($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Transcripts</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 <div class=\"sc-row sc-row-picker\">\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <p-multiSelect\n inputId=\"sc-tx-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptGenes\"\n (onChange)=\"onTranscriptGenes($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 [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 for genes to visualize\u2026\"\n [selectedItemsLabel]=\"'{0} genes selected'\"\n [maxSelectedLabels]=\"0\"\n styleClass=\"sc-full\"\n ></p-multiSelect>\n </ng-container>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n *ngIf=\"canShowAllGenes\"\n (click)=\"onToggleAllGenes()\"\n [class.active]=\"view.transcriptAllGenes\"\n [title]=\"view.transcriptAllGenes ? 'Back to the selected genes' : 'Select all genes'\"\n >\n \u00B1\n </button>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"geneMenuRef.toggle($event)\" title=\"More\">\n <i class=\"pi pi-ellipsis-v\"></i>\n </button>\n <p-menu #geneMenuRef [model]=\"geneMenu\" [popup]=\"true\" appendTo=\"body\"></p-menu>\n <input\n #geneGroupFile\n type=\"file\"\n accept=\".csv,.tsv,.txt\"\n hidden\n (change)=\"onImportGeneGroups(geneGroupFile)\"\n />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"geneGroupError\">{{ geneGroupError }}</small>\n <small class=\"sc-hint\" *ngIf=\"allGenesPreparing\">{{ allGenesPreparing }}</small>\n\n <spatial-marker-genes-form\n [controls]=\"controls\"\n [dataset]=\"dataset\"\n [view]=\"view\"\n [open]=\"markersOpen\"\n (openChange)=\"markersOpen = $event\"\n ></spatial-marker-genes-form>\n\n <spatial-gene-tree\n [controls]=\"controls\"\n [dataset]=\"dataset\"\n [view]=\"view\"\n [geneCounts]=\"geneCounts\"\n [geneTotal]=\"geneTotal\"\n ></spatial-gene-tree>\n\n <div class=\"sc-subhead\">Transcript controls</div>\n <span class=\"sc-lbl\">View transcripts as</span>\n <p-selectButton\n [options]=\"transcriptModeOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptMode === 'off' ? null : view.transcriptMode\"\n (ngModelChange)=\"onTranscriptMode($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n\n <ng-container *ngIf=\"estimate\">\n <div class=\"sc-row\">\n <span\n class=\"sc-lbl\"\n pTooltip=\"Transcripts of the visible genes estimated to be in view, against the most markers drawn at once\"\n tooltipPosition=\"top\"\n >Estimated Transcript Points</span\n >\n <button type=\"button\" class=\"sc-small-btn\" (click)=\"editingMax = !editingMax\">Edit Max</button>\n </div>\n <div class=\"sc-meter\">\n <div class=\"sc-meter-fill\" [class.over]=\"estimateOverMax\" [style.width.%]=\"estimatePercent\"></div>\n </div>\n <div class=\"sc-meter-ends\">\n <span>{{ estimate.points | number }} points</span>\n <span>{{ estimate.max | number }} max{{ estimateOverMax ? ' reached' : '' }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"editingMax\">\n <span class=\"sc-lbl\">Max markers</span>\n <p-dropdown\n [options]=\"budgetOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptBudget\"\n (ngModelChange)=\"onTranscriptBudget($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <small class=\"sc-hint\" *ngIf=\"estimateOverMax && view.transcriptMode !== 'density'\">\n Max points reached: transcripts are grouped by area, circles sized by how many they hold. Zoom in or select\n fewer genes to see individual points.\n </small>\n </ng-container>\n\n <ng-container *ngIf=\"view.transcriptMode === 'circles' || view.transcriptMode === 'glyphs'\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Colour by</span>\n <p-selectButton\n [options]=\"transcriptColorOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptColorBy\"\n (ngModelChange)=\"onTranscriptColorBy($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n </div>\n <small\n class=\"sc-hint\"\n *ngIf=\"view.transcriptAllGenes && view.transcriptColorBy === 'gene' && estimateOverMax\"\n >\n Grouped markers mix genes, so they are coloured by cell type until you zoom in to individual transcripts.\n </small>\n <div class=\"sc-row\" *ngIf=\"view.transcriptAllGenes && view.transcriptMode === 'glyphs'\">\n <span class=\"sc-lbl\">Icon</span>\n <p-dropdown\n [options]=\"glyphOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"glyphOf('*', 0)\"\n (ngModelChange)=\"onGlyph('*', $event)\"\n appendTo=\"body\"\n styleClass=\"sc-glyph-pick\"\n >\n <ng-template let-opt pTemplate=\"item\">\n <span class=\"sc-glyph-option\">\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 {{ opt.label }}\n </span>\n </ng-template>\n <ng-template let-opt pTemplate=\"selectedItem\">\n <span class=\"sc-glyph-option\">\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 </span>\n </ng-template>\n </p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Size</span>\n <p-slider\n [ngModel]=\"view.transcriptScale\"\n [min]=\"0.5\"\n [max]=\"3\"\n [step]=\"0.25\"\n (onChange)=\"onTranscriptScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.transcriptScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.transcriptOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onTranscriptOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.transcriptOpacity }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"!showingAllGenes\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.transcriptQuality === 'all'\"\n inputId=\"sc-tx-lowq\"\n (onChange)=\"onTranscriptQuality($event.checked)\"\n ></p-checkbox>\n <label class=\"sc-lbl\" for=\"sc-tx-lowq\">Include low-quality calls</label>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"view.transcriptMode === 'density'\">\n <span class=\"sc-lbl\">Density map opacity</span>\n <div class=\"sc-row sc-row-number\">\n <p-slider\n [ngModel]=\"view.densityOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onDensityOpacity($event.value)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"densityOpacityPercent\"\n (ngModelChange)=\"onDensityOpacityPercent($event)\"\n [min]=\"5\"\n [max]=\"100\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n <span class=\"sc-lbl\" pTooltip=\"Edge of each density bin\" tooltipPosition=\"top\">Density map bin size</span>\n <p-slider\n [ngModel]=\"densityBinIndex\"\n [min]=\"0\"\n [max]=\"3\"\n [step]=\"1\"\n (onChange)=\"onDensityBinIndex($event.value)\"\n styleClass=\"sc-bin-slider\"\n ></p-slider>\n <div class=\"sc-ticks\">\n <span *ngFor=\"let b of densityBins\">{{ b }}\u00D7{{ b }}\u00B5m</span>\n </div>\n <span\n class=\"sc-lbl\"\n pTooltip=\"Transcripts per \u00B5m\u00B2 at which the colour scale starts and saturates\"\n tooltipPosition=\"top\"\n >Density map scale threshold</span\n >\n <div class=\"sc-row sc-row-range\">\n <p-inputNumber\n [ngModel]=\"densityWindow[0]\"\n (ngModelChange)=\"onDensityRangeEnd(0, $event)\"\n mode=\"decimal\"\n [minFractionDigits]=\"3\"\n [maxFractionDigits]=\"3\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n <p-slider\n [ngModel]=\"densityWindow\"\n [range]=\"true\"\n [min]=\"0\"\n [max]=\"densitySliderMax\"\n [step]=\"densityStep\"\n (onChange)=\"onDensityRange($event.values)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"densityWindow[1]\"\n (ngModelChange)=\"onDensityRangeEnd(1, $event)\"\n mode=\"decimal\"\n [minFractionDigits]=\"3\"\n [maxFractionDigits]=\"3\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n <div class=\"sc-row\">\n <small class=\"sc-hint\">{{ view.densityRange ? 'Set by hand.' : 'Derived from the data.' }}</small>\n <button type=\"button\" class=\"sc-small-btn\" *ngIf=\"view.densityRange\" (click)=\"onDensityAuto()\">\n Auto\n </button>\n </div>\n <span class=\"sc-lbl\">Density map bin coloring</span>\n <div class=\"sc-row sc-row-picker\">\n <div class=\"sc-colorbar sc-full\" [style.background]=\"densityColorBarCss\"></div>\n <p-treeSelect\n [options]=\"colormapOptions\"\n [ngModel]=\"selectedDensityColormapNode\"\n selectionMode=\"single\"\n [showClear]=\"true\"\n placeholder=\"Inferno\"\n [scrollHeight]=\"'260px'\"\n appendTo=\"body\"\n styleClass=\"sc-palette\"\n (onNodeSelect)=\"onDensityColormap($event.node)\"\n (onClear)=\"onDensityColormap(null)\"\n >\n <ng-template let-node pTemplate=\"default\">\n <span class=\"sc-cmap-option\">\n <img *ngIf=\"!node.children && node.data?.src\" [src]=\"node.data.src\" class=\"sc-cmap-swatch\" alt=\"\" />\n <span>{{ node.label }}</span>\n </span>\n </ng-template>\n </p-treeSelect>\n </div>\n <div class=\"sc-colorbar-ends\">\n <span>{{ densityWindow[0] | number: '1.0-3' }}</span\n ><span>{{ densityWindow[1] | number: '1.0-3' }}</span>\n </div>\n </ng-container>\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}:host{display:contents}.sc-meter{height:.45rem;border-radius:3px;background:var(--surface-200, #e9ecef);overflow:hidden}.sc-meter-fill{height:100%;background:var(--primary-color, #2196f3)}.sc-meter-fill.over{background:var(--orange-500, #f59e0b)}.sc-meter-ends,.sc-ticks{display:flex;justify-content:space-between;font-size:.72rem;font-weight:600;color:var(--text-color-secondary, #6c757d)}\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.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: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$3.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: i4.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: InputNumberModule }, { kind: "component", type: i7$3.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "ngmodule", type: MenuModule }, { kind: "component", type: i4$1.Menu, selector: "p-menu", inputs: ["model", "popup", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex"], outputs: ["onShow", "onHide", "onBlur", "onFocus"] }, { 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: "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: SelectButtonModule }, { kind: "component", type: i8$1.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: i2$1.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: "ngmodule", type: TreeSelectModule }, { kind: "component", type: i11.TreeSelect, selector: "p-treeSelect", inputs: ["inputId", "scrollHeight", "disabled", "metaKeySelection", "variant", "display", "selectionMode", "tabindex", "ariaLabel", "ariaLabelledBy", "placeholder", "panelClass", "panelStyle", "panelStyleClass", "containerStyle", "containerStyleClass", "labelStyle", "labelStyleClass", "overlayOptions", "emptyMessage", "appendTo", "filter", "filterBy", "filterMode", "filterPlaceholder", "filterLocale", "filterInputAutoFocus", "propagateSelectionDown", "propagateSelectionUp", "showClear", "resetFilterOnHide", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autofocus", "options", "showTransitionOptions", "hideTransitionOptions", "loading"], outputs: ["onNodeExpand", "onNodeCollapse", "onShow", "onHide", "onClear", "onFilter", "onFocus", "onBlur", "onNodeUnselect", "onNodeSelect"] }, { kind: "component", type: SpatialGeneTreeComponent, selector: "spatial-gene-tree", inputs: ["controls", "dataset", "view", "geneCounts", "geneTotal"] }, { kind: "component", type: SpatialMarkerGenesFormComponent, selector: "spatial-marker-genes-form", inputs: ["controls", "dataset", "view", "open"], outputs: ["openChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
24092
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialTranscriptsPanelComponent, isStandalone: true, selector: "spatial-transcripts-panel", inputs: { controls: "controls", dataset: "dataset", view: "view", genes: "genes", estimate: "estimate", densityStats: "densityStats", geneCounts: "geneCounts", colormapOptions: "colormapOptions", open: "open" }, outputs: { openChange: "openChange" }, viewQueries: [{ propertyName: "geneGroupFileRef", first: true, predicate: ["geneGroupFile"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<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.transcriptMode !== 'off'\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onTranscriptsOn($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Transcripts</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 <div class=\"sc-row sc-row-picker\">\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <p-multiSelect\n inputId=\"sc-tx-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptGenes\"\n (onChange)=\"onTranscriptGenes($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 [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 for genes to visualize\u2026\"\n [selectedItemsLabel]=\"'{0} genes selected'\"\n [maxSelectedLabels]=\"0\"\n styleClass=\"sc-full\"\n ></p-multiSelect>\n </ng-container>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n *ngIf=\"canShowAllGenes\"\n (click)=\"onToggleAllGenes()\"\n [class.active]=\"view.transcriptAllGenes\"\n [title]=\"view.transcriptAllGenes ? 'Back to the selected genes' : 'Select all genes'\"\n >\n \u00B1\n </button>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"geneMenuRef.toggle($event)\" title=\"More\">\n <i class=\"pi pi-ellipsis-v\"></i>\n </button>\n <p-menu #geneMenuRef [model]=\"geneMenu\" [popup]=\"true\" appendTo=\"body\"></p-menu>\n <input\n #geneGroupFile\n type=\"file\"\n accept=\".csv,.tsv,.txt\"\n hidden\n (change)=\"onImportGeneGroups(geneGroupFile)\"\n />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"geneGroupError\">{{ geneGroupError }}</small>\n <small class=\"sc-hint\" *ngIf=\"allGenesPreparing\">{{ allGenesPreparing }}</small>\n\n <spatial-marker-genes-form\n [controls]=\"controls\"\n [dataset]=\"dataset\"\n [view]=\"view\"\n [open]=\"markersOpen\"\n (openChange)=\"markersOpen = $event\"\n ></spatial-marker-genes-form>\n\n <spatial-gene-tree\n [controls]=\"controls\"\n [dataset]=\"dataset\"\n [view]=\"view\"\n [geneCounts]=\"geneCounts\"\n [geneTotal]=\"geneTotal\"\n ></spatial-gene-tree>\n\n <div class=\"sc-subhead\">Transcript controls</div>\n <span class=\"sc-lbl\">View transcripts as</span>\n <p-selectButton\n [options]=\"transcriptModeOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptMode === 'off' ? null : view.transcriptMode\"\n (ngModelChange)=\"onTranscriptMode($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n\n <ng-container *ngIf=\"estimate\">\n <div class=\"sc-row\">\n <span\n class=\"sc-lbl\"\n pTooltip=\"Transcripts of the visible genes estimated to be in view, against the most markers drawn at once\"\n tooltipPosition=\"top\"\n >Estimated Transcript Points</span\n >\n <button type=\"button\" class=\"sc-small-btn\" (click)=\"editingMax = !editingMax\">Edit Max</button>\n </div>\n <div class=\"sc-meter\">\n <div class=\"sc-meter-fill\" [class.over]=\"estimateOverMax\" [style.width.%]=\"estimatePercent\"></div>\n </div>\n <div class=\"sc-meter-ends\">\n <span>{{ estimate.points | number }} points</span>\n <span>{{ estimate.max | number }} max{{ estimateOverMax ? ' reached' : '' }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"editingMax\">\n <span class=\"sc-lbl\">Max markers</span>\n <p-dropdown\n [options]=\"budgetOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptBudget\"\n (ngModelChange)=\"onTranscriptBudget($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <small class=\"sc-hint\" *ngIf=\"estimateOverMax && view.transcriptMode !== 'density'\">\n Max points reached: transcripts are grouped by area, circles sized by how many they hold. Zoom in or select\n fewer genes to see individual points.\n </small>\n </ng-container>\n\n <!-- The density raster and the grouped markers drawn from it carry no cells, so they\n cannot follow the Observations gene filter: they are not drawn while it is on. -->\n <small class=\"sc-hint\" *ngIf=\"observationsFiltered && view.transcriptMode === 'density'\">\n Observations are filtered by gene, and the density map sums every cell's transcripts, so it is hidden. Use\n Circles or Icons to see the transcripts of the observations shown.\n </small>\n <small\n class=\"sc-hint\"\n *ngIf=\"observationsFiltered && (view.transcriptMode === 'circles' || view.transcriptMode === 'glyphs')\"\n >\n Observations are filtered by gene: only the transcripts of the observations shown are drawn. Transcripts\n grouped by area when zoomed out carry no cells, so they are hidden; zoom in to see them.\n </small>\n\n <ng-container *ngIf=\"view.transcriptMode === 'circles' || view.transcriptMode === 'glyphs'\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Colour by</span>\n <p-selectButton\n [options]=\"transcriptColorOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"transcriptColorShown\"\n (ngModelChange)=\"onTranscriptColorBy($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n </div>\n <ng-container *ngIf=\"view.transcriptAllGenes && estimateOverMax\">\n <small class=\"sc-hint\" *ngIf=\"hasCellTypes && transcriptColorShown !== 'cellType'\">\n Grouped markers mix genes, so they are coloured by cell type until you zoom in to individual transcripts.\n </small>\n <small class=\"sc-hint\" *ngIf=\"!hasCellTypes\">\n Grouped markers mix genes and this dataset has no cell types, so they are drawn in one colour until you\n zoom in to individual transcripts.\n </small>\n <small class=\"sc-hint\" *ngIf=\"allGenesSomeHidden\">\n Grouped markers hold every gene: hidden genes are left out once you zoom in to individual transcripts.\n </small>\n </ng-container>\n <div class=\"sc-row\" *ngIf=\"view.transcriptAllGenes && view.transcriptMode === 'glyphs'\">\n <span class=\"sc-lbl\">Icon</span>\n <p-dropdown\n [options]=\"glyphOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"glyphOf('*', 0)\"\n (ngModelChange)=\"onGlyph('*', $event)\"\n appendTo=\"body\"\n styleClass=\"sc-glyph-pick\"\n >\n <ng-template let-opt pTemplate=\"item\">\n <span class=\"sc-glyph-option\">\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 {{ opt.label }}\n </span>\n </ng-template>\n <ng-template let-opt pTemplate=\"selectedItem\">\n <span class=\"sc-glyph-option\">\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 </span>\n </ng-template>\n </p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Size</span>\n <p-slider\n [ngModel]=\"view.transcriptScale\"\n [min]=\"0.5\"\n [max]=\"3\"\n [step]=\"0.25\"\n (onChange)=\"onTranscriptScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.transcriptScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.transcriptOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onTranscriptOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.transcriptOpacity }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"!showingAllGenes\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.transcriptQuality === 'all'\"\n inputId=\"sc-tx-lowq\"\n (onChange)=\"onTranscriptQuality($event.checked)\"\n ></p-checkbox>\n <label class=\"sc-lbl\" for=\"sc-tx-lowq\">Include low-quality calls</label>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"view.transcriptMode === 'density'\">\n <span class=\"sc-lbl\">Density map opacity</span>\n <div class=\"sc-row sc-row-number\">\n <p-slider\n [ngModel]=\"view.densityOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onDensityOpacity($event.value)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"densityOpacityPercent\"\n (ngModelChange)=\"onDensityOpacityPercent($event)\"\n [min]=\"5\"\n [max]=\"100\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n <span class=\"sc-lbl\" pTooltip=\"Edge of each density bin\" tooltipPosition=\"top\">Density map bin size</span>\n <p-slider\n [ngModel]=\"densityBinIndex\"\n [min]=\"0\"\n [max]=\"3\"\n [step]=\"1\"\n (onChange)=\"onDensityBinIndex($event.value)\"\n styleClass=\"sc-bin-slider\"\n ></p-slider>\n <div class=\"sc-ticks\">\n <span *ngFor=\"let b of densityBins\">{{ b }}\u00D7{{ b }}\u00B5m</span>\n </div>\n <span\n class=\"sc-lbl\"\n pTooltip=\"Transcripts per \u00B5m\u00B2 at which the colour scale starts and saturates\"\n tooltipPosition=\"top\"\n >Density map scale threshold</span\n >\n <div class=\"sc-row sc-row-range\">\n <p-inputNumber\n [ngModel]=\"densityWindow[0]\"\n (ngModelChange)=\"onDensityRangeEnd(0, $event)\"\n mode=\"decimal\"\n [minFractionDigits]=\"3\"\n [maxFractionDigits]=\"3\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n <p-slider\n [ngModel]=\"densityWindow\"\n [range]=\"true\"\n [min]=\"0\"\n [max]=\"densitySliderMax\"\n [step]=\"densityStep\"\n (onChange)=\"onDensityRange($event.values)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"densityWindow[1]\"\n (ngModelChange)=\"onDensityRangeEnd(1, $event)\"\n mode=\"decimal\"\n [minFractionDigits]=\"3\"\n [maxFractionDigits]=\"3\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n <div class=\"sc-row\">\n <small class=\"sc-hint\">{{ view.densityRange ? 'Set by hand.' : 'Derived from the data.' }}</small>\n <button type=\"button\" class=\"sc-small-btn\" *ngIf=\"view.densityRange\" (click)=\"onDensityAuto()\">\n Auto\n </button>\n </div>\n <span class=\"sc-lbl\">Density map bin coloring</span>\n <div class=\"sc-row sc-row-picker\">\n <div class=\"sc-colorbar sc-full\" [style.background]=\"densityColorBarCss\"></div>\n <p-treeSelect\n [options]=\"colormapOptions\"\n [ngModel]=\"selectedDensityColormapNode\"\n selectionMode=\"single\"\n [showClear]=\"true\"\n placeholder=\"Inferno\"\n [scrollHeight]=\"'260px'\"\n appendTo=\"body\"\n styleClass=\"sc-palette\"\n (onNodeSelect)=\"onDensityColormap($event.node)\"\n (onClear)=\"onDensityColormap(null)\"\n >\n <ng-template let-node pTemplate=\"default\">\n <span class=\"sc-cmap-option\">\n <img *ngIf=\"!node.children && node.data?.src\" [src]=\"node.data.src\" class=\"sc-cmap-swatch\" alt=\"\" />\n <span>{{ node.label }}</span>\n </span>\n </ng-template>\n </p-treeSelect>\n </div>\n <div class=\"sc-colorbar-ends\">\n <span>{{ densityWindow[0] | number: '1.0-3' }}</span\n ><span>{{ densityWindow[1] | number: '1.0-3' }}</span>\n </div>\n </ng-container>\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-meter{height:.45rem;border-radius:3px;background:var(--surface-200, #e9ecef);overflow:hidden}.sc-meter-fill{height:100%;background:var(--primary-color, #2196f3)}.sc-meter-fill.over{background:var(--orange-500, #f59e0b)}.sc-meter-ends,.sc-ticks{display:flex;justify-content:space-between;font-size:.72rem;font-weight:600;color:var(--text-color-secondary, #6c757d)}\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.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: 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: InputNumberModule }, { kind: "component", type: i7$3.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "ngmodule", type: MenuModule }, { kind: "component", type: i4.Menu, selector: "p-menu", inputs: ["model", "popup", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "ariaLabel", "ariaLabelledBy", "id", "tabindex"], outputs: ["onShow", "onHide", "onBlur", "onFocus"] }, { 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: "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: SelectButtonModule }, { kind: "component", type: i8$1.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: i2$1.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: "ngmodule", type: TreeSelectModule }, { kind: "component", type: i11.TreeSelect, selector: "p-treeSelect", inputs: ["inputId", "scrollHeight", "disabled", "metaKeySelection", "variant", "display", "selectionMode", "tabindex", "ariaLabel", "ariaLabelledBy", "placeholder", "panelClass", "panelStyle", "panelStyleClass", "containerStyle", "containerStyleClass", "labelStyle", "labelStyleClass", "overlayOptions", "emptyMessage", "appendTo", "filter", "filterBy", "filterMode", "filterPlaceholder", "filterLocale", "filterInputAutoFocus", "propagateSelectionDown", "propagateSelectionUp", "showClear", "resetFilterOnHide", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "autofocus", "options", "showTransitionOptions", "hideTransitionOptions", "loading"], outputs: ["onNodeExpand", "onNodeCollapse", "onShow", "onHide", "onClear", "onFilter", "onFocus", "onBlur", "onNodeUnselect", "onNodeSelect"] }, { kind: "component", type: SpatialGeneTreeComponent, selector: "spatial-gene-tree", inputs: ["controls", "dataset", "view", "geneCounts", "geneTotal"] }, { kind: "component", type: SpatialMarkerGenesFormComponent, selector: "spatial-marker-genes-form", inputs: ["controls", "dataset", "view", "open"], outputs: ["openChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23460
24093
  }
23461
24094
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialTranscriptsPanelComponent, decorators: [{
23462
24095
  type: Component,
@@ -23475,7 +24108,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
23475
24108
  TreeSelectModule,
23476
24109
  SpatialGeneTreeComponent,
23477
24110
  SpatialMarkerGenesFormComponent,
23478
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<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.transcriptMode !== 'off'\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onTranscriptsOn($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Transcripts</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 <div class=\"sc-row sc-row-picker\">\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <p-multiSelect\n inputId=\"sc-tx-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptGenes\"\n (onChange)=\"onTranscriptGenes($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 [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 for genes to visualize\u2026\"\n [selectedItemsLabel]=\"'{0} genes selected'\"\n [maxSelectedLabels]=\"0\"\n styleClass=\"sc-full\"\n ></p-multiSelect>\n </ng-container>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n *ngIf=\"canShowAllGenes\"\n (click)=\"onToggleAllGenes()\"\n [class.active]=\"view.transcriptAllGenes\"\n [title]=\"view.transcriptAllGenes ? 'Back to the selected genes' : 'Select all genes'\"\n >\n \u00B1\n </button>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"geneMenuRef.toggle($event)\" title=\"More\">\n <i class=\"pi pi-ellipsis-v\"></i>\n </button>\n <p-menu #geneMenuRef [model]=\"geneMenu\" [popup]=\"true\" appendTo=\"body\"></p-menu>\n <input\n #geneGroupFile\n type=\"file\"\n accept=\".csv,.tsv,.txt\"\n hidden\n (change)=\"onImportGeneGroups(geneGroupFile)\"\n />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"geneGroupError\">{{ geneGroupError }}</small>\n <small class=\"sc-hint\" *ngIf=\"allGenesPreparing\">{{ allGenesPreparing }}</small>\n\n <spatial-marker-genes-form\n [controls]=\"controls\"\n [dataset]=\"dataset\"\n [view]=\"view\"\n [open]=\"markersOpen\"\n (openChange)=\"markersOpen = $event\"\n ></spatial-marker-genes-form>\n\n <spatial-gene-tree\n [controls]=\"controls\"\n [dataset]=\"dataset\"\n [view]=\"view\"\n [geneCounts]=\"geneCounts\"\n [geneTotal]=\"geneTotal\"\n ></spatial-gene-tree>\n\n <div class=\"sc-subhead\">Transcript controls</div>\n <span class=\"sc-lbl\">View transcripts as</span>\n <p-selectButton\n [options]=\"transcriptModeOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptMode === 'off' ? null : view.transcriptMode\"\n (ngModelChange)=\"onTranscriptMode($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n\n <ng-container *ngIf=\"estimate\">\n <div class=\"sc-row\">\n <span\n class=\"sc-lbl\"\n pTooltip=\"Transcripts of the visible genes estimated to be in view, against the most markers drawn at once\"\n tooltipPosition=\"top\"\n >Estimated Transcript Points</span\n >\n <button type=\"button\" class=\"sc-small-btn\" (click)=\"editingMax = !editingMax\">Edit Max</button>\n </div>\n <div class=\"sc-meter\">\n <div class=\"sc-meter-fill\" [class.over]=\"estimateOverMax\" [style.width.%]=\"estimatePercent\"></div>\n </div>\n <div class=\"sc-meter-ends\">\n <span>{{ estimate.points | number }} points</span>\n <span>{{ estimate.max | number }} max{{ estimateOverMax ? ' reached' : '' }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"editingMax\">\n <span class=\"sc-lbl\">Max markers</span>\n <p-dropdown\n [options]=\"budgetOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptBudget\"\n (ngModelChange)=\"onTranscriptBudget($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <small class=\"sc-hint\" *ngIf=\"estimateOverMax && view.transcriptMode !== 'density'\">\n Max points reached: transcripts are grouped by area, circles sized by how many they hold. Zoom in or select\n fewer genes to see individual points.\n </small>\n </ng-container>\n\n <ng-container *ngIf=\"view.transcriptMode === 'circles' || view.transcriptMode === 'glyphs'\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Colour by</span>\n <p-selectButton\n [options]=\"transcriptColorOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptColorBy\"\n (ngModelChange)=\"onTranscriptColorBy($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n </div>\n <small\n class=\"sc-hint\"\n *ngIf=\"view.transcriptAllGenes && view.transcriptColorBy === 'gene' && estimateOverMax\"\n >\n Grouped markers mix genes, so they are coloured by cell type until you zoom in to individual transcripts.\n </small>\n <div class=\"sc-row\" *ngIf=\"view.transcriptAllGenes && view.transcriptMode === 'glyphs'\">\n <span class=\"sc-lbl\">Icon</span>\n <p-dropdown\n [options]=\"glyphOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"glyphOf('*', 0)\"\n (ngModelChange)=\"onGlyph('*', $event)\"\n appendTo=\"body\"\n styleClass=\"sc-glyph-pick\"\n >\n <ng-template let-opt pTemplate=\"item\">\n <span class=\"sc-glyph-option\">\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 {{ opt.label }}\n </span>\n </ng-template>\n <ng-template let-opt pTemplate=\"selectedItem\">\n <span class=\"sc-glyph-option\">\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 </span>\n </ng-template>\n </p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Size</span>\n <p-slider\n [ngModel]=\"view.transcriptScale\"\n [min]=\"0.5\"\n [max]=\"3\"\n [step]=\"0.25\"\n (onChange)=\"onTranscriptScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.transcriptScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.transcriptOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onTranscriptOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.transcriptOpacity }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"!showingAllGenes\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.transcriptQuality === 'all'\"\n inputId=\"sc-tx-lowq\"\n (onChange)=\"onTranscriptQuality($event.checked)\"\n ></p-checkbox>\n <label class=\"sc-lbl\" for=\"sc-tx-lowq\">Include low-quality calls</label>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"view.transcriptMode === 'density'\">\n <span class=\"sc-lbl\">Density map opacity</span>\n <div class=\"sc-row sc-row-number\">\n <p-slider\n [ngModel]=\"view.densityOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onDensityOpacity($event.value)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"densityOpacityPercent\"\n (ngModelChange)=\"onDensityOpacityPercent($event)\"\n [min]=\"5\"\n [max]=\"100\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n <span class=\"sc-lbl\" pTooltip=\"Edge of each density bin\" tooltipPosition=\"top\">Density map bin size</span>\n <p-slider\n [ngModel]=\"densityBinIndex\"\n [min]=\"0\"\n [max]=\"3\"\n [step]=\"1\"\n (onChange)=\"onDensityBinIndex($event.value)\"\n styleClass=\"sc-bin-slider\"\n ></p-slider>\n <div class=\"sc-ticks\">\n <span *ngFor=\"let b of densityBins\">{{ b }}\u00D7{{ b }}\u00B5m</span>\n </div>\n <span\n class=\"sc-lbl\"\n pTooltip=\"Transcripts per \u00B5m\u00B2 at which the colour scale starts and saturates\"\n tooltipPosition=\"top\"\n >Density map scale threshold</span\n >\n <div class=\"sc-row sc-row-range\">\n <p-inputNumber\n [ngModel]=\"densityWindow[0]\"\n (ngModelChange)=\"onDensityRangeEnd(0, $event)\"\n mode=\"decimal\"\n [minFractionDigits]=\"3\"\n [maxFractionDigits]=\"3\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n <p-slider\n [ngModel]=\"densityWindow\"\n [range]=\"true\"\n [min]=\"0\"\n [max]=\"densitySliderMax\"\n [step]=\"densityStep\"\n (onChange)=\"onDensityRange($event.values)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"densityWindow[1]\"\n (ngModelChange)=\"onDensityRangeEnd(1, $event)\"\n mode=\"decimal\"\n [minFractionDigits]=\"3\"\n [maxFractionDigits]=\"3\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n <div class=\"sc-row\">\n <small class=\"sc-hint\">{{ view.densityRange ? 'Set by hand.' : 'Derived from the data.' }}</small>\n <button type=\"button\" class=\"sc-small-btn\" *ngIf=\"view.densityRange\" (click)=\"onDensityAuto()\">\n Auto\n </button>\n </div>\n <span class=\"sc-lbl\">Density map bin coloring</span>\n <div class=\"sc-row sc-row-picker\">\n <div class=\"sc-colorbar sc-full\" [style.background]=\"densityColorBarCss\"></div>\n <p-treeSelect\n [options]=\"colormapOptions\"\n [ngModel]=\"selectedDensityColormapNode\"\n selectionMode=\"single\"\n [showClear]=\"true\"\n placeholder=\"Inferno\"\n [scrollHeight]=\"'260px'\"\n appendTo=\"body\"\n styleClass=\"sc-palette\"\n (onNodeSelect)=\"onDensityColormap($event.node)\"\n (onClear)=\"onDensityColormap(null)\"\n >\n <ng-template let-node pTemplate=\"default\">\n <span class=\"sc-cmap-option\">\n <img *ngIf=\"!node.children && node.data?.src\" [src]=\"node.data.src\" class=\"sc-cmap-swatch\" alt=\"\" />\n <span>{{ node.label }}</span>\n </span>\n </ng-template>\n </p-treeSelect>\n </div>\n <div class=\"sc-colorbar-ends\">\n <span>{{ densityWindow[0] | number: '1.0-3' }}</span\n ><span>{{ densityWindow[1] | number: '1.0-3' }}</span>\n </div>\n </ng-container>\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}:host{display:contents}.sc-meter{height:.45rem;border-radius:3px;background:var(--surface-200, #e9ecef);overflow:hidden}.sc-meter-fill{height:100%;background:var(--primary-color, #2196f3)}.sc-meter-fill.over{background:var(--orange-500, #f59e0b)}.sc-meter-ends,.sc-ticks{display:flex;justify-content:space-between;font-size:.72rem;font-weight:600;color:var(--text-color-secondary, #6c757d)}\n"] }]
24111
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<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.transcriptMode !== 'off'\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onTranscriptsOn($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Transcripts</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 <div class=\"sc-row sc-row-picker\">\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <p-multiSelect\n inputId=\"sc-tx-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptGenes\"\n (onChange)=\"onTranscriptGenes($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 [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 for genes to visualize\u2026\"\n [selectedItemsLabel]=\"'{0} genes selected'\"\n [maxSelectedLabels]=\"0\"\n styleClass=\"sc-full\"\n ></p-multiSelect>\n </ng-container>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n *ngIf=\"canShowAllGenes\"\n (click)=\"onToggleAllGenes()\"\n [class.active]=\"view.transcriptAllGenes\"\n [title]=\"view.transcriptAllGenes ? 'Back to the selected genes' : 'Select all genes'\"\n >\n \u00B1\n </button>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"geneMenuRef.toggle($event)\" title=\"More\">\n <i class=\"pi pi-ellipsis-v\"></i>\n </button>\n <p-menu #geneMenuRef [model]=\"geneMenu\" [popup]=\"true\" appendTo=\"body\"></p-menu>\n <input\n #geneGroupFile\n type=\"file\"\n accept=\".csv,.tsv,.txt\"\n hidden\n (change)=\"onImportGeneGroups(geneGroupFile)\"\n />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"geneGroupError\">{{ geneGroupError }}</small>\n <small class=\"sc-hint\" *ngIf=\"allGenesPreparing\">{{ allGenesPreparing }}</small>\n\n <spatial-marker-genes-form\n [controls]=\"controls\"\n [dataset]=\"dataset\"\n [view]=\"view\"\n [open]=\"markersOpen\"\n (openChange)=\"markersOpen = $event\"\n ></spatial-marker-genes-form>\n\n <spatial-gene-tree\n [controls]=\"controls\"\n [dataset]=\"dataset\"\n [view]=\"view\"\n [geneCounts]=\"geneCounts\"\n [geneTotal]=\"geneTotal\"\n ></spatial-gene-tree>\n\n <div class=\"sc-subhead\">Transcript controls</div>\n <span class=\"sc-lbl\">View transcripts as</span>\n <p-selectButton\n [options]=\"transcriptModeOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptMode === 'off' ? null : view.transcriptMode\"\n (ngModelChange)=\"onTranscriptMode($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n\n <ng-container *ngIf=\"estimate\">\n <div class=\"sc-row\">\n <span\n class=\"sc-lbl\"\n pTooltip=\"Transcripts of the visible genes estimated to be in view, against the most markers drawn at once\"\n tooltipPosition=\"top\"\n >Estimated Transcript Points</span\n >\n <button type=\"button\" class=\"sc-small-btn\" (click)=\"editingMax = !editingMax\">Edit Max</button>\n </div>\n <div class=\"sc-meter\">\n <div class=\"sc-meter-fill\" [class.over]=\"estimateOverMax\" [style.width.%]=\"estimatePercent\"></div>\n </div>\n <div class=\"sc-meter-ends\">\n <span>{{ estimate.points | number }} points</span>\n <span>{{ estimate.max | number }} max{{ estimateOverMax ? ' reached' : '' }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"editingMax\">\n <span class=\"sc-lbl\">Max markers</span>\n <p-dropdown\n [options]=\"budgetOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.transcriptBudget\"\n (ngModelChange)=\"onTranscriptBudget($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n </div>\n <small class=\"sc-hint\" *ngIf=\"estimateOverMax && view.transcriptMode !== 'density'\">\n Max points reached: transcripts are grouped by area, circles sized by how many they hold. Zoom in or select\n fewer genes to see individual points.\n </small>\n </ng-container>\n\n <!-- The density raster and the grouped markers drawn from it carry no cells, so they\n cannot follow the Observations gene filter: they are not drawn while it is on. -->\n <small class=\"sc-hint\" *ngIf=\"observationsFiltered && view.transcriptMode === 'density'\">\n Observations are filtered by gene, and the density map sums every cell's transcripts, so it is hidden. Use\n Circles or Icons to see the transcripts of the observations shown.\n </small>\n <small\n class=\"sc-hint\"\n *ngIf=\"observationsFiltered && (view.transcriptMode === 'circles' || view.transcriptMode === 'glyphs')\"\n >\n Observations are filtered by gene: only the transcripts of the observations shown are drawn. Transcripts\n grouped by area when zoomed out carry no cells, so they are hidden; zoom in to see them.\n </small>\n\n <ng-container *ngIf=\"view.transcriptMode === 'circles' || view.transcriptMode === 'glyphs'\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Colour by</span>\n <p-selectButton\n [options]=\"transcriptColorOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"transcriptColorShown\"\n (ngModelChange)=\"onTranscriptColorBy($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n </div>\n <ng-container *ngIf=\"view.transcriptAllGenes && estimateOverMax\">\n <small class=\"sc-hint\" *ngIf=\"hasCellTypes && transcriptColorShown !== 'cellType'\">\n Grouped markers mix genes, so they are coloured by cell type until you zoom in to individual transcripts.\n </small>\n <small class=\"sc-hint\" *ngIf=\"!hasCellTypes\">\n Grouped markers mix genes and this dataset has no cell types, so they are drawn in one colour until you\n zoom in to individual transcripts.\n </small>\n <small class=\"sc-hint\" *ngIf=\"allGenesSomeHidden\">\n Grouped markers hold every gene: hidden genes are left out once you zoom in to individual transcripts.\n </small>\n </ng-container>\n <div class=\"sc-row\" *ngIf=\"view.transcriptAllGenes && view.transcriptMode === 'glyphs'\">\n <span class=\"sc-lbl\">Icon</span>\n <p-dropdown\n [options]=\"glyphOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"glyphOf('*', 0)\"\n (ngModelChange)=\"onGlyph('*', $event)\"\n appendTo=\"body\"\n styleClass=\"sc-glyph-pick\"\n >\n <ng-template let-opt pTemplate=\"item\">\n <span class=\"sc-glyph-option\">\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 {{ opt.label }}\n </span>\n </ng-template>\n <ng-template let-opt pTemplate=\"selectedItem\">\n <span class=\"sc-glyph-option\">\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 </span>\n </ng-template>\n </p-dropdown>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Size</span>\n <p-slider\n [ngModel]=\"view.transcriptScale\"\n [min]=\"0.5\"\n [max]=\"3\"\n [step]=\"0.25\"\n (onChange)=\"onTranscriptScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.transcriptScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.transcriptOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onTranscriptOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.transcriptOpacity }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"!showingAllGenes\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.transcriptQuality === 'all'\"\n inputId=\"sc-tx-lowq\"\n (onChange)=\"onTranscriptQuality($event.checked)\"\n ></p-checkbox>\n <label class=\"sc-lbl\" for=\"sc-tx-lowq\">Include low-quality calls</label>\n </div>\n </ng-container>\n\n <ng-container *ngIf=\"view.transcriptMode === 'density'\">\n <span class=\"sc-lbl\">Density map opacity</span>\n <div class=\"sc-row sc-row-number\">\n <p-slider\n [ngModel]=\"view.densityOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onDensityOpacity($event.value)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"densityOpacityPercent\"\n (ngModelChange)=\"onDensityOpacityPercent($event)\"\n [min]=\"5\"\n [max]=\"100\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n <span class=\"sc-lbl\" pTooltip=\"Edge of each density bin\" tooltipPosition=\"top\">Density map bin size</span>\n <p-slider\n [ngModel]=\"densityBinIndex\"\n [min]=\"0\"\n [max]=\"3\"\n [step]=\"1\"\n (onChange)=\"onDensityBinIndex($event.value)\"\n styleClass=\"sc-bin-slider\"\n ></p-slider>\n <div class=\"sc-ticks\">\n <span *ngFor=\"let b of densityBins\">{{ b }}\u00D7{{ b }}\u00B5m</span>\n </div>\n <span\n class=\"sc-lbl\"\n pTooltip=\"Transcripts per \u00B5m\u00B2 at which the colour scale starts and saturates\"\n tooltipPosition=\"top\"\n >Density map scale threshold</span\n >\n <div class=\"sc-row sc-row-range\">\n <p-inputNumber\n [ngModel]=\"densityWindow[0]\"\n (ngModelChange)=\"onDensityRangeEnd(0, $event)\"\n mode=\"decimal\"\n [minFractionDigits]=\"3\"\n [maxFractionDigits]=\"3\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n <p-slider\n [ngModel]=\"densityWindow\"\n [range]=\"true\"\n [min]=\"0\"\n [max]=\"densitySliderMax\"\n [step]=\"densityStep\"\n (onChange)=\"onDensityRange($event.values)\"\n ></p-slider>\n <p-inputNumber\n [ngModel]=\"densityWindow[1]\"\n (ngModelChange)=\"onDensityRangeEnd(1, $event)\"\n mode=\"decimal\"\n [minFractionDigits]=\"3\"\n [maxFractionDigits]=\"3\"\n inputStyleClass=\"sc-num\"\n ></p-inputNumber>\n </div>\n <div class=\"sc-row\">\n <small class=\"sc-hint\">{{ view.densityRange ? 'Set by hand.' : 'Derived from the data.' }}</small>\n <button type=\"button\" class=\"sc-small-btn\" *ngIf=\"view.densityRange\" (click)=\"onDensityAuto()\">\n Auto\n </button>\n </div>\n <span class=\"sc-lbl\">Density map bin coloring</span>\n <div class=\"sc-row sc-row-picker\">\n <div class=\"sc-colorbar sc-full\" [style.background]=\"densityColorBarCss\"></div>\n <p-treeSelect\n [options]=\"colormapOptions\"\n [ngModel]=\"selectedDensityColormapNode\"\n selectionMode=\"single\"\n [showClear]=\"true\"\n placeholder=\"Inferno\"\n [scrollHeight]=\"'260px'\"\n appendTo=\"body\"\n styleClass=\"sc-palette\"\n (onNodeSelect)=\"onDensityColormap($event.node)\"\n (onClear)=\"onDensityColormap(null)\"\n >\n <ng-template let-node pTemplate=\"default\">\n <span class=\"sc-cmap-option\">\n <img *ngIf=\"!node.children && node.data?.src\" [src]=\"node.data.src\" class=\"sc-cmap-swatch\" alt=\"\" />\n <span>{{ node.label }}</span>\n </span>\n </ng-template>\n </p-treeSelect>\n </div>\n <div class=\"sc-colorbar-ends\">\n <span>{{ densityWindow[0] | number: '1.0-3' }}</span\n ><span>{{ densityWindow[1] | number: '1.0-3' }}</span>\n </div>\n </ng-container>\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-meter{height:.45rem;border-radius:3px;background:var(--surface-200, #e9ecef);overflow:hidden}.sc-meter-fill{height:100%;background:var(--primary-color, #2196f3)}.sc-meter-fill.over{background:var(--orange-500, #f59e0b)}.sc-meter-ends,.sc-ticks{display:flex;justify-content:space-between;font-size:.72rem;font-weight:600;color:var(--text-color-secondary, #6c757d)}\n"] }]
23479
24112
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { controls: [{
23480
24113
  type: Input
23481
24114
  }], dataset: [{
@@ -23540,6 +24173,11 @@ class SpatialObservationsPanelComponent {
23540
24173
  /** A legend row was clicked (see the key). */
23541
24174
  this.categoryClicked = new EventEmitter();
23542
24175
  this.clipOptions = CLIP_OPTIONS;
24176
+ /** How several genes of the observation filter combine. */
24177
+ this.matchOptions = [
24178
+ { label: 'Any gene', value: 'any' },
24179
+ { label: 'All genes', value: 'all' },
24180
+ ];
23543
24181
  /** Virtual-scrolled only past this many options (see the cells panel's gene dropdown). */
23544
24182
  this.geneVirtualScrollFrom = 200;
23545
24183
  /** Colour-by column choices — "None" plus every column the dataset declares. */
@@ -23552,8 +24190,17 @@ class SpatialObservationsPanelComponent {
23552
24190
  * must not sit in a template getter.
23553
24191
  */
23554
24192
  this.sections = null;
24193
+ /** Whether the gene filter could not be applied (an expression read failed); one stream per
24194
+ * `controls`, so the template's async pipe keeps its subscription. */
24195
+ this.geneFilterFailed$ = of(false);
24196
+ // ── a picked gene's icon and colour ─────────────────────────────────────
24197
+ /** The gene the icon and colour picker is open for. */
24198
+ this.styleGene = null;
23555
24199
  }
23556
24200
  ngOnChanges(changes) {
24201
+ if (changes['controls']) {
24202
+ this.geneFilterFailed$ = this.controls?.getGeneFilterFailed$?.() ?? of(false);
24203
+ }
23557
24204
  if (changes['dataset'] || changes['controls']) {
23558
24205
  this.columnOptions = columnOptions(this.dataset);
23559
24206
  // A new dataset almost certainly has different columns; drop stale UI state.
@@ -23586,6 +24233,80 @@ class SpatialObservationsPanelComponent {
23586
24233
  this.selectedColumn = null;
23587
24234
  this.controls?.colorByFeature(name);
23588
24235
  }
24236
+ // ── which observations are drawn ────────────────────────────────────────
24237
+ /** Genes picked for the observation filter; none draws every observation. */
24238
+ onObservationGenes(genes) {
24239
+ this.controls?.setViewState({ observationGenes: genes ?? [] });
24240
+ }
24241
+ onObservationGenesMatch(match) {
24242
+ if (!match)
24243
+ return;
24244
+ this.controls?.setViewState({ observationGenesMatch: match });
24245
+ }
24246
+ /** Ask for the picked genes' expression again (a failed read is not cached). */
24247
+ retryGeneFilter() {
24248
+ this.controls?.setViewState({ observationGenes: [...this.view.observationGenes] });
24249
+ }
24250
+ onObservationGenesColorBy(on) {
24251
+ this.controls?.setViewState({ observationGenesColorBy: on });
24252
+ }
24253
+ removeObservationGene(gene) {
24254
+ this.onObservationGenes(this.view.observationGenes.filter((g) => g !== gene));
24255
+ }
24256
+ isObservationGene(gene) {
24257
+ return this.view.observationGenes.includes(gene);
24258
+ }
24259
+ trackByGene(_i, gene) {
24260
+ return gene;
24261
+ }
24262
+ geneColorOf(gene) {
24263
+ return observationGeneColor(this.view, gene);
24264
+ }
24265
+ geneGlyphOf(gene) {
24266
+ return observationGeneGlyph(this.view, gene);
24267
+ }
24268
+ glyphPointsOf(gene) {
24269
+ return glyphPoints(this.geneGlyphOf(gene));
24270
+ }
24271
+ /** Open the picker on the gene's swatch. Inside the genes dropdown the click must not also
24272
+ * reach the option, which would unpick the gene. */
24273
+ openGeneStyle(event, gene, panel) {
24274
+ event.stopPropagation();
24275
+ if (this.styleGene === gene) {
24276
+ panel.toggle(event);
24277
+ return;
24278
+ }
24279
+ this.styleGene = gene;
24280
+ panel.hide();
24281
+ // Re-open anchored on the swatch that was clicked, once the panel has closed.
24282
+ setTimeout(() => panel.toggle(event));
24283
+ }
24284
+ onGeneColor(gene, hex) {
24285
+ this.controls?.setViewState({ observationGeneColors: { ...this.view.observationGeneColors, [gene]: hex } });
24286
+ }
24287
+ onGeneGlyph(gene, glyph) {
24288
+ this.controls?.setViewState({ observationGeneGlyphs: { ...this.view.observationGeneGlyphs, [gene]: glyph } });
24289
+ }
24290
+ /** Back to the gene's default colour (its place in the list) and icon (a circle). */
24291
+ resetGeneStyle(gene) {
24292
+ const colors = { ...this.view.observationGeneColors };
24293
+ const glyphs = { ...this.view.observationGeneGlyphs };
24294
+ delete colors[gene];
24295
+ delete glyphs[gene];
24296
+ this.controls?.setViewState({ observationGeneColors: colors, observationGeneGlyphs: glyphs });
24297
+ }
24298
+ /** What the filter hides, in words. */
24299
+ get geneFilterHint() {
24300
+ const genes = this.view.observationGenes;
24301
+ const which = genes.length === 1
24302
+ ? genes[0]
24303
+ : this.view.observationGenesMatch === 'all'
24304
+ ? 'all of these genes'
24305
+ : 'any of these genes';
24306
+ return (`Only observations expressing ${which} are drawn; the others are hidden, with their cell outlines and the ` +
24307
+ 'transcripts assigned to them. The transcript density map, and transcripts grouped by area when zoomed ' +
24308
+ 'out, are hidden while this is on: they carry no cells to filter by.');
24309
+ }
23589
24310
  /** Cell outlines on offer, which changes what the circles mean. */
23590
24311
  get hasCells() {
23591
24312
  return !!(this.dataset?.polygonTiles || this.dataset?.polygons);
@@ -23742,7 +24463,7 @@ class SpatialObservationsPanelComponent {
23742
24463
  this.controls?.setViewState({ percentileClip: value });
23743
24464
  }
23744
24465
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialObservationsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
23745
- 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 </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</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}:host{display:contents}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { 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$3.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: i4.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: SliderModule }, { kind: "component", type: i2$1.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
24466
+ 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$1.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: i2$1.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 }); }
23746
24467
  }
23747
24468
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialObservationsPanelComponent, decorators: [{
23748
24469
  type: Component,
@@ -23751,10 +24472,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
23751
24472
  FormsModule,
23752
24473
  CheckboxModule,
23753
24474
  DropdownModule,
24475
+ MultiSelectModule,
24476
+ OverlayPanelModule,
24477
+ SelectButtonModule,
23754
24478
  SliderModule,
23755
24479
  TooltipModule,
23756
24480
  SpatialKeyComponent,
23757
- ], 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 </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</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}:host{display:contents}\n"] }]
24481
+ SpatialGeneStyleComponent,
24482
+ ], 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"] }]
23758
24483
  }], propDecorators: { controls: [{
23759
24484
  type: Input
23760
24485
  }], dataset: [{
@@ -23856,6 +24581,7 @@ class SpatialControlsComponent {
23856
24581
  */
23857
24582
  this.genes = new GenePickerModel(() => this.controls, () => [
23858
24583
  ...(this.view?.transcriptGenes ?? []),
24584
+ ...(this.view?.observationGenes ?? []),
23859
24585
  ...(this.view?.colorBy?.kind === 'feature' ? [this.view.colorBy.name] : []),
23860
24586
  ], (fn) => this.runAndMark(fn));
23861
24587
  /** The key — legend or colour bar — for the active colouring. Kept here, not in
@@ -24121,7 +24847,7 @@ class SpatialControlsComponent {
24121
24847
  return this.key.refresh(this.controls, this.dataset, this.view, this.colormap, this.reverse);
24122
24848
  }
24123
24849
  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 }); }
24124
- 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}: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$3.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 }); }
24850
+ 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 }); }
24125
24851
  }
24126
24852
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialControlsComponent, decorators: [{
24127
24853
  type: Component,
@@ -24136,7 +24862,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
24136
24862
  SpatialTranscriptsPanelComponent,
24137
24863
  SpatialObservationsPanelComponent,
24138
24864
  SpatialChartsComponent,
24139
- ], 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}: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"] }]
24865
+ ], 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"] }]
24140
24866
  }], ctorParameters: () => [{ type: undefined, decorators: [{
24141
24867
  type: Inject,
24142
24868
  args: [VISUALIZER]
@@ -25324,7 +26050,7 @@ class VisualizerComponent {
25324
26050
  this.contributions.attachToolDialogSlot(ref?.nativeElement);
25325
26051
  }
25326
26052
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: VisualizerComponent, deps: [{ token: IMAGE_STATE_PORT }, { token: VISUALIZER }, { token: i1$2.MessageService }, { token: i0.NgZone }, { token: i0.ChangeDetectorRef }, { token: VisualizerStore }, { token: SamToolService }, { token: CellSegmentToolService }, { token: SamPointToolService }, { token: RegionOpsService }, { token: VIZ_CONFIG, optional: true }, { token: TOOLBAR_TOOLS, optional: true }, { token: SPATIAL_DATA_PORT, optional: true }, { token: PLOT_TYPE_CONTRIBUTIONS, optional: true }, { token: i0.Injector, optional: true }, { token: i0.ElementRef, optional: true }, { token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component }); }
25327
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: VisualizerComponent, isStandalone: true, selector: "visualizer", inputs: { toolbarTools: "toolbarTools", testMode: "testMode" }, outputs: { isStackEvent: "isStackEvent", isGrayscaleEvent: "isGrayscaleEvent" }, viewQueries: [{ propertyName: "contextMenu", first: true, predicate: ["cm"], descendants: true }, { propertyName: "plotModePanelSlot", first: true, predicate: ["plotModePanelSlot"], descendants: true }, { propertyName: "inset", first: true, predicate: IntensityInsetComponent, descendants: true, static: true }, { propertyName: "toolDialogSlot", first: true, predicate: ["toolDialogSlot"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- SAM toast: live status + encoder-download progress bar (jit-ui#90). Sticky\n while a segment runs; stays open until the bar reaches 100% and the run ends. -->\n<p-toast [key]=\"samToastKey\" position=\"bottom-right\">\n <ng-template let-message pTemplate=\"message\">\n <div style=\"flex: 1; min-width: 16rem\">\n <div class=\"font-bold mb-1\">{{ message.summary }}</div>\n <div>{{ segmentation.status || message.detail }}</div>\n <p-progressBar\n *ngIf=\"segmentation.downloading\"\n [value]=\"segmentation.progress\"\n styleClass=\"mt-2 sam-progress\"\n ></p-progressBar>\n <p-progressBar\n *ngIf=\"segmentation.busy && !segmentation.downloading\"\n mode=\"indeterminate\"\n styleClass=\"mt-2\"\n [style]=\"{ height: '4px' }\"\n ></p-progressBar>\n </div>\n </ng-template>\n</p-toast>\n\n<!-- Result/error notices from the library itself (segmentation outcome, region\n validation). Rendered here so they do not depend on the host mounting a\n keyless <p-toast> \u2014 without this the messages went nowhere in any host but\n jit-ui, and a failed run looked like a button that did nothing. -->\n<p-toast [key]=\"resultToastKey\" position=\"bottom-right\"></p-toast>\n\n<!-- Shared outlets for notices raised where there is no per-instance key to use:\n the root-provided PlotlyService and the region-editor dialog. Rendered by a\n single visualizer (ownsSharedToasts) so a message from the root service is\n not shown once per live viewer. -->\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizToastKey\"></p-toast>\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizAlertToastKey\" position=\"center\"></p-toast>\n\n<!-- Toolbar dock: docked across the top by default; drag the handle to detach\n it into a floating, movable window (frees the top row for the image). -->\n<div\n class=\"toolbar-dock\"\n [class.floating]=\"toolbarFloating\"\n [style.left.px]=\"toolbarFloating ? toolbarPos.x : null\"\n [style.top.px]=\"toolbarFloating ? toolbarPos.y : null\"\n>\n <div\n class=\"toolbar-drag-handle\"\n [vizFloatingDrag]=\"toolbarDragOrigin\"\n (vizFloatingDragMove)=\"toolbarPos = $event\"\n pTooltip=\"Drag to float the toolbar\"\n tooltipPosition=\"bottom\"\n >\n <i class=\"pi pi-bars\"></i>\n </div>\n <plotting-toolbar\n [imageInfo]=\"imageInfo\"\n [hasSpatialDataset]=\"hasSpatialDataset\"\n [plotTypeOptions]=\"plotTypeMenu\"\n [selectedPlotType]=\"selectedPlotTypeId\"\n [basePlotType]=\"basePlotType\"\n [isoRange]=\"isoRange\"\n [maxIndex]=\"maxIndex\"\n [zIndex]=\"zIndex\"\n [isHeatmap]=\"isHeatmap\"\n [is3dRegions]=\"isSpatial3dMode\"\n [isVolumeView]=\"isVolumeView\"\n [sliceEditor]=\"sliceEditor\"\n [activeDragMode]=\"activeDragMode\"\n [activeSurface3dMode]=\"activeSurface3dMode\"\n [axesVisible]=\"axesVisible\"\n [wireframeActive]=\"wireframeActive\"\n [resolutionScale]=\"resolutionScale\"\n [wandSensitivity]=\"wandSensitivity\"\n [brushSize]=\"brushSize\"\n [samModels]=\"samModels\"\n [samModelId]=\"samModelId\"\n [contributedTools]=\"contributedTools\"\n [dialogTools]=\"dialogTools\"\n [openDialogToolId]=\"openDialogToolId\"\n (toggleDialogTool)=\"toggleDialogTool($event)\"\n [toolModelIds]=\"toolParams.toolModelIds\"\n [vertexEraserRadius]=\"vertexEraserRadius\"\n [canUndo]=\"regionActions.canUndo\"\n [canRedo]=\"regionActions.canRedo\"\n [tools]=\"toolbarTools\"\n [imageSmoothingEnabled]=\"imageSmoothingEnabled\"\n (selectPlotType)=\"onSelectPlotType($event)\"\n (toggleImageSmoothing)=\"onToggleImageSmoothing()\"\n (addProfileLine)=\"addProfileLine()\"\n (isoRangeChange)=\"onIsoRangeChange($event)\"\n (openChannelHistogram)=\"openChannelHistogram()\"\n (openSpatialControls)=\"openSpatialControls()\"\n (openRegionEditor)=\"openRegionEditor()\"\n (toggleSliceEditor)=\"toggleSliceEditor()\"\n (selectStackOption)=\"selectStackOption($event)\"\n (zScrub)=\"onZScrub($event)\"\n (zSlide)=\"onZSlide($event)\"\n (zIndexInput)=\"zIndex = $event; updateZIndex()\"\n (reloadAndPlot)=\"reloadAndPlot()\"\n (downloadImage)=\"downloadImage()\"\n (autoscaleImage)=\"autoscaleImage()\"\n (toggleDragMode)=\"toggleDragMode($event)\"\n (zoomIn)=\"zoomIn()\"\n (zoomOut)=\"zoomOut()\"\n (toggleSurface3dMode)=\"toggleSurface3dMode($event)\"\n (resetSurfaceCamera)=\"resetSurfaceCamera()\"\n (toggleAxes)=\"toggleAxes()\"\n (toggleWireframe)=\"toggleWireframe()\"\n (selectResolution)=\"selectResolution($event)\"\n (deleteRegion)=\"deleteRegion()\"\n (undoRegion)=\"undoRegion()\"\n (redoRegion)=\"redoRegion()\"\n (segmentRegions)=\"segmentRegions()\"\n (segmentCellpose)=\"segmentCellpose()\"\n (samModelChange)=\"onSamModelChange($event)\"\n (runTool)=\"runTool($event)\"\n (toolModelChange)=\"onToolModelChange($event)\"\n (openToolParams)=\"openToolParams($event)\"\n (wandSensitivityChange)=\"onWandSensitivityChange($event)\"\n (brushSizeChange)=\"onBrushSizeChange($event)\"\n (vertexEraserRadiusChange)=\"onVertexEraserRadiusChange($event)\"\n >\n <!-- Host-projected custom toolbar buttons, forwarded into the toolbar. -->\n <ng-content></ng-content>\n </plotting-toolbar>\n <p-button\n *ngIf=\"toolbarFloating\"\n class=\"toolbar-dock-btn\"\n icon=\"pi pi-thumbtack\"\n size=\"small\"\n [text]=\"true\"\n pTooltip=\"Dock to top\"\n tooltipPosition=\"bottom\"\n (onClick)=\"dockToolbar()\"\n >\n </p-button>\n</div>\n\n<p-contextMenu #cm [model]=\"contextMenuItems\" appendTo=\"body\"></p-contextMenu>\n<!-- No `h-full` here: PrimeFlex's height:100% !important made #plot as tall as\n the whole host, so under the docked toolbar its bottom toolbar-height strip\n (incl. the OSD scale bar at bottom-left) was clipped by the host's\n overflow:hidden. The component stylesheet sizes it (.viz-plot { flex: 1 }). -->\n<!-- The plot, and while a volume view is split, the slice editor's pane on its left (its own\n viewer, with every region tool) with a divider to resize the two. The plot div keeps its\n place in the DOM either way: the backends find it by id, and a viewer mounted in it stays. -->\n<div\n class=\"viz-panes\"\n [class.viz-quad]=\"sliceEditorShown\"\n [style.grid-template-columns]=\"sliceEditorShown ? splitX + '% 6px minmax(0, 1fr)' : null\"\n [style.grid-template-rows]=\"sliceEditorShown ? splitY + '% 6px minmax(0, 1fr)' : null\"\n>\n <!-- The slice editor: the volume (the plot) top left, the slice being edited (Z) top right,\n the sections through x and y below, in their axis colours. -->\n <ng-container *ngIf=\"sliceEditorShown\">\n <div class=\"viz-cell viz-cell-z\">\n <div class=\"viz-slice-pane\" [id]=\"sliceDivName\"></div>\n <span class=\"viz-pane-badge viz-pane-badge-z\">Z \u00B7 slice {{ zIndex }}</span>\n </div>\n <div class=\"viz-cell viz-cell-x\">\n <viz-section-pane axis=\"x\" [view]=\"sections.x\" (pick)=\"onSectionPick('x', $event)\"></viz-section-pane>\n </div>\n <div class=\"viz-cell viz-cell-y\">\n <viz-section-pane axis=\"y\" [view]=\"sections.y\" (pick)=\"onSectionPick('y', $event)\"></viz-section-pane>\n </div>\n <div\n class=\"viz-pane-divider viz-pane-divider-v\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize the panes across\"\n [attr.aria-valuenow]=\"splitX\"\n aria-valuemin=\"20\"\n aria-valuemax=\"80\"\n tabindex=\"0\"\n (pointerdown)=\"startPaneResize($event, 'x')\"\n (keydown)=\"nudgePaneSize($event, 'x')\"\n ></div>\n <div\n class=\"viz-pane-divider viz-pane-divider-h\"\n role=\"separator\"\n aria-orientation=\"horizontal\"\n aria-label=\"Resize the panes down\"\n [attr.aria-valuenow]=\"splitY\"\n aria-valuemin=\"20\"\n aria-valuemax=\"80\"\n tabindex=\"0\"\n (pointerdown)=\"startPaneResize($event, 'y')\"\n (keydown)=\"nudgePaneSize($event, 'y')\"\n ></div>\n </ng-container>\n <div class=\"w-full relative viz-plot\" [id]=\"plotDivName\">\n <div *ngIf=\"imgLoading() || stackLoading() || isCaching()\" class=\"loading-overlay\">\n <!-- Determinate stack/volume progress takes precedence: while it's active show ONLY the\n progress bar + \"Loading frames...\", not the generic spinner/\"Loading image...\" on top of\n it (both were rendering at once for napari volume/isosurface loads). -->\n <ng-container *ngIf=\"stackLoading(); else imgLoadingTpl\">\n <p-progressBar class=\"progress\" [value]=\"loadingPercentage()\"></p-progressBar>\n <span class=\"text-loading\">Loading frames...</span>\n </ng-container>\n <ng-template #imgLoadingTpl>\n <!-- Cache-copy progress is shown whenever a cache download is active (isCaching), independent\n of `imgLoading`, so it's never dropped/masked mid-download. -->\n <ng-container *ngIf=\"isCaching(); else spinnerLoading\">\n <!-- Stage-aware message comes from CacheProgressService updating\n imageLoadingMessage$ (\"Caching image...\" during the GCS download,\n then \"Generating preview...\" during per-slice thumbnail gen). Once the\n download hits 100% the remaining work (server preview/tile generation)\n has no determinate progress, so fall through to the spinner template\n rather than parking a full bar under \"Loading image...\". -->\n <p-progressBar class=\"progress\" [value]=\"cacheProgress()\"></p-progressBar>\n <span class=\"text-loading\">{{ loadingMessage() }}</span>\n </ng-container>\n <ng-template #spinnerLoading>\n <div *ngIf=\"imgLoading()\" class=\"spinner\"></div>\n <span *ngIf=\"imgLoading() && !zoom()\" class=\"text-loading\">{{ loadingMessage() }}</span>\n <span *ngIf=\"imgLoading() && zoom()\" class=\"zoom-text-loading\">{{ loadingMessage() }}</span>\n </ng-template>\n </ng-template>\n <p-button\n *ngIf=\"stackLoading() || imgLoading()\"\n class=\"cancel-loading\"\n label=\"Cancel\"\n (onClick)=\"cancelLoading()\"\n >\n </p-button>\n </div>\n <!-- Translucent \"sharpening\" overlay shown after the small-tier preview is\n on screen while the large-tier render is still loading in the\n background. Lets the user see the blurry image underneath but makes\n it clear something is still loading so they don't mistake the\n low-res render for the final image. -->\n <div *ngIf=\"render.sharpening && !imgLoading() && !stackLoading() && !isCaching()\" class=\"sharpening-overlay\">\n <div class=\"spinner\"></div>\n <span class=\"text-loading\">Sharpening preview...</span>\n </div>\n </div>\n</div>\n\n<!-- Floating, draggable intensity-profile inset, shown while any intensity line\n exists. Kept OUTSIDE the plot div and fixed to the viewport, so it floats over\n the whole page rather than being clipped to the canvas. -->\n<viz-intensity-inset\n [divId]=\"intensityInsetDiv\"\n [plotDivName]=\"plotDivName\"\n [zIndex]=\"zIndex\"\n [imageView]=\"isImageView\"\n [imageInfo]=\"imageInfo\"\n></viz-intensity-inset>\n\n<!-- Channels & Histogram: non-modal floating dialog opened from the toolbar. -->\n<channel-histogram [(visible)]=\"showChannelHistogram\"></channel-histogram>\n\n<!-- Spatial-omics controls. Renders its own empty state when no dataset (or no\n SPATIAL_DATA_PORT) is present, so it is always safe to host. -->\n<spatial-controls [(visible)]=\"showSpatialControls\" [is3d]=\"isSpatial3dMode\"></spatial-controls>\n\n<!-- Side panel of the active contributed plot mode (PLOT_TYPE_CONTRIBUTIONS),\n in the same right-hand area as the spatial controls and the Region Editor.\n Exists only while the mode's session is live; there is no close button,\n because leaving the mode is what ends it. A component panel gets\n PLOT_MODE_CONTEXT / PLOT_MODE_SESSION from its injector; a `mount` panel's\n host element is attached to the slot below (see plotModePanelSlot). -->\n<p-dialog\n *ngIf=\"plotModePanel as panel\"\n [header]=\"panel.title\"\n [visible]=\"true\"\n [modal]=\"false\"\n [closable]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n appendTo=\"body\"\n position=\"right\"\n styleClass=\"plot-mode-panel\"\n [style]=\"{ width: '22rem', maxHeight: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\"\n>\n <ng-container *ngIf=\"panel.component\">\n <ng-container *ngComponentOutlet=\"panel.component; injector: panel.injector ?? undefined\"></ng-container>\n </ng-container>\n <div *ngIf=\"panel.host\" #plotModePanelSlot class=\"plot-mode-panel-slot\"></div>\n</p-dialog>\n\n<!-- An open dialog tool (TOOLBAR_TOOLS, kind: 'dialog'). Non-modal so the user can\n keep working on the image, e.g. painting with the brush the tool armed. The\n tool renders its body into a host element attached to the slot below (see\n toolDialogSlot); closing the dialog ends the tool's session. -->\n<p-dialog\n *ngIf=\"toolDialog as dlg\"\n [header]=\"dlg.title\"\n [visible]=\"true\"\n (visibleChange)=\"$event || closeDialogTool()\"\n [modal]=\"false\"\n [closable]=\"true\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n appendTo=\"body\"\n position=\"right\"\n styleClass=\"tool-dialog\"\n [style]=\"{ width: dlg.width, maxHeight: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\"\n>\n <div #toolDialogSlot class=\"tool-dialog-slot\"></div>\n</p-dialog>\n\n<!-- Region Editor: non-modal, draggable, resizable floating dialog opened from\n the toolbar (formerly the right-panel Regions tab). Modeless so regions can\n still be drawn/selected on the plot while it's open; stays linked to this\n plotting instance via the root RegionStore singleton. Opens over the right\n side of the view. -->\n<p-dialog\n header=\"Region Editor\"\n [(visible)]=\"showRegionEditor\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n [maximizable]=\"true\"\n appendTo=\"body\"\n position=\"right\"\n [style]=\"{ width: regionEditorWidth, height: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\"\n>\n <region-editor [active]=\"showRegionEditor\"></region-editor>\n</p-dialog>\n\n<!-- Simplify (Douglas\u2013Peucker): custom px-threshold picker for the selected\n region(s), opened from the right-click \"Simplify \u25B8 Custom\u2026\" action. -->\n<p-dialog\n header=\"Simplify region(s)\"\n [(visible)]=\"displaySimplifyDialog\"\n [modal]=\"true\"\n [style]=\"{ width: '22rem' }\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n>\n <div class=\"flex flex-column gap-2\">\n <span\n >Altitude threshold: <b>{{ regionActions.simplifyThreshold }} px</b></span\n >\n <p-slider [(ngModel)]=\"regionActions.simplifyThreshold\" [min]=\"1\" [max]=\"20\" [step]=\"1\"></p-slider>\n <small>Vertices within this distance of the line between their neighbours are removed.</small>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button label=\"Cancel\" [text]=\"true\" (onClick)=\"displaySimplifyDialog = false\"></p-button>\n <p-button\n label=\"Apply\"\n icon=\"pi pi-check\"\n (onClick)=\"simplifyRegions(regionActions.simplifyThreshold)\"\n ></p-button>\n </ng-template>\n</p-dialog>\n\n<!-- Parameters for whichever contributed tool is open. One dialog for all of\n them: a tool declares its fields (ToolParamSpec) and this renders them, so\n the toolbar can lay every tool out the same way and a plugin needs no\n PrimeNG of its own. Defaults come from the active checkpoint, since they\n encode what it was trained for. -->\n<p-dialog\n *ngIf=\"toolParams.openParams as open\"\n [header]=\"open.tool.label + ' parameters'\"\n [visible]=\"true\"\n (visibleChange)=\"closeToolParams()\"\n [modal]=\"true\"\n [style]=\"{ width: '30rem' }\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n appendTo=\"body\"\n>\n <div class=\"grid formgrid p-fluid\">\n <ng-container *ngFor=\"let f of open.fields\">\n <div class=\"field\" [ngClass]=\"f.spec.type === 'boolean' ? 'col-12' : 'col-6'\">\n <ng-container *ngIf=\"f.number as n\">\n <label class=\"text-sm\" [pTooltip]=\"n.tooltip || ''\">{{ n.label }}</label>\n <p-inputNumber\n [ngModel]=\"open.values[n.id]\"\n (ngModelChange)=\"open.values[n.id] = $event\"\n [min]=\"n.min\"\n [max]=\"n.max\"\n [step]=\"n.step\"\n [minFractionDigits]=\"n.fractionDigits\"\n [showButtons]=\"true\"\n ></p-inputNumber>\n </ng-container>\n\n <ng-container *ngIf=\"f.select as sel\">\n <label class=\"text-sm\" [pTooltip]=\"sel.tooltip || ''\">{{ sel.label }}</label>\n <p-dropdown\n [options]=\"sel.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"open.values[sel.id]\"\n (ngModelChange)=\"open.values[sel.id] = $event\"\n appendTo=\"body\"\n ></p-dropdown>\n </ng-container>\n\n <!-- Checkbox label sits beside the box and may run to a sentence, so\n it does not get the stacked label the other types use. -->\n <div *ngIf=\"f.spec.type === 'boolean'\" class=\"flex align-items-center gap-2\">\n <p-checkbox\n [ngModel]=\"open.values[f.spec.id]\"\n (ngModelChange)=\"open.values[f.spec.id] = $event\"\n [binary]=\"true\"\n [inputId]=\"open.tool.id + '-' + f.spec.id\"\n ></p-checkbox>\n <label [for]=\"open.tool.id + '-' + f.spec.id\" class=\"text-sm mb-0\" [pTooltip]=\"f.spec.tooltip || ''\">{{\n f.spec.label\n }}</label>\n </div>\n </div>\n </ng-container>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button\n label=\"Reset to model defaults\"\n [text]=\"true\"\n size=\"small\"\n (onClick)=\"resetToolParams(open.tool.id)\"\n ></p-button>\n <p-button label=\"Close\" size=\"small\" (onClick)=\"closeToolParams()\"></p-button>\n </ng-template>\n</p-dialog>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden}.viz-panes{flex:1;min-height:0;display:flex}.viz-plot{flex:1 1 0;min-width:0;min-height:0;height:auto!important}.viz-panes.viz-quad{display:grid}.viz-panes.viz-quad>.viz-plot{grid-area:1/1/2/2;min-height:0;overflow:hidden}.viz-cell{position:relative;min-width:0;min-height:0;overflow:hidden}.viz-cell-z{grid-area:1/3/2/4}.viz-cell-x{grid-area:3/1/4/2}.viz-cell-y{grid-area:3/3/4/4}.viz-slice-pane{position:absolute;inset:0}.viz-pane-badge{position:absolute;top:6px;left:6px;z-index:10;padding:1px 8px;border-radius:4px;color:#fff;font:600 12px/1.6 system-ui,sans-serif;pointer-events:none}.viz-pane-badge-z{background:#668cff}.viz-pane-divider{background:var(--surface-border, #cccccc);touch-action:none}.viz-pane-divider:hover,.viz-pane-divider:focus-visible{background:var(--primary-color, #3b82f6);outline:none}.viz-pane-divider-v{grid-area:1/2/4/3;cursor:col-resize}.viz-pane-divider-h{grid-area:2/1/3/4;cursor:row-resize}.toolbar-dock{display:flex;align-items:stretch;width:100%}.toolbar-dock plotting-toolbar{flex:1;min-width:0}.toolbar-dock.floating{position:fixed;z-index:1000;width:auto;max-width:calc(100% - 16px);background:var(--surface-card, #ffffff);border:1px solid var(--surface-border, #cccccc);border-radius:6px;box-shadow:0 4px 16px #0000004d}.toolbar-dock.floating plotting-toolbar{flex:0 1 auto}.toolbar-dock.floating .toolbar-drag-handle{border-radius:6px 0 0 6px}.toolbar-drag-handle{display:flex;align-items:center;justify-content:center;padding:0 .4rem;cursor:move;color:var(--primary-color);background:var(--surface-100, rgba(0, 0, 0, .04));border-right:1px solid var(--surface-border, #cccccc);-webkit-user-select:none;user-select:none}.toolbar-dock-btn{align-self:center}:host ::ng-deep .navigator{margin-bottom:80px!important;margin-right:12px!important}kbd{background:#e8e8e8;border:1px solid #bbb;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.85em}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.progress{position:absolute;width:50%;left:25%;top:55%}.text-loading{position:absolute;margin:0 auto;left:45%;top:60%;font-weight:700}.zoom-text-loading{position:absolute;margin:0 auto;left:35%;top:60%;font-weight:700;display:inline-block;width:350px}.cancel-loading{position:absolute;margin:0 auto;left:46%;top:70%}.spinner{position:absolute;left:45%;top:45%;height:100px;width:100px;margin:0 auto;-webkit-animation:rotation .6s infinite linear;-moz-animation:rotation .6s infinite linear;-o-animation:rotation .6s infinite linear;animation:rotation .6s infinite linear;border-left:6px solid rgba(0,174,239,.15);border-right:6px solid rgba(0,174,239,.15);border-bottom:6px solid rgba(0,174,239,.15);border-top:6px solid rgb(82,107,178);border-radius:100%}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff80;z-index:1000}.sharpening-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff2e;pointer-events:none;z-index:1000}.sharpening-overlay .spinner{height:60px;width:60px;border-width:4px;left:calc(50% - 30px);top:calc(50% - 60px)}.sharpening-overlay .text-loading{position:absolute;left:0;right:0;width:auto;text-align:center;top:calc(50% + 35px);font-size:.9rem;color:#526bb2}@-webkit-keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(359deg)}}@-moz-keyframes rotation{0%{-moz-transform:rotate(0deg)}to{-moz-transform:rotate(359deg)}}@-o-keyframes rotation{0%{-o-transform:rotate(0deg)}to{-o-transform:rotate(359deg)}}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(359deg)}}::ng-deep .context-menu-active>.p-menuitem-content{background-color:var(--primary-color)!important}::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-icon,::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-text{color:#fff!important}::ng-deep .context-menu-active>.p-menuitem-content .zoom-box-off-icon,::ng-deep .context-menu-active>.p-menuitem-content .wand-icon,::ng-deep .context-menu-active>.p-menuitem-content .polyline-icon,::ng-deep .context-menu-active>.p-menuitem-content .polygon-vertices-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-add-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-delete-icon,::ng-deep .context-menu-active>.p-menuitem-content .region-move-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-bezier-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-polygon-icon{filter:brightness(0) invert(1)}::ng-deep .zoom-box-off-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" fill=\"black\" fill-rule=\"evenodd\">%0A <!-- Rectangle outline -->%0A <rect x=\"2\" y=\"2\" width=\"56\" height=\"56\"/>%0A <rect x=\"9\" y=\"9\" width=\"42\" height=\"42\"/>%0A <!-- Magnifying glass ring -->%0A <circle cx=\"70\" cy=\"70\" r=\"23\"/>%0A <circle cx=\"70\" cy=\"70\" r=\"14\"/>%0A <!-- Handle -->%0A <polygon points=\"81,81 97,97 92,100 77,85\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .wand-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<svg%0A viewBox=\"0 0 100 100\"%0A fill=\"black\"%0A fill-rule=\"evenodd\"%0A version=\"1.1\"%0A id=\"svg4\"%0A sodipodi:docname=\"wand.svg\"%0A inkscape:version=\"1.4.3 (0d15f75, 2025-12-25)\"%0A xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"%0A xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"%0A xmlns=\"http://www.w3.org/2000/svg\"%0A xmlns:svg=\"http://www.w3.org/2000/svg\">%0A <defs%0A id=\"defs4\" />%0A <sodipodi:namedview%0A id=\"namedview4\"%0A pagecolor=\"%23ffffff\"%0A bordercolor=\"%23000000\"%0A borderopacity=\"0.25\"%0A inkscape:showpageshadow=\"2\"%0A inkscape:pageopacity=\"0.0\"%0A inkscape:pagecheckerboard=\"0\"%0A inkscape:deskcolor=\"%23d1d1d1\"%0A inkscape:zoom=\"8.74\"%0A inkscape:cx=\"50\"%0A inkscape:cy=\"49.942792\"%0A inkscape:window-width=\"1024\"%0A inkscape:window-height=\"1186\"%0A inkscape:window-x=\"3560\"%0A inkscape:window-y=\"131\"%0A inkscape:window-maximized=\"0\"%0A inkscape:current-layer=\"svg4\" />%0A <!-- Wand handle: thin diagonal stick from lower-left to upper-right -->%0A <polygon%0A points=\"12,92 78,26 74,22 8,88 \"%0A id=\"polygon1\"%0A transform=\"matrix(1.0375192,0.05799445,0.25179496,0.91871864,-17.85351,5.6289793)\" />%0A <!-- 4-pointed star at the wand tip -->%0A <polygon%0A points=\"62,34 76,30 80,6 84,30 98,34 84,38 80,62 76,38 \"%0A id=\"polygon2\"%0A transform=\"matrix(1.3082889,0,0,1.0245178,-38.907958,-1.4056881)\" />%0A <!-- Small sparkle to the upper-left -->%0A <polygon%0A points=\"46,30 36,32 34,42 32,32 22,30 32,28 34,18 36,28 \"%0A id=\"polygon3\"%0A transform=\"translate(-9.610984,-6.7505721)\" />%0A <!-- Small sparkle to the lower-right -->%0A <polygon%0A points=\"62,80 70,82 62,84 60,92 58,84 50,82 58,80 60,72 \"%0A id=\"polygon4\"%0A transform=\"translate(0,-1.6018307)\" />%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .brush-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d=\"M512.5 74.3L291.1 222C262 241.4 243.5 272.9 240.5 307.3C302.8 320.1 351.9 369.2 364.8 431.6C399.3 428.6 430.7 410.1 450.1 381L597.7 159.5C604.4 149.4 608 137.6 608 125.4C608 91.5 580.5 64 546.6 64C534.5 64 522.6 67.6 512.5 74.3zM320 464C320 402.1 269.9 352 208 352C146.1 352 96 402.1 96 464C96 467.9 96.2 471.8 96.6 475.6C98.4 493.1 86.4 512 68.8 512L64 512C46.3 512 32 526.3 32 544C32 561.7 46.3 576 64 576L208 576C269.9 576 320 525.9 320 464z\"/></svg>') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polyline-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\">%0A <polyline points=\"1,14 5,3 10,11 15,2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polygon-vertices-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Polygon (click-to-place) tool icon: a traced polyline with vertex dots. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.4740709,0,0,1.4740709,-6.0268793,-6.8086826)\">%0A <polyline points=\"23.877,16.627 14.657,10.767 5.25,21.551 12.757,29.397\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\"%0A stroke-linejoin=\"round\" stroke-linecap=\"round\"/>%0A <circle cx=\"23.877\" cy=\"16.627\" r=\"1.9\"/>%0A <circle cx=\"14.657\" cy=\"10.767\" r=\"1.9\"/>%0A <circle cx=\"5.25\" cy=\"21.551\" r=\"1.9\"/>%0A <circle cx=\"12.757\" cy=\"29.397\" r=\"1.9\"/>%0A </g>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-add-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Add-vertex tool icon: a region blob with three corner vertices and a plus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 9.65,29.0 H 5.85 v 5.3 H 0.55 v 3.4 H 5.85 v 5.3 H 9.65 V 37.7 H 14.95 V 34.3 H 9.65 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-delete-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Delete-vertex tool icon: a region blob with three corner vertices and a minus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 0.55,34.3 v 3.4 H 14.95 V 34.3 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .region-move-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Region-move tool icon: a region blob with a drag arrow. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <path d=\"m 17.115582,8.9648664 c 1.707597,-3.3276806 4.729468,-4.6824281 7.540287,-5.2348498 3.827703,-0.947008 7.991169,0.1052238 11.051414,3.5118228 2.523022,2.604273 4.518416,6.7079726 4.345738,11.1668036 -0.278204,2.985704 -2.398311,5.011251 -4.470451,5.274308 -2.638142,0.565574 -5.314655,0.09206 -7.943203,-0.341976 C 23.811664,22.446579 19.916808,20.499952 17.153955,16.606696 15.89724,14.462775 15.820494,11.12194 17.115585,8.9648664 Z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.1\" stroke-miterlimit=\"10\"/>%0A <path d=\"m 26.741429,13.966963 6.746922,16.786269 3.229398,-4.060758 3.583857,3.583857 2.761195,-2.761196 -3.583856,-3.571852 4.060758,-3.2354 z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-bezier-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-bezier tool icon: a smooth curve through two anchor points with control handles. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- smooth cubic curve between the two anchors -->%0A <path d=\"M 7,34 C 7,16 37,28 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\"/>%0A <!-- control handles -->%0A <line x1=\"7\" y1=\"34\" x2=\"7\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <line x1=\"37\" y1=\"10\" x2=\"37\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <circle cx=\"7\" cy=\"22\" r=\"2.2\"/>%0A <circle cx=\"37\" cy=\"22\" r=\"2.2\"/>%0A <!-- anchor points -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-polygon-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-polygon tool icon: endpoints joined by straight segments through a vertex. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- straight-segment path through the vertices -->%0A <polyline points=\"7,34 22,12 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A <!-- vertices -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"19\" y=\"9\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}:host ::ng-deep .sam-progress .p-progressbar-value{transition:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { 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: 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: 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$3.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: ContextMenuModule }, { kind: "component", type: i11$1.ContextMenu, selector: "p-contextMenu", inputs: ["model", "triggerEvent", "target", "global", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "id", "ariaLabel", "ariaLabelledBy", "pressDelay"], outputs: ["onShow", "onHide"] }, { 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: "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: DropdownModule }, { kind: "component", type: i4.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: InputNumberModule }, { kind: "component", type: i7$3.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "ngmodule", type: ProgressBarModule }, { kind: "component", type: i7$1.ProgressBar, selector: "p-progressBar", inputs: ["value", "showValue", "styleClass", "style", "unit", "mode", "color"] }, { kind: "ngmodule", type: SliderModule }, { kind: "component", type: i2$1.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "ngmodule", type: ToastModule }, { kind: "component", type: i18.Toast, selector: "p-toast", inputs: ["key", "autoZIndex", "baseZIndex", "life", "style", "styleClass", "position", "preventOpenDuplicates", "preventDuplicates", "showTransformOptions", "hideTransformOptions", "showTransitionOptions", "hideTransitionOptions", "breakpoints"], outputs: ["onClose"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: FloatingDragDirective, selector: "[vizFloatingDrag]", inputs: ["vizFloatingDrag"], outputs: ["vizFloatingDragMove"] }, { kind: "component", type: ToolbarComponent, selector: "plotting-toolbar", inputs: ["imageInfo", "hasSpatialDataset", "plotTypeOptions", "selectedPlotType", "basePlotType", "isoRange", "maxIndex", "zIndex", "isHeatmap", "is3dRegions", "isVolumeView", "sliceEditor", "activeDragMode", "activeSurface3dMode", "axesVisible", "wireframeActive", "resolutionScale", "wandSensitivity", "brushSize", "vertexEraserRadius", "canUndo", "canRedo", "stackOptions", "tools", "imageSmoothingEnabled", "samModels", "samModelId", "contributedTools", "dialogTools", "openDialogToolId", "toolModelIds"], outputs: ["selectPlotType", "addProfileLine", "toggleImageSmoothing", "isoRangeChange", "openChannelHistogram", "openSpatialControls", "openRegionEditor", "toggleSliceEditor", "selectStackOption", "zScrub", "zSlide", "zIndexInput", "reloadAndPlot", "downloadImage", "autoscaleImage", "toggleDragMode", "zoomIn", "zoomOut", "toggleSurface3dMode", "resetSurfaceCamera", "toggleAxes", "toggleWireframe", "selectResolution", "deleteRegion", "undoRegion", "redoRegion", "segmentRegions", "segmentCellpose", "runTool", "toggleDialogTool", "toolModelChange", "openToolParams", "samModelChange", "wandSensitivityChange", "brushSizeChange", "vertexEraserRadiusChange"] }, { kind: "component", type: IntensityInsetComponent, selector: "viz-intensity-inset", inputs: ["divId", "plotDivName", "zIndex", "imageView", "imageInfo"] }, { kind: "component", type: SectionPaneComponent, selector: "viz-section-pane", inputs: ["axis", "view"], outputs: ["pick"] }, { kind: "component", type: ChannelHistogramComponent, selector: "channel-histogram", inputs: ["visible"], outputs: ["visibleChange"] }, { kind: "component", type: SpatialControlsComponent, selector: "spatial-controls", inputs: ["visible", "is3d"], outputs: ["visibleChange"] }, { kind: "component", type: RegionEditorComponent, selector: "region-editor", inputs: ["active"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
26053
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: VisualizerComponent, isStandalone: true, selector: "visualizer", inputs: { toolbarTools: "toolbarTools", testMode: "testMode" }, outputs: { isStackEvent: "isStackEvent", isGrayscaleEvent: "isGrayscaleEvent" }, viewQueries: [{ propertyName: "contextMenu", first: true, predicate: ["cm"], descendants: true }, { propertyName: "plotModePanelSlot", first: true, predicate: ["plotModePanelSlot"], descendants: true }, { propertyName: "inset", first: true, predicate: IntensityInsetComponent, descendants: true, static: true }, { propertyName: "toolDialogSlot", first: true, predicate: ["toolDialogSlot"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- SAM toast: live status + encoder-download progress bar (jit-ui#90). Sticky\n while a segment runs; stays open until the bar reaches 100% and the run ends. -->\n<p-toast [key]=\"samToastKey\" position=\"bottom-right\">\n <ng-template let-message pTemplate=\"message\">\n <div style=\"flex: 1; min-width: 16rem\">\n <div class=\"font-bold mb-1\">{{ message.summary }}</div>\n <div>{{ segmentation.status || message.detail }}</div>\n <p-progressBar\n *ngIf=\"segmentation.downloading\"\n [value]=\"segmentation.progress\"\n styleClass=\"mt-2 sam-progress\"\n ></p-progressBar>\n <p-progressBar\n *ngIf=\"segmentation.busy && !segmentation.downloading\"\n mode=\"indeterminate\"\n styleClass=\"mt-2\"\n [style]=\"{ height: '4px' }\"\n ></p-progressBar>\n </div>\n </ng-template>\n</p-toast>\n\n<!-- Result/error notices from the library itself (segmentation outcome, region\n validation). Rendered here so they do not depend on the host mounting a\n keyless <p-toast> \u2014 without this the messages went nowhere in any host but\n jit-ui, and a failed run looked like a button that did nothing. -->\n<p-toast [key]=\"resultToastKey\" position=\"bottom-right\"></p-toast>\n\n<!-- Shared outlets for notices raised where there is no per-instance key to use:\n the root-provided PlotlyService and the region-editor dialog. Rendered by a\n single visualizer (ownsSharedToasts) so a message from the root service is\n not shown once per live viewer. -->\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizToastKey\"></p-toast>\n<p-toast *ngIf=\"ownsSharedToasts\" [key]=\"vizAlertToastKey\" position=\"center\"></p-toast>\n\n<!-- Toolbar dock: docked across the top by default; drag the handle to detach\n it into a floating, movable window (frees the top row for the image). -->\n<div\n class=\"toolbar-dock\"\n [class.floating]=\"toolbarFloating\"\n [style.left.px]=\"toolbarFloating ? toolbarPos.x : null\"\n [style.top.px]=\"toolbarFloating ? toolbarPos.y : null\"\n>\n <div\n class=\"toolbar-drag-handle\"\n [vizFloatingDrag]=\"toolbarDragOrigin\"\n (vizFloatingDragMove)=\"toolbarPos = $event\"\n pTooltip=\"Drag to float the toolbar\"\n tooltipPosition=\"bottom\"\n >\n <i class=\"pi pi-bars\"></i>\n </div>\n <plotting-toolbar\n [imageInfo]=\"imageInfo\"\n [hasSpatialDataset]=\"hasSpatialDataset\"\n [plotTypeOptions]=\"plotTypeMenu\"\n [selectedPlotType]=\"selectedPlotTypeId\"\n [basePlotType]=\"basePlotType\"\n [isoRange]=\"isoRange\"\n [maxIndex]=\"maxIndex\"\n [zIndex]=\"zIndex\"\n [isHeatmap]=\"isHeatmap\"\n [is3dRegions]=\"isSpatial3dMode\"\n [isVolumeView]=\"isVolumeView\"\n [sliceEditor]=\"sliceEditor\"\n [activeDragMode]=\"activeDragMode\"\n [activeSurface3dMode]=\"activeSurface3dMode\"\n [axesVisible]=\"axesVisible\"\n [wireframeActive]=\"wireframeActive\"\n [resolutionScale]=\"resolutionScale\"\n [wandSensitivity]=\"wandSensitivity\"\n [brushSize]=\"brushSize\"\n [samModels]=\"samModels\"\n [samModelId]=\"samModelId\"\n [contributedTools]=\"contributedTools\"\n [dialogTools]=\"dialogTools\"\n [openDialogToolId]=\"openDialogToolId\"\n (toggleDialogTool)=\"toggleDialogTool($event)\"\n [toolModelIds]=\"toolParams.toolModelIds\"\n [vertexEraserRadius]=\"vertexEraserRadius\"\n [canUndo]=\"regionActions.canUndo\"\n [canRedo]=\"regionActions.canRedo\"\n [tools]=\"toolbarTools\"\n [imageSmoothingEnabled]=\"imageSmoothingEnabled\"\n (selectPlotType)=\"onSelectPlotType($event)\"\n (toggleImageSmoothing)=\"onToggleImageSmoothing()\"\n (addProfileLine)=\"addProfileLine()\"\n (isoRangeChange)=\"onIsoRangeChange($event)\"\n (openChannelHistogram)=\"openChannelHistogram()\"\n (openSpatialControls)=\"openSpatialControls()\"\n (openRegionEditor)=\"openRegionEditor()\"\n (toggleSliceEditor)=\"toggleSliceEditor()\"\n (selectStackOption)=\"selectStackOption($event)\"\n (zScrub)=\"onZScrub($event)\"\n (zSlide)=\"onZSlide($event)\"\n (zIndexInput)=\"zIndex = $event; updateZIndex()\"\n (reloadAndPlot)=\"reloadAndPlot()\"\n (downloadImage)=\"downloadImage()\"\n (autoscaleImage)=\"autoscaleImage()\"\n (toggleDragMode)=\"toggleDragMode($event)\"\n (zoomIn)=\"zoomIn()\"\n (zoomOut)=\"zoomOut()\"\n (toggleSurface3dMode)=\"toggleSurface3dMode($event)\"\n (resetSurfaceCamera)=\"resetSurfaceCamera()\"\n (toggleAxes)=\"toggleAxes()\"\n (toggleWireframe)=\"toggleWireframe()\"\n (selectResolution)=\"selectResolution($event)\"\n (deleteRegion)=\"deleteRegion()\"\n (undoRegion)=\"undoRegion()\"\n (redoRegion)=\"redoRegion()\"\n (segmentRegions)=\"segmentRegions()\"\n (segmentCellpose)=\"segmentCellpose()\"\n (samModelChange)=\"onSamModelChange($event)\"\n (runTool)=\"runTool($event)\"\n (toolModelChange)=\"onToolModelChange($event)\"\n (openToolParams)=\"openToolParams($event)\"\n (wandSensitivityChange)=\"onWandSensitivityChange($event)\"\n (brushSizeChange)=\"onBrushSizeChange($event)\"\n (vertexEraserRadiusChange)=\"onVertexEraserRadiusChange($event)\"\n >\n <!-- Host-projected custom toolbar buttons, forwarded into the toolbar. -->\n <ng-content></ng-content>\n </plotting-toolbar>\n <p-button\n *ngIf=\"toolbarFloating\"\n class=\"toolbar-dock-btn\"\n icon=\"pi pi-thumbtack\"\n size=\"small\"\n [text]=\"true\"\n pTooltip=\"Dock to top\"\n tooltipPosition=\"bottom\"\n (onClick)=\"dockToolbar()\"\n >\n </p-button>\n</div>\n\n<p-contextMenu #cm [model]=\"contextMenuItems\" appendTo=\"body\"></p-contextMenu>\n<!-- No `h-full` here: PrimeFlex's height:100% !important made #plot as tall as\n the whole host, so under the docked toolbar its bottom toolbar-height strip\n (incl. the OSD scale bar at bottom-left) was clipped by the host's\n overflow:hidden. The component stylesheet sizes it (.viz-plot { flex: 1 }). -->\n<!-- The plot, and while a volume view is split, the slice editor's pane on its left (its own\n viewer, with every region tool) with a divider to resize the two. The plot div keeps its\n place in the DOM either way: the backends find it by id, and a viewer mounted in it stays. -->\n<div\n class=\"viz-panes\"\n [class.viz-quad]=\"sliceEditorShown\"\n [style.grid-template-columns]=\"sliceEditorShown ? splitX + '% 6px minmax(0, 1fr)' : null\"\n [style.grid-template-rows]=\"sliceEditorShown ? splitY + '% 6px minmax(0, 1fr)' : null\"\n>\n <!-- The slice editor: the volume (the plot) top left, the slice being edited (Z) top right,\n the sections through x and y below, in their axis colours. -->\n <ng-container *ngIf=\"sliceEditorShown\">\n <div class=\"viz-cell viz-cell-z\">\n <div class=\"viz-slice-pane\" [id]=\"sliceDivName\"></div>\n <span class=\"viz-pane-badge viz-pane-badge-z\">Z \u00B7 slice {{ zIndex }}</span>\n </div>\n <div class=\"viz-cell viz-cell-x\">\n <viz-section-pane axis=\"x\" [view]=\"sections.x\" (pick)=\"onSectionPick('x', $event)\"></viz-section-pane>\n </div>\n <div class=\"viz-cell viz-cell-y\">\n <viz-section-pane axis=\"y\" [view]=\"sections.y\" (pick)=\"onSectionPick('y', $event)\"></viz-section-pane>\n </div>\n <div\n class=\"viz-pane-divider viz-pane-divider-v\"\n role=\"separator\"\n aria-orientation=\"vertical\"\n aria-label=\"Resize the panes across\"\n [attr.aria-valuenow]=\"splitX\"\n aria-valuemin=\"20\"\n aria-valuemax=\"80\"\n tabindex=\"0\"\n (pointerdown)=\"startPaneResize($event, 'x')\"\n (keydown)=\"nudgePaneSize($event, 'x')\"\n ></div>\n <div\n class=\"viz-pane-divider viz-pane-divider-h\"\n role=\"separator\"\n aria-orientation=\"horizontal\"\n aria-label=\"Resize the panes down\"\n [attr.aria-valuenow]=\"splitY\"\n aria-valuemin=\"20\"\n aria-valuemax=\"80\"\n tabindex=\"0\"\n (pointerdown)=\"startPaneResize($event, 'y')\"\n (keydown)=\"nudgePaneSize($event, 'y')\"\n ></div>\n </ng-container>\n <div class=\"w-full relative viz-plot\" [id]=\"plotDivName\">\n <div *ngIf=\"imgLoading() || stackLoading() || isCaching()\" class=\"loading-overlay\">\n <!-- Determinate stack/volume progress takes precedence: while it's active show ONLY the\n progress bar + \"Loading frames...\", not the generic spinner/\"Loading image...\" on top of\n it (both were rendering at once for napari volume/isosurface loads). -->\n <ng-container *ngIf=\"stackLoading(); else imgLoadingTpl\">\n <p-progressBar class=\"progress\" [value]=\"loadingPercentage()\"></p-progressBar>\n <span class=\"text-loading\">Loading frames...</span>\n </ng-container>\n <ng-template #imgLoadingTpl>\n <!-- Cache-copy progress is shown whenever a cache download is active (isCaching), independent\n of `imgLoading`, so it's never dropped/masked mid-download. -->\n <ng-container *ngIf=\"isCaching(); else spinnerLoading\">\n <!-- Stage-aware message comes from CacheProgressService updating\n imageLoadingMessage$ (\"Caching image...\" during the GCS download,\n then \"Generating preview...\" during per-slice thumbnail gen). Once the\n download hits 100% the remaining work (server preview/tile generation)\n has no determinate progress, so fall through to the spinner template\n rather than parking a full bar under \"Loading image...\". -->\n <p-progressBar class=\"progress\" [value]=\"cacheProgress()\"></p-progressBar>\n <span class=\"text-loading\">{{ loadingMessage() }}</span>\n </ng-container>\n <ng-template #spinnerLoading>\n <div *ngIf=\"imgLoading()\" class=\"spinner\"></div>\n <span *ngIf=\"imgLoading() && !zoom()\" class=\"text-loading\">{{ loadingMessage() }}</span>\n <span *ngIf=\"imgLoading() && zoom()\" class=\"zoom-text-loading\">{{ loadingMessage() }}</span>\n </ng-template>\n </ng-template>\n <p-button\n *ngIf=\"stackLoading() || imgLoading()\"\n class=\"cancel-loading\"\n label=\"Cancel\"\n (onClick)=\"cancelLoading()\"\n >\n </p-button>\n </div>\n <!-- Translucent \"sharpening\" overlay shown after the small-tier preview is\n on screen while the large-tier render is still loading in the\n background. Lets the user see the blurry image underneath but makes\n it clear something is still loading so they don't mistake the\n low-res render for the final image. -->\n <div *ngIf=\"render.sharpening && !imgLoading() && !stackLoading() && !isCaching()\" class=\"sharpening-overlay\">\n <div class=\"spinner\"></div>\n <span class=\"text-loading\">Sharpening preview...</span>\n </div>\n </div>\n</div>\n\n<!-- Floating, draggable intensity-profile inset, shown while any intensity line\n exists. Kept OUTSIDE the plot div and fixed to the viewport, so it floats over\n the whole page rather than being clipped to the canvas. -->\n<viz-intensity-inset\n [divId]=\"intensityInsetDiv\"\n [plotDivName]=\"plotDivName\"\n [zIndex]=\"zIndex\"\n [imageView]=\"isImageView\"\n [imageInfo]=\"imageInfo\"\n></viz-intensity-inset>\n\n<!-- Channels & Histogram: non-modal floating dialog opened from the toolbar. -->\n<channel-histogram [(visible)]=\"showChannelHistogram\"></channel-histogram>\n\n<!-- Spatial-omics controls. Renders its own empty state when no dataset (or no\n SPATIAL_DATA_PORT) is present, so it is always safe to host. -->\n<spatial-controls [(visible)]=\"showSpatialControls\" [is3d]=\"isSpatial3dMode\"></spatial-controls>\n\n<!-- Side panel of the active contributed plot mode (PLOT_TYPE_CONTRIBUTIONS),\n in the same right-hand area as the spatial controls and the Region Editor.\n Exists only while the mode's session is live; there is no close button,\n because leaving the mode is what ends it. A component panel gets\n PLOT_MODE_CONTEXT / PLOT_MODE_SESSION from its injector; a `mount` panel's\n host element is attached to the slot below (see plotModePanelSlot). -->\n<p-dialog\n *ngIf=\"plotModePanel as panel\"\n [header]=\"panel.title\"\n [visible]=\"true\"\n [modal]=\"false\"\n [closable]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n appendTo=\"body\"\n position=\"right\"\n styleClass=\"plot-mode-panel\"\n [style]=\"{ width: '22rem', maxHeight: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\"\n>\n <ng-container *ngIf=\"panel.component\">\n <ng-container *ngComponentOutlet=\"panel.component; injector: panel.injector ?? undefined\"></ng-container>\n </ng-container>\n <div *ngIf=\"panel.host\" #plotModePanelSlot class=\"plot-mode-panel-slot\"></div>\n</p-dialog>\n\n<!-- An open dialog tool (TOOLBAR_TOOLS, kind: 'dialog'). Non-modal so the user can\n keep working on the image, e.g. painting with the brush the tool armed. The\n tool renders its body into a host element attached to the slot below (see\n toolDialogSlot); closing the dialog ends the tool's session. -->\n<p-dialog\n *ngIf=\"toolDialog as dlg\"\n [header]=\"dlg.title\"\n [visible]=\"true\"\n (visibleChange)=\"$event || closeDialogTool()\"\n [modal]=\"false\"\n [closable]=\"true\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n appendTo=\"body\"\n position=\"right\"\n styleClass=\"tool-dialog\"\n [style]=\"{ width: dlg.width, maxHeight: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\"\n>\n <div #toolDialogSlot class=\"tool-dialog-slot\"></div>\n</p-dialog>\n\n<!-- Region Editor: non-modal, draggable, resizable floating dialog opened from\n the toolbar (formerly the right-panel Regions tab). Modeless so regions can\n still be drawn/selected on the plot while it's open; stays linked to this\n plotting instance via the root RegionStore singleton. Opens over the right\n side of the view. -->\n<p-dialog\n header=\"Region Editor\"\n [(visible)]=\"showRegionEditor\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n [maximizable]=\"true\"\n appendTo=\"body\"\n position=\"right\"\n [style]=\"{ width: regionEditorWidth, height: '85vh' }\"\n [contentStyle]=\"{ overflow: 'auto' }\"\n>\n <region-editor [active]=\"showRegionEditor\"></region-editor>\n</p-dialog>\n\n<!-- Simplify (Douglas\u2013Peucker): custom px-threshold picker for the selected\n region(s), opened from the right-click \"Simplify \u25B8 Custom\u2026\" action. -->\n<p-dialog\n header=\"Simplify region(s)\"\n [(visible)]=\"displaySimplifyDialog\"\n [modal]=\"true\"\n [style]=\"{ width: '22rem' }\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n>\n <div class=\"flex flex-column gap-2\">\n <span\n >Altitude threshold: <b>{{ regionActions.simplifyThreshold }} px</b></span\n >\n <p-slider [(ngModel)]=\"regionActions.simplifyThreshold\" [min]=\"1\" [max]=\"20\" [step]=\"1\"></p-slider>\n <small>Vertices within this distance of the line between their neighbours are removed.</small>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button label=\"Cancel\" [text]=\"true\" (onClick)=\"displaySimplifyDialog = false\"></p-button>\n <p-button\n label=\"Apply\"\n icon=\"pi pi-check\"\n (onClick)=\"simplifyRegions(regionActions.simplifyThreshold)\"\n ></p-button>\n </ng-template>\n</p-dialog>\n\n<!-- Parameters for whichever contributed tool is open. One dialog for all of\n them: a tool declares its fields (ToolParamSpec) and this renders them, so\n the toolbar can lay every tool out the same way and a plugin needs no\n PrimeNG of its own. Defaults come from the active checkpoint, since they\n encode what it was trained for. -->\n<p-dialog\n *ngIf=\"toolParams.openParams as open\"\n [header]=\"open.tool.label + ' parameters'\"\n [visible]=\"true\"\n (visibleChange)=\"closeToolParams()\"\n [modal]=\"true\"\n [style]=\"{ width: '30rem' }\"\n [draggable]=\"false\"\n [resizable]=\"false\"\n appendTo=\"body\"\n>\n <div class=\"grid formgrid p-fluid\">\n <ng-container *ngFor=\"let f of open.fields\">\n <div class=\"field\" [ngClass]=\"f.spec.type === 'boolean' ? 'col-12' : 'col-6'\">\n <ng-container *ngIf=\"f.number as n\">\n <label class=\"text-sm\" [pTooltip]=\"n.tooltip || ''\">{{ n.label }}</label>\n <p-inputNumber\n [ngModel]=\"open.values[n.id]\"\n (ngModelChange)=\"open.values[n.id] = $event\"\n [min]=\"n.min\"\n [max]=\"n.max\"\n [step]=\"n.step\"\n [minFractionDigits]=\"n.fractionDigits\"\n [showButtons]=\"true\"\n ></p-inputNumber>\n </ng-container>\n\n <ng-container *ngIf=\"f.select as sel\">\n <label class=\"text-sm\" [pTooltip]=\"sel.tooltip || ''\">{{ sel.label }}</label>\n <p-dropdown\n [options]=\"sel.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"open.values[sel.id]\"\n (ngModelChange)=\"open.values[sel.id] = $event\"\n appendTo=\"body\"\n ></p-dropdown>\n </ng-container>\n\n <!-- Checkbox label sits beside the box and may run to a sentence, so\n it does not get the stacked label the other types use. -->\n <div *ngIf=\"f.spec.type === 'boolean'\" class=\"flex align-items-center gap-2\">\n <p-checkbox\n [ngModel]=\"open.values[f.spec.id]\"\n (ngModelChange)=\"open.values[f.spec.id] = $event\"\n [binary]=\"true\"\n [inputId]=\"open.tool.id + '-' + f.spec.id\"\n ></p-checkbox>\n <label [for]=\"open.tool.id + '-' + f.spec.id\" class=\"text-sm mb-0\" [pTooltip]=\"f.spec.tooltip || ''\">{{\n f.spec.label\n }}</label>\n </div>\n </div>\n </ng-container>\n </div>\n <ng-template pTemplate=\"footer\">\n <p-button\n label=\"Reset to model defaults\"\n [text]=\"true\"\n size=\"small\"\n (onClick)=\"resetToolParams(open.tool.id)\"\n ></p-button>\n <p-button label=\"Close\" size=\"small\" (onClick)=\"closeToolParams()\"></p-button>\n </ng-template>\n</p-dialog>\n", styles: [":host{display:flex;flex-direction:column;height:100%;overflow:hidden}.viz-panes{flex:1;min-height:0;display:flex}.viz-plot{flex:1 1 0;min-width:0;min-height:0;height:auto!important}.viz-panes.viz-quad{display:grid}.viz-panes.viz-quad>.viz-plot{grid-area:1/1/2/2;min-height:0;overflow:hidden}.viz-cell{position:relative;min-width:0;min-height:0;overflow:hidden}.viz-cell-z{grid-area:1/3/2/4}.viz-cell-x{grid-area:3/1/4/2}.viz-cell-y{grid-area:3/3/4/4}.viz-slice-pane{position:absolute;inset:0}.viz-pane-badge{position:absolute;top:6px;left:6px;z-index:10;padding:1px 8px;border-radius:4px;color:#fff;font:600 12px/1.6 system-ui,sans-serif;pointer-events:none}.viz-pane-badge-z{background:#668cff}.viz-pane-divider{background:var(--surface-border, #cccccc);touch-action:none}.viz-pane-divider:hover,.viz-pane-divider:focus-visible{background:var(--primary-color, #3b82f6);outline:none}.viz-pane-divider-v{grid-area:1/2/4/3;cursor:col-resize}.viz-pane-divider-h{grid-area:2/1/3/4;cursor:row-resize}.toolbar-dock{display:flex;align-items:stretch;width:100%}.toolbar-dock plotting-toolbar{flex:1;min-width:0}.toolbar-dock.floating{position:fixed;z-index:1000;width:auto;max-width:calc(100% - 16px);background:var(--surface-card, #ffffff);border:1px solid var(--surface-border, #cccccc);border-radius:6px;box-shadow:0 4px 16px #0000004d}.toolbar-dock.floating plotting-toolbar{flex:0 1 auto}.toolbar-dock.floating .toolbar-drag-handle{border-radius:6px 0 0 6px}.toolbar-drag-handle{display:flex;align-items:center;justify-content:center;padding:0 .4rem;cursor:move;color:var(--primary-color);background:var(--surface-100, rgba(0, 0, 0, .04));border-right:1px solid var(--surface-border, #cccccc);-webkit-user-select:none;user-select:none}.toolbar-dock-btn{align-self:center}:host ::ng-deep .navigator{margin-bottom:80px!important;margin-right:12px!important}kbd{background:#e8e8e8;border:1px solid #bbb;border-radius:3px;padding:1px 5px;font-family:monospace;font-size:.85em}.toolbar-separator{border-left:1px solid var(--surface-border);height:1.5rem;margin:0 .25rem;align-self:center}.progress{position:absolute;width:50%;left:25%;top:55%}.text-loading{position:absolute;margin:0 auto;left:45%;top:60%;font-weight:700}.zoom-text-loading{position:absolute;margin:0 auto;left:35%;top:60%;font-weight:700;display:inline-block;width:350px}.cancel-loading{position:absolute;margin:0 auto;left:46%;top:70%}.spinner{position:absolute;left:45%;top:45%;height:100px;width:100px;margin:0 auto;-webkit-animation:rotation .6s infinite linear;-moz-animation:rotation .6s infinite linear;-o-animation:rotation .6s infinite linear;animation:rotation .6s infinite linear;border-left:6px solid rgba(0,174,239,.15);border-right:6px solid rgba(0,174,239,.15);border-bottom:6px solid rgba(0,174,239,.15);border-top:6px solid rgb(82,107,178);border-radius:100%}.loading-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff80;z-index:1000}.sharpening-overlay{position:absolute;top:0;left:0;width:100%;height:100%;background-color:#ffffff2e;pointer-events:none;z-index:1000}.sharpening-overlay .spinner{height:60px;width:60px;border-width:4px;left:calc(50% - 30px);top:calc(50% - 60px)}.sharpening-overlay .text-loading{position:absolute;left:0;right:0;width:auto;text-align:center;top:calc(50% + 35px);font-size:.9rem;color:#526bb2}@-webkit-keyframes rotation{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(359deg)}}@-moz-keyframes rotation{0%{-moz-transform:rotate(0deg)}to{-moz-transform:rotate(359deg)}}@-o-keyframes rotation{0%{-o-transform:rotate(0deg)}to{-o-transform:rotate(359deg)}}@keyframes rotation{0%{transform:rotate(0)}to{transform:rotate(359deg)}}::ng-deep .context-menu-active>.p-menuitem-content{background-color:var(--primary-color)!important}::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-icon,::ng-deep .context-menu-active>.p-menuitem-content .p-menuitem-text{color:#fff!important}::ng-deep .context-menu-active>.p-menuitem-content .zoom-box-off-icon,::ng-deep .context-menu-active>.p-menuitem-content .wand-icon,::ng-deep .context-menu-active>.p-menuitem-content .polyline-icon,::ng-deep .context-menu-active>.p-menuitem-content .polygon-vertices-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-add-icon,::ng-deep .context-menu-active>.p-menuitem-content .vertex-delete-icon,::ng-deep .context-menu-active>.p-menuitem-content .region-move-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-bezier-icon,::ng-deep .context-menu-active>.p-menuitem-content .to-polygon-icon{filter:brightness(0) invert(1)}::ng-deep .zoom-box-off-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" fill=\"black\" fill-rule=\"evenodd\">%0A <!-- Rectangle outline -->%0A <rect x=\"2\" y=\"2\" width=\"56\" height=\"56\"/>%0A <rect x=\"9\" y=\"9\" width=\"42\" height=\"42\"/>%0A <!-- Magnifying glass ring -->%0A <circle cx=\"70\" cy=\"70\" r=\"23\"/>%0A <circle cx=\"70\" cy=\"70\" r=\"14\"/>%0A <!-- Handle -->%0A <polygon points=\"81,81 97,97 92,100 77,85\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .wand-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<svg%0A viewBox=\"0 0 100 100\"%0A fill=\"black\"%0A fill-rule=\"evenodd\"%0A version=\"1.1\"%0A id=\"svg4\"%0A sodipodi:docname=\"wand.svg\"%0A inkscape:version=\"1.4.3 (0d15f75, 2025-12-25)\"%0A xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"%0A xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"%0A xmlns=\"http://www.w3.org/2000/svg\"%0A xmlns:svg=\"http://www.w3.org/2000/svg\">%0A <defs%0A id=\"defs4\" />%0A <sodipodi:namedview%0A id=\"namedview4\"%0A pagecolor=\"%23ffffff\"%0A bordercolor=\"%23000000\"%0A borderopacity=\"0.25\"%0A inkscape:showpageshadow=\"2\"%0A inkscape:pageopacity=\"0.0\"%0A inkscape:pagecheckerboard=\"0\"%0A inkscape:deskcolor=\"%23d1d1d1\"%0A inkscape:zoom=\"8.74\"%0A inkscape:cx=\"50\"%0A inkscape:cy=\"49.942792\"%0A inkscape:window-width=\"1024\"%0A inkscape:window-height=\"1186\"%0A inkscape:window-x=\"3560\"%0A inkscape:window-y=\"131\"%0A inkscape:window-maximized=\"0\"%0A inkscape:current-layer=\"svg4\" />%0A <!-- Wand handle: thin diagonal stick from lower-left to upper-right -->%0A <polygon%0A points=\"12,92 78,26 74,22 8,88 \"%0A id=\"polygon1\"%0A transform=\"matrix(1.0375192,0.05799445,0.25179496,0.91871864,-17.85351,5.6289793)\" />%0A <!-- 4-pointed star at the wand tip -->%0A <polygon%0A points=\"62,34 76,30 80,6 84,30 98,34 84,38 80,62 76,38 \"%0A id=\"polygon2\"%0A transform=\"matrix(1.3082889,0,0,1.0245178,-38.907958,-1.4056881)\" />%0A <!-- Small sparkle to the upper-left -->%0A <polygon%0A points=\"46,30 36,32 34,42 32,32 22,30 32,28 34,18 36,28 \"%0A id=\"polygon3\"%0A transform=\"translate(-9.610984,-6.7505721)\" />%0A <!-- Small sparkle to the lower-right -->%0A <polygon%0A points=\"62,80 70,82 62,84 60,92 58,84 50,82 58,80 60,72 \"%0A id=\"polygon4\"%0A transform=\"translate(0,-1.6018307)\" />%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .brush-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 640 640\"><!--!Font Awesome Free 7.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d=\"M512.5 74.3L291.1 222C262 241.4 243.5 272.9 240.5 307.3C302.8 320.1 351.9 369.2 364.8 431.6C399.3 428.6 430.7 410.1 450.1 381L597.7 159.5C604.4 149.4 608 137.6 608 125.4C608 91.5 580.5 64 546.6 64C534.5 64 522.6 67.6 512.5 74.3zM320 464C320 402.1 269.9 352 208 352C146.1 352 96 402.1 96 464C96 467.9 96.2 471.8 96.6 475.6C98.4 493.1 86.4 512 68.8 512L64 512C46.3 512 32 526.3 32 544C32 561.7 46.3 576 64 576L208 576C269.9 576 320 525.9 320 464z\"/></svg>') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polyline-icon{background:url('data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\">%0A <polyline points=\"1,14 5,3 10,11 15,2\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .polygon-vertices-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Polygon (click-to-place) tool icon: a traced polyline with vertex dots. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.4740709,0,0,1.4740709,-6.0268793,-6.8086826)\">%0A <polyline points=\"23.877,16.627 14.657,10.767 5.25,21.551 12.757,29.397\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\"%0A stroke-linejoin=\"round\" stroke-linecap=\"round\"/>%0A <circle cx=\"23.877\" cy=\"16.627\" r=\"1.9\"/>%0A <circle cx=\"14.657\" cy=\"10.767\" r=\"1.9\"/>%0A <circle cx=\"5.25\" cy=\"21.551\" r=\"1.9\"/>%0A <circle cx=\"12.757\" cy=\"29.397\" r=\"1.9\"/>%0A </g>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-add-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Add-vertex tool icon: a region blob with three corner vertices and a plus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 9.65,29.0 H 5.85 v 5.3 H 0.55 v 3.4 H 5.85 v 5.3 H 9.65 V 37.7 H 14.95 V 34.3 H 9.65 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .vertex-delete-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Delete-vertex tool icon: a region blob with three corner vertices and a minus glyph. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <g transform=\"matrix(1.5222969,0,0,1.5222969,-8.2143505,-7.6146399)\">%0A <path d=\"m 9.35834,12.33267 c 1.78,-2.53 4.93,-3.56 7.86,-3.98 3.98,-0.72 8.33,0.08 11.52,2.67 2.62,1.98 4.71,5.1 4.53,8.49 -0.29,2.27 -2.5,3.81 -4.66,4.01 -2.75,0.43 -5.54,0.07 -8.28,-0.26 -3.99,-0.68 -8.05,-2.16 -10.93,-5.12 -1.32,-1.63 -1.39,-4.17 -0.04,-5.81 z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"1.6\" stroke-miterlimit=\"10\"/>%0A <circle cx=\"8.294\" cy=\"15.248\" r=\"2.63\"/>%0A <circle cx=\"19.781\" cy=\"8.287\" r=\"2.63\"/>%0A <circle cx=\"25.044\" cy=\"23.748\" r=\"2.63\"/>%0A </g>%0A <path d=\"M 0.55,34.3 v 3.4 H 14.95 V 34.3 Z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .region-move-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Region-move tool icon: a region blob with a drag arrow. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <path d=\"m 17.115582,8.9648664 c 1.707597,-3.3276806 4.729468,-4.6824281 7.540287,-5.2348498 3.827703,-0.947008 7.991169,0.1052238 11.051414,3.5118228 2.523022,2.604273 4.518416,6.7079726 4.345738,11.1668036 -0.278204,2.985704 -2.398311,5.011251 -4.470451,5.274308 -2.638142,0.565574 -5.314655,0.09206 -7.943203,-0.341976 C 23.811664,22.446579 19.916808,20.499952 17.153955,16.606696 15.89724,14.462775 15.820494,11.12194 17.115585,8.9648664 Z\"%0A fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.1\" stroke-miterlimit=\"10\"/>%0A <path d=\"m 26.741429,13.966963 6.746922,16.786269 3.229398,-4.060758 3.583857,3.583857 2.761195,-2.761196 -3.583856,-3.571852 4.060758,-3.2354 z\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-bezier-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-bezier tool icon: a smooth curve through two anchor points with control handles. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- smooth cubic curve between the two anchors -->%0A <path d=\"M 7,34 C 7,16 37,28 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\"/>%0A <!-- control handles -->%0A <line x1=\"7\" y1=\"34\" x2=\"7\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <line x1=\"37\" y1=\"10\" x2=\"37\" y2=\"22\" stroke=\"%23000000\" stroke-width=\"1.4\"/>%0A <circle cx=\"7\" cy=\"22\" r=\"2.2\"/>%0A <circle cx=\"37\" cy=\"22\" r=\"2.2\"/>%0A <!-- anchor points -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}::ng-deep .to-polygon-icon{background:url('data:image/svg+xml,<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>%0A<!-- Convert-to-polygon tool icon: endpoints joined by straight segments through a vertex. -->%0A<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 44 44\" width=\"44\" height=\"44\" fill=\"%23000000\">%0A <!-- straight-segment path through the vertices -->%0A <polyline points=\"7,34 22,12 37,10\" fill=\"none\" stroke=\"%23000000\" stroke-width=\"2.4\"%0A stroke-linecap=\"round\" stroke-linejoin=\"round\"/>%0A <!-- vertices -->%0A <rect x=\"4\" y=\"31\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"19\" y=\"9\" width=\"6\" height=\"6\" rx=\"1\"/>%0A <rect x=\"34\" y=\"7\" width=\"6\" height=\"6\" rx=\"1\"/>%0A</svg>%0A') no-repeat center center;background-size:contain;width:1.25rem;height:1.25rem;display:inline-block;filter:brightness(0) invert(47%) sepia(98%) saturate(567%) hue-rotate(181deg) brightness(103%) contrast(98%)}:host ::ng-deep .sam-progress .p-progressbar-value{transition:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { 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: 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: 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: ContextMenuModule }, { kind: "component", type: i11$1.ContextMenu, selector: "p-contextMenu", inputs: ["model", "triggerEvent", "target", "global", "style", "styleClass", "appendTo", "autoZIndex", "baseZIndex", "id", "ariaLabel", "ariaLabelledBy", "pressDelay"], outputs: ["onShow", "onHide"] }, { 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: "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: 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: InputNumberModule }, { kind: "component", type: i7$3.InputNumber, selector: "p-inputNumber", inputs: ["showButtons", "format", "buttonLayout", "inputId", "styleClass", "style", "placeholder", "size", "maxlength", "tabindex", "title", "ariaLabelledBy", "ariaLabel", "ariaRequired", "name", "required", "autocomplete", "min", "max", "incrementButtonClass", "decrementButtonClass", "incrementButtonIcon", "decrementButtonIcon", "readonly", "step", "allowEmpty", "locale", "localeMatcher", "mode", "currency", "currencyDisplay", "useGrouping", "variant", "minFractionDigits", "maxFractionDigits", "prefix", "suffix", "inputStyle", "inputStyleClass", "showClear", "autofocus", "disabled"], outputs: ["onInput", "onFocus", "onBlur", "onKeyDown", "onClear"] }, { kind: "ngmodule", type: ProgressBarModule }, { kind: "component", type: i7$1.ProgressBar, selector: "p-progressBar", inputs: ["value", "showValue", "styleClass", "style", "unit", "mode", "color"] }, { kind: "ngmodule", type: SliderModule }, { kind: "component", type: i2$1.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "ngmodule", type: ToastModule }, { kind: "component", type: i18.Toast, selector: "p-toast", inputs: ["key", "autoZIndex", "baseZIndex", "life", "style", "styleClass", "position", "preventOpenDuplicates", "preventDuplicates", "showTransformOptions", "hideTransformOptions", "showTransitionOptions", "hideTransitionOptions", "breakpoints"], outputs: ["onClose"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: FloatingDragDirective, selector: "[vizFloatingDrag]", inputs: ["vizFloatingDrag"], outputs: ["vizFloatingDragMove"] }, { kind: "component", type: ToolbarComponent, selector: "plotting-toolbar", inputs: ["imageInfo", "hasSpatialDataset", "plotTypeOptions", "selectedPlotType", "basePlotType", "isoRange", "maxIndex", "zIndex", "isHeatmap", "is3dRegions", "isVolumeView", "sliceEditor", "activeDragMode", "activeSurface3dMode", "axesVisible", "wireframeActive", "resolutionScale", "wandSensitivity", "brushSize", "vertexEraserRadius", "canUndo", "canRedo", "stackOptions", "tools", "imageSmoothingEnabled", "samModels", "samModelId", "contributedTools", "dialogTools", "openDialogToolId", "toolModelIds"], outputs: ["selectPlotType", "addProfileLine", "toggleImageSmoothing", "isoRangeChange", "openChannelHistogram", "openSpatialControls", "openRegionEditor", "toggleSliceEditor", "selectStackOption", "zScrub", "zSlide", "zIndexInput", "reloadAndPlot", "downloadImage", "autoscaleImage", "toggleDragMode", "zoomIn", "zoomOut", "toggleSurface3dMode", "resetSurfaceCamera", "toggleAxes", "toggleWireframe", "selectResolution", "deleteRegion", "undoRegion", "redoRegion", "segmentRegions", "segmentCellpose", "runTool", "toggleDialogTool", "toolModelChange", "openToolParams", "samModelChange", "wandSensitivityChange", "brushSizeChange", "vertexEraserRadiusChange"] }, { kind: "component", type: IntensityInsetComponent, selector: "viz-intensity-inset", inputs: ["divId", "plotDivName", "zIndex", "imageView", "imageInfo"] }, { kind: "component", type: SectionPaneComponent, selector: "viz-section-pane", inputs: ["axis", "view"], outputs: ["pick"] }, { kind: "component", type: ChannelHistogramComponent, selector: "channel-histogram", inputs: ["visible"], outputs: ["visibleChange"] }, { kind: "component", type: SpatialControlsComponent, selector: "spatial-controls", inputs: ["visible", "is3d"], outputs: ["visibleChange"] }, { kind: "component", type: RegionEditorComponent, selector: "region-editor", inputs: ["active"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
25328
26054
  }
25329
26055
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: VisualizerComponent, decorators: [{
25330
26056
  type: Component,
@@ -37767,6 +38493,8 @@ class OpenSeadragonVisualizerService extends BaseStoreVisualizer {
37767
38493
  // authoritative — push it into the shared meta so the Region Editor reports
37768
38494
  // areas in µm²/mm², matching the scale bar built from `d.mppX` below.
37769
38495
  this.store.setPhysicalPixelSize(d.mppX, d.mppY);
38496
+ // Its channel names and tints too, which a host's image meta may not carry.
38497
+ this.store.adoptChannelInfo(d.channelInfo);
37770
38498
  // Use the SAME grayscale flag the colormap dropdown / Plotly use
37771
38499
  // (imageInfo.isGrayscale ← rgbChannels === 1). The descriptor's `channels`
37772
38500
  // (channelCount) diverges from rgbChannels for stacks, which left grayscale
@@ -38340,9 +39068,13 @@ class SpatialControlsFacade {
38340
39068
  // A colour source the new dataset cannot satisfy would leave the map flat while
38341
39069
  // the panel and the charts kept naming the old column — so drop it, and only it:
38342
39070
  // point size, opacity and the rest are the user's preferences.
38343
- const by = ctx.store.currentSpatialView().colorBy;
39071
+ const current = ctx.store.currentSpatialView();
39072
+ const by = current.colorBy;
38344
39073
  if (by && !canColorBy(dataset, by))
38345
39074
  ctx.store.setSpatialView({ colorBy: null });
39075
+ // The gene filter names the old dataset's genes: kept, it could hide every observation.
39076
+ if (current.observationGenes?.length)
39077
+ ctx.store.setSpatialView({ observationGenes: [] });
38346
39078
  });
38347
39079
  this.controls = this.build();
38348
39080
  }
@@ -38417,6 +39149,7 @@ class SpatialControlsFacade {
38417
39149
  getTranscriptEstimate$: () => this.ctx.napari.transcriptEstimate$.asObservable(),
38418
39150
  getGeneCountsInView$: () => this.ctx.napari.geneCountsInView$.asObservable(),
38419
39151
  getDensityStats$: () => this.ctx.napari.densityStats$.asObservable(),
39152
+ getGeneFilterFailed$: () => this.ctx.napari.geneFilterFailed$?.asObservable() ?? of(false),
38420
39153
  categoryColors: async (name) => {
38421
39154
  const column = await port.getColumn(name);
38422
39155
  if (!isCategoricalColumn(column)) {
@@ -41035,6 +41768,8 @@ class Image2dScene {
41035
41768
  */
41036
41769
  async renderTiled(z, desc, scene) {
41037
41770
  const { ctx } = this;
41771
+ // The descriptor's channel names and tints, which a host's image meta may not carry.
41772
+ ctx.store.adoptChannelInfo(desc.channelInfo);
41038
41773
  const states = ctx.store.currentChannelStates();
41039
41774
  const channelCount = desc.channels ?? (states.length || 1);
41040
41775
  const multichannel = !!desc.multichannel && channelCount > 1;
@@ -41134,6 +41869,17 @@ class Image2dScene {
41134
41869
  this.recolorNavigator();
41135
41870
  });
41136
41871
  }
41872
+ /**
41873
+ * Put the current channel states back on the rendered layers: each channel's visibility, window,
41874
+ * gamma and tint, and which of an RGB image's layers draw it. For a caller that changed the
41875
+ * layers' visibility wholesale (the spatial view showing its tissue again), so the channels
41876
+ * the user or the defaults switched off stay off. Nothing to do before the image is drawn.
41877
+ */
41878
+ reapplyDisplayState() {
41879
+ if (!this.channelView)
41880
+ return;
41881
+ this.applyDisplayState(this.ctx.store.currentChannelStates());
41882
+ }
41137
41883
  /** Apply the current channel states / colormap to the live layers (no re-fetch), delegating the
41138
41884
  * per-channel layer mutations to the {@link MultiChannelImageView}. */
41139
41885
  applyDisplayState(channels) {
@@ -41203,8 +41949,10 @@ class Image2dScene {
41203
41949
  viewer.layers.move(layer, at + 1 + c);
41204
41950
  return layer;
41205
41951
  });
41206
- rgb.visible = false;
41207
41952
  }
41953
+ // The bands draw the image now; hidden on every pass, not only when the bands are made, so
41954
+ // a caller that showed every image layer (the spatial view) does not draw it twice.
41955
+ rgb.visible = false;
41208
41956
  this.bandLayers.forEach((layer, c) => {
41209
41957
  const st = states.find((s) => s.index === c);
41210
41958
  layer.colormap = tintColormap(st?.color ?? tintFor(c));
@@ -42687,6 +43435,26 @@ function encodeSpatialContinuous(values, view, lut, windows, muted = null) {
42687
43435
  function isPerPoint(colors) {
42688
43436
  return colors instanceof Float32Array;
42689
43437
  }
43438
+ /**
43439
+ * `colors` with every observation `shown` leaves at 0 made transparent: the observations a gene
43440
+ * filter leaves out. A broadcast colour becomes per-point, since the points now differ.
43441
+ */
43442
+ function hideUnshown(colors, shown) {
43443
+ const count = shown.length;
43444
+ let out;
43445
+ if (isPerPoint(colors)) {
43446
+ out = colors.slice(); // the input may be held elsewhere
43447
+ }
43448
+ else {
43449
+ out = new Float32Array(count * 4);
43450
+ for (let i = 0; i < count; i++)
43451
+ out.set(colors, i * 4);
43452
+ }
43453
+ for (let i = 0; i < count; i++)
43454
+ if (!shown[i])
43455
+ out[4 * i + 3] = 0;
43456
+ return out;
43457
+ }
42690
43458
  /** Per-point colours for the drawn subset. A broadcast tuple stays broadcast —
42691
43459
  * it is one colour for every point either way. */
42692
43460
  function gatherColors(colors, indices) {
@@ -43183,8 +43951,30 @@ class CategoricalLookup {
43183
43951
  const column = await this.port.getColumn(name);
43184
43952
  return isCategoricalColumn(column) ? { codes: column.codes, meta: column.meta } : null;
43185
43953
  }
43186
- /** Codes of the group column that are switched off, or null when none are. */
43954
+ /**
43955
+ * The observations not drawn: those of the groups switched off, and those the gene filter
43956
+ * leaves out. Null when nothing is hidden.
43957
+ */
43187
43958
  async hiddenCodes(dataset, view) {
43959
+ if (!hasGeneFilter(view))
43960
+ return this.hiddenGroupCodes(dataset, view);
43961
+ const count = dataset.observations.count;
43962
+ const [groups, shown] = await Promise.all([
43963
+ this.hiddenGroupCodes(dataset, view),
43964
+ observationGeneMask(view, count, (g) => this.port.getFeatureVector(g)),
43965
+ ]);
43966
+ if (!shown)
43967
+ return groups;
43968
+ // One code per observation: 1 for hidden, by its group or by the gene filter.
43969
+ const codes = new Uint16Array(count);
43970
+ for (let i = 0; i < count; i++) {
43971
+ const c = groups ? groups.codes[i] : NO_CATEGORY;
43972
+ codes[i] = !shown[i] || (c !== NO_CATEGORY && groups.hidden[c]) ? 1 : 0;
43973
+ }
43974
+ return { codes, hidden: Uint8Array.of(0, 1) };
43975
+ }
43976
+ /** Codes of the group column that are switched off, or null when none are. */
43977
+ async hiddenGroupCodes(dataset, view) {
43188
43978
  if (!view.hiddenGroups.length)
43189
43979
  return null;
43190
43980
  const name = cellTypeColumnFor(dataset, view);
@@ -43197,224 +43987,6 @@ class CategoricalLookup {
43197
43987
  }
43198
43988
  }
43199
43989
 
43200
- /**
43201
- * Merging and filtering of the spatial-tile wire format (observation ids, NO_CATEGORY): the
43202
- * pieces of the 2D view's tile loop that are pure — concatenating tiles, dropping hidden
43203
- * groups and genes, and the density-grid markers of a zoomed-out gene selection.
43204
- *
43205
- * These work on SIV's own tile contract, so they stay in SIV rather than moving to napari-js.
43206
- */
43207
- /** Rings whose owning cell is in a switched-off group removed. */
43208
- function filterRings(rings, hidden) {
43209
- if (!hidden)
43210
- return rings;
43211
- const keep = [];
43212
- let vertices = 0;
43213
- for (let i = 0; i < rings.count; i++) {
43214
- const code = hidden.codes[rings.observation[i]];
43215
- if (code !== NO_CATEGORY && hidden.hidden[code])
43216
- continue;
43217
- keep.push(i);
43218
- vertices += rings.offsets[i + 1] - rings.offsets[i];
43219
- }
43220
- if (keep.length === rings.count)
43221
- return rings;
43222
- const observation = new Uint32Array(keep.length);
43223
- const offsets = new Uint32Array(keep.length + 1);
43224
- const coords = new Float32Array(vertices * 2);
43225
- let v = 0;
43226
- keep.forEach((i, r) => {
43227
- observation[r] = rings.observation[i];
43228
- offsets[r] = v;
43229
- const a = rings.offsets[i] * 2;
43230
- const b = rings.offsets[i + 1] * 2;
43231
- coords.set(rings.coords.subarray(a, b), v * 2);
43232
- v += (b - a) / 2;
43233
- });
43234
- offsets[keep.length] = v;
43235
- return { count: keep.length, observation, offsets, coords };
43236
- }
43237
- /** Slots (indices into the selected genes) switched off with their eye toggle. */
43238
- function hiddenGeneSlots(view) {
43239
- if (!view.transcriptHiddenGenes.length)
43240
- return null;
43241
- const off = new Set(view.transcriptHiddenGenes);
43242
- return Uint8Array.from(view.transcriptGenes, (g) => (off.has(g) ? 1 : 0));
43243
- }
43244
- /** Entries in a hidden group's cell, or of a hidden gene, removed (with their sizes). */
43245
- function filterTranscripts(t, px, hidden, hiddenGenes) {
43246
- if (!hidden && !hiddenGenes)
43247
- return { merged: t, px };
43248
- const keep = [];
43249
- for (let i = 0; i < t.count; i++) {
43250
- if (hiddenGenes && hiddenGenes[t.gene[i]])
43251
- continue;
43252
- const o = t.observation[i];
43253
- if (hidden && o !== NO_OBSERVATION) {
43254
- const code = hidden.codes[o];
43255
- if (code !== NO_CATEGORY && hidden.hidden[code])
43256
- continue;
43257
- }
43258
- keep.push(i);
43259
- }
43260
- const pick = (a) => {
43261
- const out = new a.constructor(keep.length);
43262
- keep.forEach((i, k) => {
43263
- out[k] = a[i];
43264
- });
43265
- return out;
43266
- };
43267
- return {
43268
- merged: {
43269
- count: keep.length,
43270
- aggregated: t.aggregated,
43271
- x: pick(t.x),
43272
- y: pick(t.y),
43273
- z: pick(t.z),
43274
- weight: pick(t.weight),
43275
- observation: pick(t.observation),
43276
- gene: pick(t.gene),
43277
- },
43278
- px: pick(px),
43279
- };
43280
- }
43281
- /** The median of `v`, from a sample of about 1024 values. */
43282
- function median(v) {
43283
- if (!v.length)
43284
- return 0;
43285
- const step = Math.max(1, Math.floor(v.length / 1024));
43286
- const sample = [];
43287
- for (let i = 0; i < v.length; i += step)
43288
- sample.push(v[i]);
43289
- sample.sort((a, b) => a - b);
43290
- return sample[sample.length >> 1];
43291
- }
43292
- /** Concatenate tiles' rings into one set, dropping rings already seen (a cell straddling
43293
- * two tiles may be listed by both). */
43294
- function mergePolygonTiles(tiles) {
43295
- const seen = new Set();
43296
- let rings = 0;
43297
- let vertices = 0;
43298
- for (const t of tiles) {
43299
- for (let i = 0; i < t.count; i++) {
43300
- if (seen.has(t.observation[i]))
43301
- continue;
43302
- seen.add(t.observation[i]);
43303
- rings++;
43304
- vertices += t.offsets[i + 1] - t.offsets[i];
43305
- }
43306
- }
43307
- seen.clear();
43308
- const observation = new Uint32Array(rings);
43309
- const offsets = new Uint32Array(rings + 1);
43310
- const coords = new Float32Array(vertices * 2);
43311
- let r = 0;
43312
- let v = 0;
43313
- for (const t of tiles) {
43314
- for (let i = 0; i < t.count; i++) {
43315
- const o = t.observation[i];
43316
- if (seen.has(o))
43317
- continue;
43318
- seen.add(o);
43319
- observation[r] = o;
43320
- offsets[r] = v;
43321
- const a = t.offsets[i] * 2;
43322
- const b = t.offsets[i + 1] * 2;
43323
- coords.set(t.coords.subarray(a, b), v * 2);
43324
- v += (b - a) / 2;
43325
- r++;
43326
- }
43327
- }
43328
- offsets[rings] = v;
43329
- return { count: rings, observation, offsets, coords };
43330
- }
43331
- /** Concatenate transcript tiles, stopping at `limit` entries. */
43332
- function mergeTranscriptTiles(tiles, limit = Infinity) {
43333
- let n = 0;
43334
- for (const t of tiles)
43335
- n += t.count;
43336
- n = Math.min(n, limit);
43337
- const out = {
43338
- count: n,
43339
- aggregated: tiles.some((t) => t.aggregated),
43340
- x: new Float32Array(n),
43341
- y: new Float32Array(n),
43342
- z: new Float32Array(n),
43343
- weight: new Uint32Array(n),
43344
- observation: new Uint32Array(n),
43345
- gene: new Uint16Array(n),
43346
- };
43347
- let o = 0;
43348
- for (const t of tiles) {
43349
- const k = Math.min(t.count, n - o);
43350
- if (k <= 0)
43351
- break;
43352
- for (const f of ['x', 'y', 'z', 'weight', 'observation', 'gene']) {
43353
- out[f].set(t[f].subarray(0, k), o);
43354
- }
43355
- o += k;
43356
- }
43357
- return out;
43358
- }
43359
- /**
43360
- * One marker per (cluster, `bin` square) from per-cluster density grids: the counts of the grid
43361
- * cells inside `area` summed per square, at their count-weighted centre. `slots[c]` is the gene
43362
- * slot whose icon cluster c's markers take.
43363
- */
43364
- function clusterMarkers(rasters, slots, bin, area) {
43365
- const x = [];
43366
- const y = [];
43367
- const w = [];
43368
- const gene = [];
43369
- const group = [];
43370
- rasters.forEach((r, c) => {
43371
- const [cw, ch] = r.meta.gridSize;
43372
- const [ox, oy] = r.meta.origin;
43373
- const { rows, cols } = r.meta;
43374
- const c0 = Math.max(0, Math.floor((area.x0 - ox) / cw));
43375
- const c1 = Math.min(cols - 1, Math.floor((area.x1 - ox) / cw));
43376
- const r0 = Math.max(0, Math.floor((area.y0 - oy) / ch));
43377
- const r1 = Math.min(rows - 1, Math.floor((area.y1 - oy) / ch));
43378
- const squares = new Map();
43379
- for (let row = r0; row <= r1; row++) {
43380
- for (let col = c0; col <= c1; col++) {
43381
- const v = r.values[row * cols + col];
43382
- if (!(v > 0))
43383
- continue;
43384
- const cx = ox + (col + 0.5) * cw;
43385
- const cy = oy + (row + 0.5) * ch;
43386
- const key = Math.floor((cy - oy) / bin) * 1e6 + Math.floor((cx - ox) / bin);
43387
- const s = squares.get(key) ?? { n: 0, sx: 0, sy: 0 };
43388
- s.n += v;
43389
- s.sx += v * cx;
43390
- s.sy += v * cy;
43391
- squares.set(key, s);
43392
- }
43393
- }
43394
- for (const s of squares.values()) {
43395
- x.push(s.sx / s.n);
43396
- y.push(s.sy / s.n);
43397
- w.push(Math.round(s.n));
43398
- gene.push(slots[c]);
43399
- group.push(c);
43400
- }
43401
- });
43402
- const n = w.length;
43403
- return {
43404
- tile: {
43405
- count: n,
43406
- aggregated: true,
43407
- x: Float32Array.from(x),
43408
- y: Float32Array.from(y),
43409
- z: new Float32Array(n),
43410
- weight: Uint32Array.from(w),
43411
- observation: new Uint32Array(n).fill(NO_OBSERVATION),
43412
- gene: Uint16Array.from(gene),
43413
- },
43414
- group: Int32Array.from(group),
43415
- };
43416
- }
43417
-
43418
43990
  const MAX_CELL_TILES = 48;
43419
43991
  /**
43420
43992
  * The cell outlines of the 2D spatial view, at the polygon level the zoom calls for: a fill,
@@ -43609,7 +44181,7 @@ class CellLayers {
43609
44181
  */
43610
44182
  async cellGeometry(dataset, view, rect, level, hidden, ctx) {
43611
44183
  const tiled = dataset.polygonTiles;
43612
- const hiddenKey = view.hiddenGroups.join('\u0001');
44184
+ const hiddenKey = `${view.hiddenGroups.join('\u0001')}|${geneFilterKey(view)}`;
43613
44185
  let rings;
43614
44186
  let nuclei = null;
43615
44187
  let geometryKey;
@@ -43706,8 +44278,20 @@ class DensityLayer {
43706
44278
  /** Draw the view's density, or drop it; an unchanged (genes, bin, window, colormap) is a no-op. */
43707
44279
  async plan(dataset, view, ctx) {
43708
44280
  const hiddenGenes = new Set(view.transcriptHiddenGenes);
43709
- const genes = view.transcriptAllGenes ? [ALL_GENES] : view.transcriptGenes.filter((g) => !hiddenGenes.has(g));
43710
- if (!dataset.density || !this.port.getDensity || view.transcriptMode !== 'density' || !genes.length) {
44281
+ // Every gene's density is one raster: hiding a gene of it hides nothing, hiding all hides it.
44282
+ const genes = view.transcriptAllGenes
44283
+ ? allGenesHidden(view)
44284
+ ? []
44285
+ : [ALL_GENES]
44286
+ : view.transcriptGenes.filter((g) => !hiddenGenes.has(g));
44287
+ // The raster sums every cell's transcripts and carries no cells, so it cannot follow the
44288
+ // observation gene filter: drawn, it would show the transcripts of the cells it hides. Not
44289
+ // drawn while it is on (the Transcripts panel says so).
44290
+ if (!dataset.density ||
44291
+ !this.port.getDensity ||
44292
+ view.transcriptMode !== 'density' ||
44293
+ !genes.length ||
44294
+ hasGeneFilter(view)) {
43711
44295
  this.groups.drop('density');
43712
44296
  this.stats = null;
43713
44297
  this.raster = null;
@@ -43815,7 +44399,9 @@ class TranscriptEstimator {
43815
44399
  const b = counts.bounds;
43816
44400
  const share = Math.min(1, visibleArea(inView, b) / Math.max(1, (b[2] - b[0]) * (b[3] - b[1])));
43817
44401
  const selected = view.transcriptAllGenes
43818
- ? counts.total
44402
+ ? allGenesHidden(view)
44403
+ ? 0
44404
+ : counts.total
43819
44405
  : genes.reduce((sum, g) => sum + (counts.counts[g] ?? 0), 0);
43820
44406
  this.changed({ points: Math.round(selected * share), max: view.transcriptBudget });
43821
44407
  }
@@ -44212,7 +44798,9 @@ class TranscriptHover {
44212
44798
  return lines;
44213
44799
  }
44214
44800
  if (d.kind === 'individual') {
44215
- const gene = s?.topGenes?.[0]?.name;
44801
+ // Named by its code when the dataset lists its genes; else by the server's summary of
44802
+ // the spot, which can name a neighbour's gene.
44803
+ const gene = d.genes[t.gene[i]] || s?.topGenes?.[0]?.name;
44216
44804
  return [
44217
44805
  gene ? `${gene} transcript` : 'Transcript',
44218
44806
  cellLine(obs, 'in cell'),
@@ -44278,6 +44866,8 @@ class TranscriptJobPlanner {
44278
44866
  const mode = view.transcriptMode;
44279
44867
  if (mode !== 'circles' && mode !== 'glyphs')
44280
44868
  return null;
44869
+ if (allGenesHidden(view))
44870
+ return null;
44281
44871
  const geneBins = this.port.getTranscriptGeneBins && dataset.transcriptGeneBins;
44282
44872
  if (!this.port.getTranscriptTile && !geneBins)
44283
44873
  return null;
@@ -44652,20 +45242,27 @@ function transcriptPlanKey(dataset, view, job, pxPerUnit) {
44652
45242
  JSON.stringify(view.transcriptGlyphs),
44653
45243
  pxPerUnit.toPrecision(4),
44654
45244
  view.hiddenGroups.join('\u0001'),
45245
+ geneFilterKey(view),
44655
45246
  view.transcriptHiddenGenes.join(','),
45247
+ view.transcriptAllGenesHidden.join(','),
44656
45248
  JSON.stringify(view.transcriptGeneColors),
44657
45249
  JSON.stringify(view.transcriptGeneGroups),
44658
45250
  ].join('|');
44659
45251
  }
44660
45252
  /** The view the markers are coloured by, for what was loaded. */
44661
- function colorViewFor(view, kind, clustered) {
45253
+ function colorViewFor(view, kind, clustered, hasCellTypes) {
44662
45254
  // A bin of the all-gene pyramid mixes genes and carries none, so it cannot be coloured
44663
- // by gene: colour it by its dominant cell's type until individual transcripts show.
45255
+ // by gene: colour it by its dominant cell's type until individual transcripts show (with
45256
+ // no cell types, that leaves every bin unassigned: one colour).
44664
45257
  if (kind === 'bins' && view.transcriptColorBy !== 'cellType')
44665
45258
  return { ...view, transcriptColorBy: 'cellType' };
44666
45259
  // Density-grid markers have no cell to take a type from: they take their cluster's colour.
44667
45260
  if (clustered && view.transcriptColorBy === 'cellType')
44668
45261
  return { ...view, transcriptColorBy: 'cluster' };
45262
+ // No cell types to colour by (the panel then does not offer it): by gene instead.
45263
+ if (!hasCellTypes && kind !== 'bins' && view.transcriptColorBy === 'cellType') {
45264
+ return { ...view, transcriptColorBy: 'gene' };
45265
+ }
44669
45266
  return view;
44670
45267
  }
44671
45268
  const UNASSIGNED_RGBA = [0.62, 0.62, 0.62, 0.55];
@@ -44729,20 +45326,38 @@ class TranscriptLayers {
44729
45326
  const loaded = await ctx.track('Transcripts', job.load(ctx));
44730
45327
  if (ctx.stale())
44731
45328
  return;
45329
+ // Markers drawn from summed density grids (zoomed out) carry no cells, so they cannot follow
45330
+ // the observation gene filter: drawn, they would show the transcripts of the cells it hides.
45331
+ // None while it is on; zoomed in, the cell-assigned transcripts are filtered (below).
45332
+ if (loaded.clustered && hasGeneFilter(view)) {
45333
+ this.groups.drop('transcripts');
45334
+ this.hover.clear();
45335
+ this.countSource = null;
45336
+ return;
45337
+ }
44732
45338
  // Per-gene counts in view come from what was loaded, before hidden genes are dropped:
44733
45339
  // a hidden gene still has transcripts there. Summed density grids (no per-gene levels,
44734
45340
  // zoomed out) hold clusters, not genes: no per-gene counts there, rather than a guess.
45341
+ // Every gene's individual transcripts are counted by the dataset's gene list, when it has one.
45342
+ const allGeneNames = dataset.transcriptTiles?.genes;
45343
+ const kind = loaded.kind ?? job.kind;
44735
45344
  this.countSource =
44736
- (loaded.kind ?? job.kind) === 'genes' && !loaded.clustered
45345
+ kind === 'genes' && !loaded.clustered
44737
45346
  ? { merged: loaded.merged, genes: [...view.transcriptGenes] }
44738
- : null;
45347
+ : kind === 'individual' && allGeneNames
45348
+ ? { merged: loaded.merged, genes: allGeneNames }
45349
+ : null;
44739
45350
  const hidden = await this.lookup.hiddenCodes(dataset, view);
44740
45351
  if (ctx.stale())
44741
45352
  return;
44742
45353
  // Density-grid markers carry no cells and were built from the visible genes only.
44743
45354
  const filtered = loaded.clustered
44744
45355
  ? { merged: loaded.merged, px: loaded.px }
44745
- : filterTranscripts(loaded.merged, loaded.px, hidden, (loaded.kind ?? job.kind) === 'genes' ? hiddenGeneSlots(view) : null);
45356
+ : filterTranscripts(loaded.merged, loaded.px, hidden, kind === 'genes'
45357
+ ? hiddenGeneSlots(view)
45358
+ : kind === 'individual'
45359
+ ? hiddenAllGeneCodes(view, allGeneNames)
45360
+ : null);
44746
45361
  let { merged, px } = filtered;
44747
45362
  let selectionBin = loaded.clustered?.bin ?? null;
44748
45363
  let entryGroup = loaded.clustered?.group ?? null;
@@ -44752,8 +45367,7 @@ class TranscriptLayers {
44752
45367
  if (g)
44753
45368
  ({ merged, px, bin: selectionBin, group: entryGroup, names: groupNames } = g);
44754
45369
  }
44755
- const kind = loaded.kind ?? job.kind;
44756
- const colorView = colorViewFor(view, kind, !!loaded.clustered);
45370
+ const colorView = colorViewFor(view, kind, !!loaded.clustered, cellTypeColumnFor(dataset, view) !== null);
44757
45371
  // Each entry's cluster: its marker's group, or the gene-tree group its gene is in.
44758
45372
  const geneCluster = view.transcriptGenes.map((g) => view.transcriptGeneGroups.find((x) => x.genes.includes(g))?.name ?? g);
44759
45373
  const clusterOf = (i) => entryGroup && groupNames ? (groupNames[entryGroup[i]] ?? null) : (geneCluster[merged.gene[i]] ?? null);
@@ -44777,7 +45391,7 @@ class TranscriptLayers {
44777
45391
  this.drawTranscripts(merged, diam, rgba, view, place, null, 0.18 * median(diam))
44778
45392
  : this.drawTranscriptGlyphs(merged, diam, rgba, view, place, pxPerUnit);
44779
45393
  this.hover.setDrawn({
44780
- kind: loaded.kind ?? job.kind,
45394
+ kind,
44781
45395
  bin: selectionBin
44782
45396
  ? { size: selectionBin, origin: loaded.clustered?.origin ?? [0, 0] }
44783
45397
  : loaded.kind
@@ -44785,7 +45399,7 @@ class TranscriptLayers {
44785
45399
  : job.bin,
44786
45400
  merged,
44787
45401
  layer,
44788
- genes: [...view.transcriptGenes],
45402
+ genes: kind === 'individual' ? (allGeneNames ?? []) : [...view.transcriptGenes],
44789
45403
  ...(entryGroup && groupNames ? { entryGroup, groupNames } : {}),
44790
45404
  ...(loaded.clustered ? { groupGenes: loaded.clustered.genes } : {}),
44791
45405
  }, dataset.micronsPerUnit ?? null);
@@ -44878,11 +45492,13 @@ class TranscriptLayers {
44878
45492
  codeOf = (i) => code[i];
44879
45493
  }
44880
45494
  else if (view.transcriptColorBy === 'gene' || view.transcriptColorBy === 'cluster') {
44881
- // All genes: codes are the dataset's gene indices, folded onto the palette.
44882
- const n = view.transcriptAllGenes ? DEFAULT_CATEGORICAL_PALETTE.length : view.transcriptGenes.length;
44883
- rgb = Array.from({ length: n }, (_g, i) => parseHex((!view.transcriptAllGenes && view.transcriptGeneColors[view.transcriptGenes[i]]) ||
45495
+ // All genes: codes are the dataset's gene indices, coloured by name when the dataset
45496
+ // lists them (so a gene's chosen colour applies), else folded onto the palette.
45497
+ const names = view.transcriptAllGenes ? (dataset.transcriptTiles?.genes ?? null) : view.transcriptGenes;
45498
+ const n = names ? names.length : DEFAULT_CATEGORICAL_PALETTE.length;
45499
+ rgb = Array.from({ length: n }, (_g, i) => parseHex((names && view.transcriptGeneColors[names[i]]) ||
44884
45500
  DEFAULT_CATEGORICAL_PALETTE[i % DEFAULT_CATEGORICAL_PALETTE.length]));
44885
- codeOf = view.transcriptAllGenes ? (i) => t.gene[i] % n : (i) => t.gene[i];
45501
+ codeOf = names ? (i) => (t.gene[i] < n ? t.gene[i] : -1) : (i) => t.gene[i] % n;
44886
45502
  }
44887
45503
  else {
44888
45504
  const name = cellTypeColumnFor(dataset, view);
@@ -45109,6 +45725,10 @@ class SpatialSession {
45109
45725
  this.tileLayers = null;
45110
45726
  this.tileOwner = null;
45111
45727
  }
45728
+ /** Tell the panel whether the observation gene filter could be applied this rebuild. */
45729
+ reportGeneFilter(failed) {
45730
+ this.sinks.geneFilterFailed?.(failed);
45731
+ }
45112
45732
  /** The tile layers, if they were ever built. */
45113
45733
  get tiles() {
45114
45734
  return this.tileLayers;
@@ -45439,13 +46059,33 @@ class Spatial2dScene extends SpatialSceneBase {
45439
46059
  // Resolve colours BEFORE touching the scene: a gene fetch can fail or be
45440
46060
  // superseded, and dropping the existing layer first would blank the view.
45441
46061
  let faceColor;
46062
+ let symbols = null;
46063
+ let shown = null;
45442
46064
  const endLoading = this.ctx.badge.begin('Observations');
45443
46065
  try {
45444
- faceColor = dataset ? await this.faceColors(dataset, view, selection) : SPATIAL_NEUTRAL_COLOR;
45445
- }
45446
- catch (err) {
45447
- console.warn('[napari-js] spatial colouring failed — falling back to a flat colour', err);
45448
- faceColor = SPATIAL_NEUTRAL_COLOR;
46066
+ // Which observations the gene filter keeps, settled apart from their colours: a colour can
46067
+ // fall back to a flat one, the filter cannot. Drawn without it, every observation would
46068
+ // show as if it expressed the picked genes. So a failed read keeps the markers as they
46069
+ // were drawn, and the panel says the filter could not be applied.
46070
+ try {
46071
+ shown = dataset ? await this.filterMask(dataset, view) : null;
46072
+ }
46073
+ catch (err) {
46074
+ console.warn('[napari-js] gene filter could not be applied — keeping the observations as drawn', err);
46075
+ if (task())
46076
+ this.session.reportGeneFilter(true);
46077
+ return;
46078
+ }
46079
+ try {
46080
+ const style = dataset ? await this.markerStyle(dataset, view, selection) : null;
46081
+ faceColor = style?.colors ?? SPATIAL_NEUTRAL_COLOR;
46082
+ symbols = style?.symbols ?? null;
46083
+ }
46084
+ catch (err) {
46085
+ console.warn('[napari-js] spatial colouring failed — falling back to a flat colour', err);
46086
+ faceColor = SPATIAL_NEUTRAL_COLOR;
46087
+ symbols = null;
46088
+ }
45449
46089
  }
45450
46090
  finally {
45451
46091
  endLoading();
@@ -45453,6 +46093,10 @@ class Spatial2dScene extends SpatialSceneBase {
45453
46093
  // A newer rebuild (or a teardown) started while the vector was in flight.
45454
46094
  if (!task())
45455
46095
  return;
46096
+ this.session.reportGeneFilter(false);
46097
+ // The filter applies to whichever colours won, the fallback's included.
46098
+ if (shown && dataset)
46099
+ faceColor = hideUnshown(faceColor, shown);
45456
46100
  if (!dataset || dataset.observations.count === 0) {
45457
46101
  this.removeLayers(viewer);
45458
46102
  return;
@@ -45467,7 +46111,7 @@ class Spatial2dScene extends SpatialSceneBase {
45467
46111
  await this.ensureGeneMap(viewer, dataset, view, selection, slab);
45468
46112
  if (!task())
45469
46113
  return;
45470
- this.drawMarkers(viewer, dataset, view, faceColor, slab);
46114
+ this.drawMarkers(viewer, dataset, view, faceColor, slab, symbols);
45471
46115
  }
45472
46116
  /** Nothing to draw: drop the markers and the gene map. */
45473
46117
  removeLayers(viewer) {
@@ -45487,7 +46131,7 @@ class Spatial2dScene extends SpatialSceneBase {
45487
46131
  * Draw the observation markers over the image (and the gene map): restyled in place for a
45488
46132
  * display-only change, else a new layer.
45489
46133
  */
45490
- drawMarkers(viewer, dataset, view, faceColor, slab) {
46134
+ drawMarkers(viewer, dataset, view, faceColor, slab, symbols = null) {
45491
46135
  const obs = dataset.observations;
45492
46136
  const ref = slab?.ref ?? dataset.imageRef;
45493
46137
  const size = markerSizes(obs, view, slab);
@@ -45511,6 +46155,7 @@ class Spatial2dScene extends SpatialSceneBase {
45511
46155
  this.points.size = size;
45512
46156
  this.points.visible = this.pointsVisible();
45513
46157
  this.points.faceColor = gatherColors(faceColor, slab?.indices);
46158
+ this.points.symbols = gatherSymbols(symbols, slab?.indices);
45514
46159
  this.hideForeignImage(viewer, showImage);
45515
46160
  this.ctx.tools.regionOverlay?.setRegionsVisible(view.showAnnotations !== false);
45516
46161
  viewer.requestRender();
@@ -45528,6 +46173,8 @@ class Spatial2dScene extends SpatialSceneBase {
45528
46173
  name: 'observations',
45529
46174
  size,
45530
46175
  faceColor: gatherColors(faceColor, drawn),
46176
+ // Each observation's gene's icon while they are coloured by gene; the layer's disc else.
46177
+ symbols: gatherSymbols(symbols, drawn),
45531
46178
  // No border: at Visium spot density an outline per marker reads as noise,
45532
46179
  // and it costs a second colour array.
45533
46180
  borderWidth: 0,
@@ -45621,6 +46268,10 @@ class Spatial2dScene extends SpatialSceneBase {
45621
46268
  // datasets does not leave the tissue permanently hidden.
45622
46269
  layer.visible = datasetHasPixels;
45623
46270
  }
46271
+ // Showing the tissue must not show its channels the user or the defaults switched off (a
46272
+ // 22-channel plate starts with four): the channel states decide which of its layers draw.
46273
+ if (datasetHasPixels)
46274
+ this.image.reapplyDisplayState();
45624
46275
  viewer.requestRender();
45625
46276
  }
45626
46277
  /**
@@ -45798,7 +46449,38 @@ class Spatial2dScene extends SpatialSceneBase {
45798
46449
  * selected to colour by. Categorical columns use the column's own palette; continuous columns
45799
46450
  * and gene vectors go through the active colormap with a percentile-clipped window.
45800
46451
  */
45801
- async faceColors(dataset, view, selection) {
46452
+ /** The observations the view's gene filter keeps (1 = drawn), or null when it has none.
46453
+ * Rejects when an expression read fails. */
46454
+ filterMask(dataset, view) {
46455
+ const port = this.session.port;
46456
+ if (!port)
46457
+ return Promise.resolve(null);
46458
+ return observationGeneMask(view, dataset.observations.count, (g) => port.getFeatureVector(g));
46459
+ }
46460
+ /** The markers' colours (and icons, by gene) before the gene filter hides any of them. */
46461
+ async markerStyle(dataset, view, selection) {
46462
+ const port = this.session.port;
46463
+ const count = dataset.observations.count;
46464
+ // Coloured by gene: each observation in its gene's colour, drawn as its gene's icon. That is
46465
+ // a colour, so a failed read falls back to the view's colour source, as any colour does.
46466
+ const byGene = port
46467
+ ? await observationGeneStyle(view, count, (g) => port.getFeatureVector(g)).catch((err) => {
46468
+ console.warn('[napari-js] colouring by gene failed — falling back to the colour source', err);
46469
+ return null;
46470
+ })
46471
+ : null;
46472
+ const colors = byGene
46473
+ ? encodeCategorical(byGene.codes, {
46474
+ colors: byGene.colors,
46475
+ opacity: view.opacity,
46476
+ muted: mutedFromSelection(selection),
46477
+ })
46478
+ : await this.sourceColors(dataset, view, selection);
46479
+ const symbols = byGene ? glyphSymbols(byGene.codes, byGene.glyphs) : null;
46480
+ return { colors, symbols };
46481
+ }
46482
+ /** {@link faceColors} before the gene filter: what the colour source and the selection make of them. */
46483
+ async sourceColors(dataset, view, selection) {
45802
46484
  const port = this.session.port;
45803
46485
  const colorBy = view.colorBy;
45804
46486
  // Everything NOT selected is muted; with nothing selected, nothing is muted
@@ -45894,6 +46576,19 @@ function slabAt(dataset, slice) {
45894
46576
  minDiameter: SPATIAL_SLICE_MIN_DIAMETER_PX * volume.voxelSize[0],
45895
46577
  };
45896
46578
  }
46579
+ /** Per-observation napari-js symbol codes: each observation's gene's icon (`codes` index
46580
+ * `glyphs`), a disc where it has none. */
46581
+ function glyphSymbols(codes, glyphs) {
46582
+ const byGene = glyphs.map((g) => pointSymbolCode(GLYPH_SYMBOLS[g]));
46583
+ const disc = pointSymbolCode('disc');
46584
+ return Uint8Array.from(codes, (c) => (c === NO_CATEGORY ? disc : (byGene[c] ?? disc)));
46585
+ }
46586
+ /** The symbols of the drawn subset (all of them when `indices` is undefined). */
46587
+ function gatherSymbols(symbols, indices) {
46588
+ if (!symbols || !indices)
46589
+ return symbols;
46590
+ return Uint8Array.from(indices, (i) => symbols[i]);
46591
+ }
45897
46592
 
45898
46593
  /**
45899
46594
  * SPATIAL_OMICS_3D (review Appendix B, cluster K): observations as a 3D point cloud under the
@@ -46138,14 +46833,30 @@ class Spatial3dScene extends SpatialSceneBase {
46138
46833
  // the 2D path does: a gene fetch can fail or be superseded, and dropping the
46139
46834
  // layer first would blank the view.
46140
46835
  let enc = null;
46836
+ let geneShown = null;
46141
46837
  if (dataset) {
46142
46838
  const endLoading = this.ctx.badge.begin('Observations');
46839
+ const port = this.session.port;
46143
46840
  try {
46144
- enc = await this.scalarEncoding(view);
46145
- }
46146
- catch (err) {
46147
- console.warn('[napari-js] spatial 3D colouring failed — falling back to a flat colour', err);
46148
- enc = null;
46841
+ // Which observations the gene filter keeps, settled apart from their colours: a colour
46842
+ // can fall back to a flat one, the filter cannot. Drawn without it, every observation
46843
+ // would show as if it expressed the picked genes. So a failed read keeps the cloud as it
46844
+ // was drawn, and the panel says the filter could not be applied.
46845
+ try {
46846
+ geneShown = port
46847
+ ? await observationGeneMask(view, dataset.observations.count, (g) => port.getFeatureVector(g))
46848
+ : null;
46849
+ }
46850
+ catch (err) {
46851
+ console.warn('[napari-js] spatial 3D gene filter could not be applied — keeping the cloud as drawn', err);
46852
+ if (task())
46853
+ this.session.reportGeneFilter(true);
46854
+ return;
46855
+ }
46856
+ enc = await this.scalarEncoding(view, dataset.observations.count).catch((err) => {
46857
+ console.warn('[napari-js] spatial 3D colouring failed — falling back to a flat colour', err);
46858
+ return null;
46859
+ });
46149
46860
  }
46150
46861
  finally {
46151
46862
  endLoading();
@@ -46153,6 +46864,7 @@ class Spatial3dScene extends SpatialSceneBase {
46153
46864
  }
46154
46865
  if (!task())
46155
46866
  return;
46867
+ this.session.reportGeneFilter(false);
46156
46868
  const obs = dataset?.observations;
46157
46869
  // No z means nothing to draw in 3D. The plot type is gated on `requiresSpatial3d`
46158
46870
  // so this should be unreachable from the UI, but a host can set the type directly.
@@ -46189,13 +46901,13 @@ class Spatial3dScene extends SpatialSceneBase {
46189
46901
  this.volume.visible = view.showVolume;
46190
46902
  this.volume.opacity = view.volumeOpacity;
46191
46903
  }
46192
- this.drawCloud(viewer, dataset, view, selection, enc);
46904
+ this.drawCloud(viewer, dataset, view, selection, enc, geneShown);
46193
46905
  }
46194
46906
  /**
46195
46907
  * Draw the point cloud: a new layer for new geometry (a dataset or a section), the colour
46196
46908
  * source swapped in place otherwise; then the selection and the visibility.
46197
46909
  */
46198
- drawCloud(viewer, dataset, view, selection, enc) {
46910
+ drawCloud(viewer, dataset, view, selection, enc, geneShown = null) {
46199
46911
  const obs = dataset.observations;
46200
46912
  const scale = view.pointScale > 0 ? view.pointScale : 1;
46201
46913
  const size = SPATIAL_3D_BASE_SIZE * scale;
@@ -46206,7 +46918,7 @@ class Spatial3dScene extends SpatialSceneBase {
46206
46918
  const scalars = scalar?.values ?? new Float32Array(obs.count);
46207
46919
  const colormap = scalar?.colormap ?? spatialFlatColormap();
46208
46920
  const contrastLimits = scalar?.contrastLimits ?? [0, 1];
46209
- const { shown, key, scalarKey } = cloudSubset(dataset, view);
46921
+ const { shown, key, scalarKey } = cloudSubset(dataset, view, geneShown);
46210
46922
  if (key !== this.layerKey) {
46211
46923
  // New geometry: interleave x,y,z (the layer's documented layout, x-fastest)
46212
46924
  // and cache it, so later colour changes rebuild the layer without walking
@@ -46670,8 +47382,18 @@ class Spatial3dScene extends SpatialSceneBase {
46670
47382
  * uses. Categorical data is one RGBA per point from the column's palette, so any number of
46671
47383
  * categories keeps its own colour — see `encodeSpatial3dCategorical`.
46672
47384
  */
46673
- async scalarEncoding(view) {
47385
+ async scalarEncoding(view, count) {
46674
47386
  const port = this.session.port;
47387
+ // Coloured by the gene filter: each observation in its gene's colour.
47388
+ // A colour like any other: a failed read falls back to the view's colour source.
47389
+ const byGene = port && count
47390
+ ? await observationGeneStyle(view, count, (g) => port.getFeatureVector(g)).catch((err) => {
47391
+ console.warn('[napari-js] spatial 3D colouring by gene failed — falling back to the colour source', err);
47392
+ return null;
47393
+ })
47394
+ : null;
47395
+ if (byGene)
47396
+ return encodeSpatial3dCategorical(byGene.codes, byGene.colors);
46675
47397
  const colorBy = view.colorBy;
46676
47398
  if (!port || !colorBy)
46677
47399
  return null;
@@ -46699,23 +47421,44 @@ class Spatial3dScene extends SpatialSceneBase {
46699
47421
  * geometry's (`colorBy` plus the transforms feeding the encoding): a new colour source swaps
46700
47422
  * `values` in place (napari-js ≥ 0.14), while a new geometry needs a new layer.
46701
47423
  */
46702
- function cloudSubset(dataset, view) {
47424
+ function cloudSubset(dataset, view, geneShown = null) {
46703
47425
  const obs = dataset.observations;
46704
47426
  const sections = sectionsOf(obs);
46705
47427
  const section = view.pointSection != null && sections && sections.length > 0
46706
47428
  ? sections[Math.max(0, Math.min(sections.length - 1, view.pointSection))]
46707
47429
  : null;
46708
- const shown = section != null ? observationsInSection(obs, section) : null;
46709
- const key = `${dataset.id}:${obs.count}:${section ?? 'all'}`;
47430
+ let shown = section != null ? observationsInSection(obs, section) : null;
47431
+ // The gene filter leaves its unmatched observations out of the geometry, so they are neither
47432
+ // drawn nor picked.
47433
+ if (geneShown)
47434
+ shown = keepShown(shown, geneShown, obs.count);
47435
+ const key = `${dataset.id}:${obs.count}:${section ?? 'all'}:${geneShown ? geneFilterKey(view) : ''}`;
46710
47436
  const clip = view.percentileClip ?? [0.01, 0.99];
46711
47437
  const scalarKey = [
46712
47438
  key,
46713
47439
  view.colorBy ? `${view.colorBy.kind}:${view.colorBy.name}` : 'flat',
47440
+ observationGeneStyleKey(view),
46714
47441
  view.logScale ? 'log' : 'lin',
46715
47442
  clip.join(','),
46716
47443
  ].join('|');
46717
47444
  return { shown, key, scalarKey };
46718
47445
  }
47446
+ /** The observations of `indices` (every one when null) that `shown` keeps. */
47447
+ function keepShown(indices, shown, count) {
47448
+ const out = new Uint32Array(indices ? indices.length : count);
47449
+ let n = 0;
47450
+ if (indices) {
47451
+ for (const i of indices)
47452
+ if (shown[i])
47453
+ out[n++] = i;
47454
+ }
47455
+ else {
47456
+ for (let i = 0; i < count; i++)
47457
+ if (shown[i])
47458
+ out[n++] = i;
47459
+ }
47460
+ return out.subarray(0, n);
47461
+ }
46719
47462
 
46720
47463
  /**
46721
47464
  * The WebGPU image backend, built on the published `napari-js` library (jit-ui#102: a
@@ -46767,6 +47510,8 @@ class NapariVisualizerService extends BaseStoreVisualizer {
46767
47510
  /** Transcripts of each selected gene in view (see NapariSpatialTileLayers.geneCountsIn). */
46768
47511
  this.geneCountsInView$ = new BehaviorSubject(null);
46769
47512
  this.densityStats$ = new BehaviorSubject(null);
47513
+ /** The observation gene filter could not be applied (an expression read failed). */
47514
+ this.geneFilterFailed$ = new BehaviorSubject(false);
46770
47515
  /** "x reloading…" at the bottom of the canvas: the tile layers' loads and the observations'. */
46771
47516
  this.badge = new LoadingBadgeState();
46772
47517
  /**
@@ -46834,6 +47579,7 @@ class NapariVisualizerService extends BaseStoreVisualizer {
46834
47579
  estimateChanged: (e) => this.inZone(() => this.transcriptEstimate$.next(e)),
46835
47580
  geneCountsChanged: (c) => this.inZone(() => this.geneCountsInView$.next(c)),
46836
47581
  densityChanged: (d) => this.inZone(() => this.densityStats$.next(d)),
47582
+ geneFilterFailed: (failed) => this.inZone(() => this.geneFilterFailed$.next(failed)),
46837
47583
  loadingChanged: (layers) => this.badge.setTileLayers(layers),
46838
47584
  });
46839
47585
  this.tools = new NapariToolBridge({
@@ -49296,5 +50042,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
49296
50042
  * Generated bundle index. Do not edit.
49297
50043
  */
49298
50044
 
49299
- export { TILE_ACCESS_PORT as $, ALL_GENES as A, ProcessingImage as B, CANVAS_TOOL_IDS as C, DEFAULT_CATEGORICAL_PALETTE as D, REGION_IO_PORT as E, REGION_SOURCE_MIN_CHARS as F, Rectangle as G, HexColorPickerComponent as H, IMAGE_STATE_PORT as I, Region as J, RegionEditorComponent as K, LUT_COLORS as L, MISSING_COLOR as M, NAPARI_DECIMATE_OPTIONS as N, RoutingVisualizerService as O, PLOT_MODE_CONTEXT as P, SEMANTIC_SEGMENTER as Q, REGION_EDITOR_API as R, SAM_MODELS as S, SPATIAL_DATA_PORT as T, SPATIAL_WIRE_VERSION as U, ShapeSelection as V, SpatialChartsComponent as W, SpatialControlsComponent as X, SpatialDataHttpService as Y, SpatialSelectionStore as Z, SupersededError as _, runDecoder as a, openGeoJsonRegionSource as a$, TOOLBAR_TOOLS as a0, VISUALIZER as a1, VIZ_ALERT_TOAST_KEY as a2, VIZ_CONFIG as a3, VIZ_TOAST_KEY as a4, ViewerFeature as a5, VisualizationModule as a6, VisualizerComponent as a7, assertManifestVersion as a8, benefitsFromGrouping as a9, isBuiltinPlotType as aA, isCanvasToolId as aB, isCategoricalColumn as aC, isContinuousColumn as aD, isDialogToolContribution as aE, isEmptyPresetSet as aF, isLabelVolumeName as aG, isLittleEndian as aH, isNapari3d as aI, isNapariIsosurface as aJ, isNapariScatter as aK, isNapariScatter3d as aL, isNapariSurface as aM, isNapariVolume as aN, isSamModelReady as aO, isSpatialOmics as aP, isSpatialOmics3d as aQ, isThreeDimensional as aR, isZipName as aS, labelClasses as aT, labelSliceGeoJson as aU, lutFor as aV, markerDiameters as aW, maskToIndices as aX, mutedFromSelection as aY, omicsLayout as aZ, openAnnotations as a_, buildOmicsTraces as aa, capabilitiesOf as ab, clearSamModelCache as ac, contrastWindow as ad, contributedPlotTypeOption as ae, countMask as af, cropImage as ag, datasetFromManifest as ah, decodeColumn as ai, decodeCoords as aj, decodeFeatureVector as ak, decodePolygons as al, decodeRadius as am, defaultPresetSet as an, dialogToolContributions as ao, emptySelection as ap, encodeCategorical as aq, encodeContinuous as ar, expandZips as as, findColumnMeta as at, getDefaultSamModelId as au, getPlotTypeDescriptor as av, getSamModel as aw, hasZarrMetadata as ax, hydrateBounds as ay, importFile as az, ALL_TOOLBAR_TOOLS as b, openHttpRegionSource as b0, parsePresetSet as b1, planRegionImport as b2, pointInRing as b3, provideVisualization as b4, readAnnotationSlices as b5, readLabelVolume as b6, readOmeZarrLabels as b7, readZip as b8, rendererOwnsWheel as b9, resolveCategoryColors as ba, selectByCategory as bb, selectInRegions as bc, setDefaultSamModel as bd, setOrtWasmBase as be, setSamModelUrls as bf, sortToolContributions as bg, visibleToolContributions as bh, 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, INSTANCE_SEGMENTER as n, ImageConverterService as o, MultiPolygon as p, NAPARI_DEFAULT_DECIMATE as q, runEncoder as r, NO_CATEGORY as s, NO_OBSERVATION as t, PLOT_MODE_SESSION as u, PLOT_TYPE_CONTRIBUTIONS as v, PLOT_TYPE_DESCRIPTORS as w, PREFERENCES_PORT as x, PlotType as y, Polygon as z };
49300
- //# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DURPqs5a.mjs.map
50045
+ 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 };
50046
+ //# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DvFp1jNH.mjs.map