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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/esm2022/lib/contracts/display-types.mjs +2 -1
  2. package/esm2022/lib/implementations/napari-js/napari-loading-state.mjs +1 -1
  3. package/esm2022/lib/implementations/napari-js/napari-spatial-2d-scene.mjs +10 -8
  4. package/esm2022/lib/implementations/napari-js/napari-spatial-3d-scene.mjs +25 -7
  5. package/esm2022/lib/implementations/napari-js/napari-spatial-3d-transcripts.mjs +269 -0
  6. package/esm2022/lib/implementations/napari-js/napari-spatial-encoding.mjs +11 -4
  7. package/esm2022/lib/implementations/napari-js/napari-spatial-tiles.mjs +6 -7
  8. package/esm2022/lib/implementations/napari-js/napari-volume-region-meshes.mjs +33 -8
  9. package/esm2022/lib/implementations/napari-js/napari-volume-scene.mjs +6 -2
  10. package/esm2022/lib/implementations/napari-js/spatial-tiles/cell-layers.mjs +10 -6
  11. package/esm2022/lib/implementations/napari-js/spatial-tiles/transcript-layers.mjs +9 -5
  12. package/esm2022/lib/spatial/spatial-controls.facade.mjs +7 -4
  13. package/esm2022/lib/spatial/spatial-encoding.mjs +18 -1
  14. package/esm2022/lib/spatial-controls/spatial-charts/spatial-charts.component.mjs +6 -2
  15. package/esm2022/lib/spatial-controls/spatial-controls.component.mjs +7 -3
  16. package/esm2022/lib/spatial-controls/spatial-gene-style/spatial-gene-style.component.mjs +7 -3
  17. package/esm2022/lib/spatial-controls/spatial-gene-tree/spatial-gene-tree.component.mjs +21 -6
  18. package/esm2022/lib/spatial-controls/spatial-groups-panel/spatial-groups-panel.component.mjs +61 -8
  19. package/esm2022/lib/spatial-controls/spatial-observations-panel/spatial-observations-panel.component.mjs +3 -3
  20. package/fesm2022/{jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-CYsPzX3J.mjs → jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DHrHO2ay.mjs} +533 -113
  21. package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DHrHO2ay.mjs.map +1 -0
  22. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-DaZVXq1s.mjs → jax-data-science-sci-image-visualizer-onnx-sam-session-CKEUwNta.mjs} +2 -2
  23. package/fesm2022/{jax-data-science-sci-image-visualizer-onnx-sam-session-DaZVXq1s.mjs.map → jax-data-science-sci-image-visualizer-onnx-sam-session-CKEUwNta.mjs.map} +1 -1
  24. package/fesm2022/jax-data-science-sci-image-visualizer.mjs +1 -1
  25. package/lib/contracts/display-types.d.ts +9 -2
  26. package/lib/implementations/napari-js/napari-loading-state.d.ts +1 -1
  27. package/lib/implementations/napari-js/napari-spatial-2d-scene.d.ts +3 -3
  28. package/lib/implementations/napari-js/napari-spatial-3d-scene.d.ts +2 -0
  29. package/lib/implementations/napari-js/napari-spatial-3d-transcripts.d.ts +62 -0
  30. package/lib/implementations/napari-js/napari-spatial-encoding.d.ts +9 -2
  31. package/lib/implementations/napari-js/napari-spatial-tiles.d.ts +5 -6
  32. package/lib/implementations/napari-js/napari-volume-region-meshes.d.ts +16 -3
  33. package/lib/implementations/napari-js/spatial-tiles/cell-layers.d.ts +1 -1
  34. package/lib/spatial/spatial-encoding.d.ts +10 -0
  35. package/lib/spatial-controls/spatial-controls.component.d.ts +2 -0
  36. package/lib/spatial-controls/spatial-gene-style/spatial-gene-style.component.d.ts +3 -1
  37. package/lib/spatial-controls/spatial-gene-tree/spatial-gene-tree.component.d.ts +7 -0
  38. package/lib/spatial-controls/spatial-groups-panel/spatial-groups-panel.component.d.ts +14 -3
  39. package/package.json +2 -2
  40. package/fesm2022/jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-CYsPzX3J.mjs.map +0 -1
@@ -729,6 +729,7 @@ const DEFAULT_SPATIAL_VIEW = {
729
729
  transcriptGenes: [],
730
730
  transcriptHiddenGenes: [],
731
731
  transcriptGeneColors: {},
732
+ groupColors: {},
732
733
  transcriptGeneGroups: [],
733
734
  transcriptAllGenes: false,
734
735
  transcriptAllGenesHidden: [],
@@ -2831,6 +2832,23 @@ function resolveCategoryColors(meta, palette = DEFAULT_CATEGORICAL_PALETTE) {
2831
2832
  return fallbackColorFor(name, palette);
2832
2833
  });
2833
2834
  }
2835
+ /**
2836
+ * Categorical column `column`'s colours ({@link resolveCategoryColors}) with the user's choices
2837
+ * for its groups over them. Every place a grouping is coloured goes through this, so a group
2838
+ * recoloured in the list is that colour on the cells, the markers, the legend and the charts.
2839
+ */
2840
+ function categoryColorsFor(column, meta, overrides) {
2841
+ const base = resolveCategoryColors(meta);
2842
+ const own = overrides?.[column];
2843
+ if (!own)
2844
+ return base;
2845
+ return meta.categories.map((name, i) => own[name] || base[i]);
2846
+ }
2847
+ /** What a cache keyed on a grouping's colours must include of the user's choices for it. */
2848
+ function groupColorsKey(column, overrides) {
2849
+ const own = column ? overrides?.[column] : undefined;
2850
+ return own ? JSON.stringify(own) : '';
2851
+ }
2834
2852
  /** Categorical column → flat per-point RGBA (length `4 * codes.length`). */
2835
2853
  function encodeCategorical(codes, opts) {
2836
2854
  const n = codes.length;
@@ -10580,7 +10598,7 @@ function frameToRgba(cached, frameIndex) {
10580
10598
  /** Lazy-import so onnxruntime-web is never pulled into unit tests or the
10581
10599
  * initial bundle — only when segmentation actually runs. */
10582
10600
  const onnxSessionFactory = async () => {
10583
- const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-DaZVXq1s.mjs');
10601
+ const { OnnxSamSession } = await import('./jax-data-science-sci-image-visualizer-onnx-sam-session-CKEUwNta.mjs');
10584
10602
  return new OnnxSamSession();
10585
10603
  };
10586
10604
  /** Thrown inside a load that a model switch superseded. */
@@ -20979,6 +20997,8 @@ class SpatialChartsComponent {
20979
20997
  .pipe(takeUntilDestroyed(this.destroyRef))
20980
20998
  .subscribe(([view, selection]) => {
20981
20999
  const sourceChanged = view.colorBy?.kind !== this.view.colorBy?.kind || view.colorBy?.name !== this.view.colorBy?.name;
21000
+ // A group recoloured in the Cells panel: the categories are fetched again in their new colours.
21001
+ const groupColorsChanged = this.primed && JSON.stringify(view.groupColors) !== JSON.stringify(this.view.groupColors);
20982
21002
  this.view = view;
20983
21003
  this.colorBy = view.colorBy;
20984
21004
  this.selection = selection;
@@ -20987,9 +21007,11 @@ class SpatialChartsComponent {
20987
21007
  // needs its vector fetched first. The FIRST emission always reloads:
20988
21008
  // otherwise `null -> null` reads as "unchanged" and the component sits
20989
21009
  // with no data and no explanation until something else moves.
20990
- if (!this.primed || sourceChanged) {
21010
+ if (!this.primed || sourceChanged || groupColorsChanged) {
20991
21011
  this.primed = true;
20992
21012
  void this.reload();
21013
+ if (groupColorsChanged && this.groupBy)
21014
+ void this.onGroupBy(this.groupBy);
20993
21015
  }
20994
21016
  else {
20995
21017
  // A selection or log-scale change needs only a redraw of the same vector.
@@ -22724,13 +22746,72 @@ function colormapNodeFor(options, value) {
22724
22746
  return null;
22725
22747
  }
22726
22748
 
22749
+ /**
22750
+ * One gene's icon and colour: the icon grid, a colour picker with its hex value, presets, and
22751
+ * a reset — the content of a popover its host opens on the gene's swatch. Writes nothing itself:
22752
+ * it reports the choice, and the host stores it where it belongs.
22753
+ */
22754
+ class SpatialGeneStyleComponent {
22755
+ constructor() {
22756
+ /** The gene, for the controls' labels. */
22757
+ this.gene = '';
22758
+ /** What the icons and colour are of ("Observation", "Transcript"), for the titles. */
22759
+ this.what = '';
22760
+ /** The gene's colour, `#rrggbb`. */
22761
+ this.color = '#ffffff';
22762
+ /** The gene's icon. */
22763
+ this.glyph = 'circle';
22764
+ /** A note under the icons, e.g. where they show; none when null. */
22765
+ this.iconHint = null;
22766
+ /** False for a colour alone (a cell group, or every gene drawn with one icon): no icon grid. */
22767
+ this.showIcons = true;
22768
+ this.colorChange = new EventEmitter();
22769
+ this.glyphChange = new EventEmitter();
22770
+ /** Back to the gene's default icon and colour. */
22771
+ this.resetStyle = new EventEmitter();
22772
+ this.glyphOptions = GLYPH_OPTIONS;
22773
+ this.colorPresets = DEFAULT_CATEGORICAL_PALETTE.slice(0, 10).map((c) => c.toLowerCase());
22774
+ }
22775
+ /** A typed hex colour, accepted as `#rrggbb` or `rrggbb`; anything else is ignored. */
22776
+ onHex(text) {
22777
+ const t = text.trim();
22778
+ const rgb = /^#?[0-9a-f]{6}$/i.test(t) ? parseCssColor(t) : null;
22779
+ if (rgb)
22780
+ this.colorChange.emit(rgbToHex(rgb));
22781
+ }
22782
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneStyleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
22783
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGeneStyleComponent, isStandalone: true, selector: "spatial-gene-style", inputs: { gene: "gene", what: "what", color: "color", glyph: "glyph", iconHint: "iconHint", showIcons: "showIcons" }, outputs: { colorChange: "colorChange", glyphChange: "glyphChange", resetStyle: "resetStyle" }, ngImport: i0, template: "<div class=\"sc-style-pop\">\n <ng-container *ngIf=\"showIcons\">\n <div class=\"sc-style-title\">{{ what }} Icon</div>\n <div class=\"sc-style-icons\" role=\"radiogroup\" [attr.aria-label]=\"'Icon for ' + gene\">\n <button\n type=\"button\"\n *ngFor=\"let opt of glyphOptions\"\n class=\"sc-style-icon\"\n role=\"radio\"\n [attr.aria-checked]=\"opt.value === glyph\"\n [class.active]=\"opt.value === glyph\"\n [title]=\"opt.label\"\n (click)=\"glyphChange.emit(opt.value)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"opt.points\"></polygon>\n </svg>\n </button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"iconHint\">{{ iconHint }}</small>\n </ng-container>\n <div class=\"sc-style-title\">{{ what }} Color</div>\n <div class=\"sc-style-color\">\n <label class=\"sc-style-swatch\" [style.background]=\"color\" title=\"Pick a colour\">\n <input type=\"color\" [value]=\"color\" (change)=\"colorChange.emit($any($event.target).value)\" />\n </label>\n <input\n pInputText\n class=\"sc-style-hex\"\n [value]=\"color\"\n maxlength=\"7\"\n [attr.aria-label]=\"'Hex colour for ' + gene\"\n (change)=\"onHex($any($event.target).value)\"\n (keydown.enter)=\"onHex($any($event.target).value)\"\n />\n </div>\n <div class=\"sc-style-title sc-style-sub\">Presets</div>\n <div class=\"sc-style-presets\">\n <button\n type=\"button\"\n *ngFor=\"let c of colorPresets\"\n class=\"sc-style-preset\"\n [style.background]=\"c\"\n [class.active]=\"c === color.toLowerCase()\"\n [title]=\"c\"\n [attr.aria-label]=\"'Colour ' + c\"\n (click)=\"colorChange.emit(c)\"\n ></button>\n </div>\n <p-button\n label=\"Reset to default\"\n icon=\"pi pi-refresh\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"sc-style-reset\"\n (onClick)=\"resetStyle.emit()\"\n ></p-button>\n</div>\n", styles: ["::ng-deep .sc-gene-style .p-overlaypanel-content{padding:.75rem}.sc-style-pop{display:flex;flex-direction:column;gap:.5rem;width:15rem}.sc-style-title{font-weight:600;font-size:.9rem}.sc-style-sub{font-size:.75rem;text-transform:uppercase;opacity:.7}.sc-style-icons,.sc-style-presets{display:grid;grid-template-columns:repeat(5,1fr);gap:.35rem}.sc-style-icon{display:inline-flex;align-items:center;justify-content:center;height:1.9rem;border:1px solid var(--surface-border, rgba(0, 0, 0, .15));border-radius:4px;background:transparent;color:var(--text-color, inherit);cursor:pointer}.sc-style-icon .sc-glyph-svg{width:1.1rem;height:1.1rem}.sc-style-icon.active{border-color:var(--primary-color, #3b82f6);background:var(--highlight-bg, rgba(59, 130, 246, .12))}.sc-style-color{display:flex;gap:.5rem;align-items:center}.sc-style-swatch{position:relative;flex:0 0 3rem;height:2rem;border-radius:4px;border:1px solid rgba(0,0,0,.2);cursor:pointer}.sc-style-swatch input{position:absolute;inset:0;opacity:0;width:100%;height:100%;cursor:pointer}.sc-style-hex{flex:1 1 auto;min-width:0;font-family:monospace}.sc-style-preset{width:1.4rem;height:1.4rem;justify-self:center;border-radius:50%;border:2px solid transparent;cursor:pointer}.sc-style-preset.active{border-color:var(--text-color, #000)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
22784
+ }
22785
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneStyleComponent, decorators: [{
22786
+ type: Component,
22787
+ args: [{ selector: 'spatial-gene-style', standalone: true, imports: [CommonModule, ButtonModule, InputTextModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"sc-style-pop\">\n <ng-container *ngIf=\"showIcons\">\n <div class=\"sc-style-title\">{{ what }} Icon</div>\n <div class=\"sc-style-icons\" role=\"radiogroup\" [attr.aria-label]=\"'Icon for ' + gene\">\n <button\n type=\"button\"\n *ngFor=\"let opt of glyphOptions\"\n class=\"sc-style-icon\"\n role=\"radio\"\n [attr.aria-checked]=\"opt.value === glyph\"\n [class.active]=\"opt.value === glyph\"\n [title]=\"opt.label\"\n (click)=\"glyphChange.emit(opt.value)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"opt.points\"></polygon>\n </svg>\n </button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"iconHint\">{{ iconHint }}</small>\n </ng-container>\n <div class=\"sc-style-title\">{{ what }} Color</div>\n <div class=\"sc-style-color\">\n <label class=\"sc-style-swatch\" [style.background]=\"color\" title=\"Pick a colour\">\n <input type=\"color\" [value]=\"color\" (change)=\"colorChange.emit($any($event.target).value)\" />\n </label>\n <input\n pInputText\n class=\"sc-style-hex\"\n [value]=\"color\"\n maxlength=\"7\"\n [attr.aria-label]=\"'Hex colour for ' + gene\"\n (change)=\"onHex($any($event.target).value)\"\n (keydown.enter)=\"onHex($any($event.target).value)\"\n />\n </div>\n <div class=\"sc-style-title sc-style-sub\">Presets</div>\n <div class=\"sc-style-presets\">\n <button\n type=\"button\"\n *ngFor=\"let c of colorPresets\"\n class=\"sc-style-preset\"\n [style.background]=\"c\"\n [class.active]=\"c === color.toLowerCase()\"\n [title]=\"c\"\n [attr.aria-label]=\"'Colour ' + c\"\n (click)=\"colorChange.emit(c)\"\n ></button>\n </div>\n <p-button\n label=\"Reset to default\"\n icon=\"pi pi-refresh\"\n size=\"small\"\n [text]=\"true\"\n styleClass=\"sc-style-reset\"\n (onClick)=\"resetStyle.emit()\"\n ></p-button>\n</div>\n", styles: ["::ng-deep .sc-gene-style .p-overlaypanel-content{padding:.75rem}.sc-style-pop{display:flex;flex-direction:column;gap:.5rem;width:15rem}.sc-style-title{font-weight:600;font-size:.9rem}.sc-style-sub{font-size:.75rem;text-transform:uppercase;opacity:.7}.sc-style-icons,.sc-style-presets{display:grid;grid-template-columns:repeat(5,1fr);gap:.35rem}.sc-style-icon{display:inline-flex;align-items:center;justify-content:center;height:1.9rem;border:1px solid var(--surface-border, rgba(0, 0, 0, .15));border-radius:4px;background:transparent;color:var(--text-color, inherit);cursor:pointer}.sc-style-icon .sc-glyph-svg{width:1.1rem;height:1.1rem}.sc-style-icon.active{border-color:var(--primary-color, #3b82f6);background:var(--highlight-bg, rgba(59, 130, 246, .12))}.sc-style-color{display:flex;gap:.5rem;align-items:center}.sc-style-swatch{position:relative;flex:0 0 3rem;height:2rem;border-radius:4px;border:1px solid rgba(0,0,0,.2);cursor:pointer}.sc-style-swatch input{position:absolute;inset:0;opacity:0;width:100%;height:100%;cursor:pointer}.sc-style-hex{flex:1 1 auto;min-width:0;font-family:monospace}.sc-style-preset{width:1.4rem;height:1.4rem;justify-self:center;border-radius:50%;border:2px solid transparent;cursor:pointer}.sc-style-preset.active{border-color:var(--text-color, #000)}\n"] }]
22788
+ }], propDecorators: { gene: [{
22789
+ type: Input
22790
+ }], what: [{
22791
+ type: Input
22792
+ }], color: [{
22793
+ type: Input
22794
+ }], glyph: [{
22795
+ type: Input
22796
+ }], iconHint: [{
22797
+ type: Input
22798
+ }], showIcons: [{
22799
+ type: Input
22800
+ }], colorChange: [{
22801
+ type: Output
22802
+ }], glyphChange: [{
22803
+ type: Output
22804
+ }], resetStyle: [{
22805
+ type: Output
22806
+ }] } });
22807
+
22727
22808
  /**
22728
22809
  * The cells' grouping, as Xenium Explorer's Groups section: the group picker (columns under
22729
22810
  * their section heading, a k-means family once, with its k beside it), CSV import of a new
22730
- * grouping, and the grouping's categories with their colours, cell counts and show/hide
22731
- * switches.
22811
+ * grouping, and the grouping's categories with their colours (each one's swatch opens a colour
22812
+ * picker), cell counts and show/hide switches.
22732
22813
  *
22733
- * Writes only through `controls.setViewState` (`cellTypeColumn`, `hiddenGroups`).
22814
+ * Writes only through `controls.setViewState` (`cellTypeColumn`, `hiddenGroups`, `groupColors`).
22734
22815
  */
22735
22816
  class SpatialGroupsPanelComponent {
22736
22817
  constructor(zone, cdr) {
@@ -22763,6 +22844,8 @@ class SpatialGroupsPanelComponent {
22763
22844
  this.groupRowsLoad = new Supersede();
22764
22845
  this.groupImportError = null;
22765
22846
  this.groupImporting = false;
22847
+ /** The group the open colour picker edits. */
22848
+ this.styleGroup = null;
22766
22849
  /** {@link SpatialViewState.hiddenGroups} as a set, for the per-row checks. */
22767
22850
  this.hiddenGroups = new Set();
22768
22851
  this.trackByLabel = (_i, row) => row.label;
@@ -22817,6 +22900,43 @@ class SpatialGroupsPanelComponent {
22817
22900
  onAllGroupsShown(on) {
22818
22901
  this.controls?.setViewState({ hiddenGroups: on ? [] : this.groupRows.map((r) => r.label) });
22819
22902
  }
22903
+ // ── per-group colour picker ──────────────────────────────────────────────
22904
+ openGroupStyle(event, label, panel) {
22905
+ if (this.styleGroup === label) {
22906
+ panel.toggle(event);
22907
+ return;
22908
+ }
22909
+ this.styleGroup = label;
22910
+ panel.hide();
22911
+ // Re-open anchored on the row that was clicked, once the panel has closed.
22912
+ setTimeout(() => panel.toggle(event));
22913
+ }
22914
+ /** The colour group `label` is drawn in now (the user's, else the column's or the palette's). */
22915
+ groupColorOf(label) {
22916
+ return this.groupRows.find((r) => r.label === label)?.color ?? '#999999';
22917
+ }
22918
+ onGroupColor(label, hex) {
22919
+ const column = this.activeCellTypeColumn;
22920
+ if (!column)
22921
+ return;
22922
+ const all = this.view.groupColors ?? {};
22923
+ this.controls?.setViewState({ groupColors: { ...all, [column]: { ...all[column], [label]: hex } } });
22924
+ }
22925
+ /** Back to the group's own colour (the column's, else the palette's). */
22926
+ resetGroupColor(label) {
22927
+ const column = this.activeCellTypeColumn;
22928
+ const own = column ? this.view.groupColors?.[column] : undefined;
22929
+ if (!column || !own || !(label in own))
22930
+ return;
22931
+ const rest = { ...own };
22932
+ delete rest[label];
22933
+ const all = { ...this.view.groupColors };
22934
+ if (Object.keys(rest).length)
22935
+ all[column] = rest;
22936
+ else
22937
+ delete all[column];
22938
+ this.controls?.setViewState({ groupColors: all });
22939
+ }
22820
22940
  get canImportGroups() {
22821
22941
  return !!this.controls?.importGroups;
22822
22942
  }
@@ -22859,9 +22979,11 @@ class SpatialGroupsPanelComponent {
22859
22979
  this.groupRowsFor = null;
22860
22980
  return;
22861
22981
  }
22862
- if (name === this.groupRowsFor)
22982
+ // The user's colours for the grouping are part of what the rows show.
22983
+ const key = `${name}\u0001${groupColorsKey(name, this.view.groupColors)}`;
22984
+ if (key === this.groupRowsFor)
22863
22985
  return;
22864
- this.groupRowsFor = name;
22986
+ this.groupRowsFor = key;
22865
22987
  const task = this.groupRowsLoad.next();
22866
22988
  try {
22867
22989
  const v = await this.controls.categoricalView(name);
@@ -22888,11 +23010,19 @@ class SpatialGroupsPanelComponent {
22888
23010
  });
22889
23011
  }
22890
23012
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGroupsPanelComponent, deps: [{ token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
22891
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGroupsPanelComponent, isStandalone: true, selector: "spatial-groups-panel", inputs: { controls: "controls", dataset: "dataset", view: "view" }, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"groupOptions.length\">\n <div class=\"sc-subhead\">Groups</div>\n <div class=\"sc-row sc-row-picker\">\n <p-dropdown\n [options]=\"groupOptions\"\n [group]=\"true\"\n optionLabel=\"label\"\n optionValue=\"value\"\n optionGroupLabel=\"label\"\n optionGroupChildren=\"items\"\n [ngModel]=\"activeGroupEntry\"\n (ngModelChange)=\"onGroupEntry($event)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n >\n <ng-template let-group pTemplate=\"group\">\n <span class=\"sc-group-heading\">{{ group.label }}</span>\n </ng-template>\n </p-dropdown>\n <p-dropdown\n *ngIf=\"groupVariantOptions.length\"\n [options]=\"groupVariantOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n styleClass=\"sc-variant\"\n [ngModel]=\"activeCellTypeColumn\"\n (ngModelChange)=\"onGroupVariant($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n <button\n *ngIf=\"canImportGroups\"\n type=\"button\"\n class=\"sc-icon-btn\"\n [disabled]=\"groupImporting\"\n (click)=\"groupFile.click()\"\n title=\"Import cell groups (CSV of cell_id, group)\"\n >\n <i\n class=\"pi\"\n [class.pi-plus-circle]=\"!groupImporting\"\n [class.pi-spin]=\"groupImporting\"\n [class.pi-spinner]=\"groupImporting\"\n ></i>\n </button>\n <input #groupFile type=\"file\" accept=\".csv,.tsv,.txt\" hidden (change)=\"onImportGroupsFile(groupFile)\" />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"groupImportError\">{{ groupImportError }}</small>\n\n <div class=\"sc-group-list\" *ngIf=\"groupRows.length\">\n <div class=\"sc-group-row sc-group-all\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"allGroupsShown\"\n (onChange)=\"onAllGroupsShown($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-group-label\">All groups</span>\n <span class=\"sc-count\">({{ groupTotal | number }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"groupsExpanded = !groupsExpanded\">\n <i class=\"pi\" [class.pi-chevron-down]=\"groupsExpanded\" [class.pi-chevron-right]=\"!groupsExpanded\"></i>\n </button>\n </div>\n <ng-container *ngIf=\"groupsExpanded\">\n <div class=\"sc-group-row\" *ngFor=\"let row of groupRows; trackBy: trackByLabel\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isGroupShown(row.label)\"\n (onChange)=\"onGroupShown(row.label, $event.checked)\"\n ></p-checkbox>\n <span class=\"sc-dot\" [style.background]=\"row.color\"></span>\n <span class=\"sc-group-label\" [title]=\"row.label\">{{ row.label }}</span>\n <span class=\"sc-count\">({{ row.count | number }})</span>\n </div>\n </ng-container>\n </div>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-dot{flex:0 0 auto;width:.7rem;height:.7rem;border-radius:50%;border:1px solid rgba(0,0,0,.2)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i5$2.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23013
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGroupsPanelComponent, isStandalone: true, selector: "spatial-groups-panel", inputs: { controls: "controls", dataset: "dataset", view: "view" }, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"groupOptions.length\">\n <div class=\"sc-subhead\">Groups</div>\n <div class=\"sc-row sc-row-picker\">\n <p-dropdown\n [options]=\"groupOptions\"\n [group]=\"true\"\n optionLabel=\"label\"\n optionValue=\"value\"\n optionGroupLabel=\"label\"\n optionGroupChildren=\"items\"\n [ngModel]=\"activeGroupEntry\"\n (ngModelChange)=\"onGroupEntry($event)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n >\n <ng-template let-group pTemplate=\"group\">\n <span class=\"sc-group-heading\">{{ group.label }}</span>\n </ng-template>\n </p-dropdown>\n <p-dropdown\n *ngIf=\"groupVariantOptions.length\"\n [options]=\"groupVariantOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n styleClass=\"sc-variant\"\n [ngModel]=\"activeCellTypeColumn\"\n (ngModelChange)=\"onGroupVariant($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n <button\n *ngIf=\"canImportGroups\"\n type=\"button\"\n class=\"sc-icon-btn\"\n [disabled]=\"groupImporting\"\n (click)=\"groupFile.click()\"\n title=\"Import cell groups (CSV of cell_id, group)\"\n >\n <i\n class=\"pi\"\n [class.pi-plus-circle]=\"!groupImporting\"\n [class.pi-spin]=\"groupImporting\"\n [class.pi-spinner]=\"groupImporting\"\n ></i>\n </button>\n <input #groupFile type=\"file\" accept=\".csv,.tsv,.txt\" hidden (change)=\"onImportGroupsFile(groupFile)\" />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"groupImportError\">{{ groupImportError }}</small>\n\n <div class=\"sc-group-list\" *ngIf=\"groupRows.length\">\n <div class=\"sc-group-row sc-group-all\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"allGroupsShown\"\n (onChange)=\"onAllGroupsShown($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-group-label\">All groups</span>\n <span class=\"sc-count\">({{ groupTotal | number }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"groupsExpanded = !groupsExpanded\">\n <i class=\"pi\" [class.pi-chevron-down]=\"groupsExpanded\" [class.pi-chevron-right]=\"!groupsExpanded\"></i>\n </button>\n </div>\n <ng-container *ngIf=\"groupsExpanded\">\n <div class=\"sc-group-row\" *ngFor=\"let row of groupRows; trackBy: trackByLabel\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isGroupShown(row.label)\"\n (onChange)=\"onGroupShown(row.label, $event.checked)\"\n ></p-checkbox>\n <!-- The group's colour; clicking it opens the colour picker. -->\n <button\n type=\"button\"\n class=\"sc-dot sc-dot-btn\"\n [style.background]=\"row.color\"\n [attr.aria-label]=\"'Colour for ' + row.label\"\n title=\"Colour\"\n (click)=\"openGroupStyle($event, row.label, groupPop)\"\n ></button>\n <span class=\"sc-group-label\" [title]=\"row.label\">{{ row.label }}</span>\n <span class=\"sc-count\">({{ row.count | number }})</span>\n </div>\n </ng-container>\n </div>\n <!-- One picker for every group row, filled for the group that opened it. -->\n <p-overlayPanel #groupPop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGroup as group\"\n [gene]=\"group\"\n what=\"Group\"\n [color]=\"groupColorOf(group)\"\n [showIcons]=\"false\"\n (colorChange)=\"onGroupColor(group, $event)\"\n (resetStyle)=\"resetGroupColor(group)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-dot{flex:0 0 auto;width:.7rem;height:.7rem;border-radius:50%;border:1px solid rgba(0,0,0,.2)}.sc-dot-btn{padding:0;cursor:pointer}.sc-dot-btn:hover,.sc-dot-btn:focus-visible{outline:2px solid var(--surface-border, rgba(0, 0, 0, .35));outline-offset:1px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i5$2.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "component", type: SpatialGeneStyleComponent, selector: "spatial-gene-style", inputs: ["gene", "what", "color", "glyph", "iconHint", "showIcons"], outputs: ["colorChange", "glyphChange", "resetStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
22892
23014
  }
22893
23015
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGroupsPanelComponent, decorators: [{
22894
23016
  type: Component,
22895
- args: [{ selector: 'spatial-groups-panel', standalone: true, imports: [CommonModule, FormsModule, SharedModule, CheckboxModule, DropdownModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"groupOptions.length\">\n <div class=\"sc-subhead\">Groups</div>\n <div class=\"sc-row sc-row-picker\">\n <p-dropdown\n [options]=\"groupOptions\"\n [group]=\"true\"\n optionLabel=\"label\"\n optionValue=\"value\"\n optionGroupLabel=\"label\"\n optionGroupChildren=\"items\"\n [ngModel]=\"activeGroupEntry\"\n (ngModelChange)=\"onGroupEntry($event)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n >\n <ng-template let-group pTemplate=\"group\">\n <span class=\"sc-group-heading\">{{ group.label }}</span>\n </ng-template>\n </p-dropdown>\n <p-dropdown\n *ngIf=\"groupVariantOptions.length\"\n [options]=\"groupVariantOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n styleClass=\"sc-variant\"\n [ngModel]=\"activeCellTypeColumn\"\n (ngModelChange)=\"onGroupVariant($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n <button\n *ngIf=\"canImportGroups\"\n type=\"button\"\n class=\"sc-icon-btn\"\n [disabled]=\"groupImporting\"\n (click)=\"groupFile.click()\"\n title=\"Import cell groups (CSV of cell_id, group)\"\n >\n <i\n class=\"pi\"\n [class.pi-plus-circle]=\"!groupImporting\"\n [class.pi-spin]=\"groupImporting\"\n [class.pi-spinner]=\"groupImporting\"\n ></i>\n </button>\n <input #groupFile type=\"file\" accept=\".csv,.tsv,.txt\" hidden (change)=\"onImportGroupsFile(groupFile)\" />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"groupImportError\">{{ groupImportError }}</small>\n\n <div class=\"sc-group-list\" *ngIf=\"groupRows.length\">\n <div class=\"sc-group-row sc-group-all\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"allGroupsShown\"\n (onChange)=\"onAllGroupsShown($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-group-label\">All groups</span>\n <span class=\"sc-count\">({{ groupTotal | number }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"groupsExpanded = !groupsExpanded\">\n <i class=\"pi\" [class.pi-chevron-down]=\"groupsExpanded\" [class.pi-chevron-right]=\"!groupsExpanded\"></i>\n </button>\n </div>\n <ng-container *ngIf=\"groupsExpanded\">\n <div class=\"sc-group-row\" *ngFor=\"let row of groupRows; trackBy: trackByLabel\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isGroupShown(row.label)\"\n (onChange)=\"onGroupShown(row.label, $event.checked)\"\n ></p-checkbox>\n <span class=\"sc-dot\" [style.background]=\"row.color\"></span>\n <span class=\"sc-group-label\" [title]=\"row.label\">{{ row.label }}</span>\n <span class=\"sc-count\">({{ row.count | number }})</span>\n </div>\n </ng-container>\n </div>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-dot{flex:0 0 auto;width:.7rem;height:.7rem;border-radius:50%;border:1px solid rgba(0,0,0,.2)}\n"] }]
23017
+ args: [{ selector: 'spatial-groups-panel', standalone: true, imports: [
23018
+ CommonModule,
23019
+ FormsModule,
23020
+ SharedModule,
23021
+ CheckboxModule,
23022
+ DropdownModule,
23023
+ OverlayPanelModule,
23024
+ SpatialGeneStyleComponent,
23025
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"groupOptions.length\">\n <div class=\"sc-subhead\">Groups</div>\n <div class=\"sc-row sc-row-picker\">\n <p-dropdown\n [options]=\"groupOptions\"\n [group]=\"true\"\n optionLabel=\"label\"\n optionValue=\"value\"\n optionGroupLabel=\"label\"\n optionGroupChildren=\"items\"\n [ngModel]=\"activeGroupEntry\"\n (ngModelChange)=\"onGroupEntry($event)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n >\n <ng-template let-group pTemplate=\"group\">\n <span class=\"sc-group-heading\">{{ group.label }}</span>\n </ng-template>\n </p-dropdown>\n <p-dropdown\n *ngIf=\"groupVariantOptions.length\"\n [options]=\"groupVariantOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n styleClass=\"sc-variant\"\n [ngModel]=\"activeCellTypeColumn\"\n (ngModelChange)=\"onGroupVariant($event)\"\n appendTo=\"body\"\n ></p-dropdown>\n <button\n *ngIf=\"canImportGroups\"\n type=\"button\"\n class=\"sc-icon-btn\"\n [disabled]=\"groupImporting\"\n (click)=\"groupFile.click()\"\n title=\"Import cell groups (CSV of cell_id, group)\"\n >\n <i\n class=\"pi\"\n [class.pi-plus-circle]=\"!groupImporting\"\n [class.pi-spin]=\"groupImporting\"\n [class.pi-spinner]=\"groupImporting\"\n ></i>\n </button>\n <input #groupFile type=\"file\" accept=\".csv,.tsv,.txt\" hidden (change)=\"onImportGroupsFile(groupFile)\" />\n </div>\n <small class=\"sc-hint sc-error\" *ngIf=\"groupImportError\">{{ groupImportError }}</small>\n\n <div class=\"sc-group-list\" *ngIf=\"groupRows.length\">\n <div class=\"sc-group-row sc-group-all\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"allGroupsShown\"\n (onChange)=\"onAllGroupsShown($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-group-label\">All groups</span>\n <span class=\"sc-count\">({{ groupTotal | number }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"groupsExpanded = !groupsExpanded\">\n <i class=\"pi\" [class.pi-chevron-down]=\"groupsExpanded\" [class.pi-chevron-right]=\"!groupsExpanded\"></i>\n </button>\n </div>\n <ng-container *ngIf=\"groupsExpanded\">\n <div class=\"sc-group-row\" *ngFor=\"let row of groupRows; trackBy: trackByLabel\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"isGroupShown(row.label)\"\n (onChange)=\"onGroupShown(row.label, $event.checked)\"\n ></p-checkbox>\n <!-- The group's colour; clicking it opens the colour picker. -->\n <button\n type=\"button\"\n class=\"sc-dot sc-dot-btn\"\n [style.background]=\"row.color\"\n [attr.aria-label]=\"'Colour for ' + row.label\"\n title=\"Colour\"\n (click)=\"openGroupStyle($event, row.label, groupPop)\"\n ></button>\n <span class=\"sc-group-label\" [title]=\"row.label\">{{ row.label }}</span>\n <span class=\"sc-count\">({{ row.count | number }})</span>\n </div>\n </ng-container>\n </div>\n <!-- One picker for every group row, filled for the group that opened it. -->\n <p-overlayPanel #groupPop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGroup as group\"\n [gene]=\"group\"\n what=\"Group\"\n [color]=\"groupColorOf(group)\"\n [showIcons]=\"false\"\n (colorChange)=\"onGroupColor(group, $event)\"\n (resetStyle)=\"resetGroupColor(group)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-dot{flex:0 0 auto;width:.7rem;height:.7rem;border-radius:50%;border:1px solid rgba(0,0,0,.2)}.sc-dot-btn{padding:0;cursor:pointer}.sc-dot-btn:hover,.sc-dot-btn:focus-visible{outline:2px solid var(--surface-border, rgba(0, 0, 0, .35));outline-offset:1px}\n"] }]
22896
23026
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { controls: [{
22897
23027
  type: Input
22898
23028
  }], dataset: [{
@@ -23324,61 +23454,6 @@ function clusterMarkers(rasters, slots, bin, area) {
23324
23454
  };
23325
23455
  }
23326
23456
 
23327
- /**
23328
- * One gene's icon and colour: the icon grid, a colour picker with its hex value, presets, and
23329
- * a reset — the content of a popover its host opens on the gene's swatch. Writes nothing itself:
23330
- * it reports the choice, and the host stores it where it belongs.
23331
- */
23332
- class SpatialGeneStyleComponent {
23333
- constructor() {
23334
- /** The gene, for the controls' labels. */
23335
- this.gene = '';
23336
- /** What the icons and colour are of ("Observation", "Transcript"), for the titles. */
23337
- this.what = '';
23338
- /** The gene's colour, `#rrggbb`. */
23339
- this.color = '#ffffff';
23340
- /** The gene's icon. */
23341
- this.glyph = 'circle';
23342
- /** A note under the icons, e.g. where they show; none when null. */
23343
- this.iconHint = null;
23344
- this.colorChange = new EventEmitter();
23345
- this.glyphChange = new EventEmitter();
23346
- /** Back to the gene's default icon and colour. */
23347
- this.resetStyle = new EventEmitter();
23348
- this.glyphOptions = GLYPH_OPTIONS;
23349
- this.colorPresets = DEFAULT_CATEGORICAL_PALETTE.slice(0, 10).map((c) => c.toLowerCase());
23350
- }
23351
- /** A typed hex colour, accepted as `#rrggbb` or `rrggbb`; anything else is ignored. */
23352
- onHex(text) {
23353
- const t = text.trim();
23354
- const rgb = /^#?[0-9a-f]{6}$/i.test(t) ? parseCssColor(t) : null;
23355
- if (rgb)
23356
- this.colorChange.emit(rgbToHex(rgb));
23357
- }
23358
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneStyleComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
23359
- 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 }); }
23360
- }
23361
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneStyleComponent, decorators: [{
23362
- type: Component,
23363
- 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"] }]
23364
- }], propDecorators: { gene: [{
23365
- type: Input
23366
- }], what: [{
23367
- type: Input
23368
- }], color: [{
23369
- type: Input
23370
- }], glyph: [{
23371
- type: Input
23372
- }], iconHint: [{
23373
- type: Input
23374
- }], colorChange: [{
23375
- type: Output
23376
- }], glyphChange: [{
23377
- type: Output
23378
- }], resetStyle: [{
23379
- type: Output
23380
- }] } });
23381
-
23382
23457
  /** Rows of the all-genes list drawn at once; the filter narrows the rest. */
23383
23458
  const ALL_GENES_ROWS = 200;
23384
23459
  /** Names in reading order: case aside, and `G9` before `G10`. */
@@ -23532,6 +23607,7 @@ class SpatialGeneTreeComponent {
23532
23607
  gene,
23533
23608
  shown: shown(gene),
23534
23609
  swatch: this.canHideAllGenes ? this.allGeneColorOf(gene, codes.get(gene)) : null,
23610
+ code: codes.get(gene),
23535
23611
  count: this.geneCountOf(gene),
23536
23612
  }));
23537
23613
  this.allGenesMatched = codes.size;
@@ -23636,6 +23712,18 @@ class SpatialGeneTreeComponent {
23636
23712
  geneColorOf(gene) {
23637
23713
  return this.geneColor(Math.max(0, this.view.transcriptGenes.indexOf(gene)));
23638
23714
  }
23715
+ /**
23716
+ * The colour the open picker shows: with "All genes", the gene's colour among every gene (its
23717
+ * chosen one, else the palette's by its code), as its row and its markers have it.
23718
+ */
23719
+ styleColorOf(gene) {
23720
+ if (this.view.transcriptAllGenes) {
23721
+ const row = this.allRows.find((r) => r.gene === gene);
23722
+ if (row)
23723
+ return this.allGeneColorOf(gene, row.code);
23724
+ }
23725
+ return this.geneColorOf(gene);
23726
+ }
23639
23727
  /** In Cluster colouring, a gene's swatch is its cluster's colour, as its markers are. */
23640
23728
  geneSwatchOf(gene) {
23641
23729
  if (this.view.transcriptColorBy !== 'cluster')
@@ -23661,9 +23749,11 @@ class SpatialGeneTreeComponent {
23661
23749
  }
23662
23750
  async refreshCellGroupColors() {
23663
23751
  const column = this.dataset ? cellTypeColumnFor(this.dataset, this.view) : null;
23664
- if (column === this.cellGroupColorsFor)
23752
+ // The user's colours for the grouping are part of what the cluster swatches show.
23753
+ const key = column === null ? null : `${column}\u0001${groupColorsKey(column, this.view.groupColors)}`;
23754
+ if (key === this.cellGroupColorsFor)
23665
23755
  return;
23666
- this.cellGroupColorsFor = column;
23756
+ this.cellGroupColorsFor = key;
23667
23757
  const task = this.cellGroupColorsLoad.next();
23668
23758
  const map = new Map();
23669
23759
  const meta = column ? this.dataset?.columns.find((c) => c.name === column) : null;
@@ -23689,7 +23779,7 @@ class SpatialGeneTreeComponent {
23689
23779
  });
23690
23780
  }
23691
23781
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneTreeComponent, deps: [{ token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
23692
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGeneTreeComponent, isStandalone: true, selector: "spatial-gene-tree", inputs: { controls: "controls", dataset: "dataset", view: "view", geneCounts: "geneCounts", geneTotal: "geneTotal" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"sc-subhead\">Selected genes</div>\n<div class=\"sc-gene-sort\" *ngIf=\"sortShown\" role=\"group\" aria-label=\"Sort genes\">\n <span class=\"sc-lbl\">Sort by</span>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"!byCount\"\n [attr.aria-pressed]=\"!byCount\"\n (click)=\"onGeneSort('name')\"\n >\n Name\n </button>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"byCount\"\n [attr.aria-pressed]=\"byCount\"\n title=\"Transcripts in view, most first\"\n (click)=\"onGeneSort('count')\"\n >\n Count\n </button>\n</div>\n<div class=\"sc-gene-tree\">\n <div class=\"sc-gene-row sc-gene-root\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n (click)=\"\n view.transcriptAllGenes ? onAllGenesShown(!rootShown) : onGenesShown(view.transcriptGenes, !rootShown)\n \"\n >\n <i class=\"pi\" [class.pi-eye]=\"rootShown\" [class.pi-eye-slash]=\"!rootShown\"></i>\n </button>\n <span class=\"sc-group-label\">Genes</span>\n <span class=\"sc-count\"\n >({{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{\n geneTotal | number\n }})</span\n >\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"geneTreeOpen = !geneTreeOpen\"\n *ngIf=\"!view.transcriptAllGenes && view.transcriptGenes.length\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"geneTreeOpen\" [class.pi-chevron-right]=\"!geneTreeOpen\"></i>\n </button>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"toggleAllGenesOpen()\"\n *ngIf=\"view.transcriptAllGenes && allGeneList?.length\"\n [attr.aria-label]=\"allGenesOpen ? 'Hide the gene list' : 'Show every gene'\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"allGenesOpen\" [class.pi-chevron-right]=\"!allGenesOpen\"></i>\n </button>\n </div>\n <!-- \"All genes\": every gene of the dataset, each with its eye when the dataset says which\n transcripts are whose (its gene list in code order). -->\n <ng-container *ngIf=\"allGenesOpen && view.transcriptAllGenes && allGeneList?.length\">\n <input\n pInputText\n class=\"sc-gene-filter\"\n placeholder=\"Filter genes\u2026\"\n aria-label=\"Filter genes\"\n [value]=\"allGenesFilter\"\n (input)=\"onAllGenesFilter($any($event.target).value)\"\n />\n <small class=\"sc-hint\" *ngIf=\"!canHideAllGenes\">\n This dataset does not say which transcripts are whose, so its genes cannot be hidden one by one.\n </small>\n <small class=\"sc-hint\" *ngIf=\"canHideAllGenes && view.transcriptMode === 'density'\">\n The density map draws every gene as one raster: genes are hidden one by one as circles or icons.\n </small>\n <div class=\"sc-gene-row sc-gene-leaf nested\" *ngFor=\"let row of allRows; trackBy: trackByAllRow\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n [disabled]=\"!canHideAllGenes || view.transcriptMode === 'density'\"\n (click)=\"onAllGeneShown(row.gene, !row.shown)\"\n >\n <i class=\"pi\" [class.pi-eye]=\"row.shown\" [class.pi-eye-slash]=\"!row.shown\"></i>\n </button>\n <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 sortedTree; trackBy: trackByNode\">\n <div class=\"sc-gene-row sc-gene-group\" *ngIf=\"node.name\">\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown(node.genes, !groupShown.get(node.name))\">\n <i\n class=\"pi\"\n [class.pi-eye]=\"groupShown.get(node.name)\"\n [class.pi-eye-slash]=\"!groupShown.get(node.name)\"\n ></i>\n </button>\n <span class=\"sc-group-label\" [title]=\"node.name\">{{ node.name }}</span>\n <span class=\"sc-count\">({{ node.genes.length }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"toggleGeneGroup(node.name)\">\n <i\n class=\"pi\"\n [class.pi-chevron-down]=\"!collapsedGeneGroups.has(node.name)\"\n [class.pi-chevron-right]=\"collapsedGeneGroups.has(node.name)\"\n ></i>\n </button>\n </div>\n <ng-container *ngIf=\"!node.name || !collapsedGeneGroups.has(node.name)\">\n <div\n class=\"sc-gene-row sc-gene-leaf\"\n [class.nested]=\"!!node.name\"\n *ngFor=\"let gene of node.genes; trackBy: trackByLabelString\"\n >\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown([gene], !rows.get(gene)?.shown)\">\n <i class=\"pi\" [class.pi-eye]=\"rows.get(gene)?.shown\" [class.pi-eye-slash]=\"!rows.get(gene)?.shown\"></i>\n </button>\n <!-- The gene's marker, in its colour: its icon in Icons mode, a dot for circles.\n Clicking it opens the icon and colour picker (as Xenium Explorer does). -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"rows.get(gene)?.swatch\"\n [attr.aria-label]=\"'Icon and colour for ' + gene\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon\n *ngIf=\"view.transcriptMode === 'glyphs'; else dotIcon\"\n [attr.points]=\"rows.get(gene)?.points\"\n ></polygon>\n <ng-template #dotIcon><circle r=\"0.75\"></circle></ng-template>\n </svg>\n </button>\n <span class=\"sc-group-label\">{{ gene }}</span>\n <span\n class=\"sc-gene-count\"\n *ngIf=\"rows.get(gene)?.count as n; else zeroCount\"\n title=\"Transcripts in view\"\n >{{ n | number }}</span\n >\n <ng-template #zeroCount>\n <span class=\"sc-gene-count\" *ngIf=\"geneCounts\" title=\"Transcripts in view\">0</span>\n </ng-template>\n </div>\n </ng-container>\n </ng-container>\n </ng-container>\n <!-- One picker for every gene row, filled for the gene that opened it. -->\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as gene\"\n [gene]=\"gene\"\n what=\"Transcript\"\n [color]=\"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-sort{display:flex;align-items:center;gap:.25rem;font-size:.8rem}.sc-gene-sort .sc-lbl{margin-right:auto}.sc-sort-btn{border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);color:var(--text-color, #333);border-radius:4px;padding:.1rem .5rem;font:inherit;cursor:pointer}.sc-sort-btn.active{border-color:var(--primary-color, #2196f3);color:var(--primary-color, #2196f3);font-weight:600}.sc-gene-filter{width:calc(100% - 1rem);margin:.2rem 0 .2rem 1rem;font-size:.8rem;padding:.2rem .4rem}.sc-gene-footer{text-align:right;font-size:.78rem;padding:.2rem 0;color:var(--text-color-secondary, #6c757d)}.sc-gene-count{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.8;padding-left:.5rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialGeneStyleComponent, selector: "spatial-gene-style", inputs: ["gene", "what", "color", "glyph", "iconHint"], outputs: ["colorChange", "glyphChange", "resetStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23782
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialGeneTreeComponent, isStandalone: true, selector: "spatial-gene-tree", inputs: { controls: "controls", dataset: "dataset", view: "view", geneCounts: "geneCounts", geneTotal: "geneTotal" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"sc-subhead\">Selected genes</div>\n<div class=\"sc-gene-sort\" *ngIf=\"sortShown\" role=\"group\" aria-label=\"Sort genes\">\n <span class=\"sc-lbl\">Sort by</span>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"!byCount\"\n [attr.aria-pressed]=\"!byCount\"\n (click)=\"onGeneSort('name')\"\n >\n Name\n </button>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"byCount\"\n [attr.aria-pressed]=\"byCount\"\n title=\"Transcripts in view, most first\"\n (click)=\"onGeneSort('count')\"\n >\n Count\n </button>\n</div>\n<div class=\"sc-gene-tree\">\n <div class=\"sc-gene-row sc-gene-root\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n (click)=\"\n view.transcriptAllGenes ? onAllGenesShown(!rootShown) : onGenesShown(view.transcriptGenes, !rootShown)\n \"\n >\n <i class=\"pi\" [class.pi-eye]=\"rootShown\" [class.pi-eye-slash]=\"!rootShown\"></i>\n </button>\n <span class=\"sc-group-label\">Genes</span>\n <span class=\"sc-count\"\n >({{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{\n geneTotal | number\n }})</span\n >\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"geneTreeOpen = !geneTreeOpen\"\n *ngIf=\"!view.transcriptAllGenes && view.transcriptGenes.length\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"geneTreeOpen\" [class.pi-chevron-right]=\"!geneTreeOpen\"></i>\n </button>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"toggleAllGenesOpen()\"\n *ngIf=\"view.transcriptAllGenes && allGeneList?.length\"\n [attr.aria-label]=\"allGenesOpen ? 'Hide the gene list' : 'Show every gene'\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"allGenesOpen\" [class.pi-chevron-right]=\"!allGenesOpen\"></i>\n </button>\n </div>\n <!-- \"All genes\": every gene of the dataset, each with its eye when the dataset says which\n transcripts are whose (its gene list in code order). -->\n <ng-container *ngIf=\"allGenesOpen && view.transcriptAllGenes && allGeneList?.length\">\n <input\n pInputText\n class=\"sc-gene-filter\"\n placeholder=\"Filter genes\u2026\"\n aria-label=\"Filter genes\"\n [value]=\"allGenesFilter\"\n (input)=\"onAllGenesFilter($any($event.target).value)\"\n />\n <small class=\"sc-hint\" *ngIf=\"!canHideAllGenes\">\n This dataset does not say which transcripts are whose, so its genes cannot be hidden one by one.\n </small>\n <small class=\"sc-hint\" *ngIf=\"canHideAllGenes && view.transcriptMode === 'density'\">\n The density map draws every gene as one raster: genes are hidden one by one as circles or icons.\n </small>\n <div class=\"sc-gene-row sc-gene-leaf nested\" *ngFor=\"let row of allRows; trackBy: trackByAllRow\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n [disabled]=\"!canHideAllGenes || view.transcriptMode === 'density'\"\n (click)=\"onAllGeneShown(row.gene, !row.shown)\"\n >\n <i class=\"pi\" [class.pi-eye]=\"row.shown\" [class.pi-eye-slash]=\"!row.shown\"></i>\n </button>\n <!-- The gene's colour among every gene; clicking it opens the colour picker. -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n *ngIf=\"row.swatch\"\n [style.color]=\"row.swatch\"\n [attr.aria-label]=\"'Colour for ' + row.gene\"\n pTooltip=\"Colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, row.gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\"><circle r=\"0.75\"></circle></svg>\n </button>\n <span class=\"sc-group-label\" [title]=\"row.gene\">{{ row.gene }}</span>\n <span class=\"sc-gene-count\" *ngIf=\"row.count !== null\" title=\"Transcripts in view\">{{\n row.count | number\n }}</span>\n </div>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched > allRows.length\">\n Showing {{ allRows.length | number }} of {{ allGenesMatched | number }} genes: filter to find the others.\n </small>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched === 0\">No gene matches.</small>\n </ng-container>\n <ng-container *ngIf=\"geneTreeOpen && !view.transcriptAllGenes\">\n <ng-container *ngFor=\"let node of sortedTree; trackBy: trackByNode\">\n <div class=\"sc-gene-row sc-gene-group\" *ngIf=\"node.name\">\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown(node.genes, !groupShown.get(node.name))\">\n <i\n class=\"pi\"\n [class.pi-eye]=\"groupShown.get(node.name)\"\n [class.pi-eye-slash]=\"!groupShown.get(node.name)\"\n ></i>\n </button>\n <span class=\"sc-group-label\" [title]=\"node.name\">{{ node.name }}</span>\n <span class=\"sc-count\">({{ node.genes.length }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"toggleGeneGroup(node.name)\">\n <i\n class=\"pi\"\n [class.pi-chevron-down]=\"!collapsedGeneGroups.has(node.name)\"\n [class.pi-chevron-right]=\"collapsedGeneGroups.has(node.name)\"\n ></i>\n </button>\n </div>\n <ng-container *ngIf=\"!node.name || !collapsedGeneGroups.has(node.name)\">\n <div\n class=\"sc-gene-row sc-gene-leaf\"\n [class.nested]=\"!!node.name\"\n *ngFor=\"let gene of node.genes; trackBy: trackByLabelString\"\n >\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown([gene], !rows.get(gene)?.shown)\">\n <i class=\"pi\" [class.pi-eye]=\"rows.get(gene)?.shown\" [class.pi-eye-slash]=\"!rows.get(gene)?.shown\"></i>\n </button>\n <!-- The gene's marker, in its colour: its icon in Icons mode, a dot for circles.\n Clicking it opens the icon and colour picker (as Xenium Explorer does). -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"rows.get(gene)?.swatch\"\n [attr.aria-label]=\"'Icon and colour for ' + gene\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon\n *ngIf=\"view.transcriptMode === 'glyphs'; else dotIcon\"\n [attr.points]=\"rows.get(gene)?.points\"\n ></polygon>\n <ng-template #dotIcon><circle r=\"0.75\"></circle></ng-template>\n </svg>\n </button>\n <span class=\"sc-group-label\">{{ gene }}</span>\n <span\n class=\"sc-gene-count\"\n *ngIf=\"rows.get(gene)?.count as n; else zeroCount\"\n title=\"Transcripts in view\"\n >{{ n | number }}</span\n >\n <ng-template #zeroCount>\n <span class=\"sc-gene-count\" *ngIf=\"geneCounts\" title=\"Transcripts in view\">0</span>\n </ng-template>\n </div>\n </ng-container>\n </ng-container>\n </ng-container>\n <!-- One picker for every gene row, filled for the gene that opened it. -->\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as gene\"\n [gene]=\"gene\"\n what=\"Transcript\"\n [color]=\"styleColorOf(gene)\"\n [glyph]=\"styleGlyph ?? 'circle'\"\n [showIcons]=\"!view.transcriptAllGenes\"\n [iconHint]=\"\n view.transcriptMode !== 'glyphs' ? 'Icons show when the transcript point style is Icons.' : null\n \"\n (colorChange)=\"onGeneColor(gene, $event)\"\n (glyphChange)=\"onGlyph(gene, $event)\"\n (resetStyle)=\"resetGeneStyle(gene)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n <div class=\"sc-gene-footer\">\n {{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{ geneTotal | number }}\n genes selected\n </div>\n</div>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-gene-group,.sc-gene-leaf{padding-left:1rem}.sc-gene-leaf.nested{padding-left:2rem}.sc-eye{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:0;color:var(--text-color-secondary, #6c757d)}.sc-eye .pi-eye{color:var(--text-color, #333)}.sc-eye:disabled{cursor:default;opacity:.5}.sc-gene-sort{display:flex;align-items:center;gap:.25rem;font-size:.8rem}.sc-gene-sort .sc-lbl{margin-right:auto}.sc-sort-btn{border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);color:var(--text-color, #333);border-radius:4px;padding:.1rem .5rem;font:inherit;cursor:pointer}.sc-sort-btn.active{border-color:var(--primary-color, #2196f3);color:var(--primary-color, #2196f3);font-weight:600}.sc-gene-filter{width:calc(100% - 1rem);margin:.2rem 0 .2rem 1rem;font-size:.8rem;padding:.2rem .4rem}.sc-gene-footer{text-align:right;font-size:.78rem;padding:.2rem 0;color:var(--text-color-secondary, #6c757d)}.sc-gene-count{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.8;padding-left:.5rem}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: SharedModule }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: InputTextModule }, { kind: "directive", type: i6.InputText, selector: "[pInputText]", inputs: ["variant"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialGeneStyleComponent, selector: "spatial-gene-style", inputs: ["gene", "what", "color", "glyph", "iconHint", "showIcons"], outputs: ["colorChange", "glyphChange", "resetStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
23693
23783
  }
23694
23784
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialGeneTreeComponent, decorators: [{
23695
23785
  type: Component,
@@ -23700,7 +23790,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
23700
23790
  OverlayPanelModule,
23701
23791
  TooltipModule,
23702
23792
  SpatialGeneStyleComponent,
23703
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"sc-subhead\">Selected genes</div>\n<div class=\"sc-gene-sort\" *ngIf=\"sortShown\" role=\"group\" aria-label=\"Sort genes\">\n <span class=\"sc-lbl\">Sort by</span>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"!byCount\"\n [attr.aria-pressed]=\"!byCount\"\n (click)=\"onGeneSort('name')\"\n >\n Name\n </button>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"byCount\"\n [attr.aria-pressed]=\"byCount\"\n title=\"Transcripts in view, most first\"\n (click)=\"onGeneSort('count')\"\n >\n Count\n </button>\n</div>\n<div class=\"sc-gene-tree\">\n <div class=\"sc-gene-row sc-gene-root\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n (click)=\"\n view.transcriptAllGenes ? onAllGenesShown(!rootShown) : onGenesShown(view.transcriptGenes, !rootShown)\n \"\n >\n <i class=\"pi\" [class.pi-eye]=\"rootShown\" [class.pi-eye-slash]=\"!rootShown\"></i>\n </button>\n <span class=\"sc-group-label\">Genes</span>\n <span class=\"sc-count\"\n >({{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{\n geneTotal | number\n }})</span\n >\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"geneTreeOpen = !geneTreeOpen\"\n *ngIf=\"!view.transcriptAllGenes && view.transcriptGenes.length\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"geneTreeOpen\" [class.pi-chevron-right]=\"!geneTreeOpen\"></i>\n </button>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"toggleAllGenesOpen()\"\n *ngIf=\"view.transcriptAllGenes && allGeneList?.length\"\n [attr.aria-label]=\"allGenesOpen ? 'Hide the gene list' : 'Show every gene'\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"allGenesOpen\" [class.pi-chevron-right]=\"!allGenesOpen\"></i>\n </button>\n </div>\n <!-- \"All genes\": every gene of the dataset, each with its eye when the dataset says which\n transcripts are whose (its gene list in code order). -->\n <ng-container *ngIf=\"allGenesOpen && view.transcriptAllGenes && allGeneList?.length\">\n <input\n pInputText\n class=\"sc-gene-filter\"\n placeholder=\"Filter genes\u2026\"\n aria-label=\"Filter genes\"\n [value]=\"allGenesFilter\"\n (input)=\"onAllGenesFilter($any($event.target).value)\"\n />\n <small class=\"sc-hint\" *ngIf=\"!canHideAllGenes\">\n This dataset does not say which transcripts are whose, so its genes cannot be hidden one by one.\n </small>\n <small class=\"sc-hint\" *ngIf=\"canHideAllGenes && view.transcriptMode === 'density'\">\n The density map draws every gene as one raster: genes are hidden one by one as circles or icons.\n </small>\n <div class=\"sc-gene-row sc-gene-leaf nested\" *ngFor=\"let row of allRows; trackBy: trackByAllRow\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n [disabled]=\"!canHideAllGenes || view.transcriptMode === 'density'\"\n (click)=\"onAllGeneShown(row.gene, !row.shown)\"\n >\n <i class=\"pi\" [class.pi-eye]=\"row.shown\" [class.pi-eye-slash]=\"!row.shown\"></i>\n </button>\n <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 sortedTree; trackBy: trackByNode\">\n <div class=\"sc-gene-row sc-gene-group\" *ngIf=\"node.name\">\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown(node.genes, !groupShown.get(node.name))\">\n <i\n class=\"pi\"\n [class.pi-eye]=\"groupShown.get(node.name)\"\n [class.pi-eye-slash]=\"!groupShown.get(node.name)\"\n ></i>\n </button>\n <span class=\"sc-group-label\" [title]=\"node.name\">{{ node.name }}</span>\n <span class=\"sc-count\">({{ node.genes.length }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"toggleGeneGroup(node.name)\">\n <i\n class=\"pi\"\n [class.pi-chevron-down]=\"!collapsedGeneGroups.has(node.name)\"\n [class.pi-chevron-right]=\"collapsedGeneGroups.has(node.name)\"\n ></i>\n </button>\n </div>\n <ng-container *ngIf=\"!node.name || !collapsedGeneGroups.has(node.name)\">\n <div\n class=\"sc-gene-row sc-gene-leaf\"\n [class.nested]=\"!!node.name\"\n *ngFor=\"let gene of node.genes; trackBy: trackByLabelString\"\n >\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown([gene], !rows.get(gene)?.shown)\">\n <i class=\"pi\" [class.pi-eye]=\"rows.get(gene)?.shown\" [class.pi-eye-slash]=\"!rows.get(gene)?.shown\"></i>\n </button>\n <!-- The gene's marker, in its colour: its icon in Icons mode, a dot for circles.\n Clicking it opens the icon and colour picker (as Xenium Explorer does). -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"rows.get(gene)?.swatch\"\n [attr.aria-label]=\"'Icon and colour for ' + gene\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon\n *ngIf=\"view.transcriptMode === 'glyphs'; else dotIcon\"\n [attr.points]=\"rows.get(gene)?.points\"\n ></polygon>\n <ng-template #dotIcon><circle r=\"0.75\"></circle></ng-template>\n </svg>\n </button>\n <span class=\"sc-group-label\">{{ gene }}</span>\n <span\n class=\"sc-gene-count\"\n *ngIf=\"rows.get(gene)?.count as n; else zeroCount\"\n title=\"Transcripts in view\"\n >{{ n | number }}</span\n >\n <ng-template #zeroCount>\n <span class=\"sc-gene-count\" *ngIf=\"geneCounts\" title=\"Transcripts in view\">0</span>\n </ng-template>\n </div>\n </ng-container>\n </ng-container>\n </ng-container>\n <!-- One picker for every gene row, filled for the gene that opened it. -->\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as gene\"\n [gene]=\"gene\"\n what=\"Transcript\"\n [color]=\"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-sort{display:flex;align-items:center;gap:.25rem;font-size:.8rem}.sc-gene-sort .sc-lbl{margin-right:auto}.sc-sort-btn{border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);color:var(--text-color, #333);border-radius:4px;padding:.1rem .5rem;font:inherit;cursor:pointer}.sc-sort-btn.active{border-color:var(--primary-color, #2196f3);color:var(--primary-color, #2196f3);font-weight:600}.sc-gene-filter{width:calc(100% - 1rem);margin:.2rem 0 .2rem 1rem;font-size:.8rem;padding:.2rem .4rem}.sc-gene-footer{text-align:right;font-size:.78rem;padding:.2rem 0;color:var(--text-color-secondary, #6c757d)}.sc-gene-count{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.8;padding-left:.5rem}\n"] }]
23793
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"sc-subhead\">Selected genes</div>\n<div class=\"sc-gene-sort\" *ngIf=\"sortShown\" role=\"group\" aria-label=\"Sort genes\">\n <span class=\"sc-lbl\">Sort by</span>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"!byCount\"\n [attr.aria-pressed]=\"!byCount\"\n (click)=\"onGeneSort('name')\"\n >\n Name\n </button>\n <button\n type=\"button\"\n class=\"sc-sort-btn\"\n [class.active]=\"byCount\"\n [attr.aria-pressed]=\"byCount\"\n title=\"Transcripts in view, most first\"\n (click)=\"onGeneSort('count')\"\n >\n Count\n </button>\n</div>\n<div class=\"sc-gene-tree\">\n <div class=\"sc-gene-row sc-gene-root\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n (click)=\"\n view.transcriptAllGenes ? onAllGenesShown(!rootShown) : onGenesShown(view.transcriptGenes, !rootShown)\n \"\n >\n <i class=\"pi\" [class.pi-eye]=\"rootShown\" [class.pi-eye-slash]=\"!rootShown\"></i>\n </button>\n <span class=\"sc-group-label\">Genes</span>\n <span class=\"sc-count\"\n >({{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{\n geneTotal | number\n }})</span\n >\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"geneTreeOpen = !geneTreeOpen\"\n *ngIf=\"!view.transcriptAllGenes && view.transcriptGenes.length\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"geneTreeOpen\" [class.pi-chevron-right]=\"!geneTreeOpen\"></i>\n </button>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n (click)=\"toggleAllGenesOpen()\"\n *ngIf=\"view.transcriptAllGenes && allGeneList?.length\"\n [attr.aria-label]=\"allGenesOpen ? 'Hide the gene list' : 'Show every gene'\"\n >\n <i class=\"pi\" [class.pi-chevron-down]=\"allGenesOpen\" [class.pi-chevron-right]=\"!allGenesOpen\"></i>\n </button>\n </div>\n <!-- \"All genes\": every gene of the dataset, each with its eye when the dataset says which\n transcripts are whose (its gene list in code order). -->\n <ng-container *ngIf=\"allGenesOpen && view.transcriptAllGenes && allGeneList?.length\">\n <input\n pInputText\n class=\"sc-gene-filter\"\n placeholder=\"Filter genes\u2026\"\n aria-label=\"Filter genes\"\n [value]=\"allGenesFilter\"\n (input)=\"onAllGenesFilter($any($event.target).value)\"\n />\n <small class=\"sc-hint\" *ngIf=\"!canHideAllGenes\">\n This dataset does not say which transcripts are whose, so its genes cannot be hidden one by one.\n </small>\n <small class=\"sc-hint\" *ngIf=\"canHideAllGenes && view.transcriptMode === 'density'\">\n The density map draws every gene as one raster: genes are hidden one by one as circles or icons.\n </small>\n <div class=\"sc-gene-row sc-gene-leaf nested\" *ngFor=\"let row of allRows; trackBy: trackByAllRow\">\n <button\n type=\"button\"\n class=\"sc-eye\"\n [disabled]=\"!canHideAllGenes || view.transcriptMode === 'density'\"\n (click)=\"onAllGeneShown(row.gene, !row.shown)\"\n >\n <i class=\"pi\" [class.pi-eye]=\"row.shown\" [class.pi-eye-slash]=\"!row.shown\"></i>\n </button>\n <!-- The gene's colour among every gene; clicking it opens the colour picker. -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n *ngIf=\"row.swatch\"\n [style.color]=\"row.swatch\"\n [attr.aria-label]=\"'Colour for ' + row.gene\"\n pTooltip=\"Colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, row.gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\"><circle r=\"0.75\"></circle></svg>\n </button>\n <span class=\"sc-group-label\" [title]=\"row.gene\">{{ row.gene }}</span>\n <span class=\"sc-gene-count\" *ngIf=\"row.count !== null\" title=\"Transcripts in view\">{{\n row.count | number\n }}</span>\n </div>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched > allRows.length\">\n Showing {{ allRows.length | number }} of {{ allGenesMatched | number }} genes: filter to find the others.\n </small>\n <small class=\"sc-hint\" *ngIf=\"allGenesMatched === 0\">No gene matches.</small>\n </ng-container>\n <ng-container *ngIf=\"geneTreeOpen && !view.transcriptAllGenes\">\n <ng-container *ngFor=\"let node of sortedTree; trackBy: trackByNode\">\n <div class=\"sc-gene-row sc-gene-group\" *ngIf=\"node.name\">\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown(node.genes, !groupShown.get(node.name))\">\n <i\n class=\"pi\"\n [class.pi-eye]=\"groupShown.get(node.name)\"\n [class.pi-eye-slash]=\"!groupShown.get(node.name)\"\n ></i>\n </button>\n <span class=\"sc-group-label\" [title]=\"node.name\">{{ node.name }}</span>\n <span class=\"sc-count\">({{ node.genes.length }})</span>\n <button type=\"button\" class=\"sc-icon-btn\" (click)=\"toggleGeneGroup(node.name)\">\n <i\n class=\"pi\"\n [class.pi-chevron-down]=\"!collapsedGeneGroups.has(node.name)\"\n [class.pi-chevron-right]=\"collapsedGeneGroups.has(node.name)\"\n ></i>\n </button>\n </div>\n <ng-container *ngIf=\"!node.name || !collapsedGeneGroups.has(node.name)\">\n <div\n class=\"sc-gene-row sc-gene-leaf\"\n [class.nested]=\"!!node.name\"\n *ngFor=\"let gene of node.genes; trackBy: trackByLabelString\"\n >\n <button type=\"button\" class=\"sc-eye\" (click)=\"onGenesShown([gene], !rows.get(gene)?.shown)\">\n <i class=\"pi\" [class.pi-eye]=\"rows.get(gene)?.shown\" [class.pi-eye-slash]=\"!rows.get(gene)?.shown\"></i>\n </button>\n <!-- The gene's marker, in its colour: its icon in Icons mode, a dot for circles.\n Clicking it opens the icon and colour picker (as Xenium Explorer does). -->\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"rows.get(gene)?.swatch\"\n [attr.aria-label]=\"'Icon and colour for ' + gene\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, gene, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon\n *ngIf=\"view.transcriptMode === 'glyphs'; else dotIcon\"\n [attr.points]=\"rows.get(gene)?.points\"\n ></polygon>\n <ng-template #dotIcon><circle r=\"0.75\"></circle></ng-template>\n </svg>\n </button>\n <span class=\"sc-group-label\">{{ gene }}</span>\n <span\n class=\"sc-gene-count\"\n *ngIf=\"rows.get(gene)?.count as n; else zeroCount\"\n title=\"Transcripts in view\"\n >{{ n | number }}</span\n >\n <ng-template #zeroCount>\n <span class=\"sc-gene-count\" *ngIf=\"geneCounts\" title=\"Transcripts in view\">0</span>\n </ng-template>\n </div>\n </ng-container>\n </ng-container>\n </ng-container>\n <!-- One picker for every gene row, filled for the gene that opened it. -->\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as gene\"\n [gene]=\"gene\"\n what=\"Transcript\"\n [color]=\"styleColorOf(gene)\"\n [glyph]=\"styleGlyph ?? 'circle'\"\n [showIcons]=\"!view.transcriptAllGenes\"\n [iconHint]=\"\n view.transcriptMode !== 'glyphs' ? 'Icons show when the transcript point style is Icons.' : null\n \"\n (colorChange)=\"onGeneColor(gene, $event)\"\n (glyphChange)=\"onGlyph(gene, $event)\"\n (resetStyle)=\"resetGeneStyle(gene)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n <div class=\"sc-gene-footer\">\n {{ (view.transcriptAllGenes ? geneTotal : view.transcriptGenes.length) | number }}/{{ geneTotal | number }}\n genes selected\n </div>\n</div>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-gene-group,.sc-gene-leaf{padding-left:1rem}.sc-gene-leaf.nested{padding-left:2rem}.sc-eye{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:0;color:var(--text-color-secondary, #6c757d)}.sc-eye .pi-eye{color:var(--text-color, #333)}.sc-eye:disabled{cursor:default;opacity:.5}.sc-gene-sort{display:flex;align-items:center;gap:.25rem;font-size:.8rem}.sc-gene-sort .sc-lbl{margin-right:auto}.sc-sort-btn{border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);color:var(--text-color, #333);border-radius:4px;padding:.1rem .5rem;font:inherit;cursor:pointer}.sc-sort-btn.active{border-color:var(--primary-color, #2196f3);color:var(--primary-color, #2196f3);font-weight:600}.sc-gene-filter{width:calc(100% - 1rem);margin:.2rem 0 .2rem 1rem;font-size:.8rem;padding:.2rem .4rem}.sc-gene-footer{text-align:right;font-size:.78rem;padding:.2rem 0;color:var(--text-color-secondary, #6c757d)}.sc-gene-count{margin-left:auto;font-variant-numeric:tabular-nums;opacity:.8;padding-left:.5rem}\n"] }]
23704
23794
  }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { controls: [{
23705
23795
  type: Input
23706
23796
  }], dataset: [{
@@ -24677,7 +24767,7 @@ class SpatialObservationsPanelComponent {
24677
24767
  this.controls?.setViewState({ percentileClip: value });
24678
24768
  }
24679
24769
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialObservationsPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
24680
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialObservationsPanelComponent, isStandalone: true, selector: "spatial-observations-panel", inputs: { controls: "controls", dataset: "dataset", view: "view", genes: "genes", legend: "legend", colorBarCss: "colorBarCss", selection: "selection", selectedCategory: "selectedCategory", colormapOptions: "colormapOptions", is3d: "is3d", open: "open" }, outputs: { openChange: "openChange", categoryClicked: "categoryClicked" }, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"dataset as ds\">\n <!-- Observations: one marker per observation (cell centroid, Visium spot), and what\n colours them. The lead section for datasets without cell outlines. -->\n <div class=\"sc-panel\" [class.open]=\"open\">\n <div class=\"sc-panel-head\" (click)=\"openChange.emit(!open)\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowPoints($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Observations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open\" [class.pi-chevron-right]=\"!open\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open\">\n <small class=\"sc-hint\" *ngIf=\"hasCells && !is3d\">\n One circle per cell. With cell outlines on, circles are only drawn where cells are too small on screen to\n outline.\n </small>\n <!-- Colour source. Column and gene are mutually exclusive: picking one\n clears the other, so what drives the colours is never ambiguous. -->\n <div class=\"sc-section\">\n <label class=\"sc-lbl\" for=\"sc-column\">Colour by column</label>\n <p-dropdown\n inputId=\"sc-column\"\n [options]=\"columnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedColumn\"\n (ngModelChange)=\"onColumn($event)\"\n placeholder=\"None (flat colour)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n </div>\n\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-gene\">Colour by gene</label>\n <!-- A filterable dropdown, not a free-text box: the genes are visible before\n anything is typed, and each keystroke narrows a list the user can see.\n Virtual-scrolled because a targeted panel runs to a few thousand names. -->\n <p-dropdown\n inputId=\"sc-gene\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedGene\"\n (ngModelChange)=\"onGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [showClear]=\"true\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Search {{ ds.features.count | number }} genes\u2026\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n <small class=\"sc-hint\" *ngIf=\"gene.failed\">Gene lookup failed.</small>\n </div>\n\n <!-- Which observations are drawn at all, by what they express. Several genes at\n once, unlike the colour, which reads one: picking genes here hides every\n observation that expresses none of them (or not all of them), and by default\n colours (and, in 2D, marks) each drawn one by its gene. -->\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-show-genes\">Show observations expressing</label>\n <p-multiSelect\n inputId=\"sc-show-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenes\"\n (onChange)=\"onObservationGenes($event.value)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onPanelShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [showToggleAll]=\"false\"\n [showClear]=\"true\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Any gene (every observation)\"\n [selectedItemsLabel]=\"'{0} genes'\"\n [maxSelectedLabels]=\"3\"\n styleClass=\"sc-full\"\n >\n <!-- A picked gene shows its icon in its colour; clicking it edits them. -->\n <ng-template let-option pTemplate=\"item\">\n <div class=\"sc-obs-gene-option\">\n <span class=\"sc-obs-gene-name\">{{ option.label }}</span>\n <ng-container *ngIf=\"isObservationGene(option.value)\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: option.value }\"></ng-container>\n </ng-container>\n </div>\n </ng-template>\n </p-multiSelect>\n <p-selectButton\n *ngIf=\"view.observationGenes.length > 1\"\n [options]=\"matchOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenesMatch\"\n (ngModelChange)=\"onObservationGenesMatch($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <!-- The picked genes: each one's icon and colour (click to edit), and a way out. -->\n <div class=\"sc-obs-genes\" *ngIf=\"view.observationGenes.length\">\n <div class=\"sc-obs-gene-row\" *ngFor=\"let g of view.observationGenes; trackBy: trackByGene\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: g }\"></ng-container>\n <span class=\"sc-obs-gene-name\">{{ g }}</span>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n [attr.aria-label]=\"'Stop showing ' + g\"\n pTooltip=\"Remove\"\n tooltipPosition=\"top\"\n (click)=\"removeObservationGene(g)\"\n >\n <i class=\"pi pi-times\"></i>\n </button>\n </div>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.observationGenes.length\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.observationGenesColorBy\"\n (onChange)=\"onObservationGenesColorBy($event.checked)\"\n label=\"Colour by these genes\"\n pTooltip=\"Each observation in the colour (and, in 2D, the icon) of the picked gene it expresses most, relative to that gene's highest value. Off, Colour by column or gene applies.\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.observationGenes.length\">{{ geneFilterHint }}</small>\n <!-- The filter is never dropped silently: a failed expression read keeps the observations\n as they were drawn, and says so. -->\n <small class=\"sc-hint sc-error\" *ngIf=\"view.observationGenes.length && (geneFilterFailed$ | async)\">\n The expression of the picked genes could not be read, so the observations still show the previous\n filter.\n <a href=\"\" (click)=\"$event.preventDefault(); retryGeneFilter()\">Try again</a>\n </small>\n </div>\n </ng-container>\n\n <spatial-key\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n (categoryClicked)=\"categoryClicked.emit($event)\"\n ></spatial-key>\n\n <!-- Display -->\n <div class=\"sc-section\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Point size</span>\n <p-slider\n [ngModel]=\"view.pointScale\"\n [min]=\"0.25\"\n [max]=\"5\"\n [step]=\"0.25\"\n (onChange)=\"onPointScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.pointScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.opacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.opacity }}</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Only for a gene: the map draws that gene's expression as a field. In 2D\n it goes under the cells of the displayed plane; in 3D it is one sheet per\n imaged section, optionally smoothed along z into a volume. -->\n <div class=\"sc-section\" *ngIf=\"canMapGene\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMap\"\n (onChange)=\"onGeneMap($event.checked)\"\n label=\"Gene map (expression field)\"\n [pTooltip]=\"\n is3d\n ? 'Draw this gene\\'s expression as one field per imaged section \u2014 an estimate between the cells, empty where nothing was measured'\n : 'Draw this gene\\'s expression as a smooth field UNDER the cells \u2014 an estimate between them, transparent where nothing was measured'\n \"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- 3D only: interpolating along z turns the measured sheets into an\n estimate, so it is its own decision and its own label. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMapVolume\"\n (onChange)=\"onGeneMapVolume($event.checked)\"\n label=\"Volume rendering (interpolate along z)\"\n pTooltip=\"Smooth the per-section fields into a continuous volume \u2014 an ESTIMATE between the imaged sections, not measured tissue\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Meaningless while interpolating: a volume built from one section would\n smear that slide through the specimen's whole depth. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"geneMapOneSection\"\n (onChange)=\"onGeneMapOneSection($event.checked)\"\n label=\"One map section at a time\"\n pTooltip=\"Draw a single imaged section's field instead of the whole stack of sheets\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume && geneMapOneSection\">\n <span class=\"sc-lbl\">Map section</span>\n <p-slider\n [ngModel]=\"view.geneMapSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onGeneMapSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ geneMapSectionLabel }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.geneMapSmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onGeneMapSmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapSmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Map opacity</span>\n <p-slider\n [ngModel]=\"view.geneMapOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onGeneMapOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapOpacity }}</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && !is3d\">\n Kernel-weighted mean per cell, not a sum \u2014 so a crowded region does not glow for being crowded. Transparent\n where no cell was measured. Turn the cells' own Opacity down to read the field under them.\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && is3d\">\n {{ geneMapVolumeNote }}\n </div>\n </div>\n\n <!-- 3D only: the volume, the cloud and the density volumes occupy the same\n space, so each hides the others. Independent toggles, because the useful\n views are the combinations \u2014 the field alone, one section's cells over it,\n the anatomy alone. -->\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <span class=\"sc-lbl\">Show</span>\n <div class=\"sc-row\" *ngIf=\"ds.volume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showVolume\"\n (onChange)=\"onShowVolume($event.checked)\"\n label=\"Reference volume\"\n pTooltip=\"The registered anatomy the observations sit in\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- The backdrop's own opacity: reading the cloud or a density field\n through the anatomy means turning the anatomy down, not the data. -->\n <div class=\"sc-row\" *ngIf=\"ds.volume && view.showVolume\">\n <span class=\"sc-lbl\">Volume opacity</span>\n <p-slider\n [ngModel]=\"view.volumeOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onVolumeOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.volumeOpacity }}</span>\n </div>\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (onChange)=\"onShowPoints($event.checked)\"\n label=\"Observations\"\n pTooltip=\"The measured cells. Hide them to read the density volumes underneath\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Only for data that really is sectioned; a continuous z has no sections\n to step through. -->\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"oneSection\"\n (onChange)=\"onOneSection($event.checked)\"\n label=\"One section at a time\"\n pTooltip=\"Draw a single imaged section's cells, so the density volumes between the sections stay visible\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned && oneSection\">\n <span class=\"sc-lbl\">Section</span>\n <p-slider\n [ngModel]=\"view.pointSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onPointSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ sectionLabel }}</span>\n </div>\n </div>\n\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.densityVolume\"\n (onChange)=\"onDensityVolume($event.checked)\"\n label=\"Cluster density volumes\"\n pTooltip=\"Raymarch each cluster as a smooth density field \u2014 an ESTIMATE between the imaged sections, not measured cells\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.densityVolume\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.densitySmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onDensitySmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.densitySmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.densityVolume\">{{ densityNote }}</div>\n </div>\n\n <!-- Continuous-only knobs: meaningless for a categorical palette. -->\n <div class=\"sc-section\" *ngIf=\"isContinuous\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.logScale\"\n (onChange)=\"onLogScale($event.checked)\"\n label=\"Log scale\"\n pTooltip=\"Counts collapse against their outliers on a linear ramp\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clip outliers</span>\n <p-dropdown\n [options]=\"clipOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.percentileClip\"\n (ngModelChange)=\"onClip($event)\"\n appendTo=\"body\"\n pTooltip=\"Percentile the contrast window is taken at\"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- Reserves the readout column this row has no readout for, so the\n control's right edge lines up with every slider's. -->\n <span class=\"sc-val\"></span>\n </div>\n </div>\n\n <!-- A gene's icon in its colour: the button that opens its icon and colour picker. -->\n <ng-template #geneMark let-g>\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"geneColorOf(g)\"\n [attr.aria-label]=\"'Icon and colour for ' + g\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, g, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"glyphPointsOf(g)\"></polygon>\n </svg>\n </button>\n </ng-template>\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as g\"\n [gene]=\"g\"\n what=\"Observation\"\n [color]=\"geneColorOf(g)\"\n [glyph]=\"geneGlyphOf(g)\"\n iconHint=\"Icons show on the 2D observation markers.\"\n (colorChange)=\"onGeneColor(g, $event)\"\n (glyphChange)=\"onGeneGlyph(g, $event)\"\n (resetStyle)=\"resetGeneStyle(g)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-obs-genes{display:flex;flex-direction:column;gap:.15rem;margin-top:.35rem}.sc-obs-gene-row,.sc-obs-gene-option{display:flex;align-items:center;gap:.4rem;min-width:0}.sc-obs-gene-option{flex:1 1 auto;justify-content:space-between}.sc-obs-gene-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i5$2.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i3$2.MultiSelect, selector: "p-multiSelect", inputs: ["id", "ariaLabel", "style", "styleClass", "panelStyle", "panelStyleClass", "inputId", "disabled", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "variant", "appendTo", "dataKey", "name", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "defaultLabel", "placeholder", "options", "filterValue", "itemSize", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i8$2.SelectButton, selector: "p-selectButton", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "unselectable", "tabindex", "multiple", "allowEmpty", "style", "styleClass", "ariaLabelledBy", "disabled", "dataKey", "autofocus"], outputs: ["onOptionClick", "onChange"] }, { kind: "ngmodule", type: SliderModule }, { kind: "component", type: i8.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialKeyComponent, selector: "spatial-key", inputs: ["controls", "dataset", "view", "legend", "colorBarCss", "selectedCategory", "colormapOptions"], outputs: ["categoryClicked"] }, { kind: "component", type: SpatialGeneStyleComponent, selector: "spatial-gene-style", inputs: ["gene", "what", "color", "glyph", "iconHint"], outputs: ["colorChange", "glyphChange", "resetStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
24770
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialObservationsPanelComponent, isStandalone: true, selector: "spatial-observations-panel", inputs: { controls: "controls", dataset: "dataset", view: "view", genes: "genes", legend: "legend", colorBarCss: "colorBarCss", selection: "selection", selectedCategory: "selectedCategory", colormapOptions: "colormapOptions", is3d: "is3d", open: "open" }, outputs: { openChange: "openChange", categoryClicked: "categoryClicked" }, usesOnChanges: true, ngImport: i0, template: "<ng-container *ngIf=\"dataset as ds\">\n <!-- Observations: one marker per observation (cell centroid, Visium spot), and what\n colours them. The lead section for datasets without cell outlines. -->\n <div class=\"sc-panel\" [class.open]=\"open\">\n <div class=\"sc-panel-head\" (click)=\"openChange.emit(!open)\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowPoints($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Observations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open\" [class.pi-chevron-right]=\"!open\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open\">\n <small class=\"sc-hint\" *ngIf=\"hasCells && !is3d\">\n One circle per cell, at every zoom. Cell outlines are drawn over the circles once cells are big enough on\n screen to outline.\n </small>\n <!-- Colour source. Column and gene are mutually exclusive: picking one\n clears the other, so what drives the colours is never ambiguous. -->\n <div class=\"sc-section\">\n <label class=\"sc-lbl\" for=\"sc-column\">Colour by column</label>\n <p-dropdown\n inputId=\"sc-column\"\n [options]=\"columnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedColumn\"\n (ngModelChange)=\"onColumn($event)\"\n placeholder=\"None (flat colour)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n </div>\n\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-gene\">Colour by gene</label>\n <!-- A filterable dropdown, not a free-text box: the genes are visible before\n anything is typed, and each keystroke narrows a list the user can see.\n Virtual-scrolled because a targeted panel runs to a few thousand names. -->\n <p-dropdown\n inputId=\"sc-gene\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedGene\"\n (ngModelChange)=\"onGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [showClear]=\"true\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Search {{ ds.features.count | number }} genes\u2026\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n <small class=\"sc-hint\" *ngIf=\"gene.failed\">Gene lookup failed.</small>\n </div>\n\n <!-- Which observations are drawn at all, by what they express. Several genes at\n once, unlike the colour, which reads one: picking genes here hides every\n observation that expresses none of them (or not all of them), and by default\n colours (and, in 2D, marks) each drawn one by its gene. -->\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-show-genes\">Show observations expressing</label>\n <p-multiSelect\n inputId=\"sc-show-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenes\"\n (onChange)=\"onObservationGenes($event.value)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onPanelShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [showToggleAll]=\"false\"\n [showClear]=\"true\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Any gene (every observation)\"\n [selectedItemsLabel]=\"'{0} genes'\"\n [maxSelectedLabels]=\"3\"\n styleClass=\"sc-full\"\n >\n <!-- A picked gene shows its icon in its colour; clicking it edits them. -->\n <ng-template let-option pTemplate=\"item\">\n <div class=\"sc-obs-gene-option\">\n <span class=\"sc-obs-gene-name\">{{ option.label }}</span>\n <ng-container *ngIf=\"isObservationGene(option.value)\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: option.value }\"></ng-container>\n </ng-container>\n </div>\n </ng-template>\n </p-multiSelect>\n <p-selectButton\n *ngIf=\"view.observationGenes.length > 1\"\n [options]=\"matchOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenesMatch\"\n (ngModelChange)=\"onObservationGenesMatch($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <!-- The picked genes: each one's icon and colour (click to edit), and a way out. -->\n <div class=\"sc-obs-genes\" *ngIf=\"view.observationGenes.length\">\n <div class=\"sc-obs-gene-row\" *ngFor=\"let g of view.observationGenes; trackBy: trackByGene\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: g }\"></ng-container>\n <span class=\"sc-obs-gene-name\">{{ g }}</span>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n [attr.aria-label]=\"'Stop showing ' + g\"\n pTooltip=\"Remove\"\n tooltipPosition=\"top\"\n (click)=\"removeObservationGene(g)\"\n >\n <i class=\"pi pi-times\"></i>\n </button>\n </div>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.observationGenes.length\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.observationGenesColorBy\"\n (onChange)=\"onObservationGenesColorBy($event.checked)\"\n label=\"Colour by these genes\"\n pTooltip=\"Each observation in the colour (and, in 2D, the icon) of the picked gene it expresses most, relative to that gene's highest value. Off, Colour by column or gene applies.\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.observationGenes.length\">{{ geneFilterHint }}</small>\n <!-- The filter is never dropped silently: a failed expression read keeps the observations\n as they were drawn, and says so. -->\n <small class=\"sc-hint sc-error\" *ngIf=\"view.observationGenes.length && (geneFilterFailed$ | async)\">\n The expression of the picked genes could not be read, so the observations still show the previous\n filter.\n <a href=\"\" (click)=\"$event.preventDefault(); retryGeneFilter()\">Try again</a>\n </small>\n </div>\n </ng-container>\n\n <spatial-key\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n (categoryClicked)=\"categoryClicked.emit($event)\"\n ></spatial-key>\n\n <!-- Display -->\n <div class=\"sc-section\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Point size</span>\n <p-slider\n [ngModel]=\"view.pointScale\"\n [min]=\"0.25\"\n [max]=\"5\"\n [step]=\"0.25\"\n (onChange)=\"onPointScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.pointScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.opacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.opacity }}</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Only for a gene: the map draws that gene's expression as a field. In 2D\n it goes under the cells of the displayed plane; in 3D it is one sheet per\n imaged section, optionally smoothed along z into a volume. -->\n <div class=\"sc-section\" *ngIf=\"canMapGene\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMap\"\n (onChange)=\"onGeneMap($event.checked)\"\n label=\"Gene map (expression field)\"\n [pTooltip]=\"\n is3d\n ? 'Draw this gene\\'s expression as one field per imaged section \u2014 an estimate between the cells, empty where nothing was measured'\n : 'Draw this gene\\'s expression as a smooth field UNDER the cells \u2014 an estimate between them, transparent where nothing was measured'\n \"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- 3D only: interpolating along z turns the measured sheets into an\n estimate, so it is its own decision and its own label. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMapVolume\"\n (onChange)=\"onGeneMapVolume($event.checked)\"\n label=\"Volume rendering (interpolate along z)\"\n pTooltip=\"Smooth the per-section fields into a continuous volume \u2014 an ESTIMATE between the imaged sections, not measured tissue\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Meaningless while interpolating: a volume built from one section would\n smear that slide through the specimen's whole depth. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"geneMapOneSection\"\n (onChange)=\"onGeneMapOneSection($event.checked)\"\n label=\"One map section at a time\"\n pTooltip=\"Draw a single imaged section's field instead of the whole stack of sheets\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume && geneMapOneSection\">\n <span class=\"sc-lbl\">Map section</span>\n <p-slider\n [ngModel]=\"view.geneMapSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onGeneMapSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ geneMapSectionLabel }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.geneMapSmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onGeneMapSmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapSmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Map opacity</span>\n <p-slider\n [ngModel]=\"view.geneMapOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onGeneMapOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapOpacity }}</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && !is3d\">\n Kernel-weighted mean per cell, not a sum \u2014 so a crowded region does not glow for being crowded. Transparent\n where no cell was measured. Turn the cells' own Opacity down to read the field under them.\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && is3d\">\n {{ geneMapVolumeNote }}\n </div>\n </div>\n\n <!-- 3D only: the volume, the cloud and the density volumes occupy the same\n space, so each hides the others. Independent toggles, because the useful\n views are the combinations \u2014 the field alone, one section's cells over it,\n the anatomy alone. -->\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <span class=\"sc-lbl\">Show</span>\n <div class=\"sc-row\" *ngIf=\"ds.volume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showVolume\"\n (onChange)=\"onShowVolume($event.checked)\"\n label=\"Reference volume\"\n pTooltip=\"The registered anatomy the observations sit in\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- The backdrop's own opacity: reading the cloud or a density field\n through the anatomy means turning the anatomy down, not the data. -->\n <div class=\"sc-row\" *ngIf=\"ds.volume && view.showVolume\">\n <span class=\"sc-lbl\">Volume opacity</span>\n <p-slider\n [ngModel]=\"view.volumeOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onVolumeOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.volumeOpacity }}</span>\n </div>\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (onChange)=\"onShowPoints($event.checked)\"\n label=\"Observations\"\n pTooltip=\"The measured cells. Hide them to read the density volumes underneath\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Only for data that really is sectioned; a continuous z has no sections\n to step through. -->\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"oneSection\"\n (onChange)=\"onOneSection($event.checked)\"\n label=\"One section at a time\"\n pTooltip=\"Draw a single imaged section's cells, so the density volumes between the sections stay visible\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned && oneSection\">\n <span class=\"sc-lbl\">Section</span>\n <p-slider\n [ngModel]=\"view.pointSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onPointSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ sectionLabel }}</span>\n </div>\n </div>\n\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.densityVolume\"\n (onChange)=\"onDensityVolume($event.checked)\"\n label=\"Cluster density volumes\"\n pTooltip=\"Raymarch each cluster as a smooth density field \u2014 an ESTIMATE between the imaged sections, not measured cells\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.densityVolume\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.densitySmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onDensitySmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.densitySmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.densityVolume\">{{ densityNote }}</div>\n </div>\n\n <!-- Continuous-only knobs: meaningless for a categorical palette. -->\n <div class=\"sc-section\" *ngIf=\"isContinuous\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.logScale\"\n (onChange)=\"onLogScale($event.checked)\"\n label=\"Log scale\"\n pTooltip=\"Counts collapse against their outliers on a linear ramp\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clip outliers</span>\n <p-dropdown\n [options]=\"clipOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.percentileClip\"\n (ngModelChange)=\"onClip($event)\"\n appendTo=\"body\"\n pTooltip=\"Percentile the contrast window is taken at\"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- Reserves the readout column this row has no readout for, so the\n control's right edge lines up with every slider's. -->\n <span class=\"sc-val\"></span>\n </div>\n </div>\n\n <!-- A gene's icon in its colour: the button that opens its icon and colour picker. -->\n <ng-template #geneMark let-g>\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"geneColorOf(g)\"\n [attr.aria-label]=\"'Icon and colour for ' + g\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, g, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"glyphPointsOf(g)\"></polygon>\n </svg>\n </button>\n </ng-template>\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as g\"\n [gene]=\"g\"\n what=\"Observation\"\n [color]=\"geneColorOf(g)\"\n [glyph]=\"geneGlyphOf(g)\"\n iconHint=\"Icons show on the 2D observation markers.\"\n (colorChange)=\"onGeneColor(g, $event)\"\n (glyphChange)=\"onGeneGlyph(g, $event)\"\n (resetStyle)=\"resetGeneStyle(g)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-obs-genes{display:flex;flex-direction:column;gap:.15rem;margin-top:.35rem}.sc-obs-gene-row,.sc-obs-gene-option{display:flex;align-items:center;gap:.4rem;min-width:0}.sc-obs-gene-option{flex:1 1 auto;justify-content:space-between}.sc-obs-gene-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "pipe", type: i1$1.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "directive", type: i1$2.PrimeTemplate, selector: "[pTemplate]", inputs: ["type", "pTemplate"] }, { kind: "ngmodule", type: DropdownModule }, { kind: "component", type: i5$2.Dropdown, selector: "p-dropdown", inputs: ["id", "scrollHeight", "filter", "name", "style", "panelStyle", "styleClass", "panelStyleClass", "readonly", "required", "editable", "appendTo", "tabindex", "placeholder", "loadingIcon", "filterPlaceholder", "filterLocale", "variant", "inputId", "dataKey", "filterBy", "filterFields", "autofocus", "resetFilterOnHide", "checkmark", "dropdownIcon", "loading", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "autoDisplayFirst", "group", "showClear", "emptyFilterMessage", "emptyMessage", "lazy", "virtualScroll", "virtualScrollItemSize", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "ariaLabel", "ariaLabelledBy", "filterMatchMode", "maxlength", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "focusOnHover", "selectOnFocus", "autoOptionFocus", "autofocusFilter", "autoShowPanelOnPrintableCharacterKeyDown", "disabled", "itemSize", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "filterValue", "options"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onShow", "onHide", "onClear", "onLazyLoad"] }, { kind: "ngmodule", type: MultiSelectModule }, { kind: "component", type: i3$2.MultiSelect, selector: "p-multiSelect", inputs: ["id", "ariaLabel", "style", "styleClass", "panelStyle", "panelStyleClass", "inputId", "disabled", "readonly", "group", "filter", "filterPlaceHolder", "filterLocale", "overlayVisible", "tabindex", "variant", "appendTo", "dataKey", "name", "ariaLabelledBy", "displaySelectedLabel", "maxSelectedLabels", "selectionLimit", "selectedItemsLabel", "showToggleAll", "emptyFilterMessage", "emptyMessage", "resetFilterOnHide", "dropdownIcon", "optionLabel", "optionValue", "optionDisabled", "optionGroupLabel", "optionGroupChildren", "showHeader", "filterBy", "scrollHeight", "lazy", "virtualScroll", "loading", "virtualScrollItemSize", "loadingIcon", "virtualScrollOptions", "overlayOptions", "ariaFilterLabel", "filterMatchMode", "tooltip", "tooltipPosition", "tooltipPositionStyle", "tooltipStyleClass", "autofocusFilter", "display", "autocomplete", "showClear", "autofocus", "autoZIndex", "baseZIndex", "showTransitionOptions", "hideTransitionOptions", "defaultLabel", "placeholder", "options", "filterValue", "itemSize", "selectAll", "focusOnHover", "filterFields", "selectOnFocus", "autoOptionFocus"], outputs: ["onChange", "onFilter", "onFocus", "onBlur", "onClick", "onClear", "onPanelShow", "onPanelHide", "onLazyLoad", "onRemove", "onSelectAllChange"] }, { kind: "ngmodule", type: OverlayPanelModule }, { kind: "component", type: i10.OverlayPanel, selector: "p-overlayPanel", inputs: ["ariaLabel", "ariaLabelledBy", "dismissable", "showCloseIcon", "style", "styleClass", "appendTo", "autoZIndex", "ariaCloseLabel", "baseZIndex", "focusOnShow", "showTransitionOptions", "hideTransitionOptions"], outputs: ["onShow", "onHide"] }, { kind: "ngmodule", type: SelectButtonModule }, { kind: "component", type: i8$2.SelectButton, selector: "p-selectButton", inputs: ["options", "optionLabel", "optionValue", "optionDisabled", "unselectable", "tabindex", "multiple", "allowEmpty", "style", "styleClass", "ariaLabelledBy", "disabled", "dataKey", "autofocus"], outputs: ["onOptionClick", "onChange"] }, { kind: "ngmodule", type: SliderModule }, { kind: "component", type: i8.Slider, selector: "p-slider", inputs: ["animate", "disabled", "min", "max", "orientation", "step", "range", "style", "styleClass", "ariaLabel", "ariaLabelledBy", "tabindex", "autofocus"], outputs: ["onChange", "onSlideEnd"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialKeyComponent, selector: "spatial-key", inputs: ["controls", "dataset", "view", "legend", "colorBarCss", "selectedCategory", "colormapOptions"], outputs: ["categoryClicked"] }, { kind: "component", type: SpatialGeneStyleComponent, selector: "spatial-gene-style", inputs: ["gene", "what", "color", "glyph", "iconHint", "showIcons"], outputs: ["colorChange", "glyphChange", "resetStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
24681
24771
  }
24682
24772
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialObservationsPanelComponent, decorators: [{
24683
24773
  type: Component,
@@ -24693,7 +24783,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
24693
24783
  TooltipModule,
24694
24784
  SpatialKeyComponent,
24695
24785
  SpatialGeneStyleComponent,
24696
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"dataset as ds\">\n <!-- Observations: one marker per observation (cell centroid, Visium spot), and what\n colours them. The lead section for datasets without cell outlines. -->\n <div class=\"sc-panel\" [class.open]=\"open\">\n <div class=\"sc-panel-head\" (click)=\"openChange.emit(!open)\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowPoints($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Observations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open\" [class.pi-chevron-right]=\"!open\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open\">\n <small class=\"sc-hint\" *ngIf=\"hasCells && !is3d\">\n One circle per cell. With cell outlines on, circles are only drawn where cells are too small on screen to\n outline.\n </small>\n <!-- Colour source. Column and gene are mutually exclusive: picking one\n clears the other, so what drives the colours is never ambiguous. -->\n <div class=\"sc-section\">\n <label class=\"sc-lbl\" for=\"sc-column\">Colour by column</label>\n <p-dropdown\n inputId=\"sc-column\"\n [options]=\"columnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedColumn\"\n (ngModelChange)=\"onColumn($event)\"\n placeholder=\"None (flat colour)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n </div>\n\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-gene\">Colour by gene</label>\n <!-- A filterable dropdown, not a free-text box: the genes are visible before\n anything is typed, and each keystroke narrows a list the user can see.\n Virtual-scrolled because a targeted panel runs to a few thousand names. -->\n <p-dropdown\n inputId=\"sc-gene\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedGene\"\n (ngModelChange)=\"onGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [showClear]=\"true\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Search {{ ds.features.count | number }} genes\u2026\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n <small class=\"sc-hint\" *ngIf=\"gene.failed\">Gene lookup failed.</small>\n </div>\n\n <!-- Which observations are drawn at all, by what they express. Several genes at\n once, unlike the colour, which reads one: picking genes here hides every\n observation that expresses none of them (or not all of them), and by default\n colours (and, in 2D, marks) each drawn one by its gene. -->\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-show-genes\">Show observations expressing</label>\n <p-multiSelect\n inputId=\"sc-show-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenes\"\n (onChange)=\"onObservationGenes($event.value)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onPanelShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [showToggleAll]=\"false\"\n [showClear]=\"true\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Any gene (every observation)\"\n [selectedItemsLabel]=\"'{0} genes'\"\n [maxSelectedLabels]=\"3\"\n styleClass=\"sc-full\"\n >\n <!-- A picked gene shows its icon in its colour; clicking it edits them. -->\n <ng-template let-option pTemplate=\"item\">\n <div class=\"sc-obs-gene-option\">\n <span class=\"sc-obs-gene-name\">{{ option.label }}</span>\n <ng-container *ngIf=\"isObservationGene(option.value)\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: option.value }\"></ng-container>\n </ng-container>\n </div>\n </ng-template>\n </p-multiSelect>\n <p-selectButton\n *ngIf=\"view.observationGenes.length > 1\"\n [options]=\"matchOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenesMatch\"\n (ngModelChange)=\"onObservationGenesMatch($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <!-- The picked genes: each one's icon and colour (click to edit), and a way out. -->\n <div class=\"sc-obs-genes\" *ngIf=\"view.observationGenes.length\">\n <div class=\"sc-obs-gene-row\" *ngFor=\"let g of view.observationGenes; trackBy: trackByGene\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: g }\"></ng-container>\n <span class=\"sc-obs-gene-name\">{{ g }}</span>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n [attr.aria-label]=\"'Stop showing ' + g\"\n pTooltip=\"Remove\"\n tooltipPosition=\"top\"\n (click)=\"removeObservationGene(g)\"\n >\n <i class=\"pi pi-times\"></i>\n </button>\n </div>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.observationGenes.length\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.observationGenesColorBy\"\n (onChange)=\"onObservationGenesColorBy($event.checked)\"\n label=\"Colour by these genes\"\n pTooltip=\"Each observation in the colour (and, in 2D, the icon) of the picked gene it expresses most, relative to that gene's highest value. Off, Colour by column or gene applies.\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.observationGenes.length\">{{ geneFilterHint }}</small>\n <!-- The filter is never dropped silently: a failed expression read keeps the observations\n as they were drawn, and says so. -->\n <small class=\"sc-hint sc-error\" *ngIf=\"view.observationGenes.length && (geneFilterFailed$ | async)\">\n The expression of the picked genes could not be read, so the observations still show the previous\n filter.\n <a href=\"\" (click)=\"$event.preventDefault(); retryGeneFilter()\">Try again</a>\n </small>\n </div>\n </ng-container>\n\n <spatial-key\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n (categoryClicked)=\"categoryClicked.emit($event)\"\n ></spatial-key>\n\n <!-- Display -->\n <div class=\"sc-section\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Point size</span>\n <p-slider\n [ngModel]=\"view.pointScale\"\n [min]=\"0.25\"\n [max]=\"5\"\n [step]=\"0.25\"\n (onChange)=\"onPointScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.pointScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.opacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.opacity }}</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Only for a gene: the map draws that gene's expression as a field. In 2D\n it goes under the cells of the displayed plane; in 3D it is one sheet per\n imaged section, optionally smoothed along z into a volume. -->\n <div class=\"sc-section\" *ngIf=\"canMapGene\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMap\"\n (onChange)=\"onGeneMap($event.checked)\"\n label=\"Gene map (expression field)\"\n [pTooltip]=\"\n is3d\n ? 'Draw this gene\\'s expression as one field per imaged section \u2014 an estimate between the cells, empty where nothing was measured'\n : 'Draw this gene\\'s expression as a smooth field UNDER the cells \u2014 an estimate between them, transparent where nothing was measured'\n \"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- 3D only: interpolating along z turns the measured sheets into an\n estimate, so it is its own decision and its own label. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMapVolume\"\n (onChange)=\"onGeneMapVolume($event.checked)\"\n label=\"Volume rendering (interpolate along z)\"\n pTooltip=\"Smooth the per-section fields into a continuous volume \u2014 an ESTIMATE between the imaged sections, not measured tissue\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Meaningless while interpolating: a volume built from one section would\n smear that slide through the specimen's whole depth. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"geneMapOneSection\"\n (onChange)=\"onGeneMapOneSection($event.checked)\"\n label=\"One map section at a time\"\n pTooltip=\"Draw a single imaged section's field instead of the whole stack of sheets\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume && geneMapOneSection\">\n <span class=\"sc-lbl\">Map section</span>\n <p-slider\n [ngModel]=\"view.geneMapSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onGeneMapSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ geneMapSectionLabel }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.geneMapSmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onGeneMapSmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapSmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Map opacity</span>\n <p-slider\n [ngModel]=\"view.geneMapOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onGeneMapOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapOpacity }}</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && !is3d\">\n Kernel-weighted mean per cell, not a sum \u2014 so a crowded region does not glow for being crowded. Transparent\n where no cell was measured. Turn the cells' own Opacity down to read the field under them.\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && is3d\">\n {{ geneMapVolumeNote }}\n </div>\n </div>\n\n <!-- 3D only: the volume, the cloud and the density volumes occupy the same\n space, so each hides the others. Independent toggles, because the useful\n views are the combinations \u2014 the field alone, one section's cells over it,\n the anatomy alone. -->\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <span class=\"sc-lbl\">Show</span>\n <div class=\"sc-row\" *ngIf=\"ds.volume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showVolume\"\n (onChange)=\"onShowVolume($event.checked)\"\n label=\"Reference volume\"\n pTooltip=\"The registered anatomy the observations sit in\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- The backdrop's own opacity: reading the cloud or a density field\n through the anatomy means turning the anatomy down, not the data. -->\n <div class=\"sc-row\" *ngIf=\"ds.volume && view.showVolume\">\n <span class=\"sc-lbl\">Volume opacity</span>\n <p-slider\n [ngModel]=\"view.volumeOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onVolumeOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.volumeOpacity }}</span>\n </div>\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (onChange)=\"onShowPoints($event.checked)\"\n label=\"Observations\"\n pTooltip=\"The measured cells. Hide them to read the density volumes underneath\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Only for data that really is sectioned; a continuous z has no sections\n to step through. -->\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"oneSection\"\n (onChange)=\"onOneSection($event.checked)\"\n label=\"One section at a time\"\n pTooltip=\"Draw a single imaged section's cells, so the density volumes between the sections stay visible\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned && oneSection\">\n <span class=\"sc-lbl\">Section</span>\n <p-slider\n [ngModel]=\"view.pointSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onPointSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ sectionLabel }}</span>\n </div>\n </div>\n\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.densityVolume\"\n (onChange)=\"onDensityVolume($event.checked)\"\n label=\"Cluster density volumes\"\n pTooltip=\"Raymarch each cluster as a smooth density field \u2014 an ESTIMATE between the imaged sections, not measured cells\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.densityVolume\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.densitySmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onDensitySmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.densitySmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.densityVolume\">{{ densityNote }}</div>\n </div>\n\n <!-- Continuous-only knobs: meaningless for a categorical palette. -->\n <div class=\"sc-section\" *ngIf=\"isContinuous\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.logScale\"\n (onChange)=\"onLogScale($event.checked)\"\n label=\"Log scale\"\n pTooltip=\"Counts collapse against their outliers on a linear ramp\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clip outliers</span>\n <p-dropdown\n [options]=\"clipOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.percentileClip\"\n (ngModelChange)=\"onClip($event)\"\n appendTo=\"body\"\n pTooltip=\"Percentile the contrast window is taken at\"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- Reserves the readout column this row has no readout for, so the\n control's right edge lines up with every slider's. -->\n <span class=\"sc-val\"></span>\n </div>\n </div>\n\n <!-- A gene's icon in its colour: the button that opens its icon and colour picker. -->\n <ng-template #geneMark let-g>\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"geneColorOf(g)\"\n [attr.aria-label]=\"'Icon and colour for ' + g\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, g, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"glyphPointsOf(g)\"></polygon>\n </svg>\n </button>\n </ng-template>\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as g\"\n [gene]=\"g\"\n what=\"Observation\"\n [color]=\"geneColorOf(g)\"\n [glyph]=\"geneGlyphOf(g)\"\n iconHint=\"Icons show on the 2D observation markers.\"\n (colorChange)=\"onGeneColor(g, $event)\"\n (glyphChange)=\"onGeneGlyph(g, $event)\"\n (resetStyle)=\"resetGeneStyle(g)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-obs-genes{display:flex;flex-direction:column;gap:.15rem;margin-top:.35rem}.sc-obs-gene-row,.sc-obs-gene-option{display:flex;align-items:center;gap:.4rem;min-width:0}.sc-obs-gene-option{flex:1 1 auto;justify-content:space-between}.sc-obs-gene-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
24786
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<ng-container *ngIf=\"dataset as ds\">\n <!-- Observations: one marker per observation (cell centroid, Visium spot), and what\n colours them. The lead section for datasets without cell outlines. -->\n <div class=\"sc-panel\" [class.open]=\"open\">\n <div class=\"sc-panel-head\" (click)=\"openChange.emit(!open)\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowPoints($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Observations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open\" [class.pi-chevron-right]=\"!open\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open\">\n <small class=\"sc-hint\" *ngIf=\"hasCells && !is3d\">\n One circle per cell, at every zoom. Cell outlines are drawn over the circles once cells are big enough on\n screen to outline.\n </small>\n <!-- Colour source. Column and gene are mutually exclusive: picking one\n clears the other, so what drives the colours is never ambiguous. -->\n <div class=\"sc-section\">\n <label class=\"sc-lbl\" for=\"sc-column\">Colour by column</label>\n <p-dropdown\n inputId=\"sc-column\"\n [options]=\"columnOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedColumn\"\n (ngModelChange)=\"onColumn($event)\"\n placeholder=\"None (flat colour)\"\n appendTo=\"body\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n </div>\n\n <ng-container *ngIf=\"genes?.state$ | async as gene\">\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-gene\">Colour by gene</label>\n <!-- A filterable dropdown, not a free-text box: the genes are visible before\n anything is typed, and each keystroke narrows a list the user can see.\n Virtual-scrolled because a targeted panel runs to a few thousand names. -->\n <p-dropdown\n inputId=\"sc-gene\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"selectedGene\"\n (ngModelChange)=\"onGene($event)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onShow)=\"genes?.ensureList()\"\n [showClear]=\"true\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Search {{ ds.features.count | number }} genes\u2026\"\n styleClass=\"sc-full\"\n ></p-dropdown>\n <small class=\"sc-hint\" *ngIf=\"gene.failed\">Gene lookup failed.</small>\n </div>\n\n <!-- Which observations are drawn at all, by what they express. Several genes at\n once, unlike the colour, which reads one: picking genes here hides every\n observation that expresses none of them (or not all of them), and by default\n colours (and, in 2D, marks) each drawn one by its gene. -->\n <div class=\"sc-section\" *ngIf=\"ds.features\">\n <label class=\"sc-lbl\" for=\"sc-show-genes\">Show observations expressing</label>\n <p-multiSelect\n inputId=\"sc-show-genes\"\n [options]=\"gene.options\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenes\"\n (onChange)=\"onObservationGenes($event.value)\"\n [filter]=\"true\"\n filterBy=\"label\"\n [resetFilterOnHide]=\"true\"\n (onFilter)=\"genes?.onFilter($event.filter)\"\n (onPanelShow)=\"genes?.ensureList()\"\n [virtualScroll]=\"gene.options.length > geneVirtualScrollFrom\"\n [virtualScrollItemSize]=\"30\"\n [showToggleAll]=\"false\"\n [showClear]=\"true\"\n [emptyFilterMessage]=\"gene.remote ? 'No match \u2014 keep typing' : 'No match'\"\n [emptyMessage]=\"gene.remote ? 'Type to search genes' : 'No genes'\"\n appendTo=\"body\"\n placeholder=\"Any gene (every observation)\"\n [selectedItemsLabel]=\"'{0} genes'\"\n [maxSelectedLabels]=\"3\"\n styleClass=\"sc-full\"\n >\n <!-- A picked gene shows its icon in its colour; clicking it edits them. -->\n <ng-template let-option pTemplate=\"item\">\n <div class=\"sc-obs-gene-option\">\n <span class=\"sc-obs-gene-name\">{{ option.label }}</span>\n <ng-container *ngIf=\"isObservationGene(option.value)\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: option.value }\"></ng-container>\n </ng-container>\n </div>\n </ng-template>\n </p-multiSelect>\n <p-selectButton\n *ngIf=\"view.observationGenes.length > 1\"\n [options]=\"matchOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.observationGenesMatch\"\n (ngModelChange)=\"onObservationGenesMatch($event)\"\n [allowEmpty]=\"false\"\n styleClass=\"sc-buttons\"\n ></p-selectButton>\n <!-- The picked genes: each one's icon and colour (click to edit), and a way out. -->\n <div class=\"sc-obs-genes\" *ngIf=\"view.observationGenes.length\">\n <div class=\"sc-obs-gene-row\" *ngFor=\"let g of view.observationGenes; trackBy: trackByGene\">\n <ng-container *ngTemplateOutlet=\"geneMark; context: { $implicit: g }\"></ng-container>\n <span class=\"sc-obs-gene-name\">{{ g }}</span>\n <button\n type=\"button\"\n class=\"sc-icon-btn\"\n [attr.aria-label]=\"'Stop showing ' + g\"\n pTooltip=\"Remove\"\n tooltipPosition=\"top\"\n (click)=\"removeObservationGene(g)\"\n >\n <i class=\"pi pi-times\"></i>\n </button>\n </div>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.observationGenes.length\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.observationGenesColorBy\"\n (onChange)=\"onObservationGenesColorBy($event.checked)\"\n label=\"Colour by these genes\"\n pTooltip=\"Each observation in the colour (and, in 2D, the icon) of the picked gene it expresses most, relative to that gene's highest value. Off, Colour by column or gene applies.\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <small class=\"sc-hint\" *ngIf=\"view.observationGenes.length\">{{ geneFilterHint }}</small>\n <!-- The filter is never dropped silently: a failed expression read keeps the observations\n as they were drawn, and says so. -->\n <small class=\"sc-hint sc-error\" *ngIf=\"view.observationGenes.length && (geneFilterFailed$ | async)\">\n The expression of the picked genes could not be read, so the observations still show the previous\n filter.\n <a href=\"\" (click)=\"$event.preventDefault(); retryGeneFilter()\">Try again</a>\n </small>\n </div>\n </ng-container>\n\n <spatial-key\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n (categoryClicked)=\"categoryClicked.emit($event)\"\n ></spatial-key>\n\n <!-- Display -->\n <div class=\"sc-section\">\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Point size</span>\n <p-slider\n [ngModel]=\"view.pointScale\"\n [min]=\"0.25\"\n [max]=\"5\"\n [step]=\"0.25\"\n (onChange)=\"onPointScale($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.pointScale }}\u00D7</span>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Opacity</span>\n <p-slider\n [ngModel]=\"view.opacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.opacity }}</span>\n </div>\n </div>\n </div>\n </div>\n\n <!-- Only for a gene: the map draws that gene's expression as a field. In 2D\n it goes under the cells of the displayed plane; in 3D it is one sheet per\n imaged section, optionally smoothed along z into a volume. -->\n <div class=\"sc-section\" *ngIf=\"canMapGene\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMap\"\n (onChange)=\"onGeneMap($event.checked)\"\n label=\"Gene map (expression field)\"\n [pTooltip]=\"\n is3d\n ? 'Draw this gene\\'s expression as one field per imaged section \u2014 an estimate between the cells, empty where nothing was measured'\n : 'Draw this gene\\'s expression as a smooth field UNDER the cells \u2014 an estimate between them, transparent where nothing was measured'\n \"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- 3D only: interpolating along z turns the measured sheets into an\n estimate, so it is its own decision and its own label. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.geneMapVolume\"\n (onChange)=\"onGeneMapVolume($event.checked)\"\n label=\"Volume rendering (interpolate along z)\"\n pTooltip=\"Smooth the per-section fields into a continuous volume \u2014 an ESTIMATE between the imaged sections, not measured tissue\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Meaningless while interpolating: a volume built from one section would\n smear that slide through the specimen's whole depth. -->\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"geneMapOneSection\"\n (onChange)=\"onGeneMapOneSection($event.checked)\"\n label=\"One map section at a time\"\n pTooltip=\"Draw a single imaged section's field instead of the whole stack of sheets\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap && is3d && isSectioned && !view.geneMapVolume && geneMapOneSection\">\n <span class=\"sc-lbl\">Map section</span>\n <p-slider\n [ngModel]=\"view.geneMapSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onGeneMapSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ geneMapSectionLabel }}</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.geneMapSmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onGeneMapSmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapSmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.geneMap\">\n <span class=\"sc-lbl\">Map opacity</span>\n <p-slider\n [ngModel]=\"view.geneMapOpacity\"\n [min]=\"0.1\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onGeneMapOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.geneMapOpacity }}</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && !is3d\">\n Kernel-weighted mean per cell, not a sum \u2014 so a crowded region does not glow for being crowded. Transparent\n where no cell was measured. Turn the cells' own Opacity down to read the field under them.\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.geneMap && is3d\">\n {{ geneMapVolumeNote }}\n </div>\n </div>\n\n <!-- 3D only: the volume, the cloud and the density volumes occupy the same\n space, so each hides the others. Independent toggles, because the useful\n views are the combinations \u2014 the field alone, one section's cells over it,\n the anatomy alone. -->\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <span class=\"sc-lbl\">Show</span>\n <div class=\"sc-row\" *ngIf=\"ds.volume\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showVolume\"\n (onChange)=\"onShowVolume($event.checked)\"\n label=\"Reference volume\"\n pTooltip=\"The registered anatomy the observations sit in\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- The backdrop's own opacity: reading the cloud or a density field\n through the anatomy means turning the anatomy down, not the data. -->\n <div class=\"sc-row\" *ngIf=\"ds.volume && view.showVolume\">\n <span class=\"sc-lbl\">Volume opacity</span>\n <p-slider\n [ngModel]=\"view.volumeOpacity\"\n [min]=\"0.05\"\n [max]=\"1\"\n [step]=\"0.05\"\n (onChange)=\"onVolumeOpacity($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.volumeOpacity }}</span>\n </div>\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showPoints\"\n (onChange)=\"onShowPoints($event.checked)\"\n label=\"Observations\"\n pTooltip=\"The measured cells. Hide them to read the density volumes underneath\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <!-- Only for data that really is sectioned; a continuous z has no sections\n to step through. -->\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"oneSection\"\n (onChange)=\"onOneSection($event.checked)\"\n label=\"One section at a time\"\n pTooltip=\"Draw a single imaged section's cells, so the density volumes between the sections stay visible\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.showPoints && isSectioned && oneSection\">\n <span class=\"sc-lbl\">Section</span>\n <p-slider\n [ngModel]=\"view.pointSection\"\n [min]=\"0\"\n [max]=\"lastSection\"\n [step]=\"1\"\n (onChange)=\"onPointSection($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ sectionLabel }}</span>\n </div>\n </div>\n\n <div class=\"sc-section\" *ngIf=\"is3d\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.densityVolume\"\n (onChange)=\"onDensityVolume($event.checked)\"\n label=\"Cluster density volumes\"\n pTooltip=\"Raymarch each cluster as a smooth density field \u2014 an ESTIMATE between the imaged sections, not measured cells\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\" *ngIf=\"view.densityVolume\">\n <span class=\"sc-lbl\">Smoothing</span>\n <p-slider\n [ngModel]=\"view.densitySmoothing\"\n [min]=\"0.5\"\n [max]=\"4\"\n [step]=\"0.5\"\n (onChange)=\"onDensitySmoothing($event.value)\"\n ></p-slider>\n <span class=\"sc-val\">{{ view.densitySmoothing }}\u00D7</span>\n </div>\n <div class=\"sc-hint\" *ngIf=\"view.densityVolume\">{{ densityNote }}</div>\n </div>\n\n <!-- Continuous-only knobs: meaningless for a categorical palette. -->\n <div class=\"sc-section\" *ngIf=\"isContinuous\">\n <div class=\"sc-row\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.logScale\"\n (onChange)=\"onLogScale($event.checked)\"\n label=\"Log scale\"\n pTooltip=\"Counts collapse against their outliers on a linear ramp\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n </div>\n <div class=\"sc-row\">\n <span class=\"sc-lbl\">Clip outliers</span>\n <p-dropdown\n [options]=\"clipOptions\"\n optionLabel=\"label\"\n optionValue=\"value\"\n [ngModel]=\"view.percentileClip\"\n (ngModelChange)=\"onClip($event)\"\n appendTo=\"body\"\n pTooltip=\"Percentile the contrast window is taken at\"\n tooltipPosition=\"top\"\n ></p-dropdown>\n <!-- Reserves the readout column this row has no readout for, so the\n control's right edge lines up with every slider's. -->\n <span class=\"sc-val\"></span>\n </div>\n </div>\n\n <!-- A gene's icon in its colour: the button that opens its icon and colour picker. -->\n <ng-template #geneMark let-g>\n <button\n type=\"button\"\n class=\"sc-gene-icon\"\n [style.color]=\"geneColorOf(g)\"\n [attr.aria-label]=\"'Icon and colour for ' + g\"\n pTooltip=\"Icon and colour\"\n tooltipPosition=\"top\"\n (click)=\"openGeneStyle($event, g, genePop)\"\n >\n <svg viewBox=\"-1.2 -1.2 2.4 2.4\" class=\"sc-glyph-svg\">\n <polygon [attr.points]=\"glyphPointsOf(g)\"></polygon>\n </svg>\n </button>\n </ng-template>\n <p-overlayPanel #genePop styleClass=\"sc-gene-style\" appendTo=\"body\">\n <ng-template pTemplate=\"content\">\n <spatial-gene-style\n *ngIf=\"styleGene as g\"\n [gene]=\"g\"\n what=\"Observation\"\n [color]=\"geneColorOf(g)\"\n [glyph]=\"geneGlyphOf(g)\"\n iconHint=\"Icons show on the 2D observation markers.\"\n (colorChange)=\"onGeneColor(g, $event)\"\n (glyphChange)=\"onGeneGlyph(g, $event)\"\n (resetStyle)=\"resetGeneStyle(g)\"\n ></spatial-gene-style>\n </ng-template>\n </p-overlayPanel>\n</ng-container>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host{display:contents}.sc-obs-genes{display:flex;flex-direction:column;gap:.15rem;margin-top:.35rem}.sc-obs-gene-row,.sc-obs-gene-option{display:flex;align-items:center;gap:.4rem;min-width:0}.sc-obs-gene-option{flex:1 1 auto;justify-content:space-between}.sc-obs-gene-name{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
24697
24787
  }], propDecorators: { controls: [{
24698
24788
  type: Input
24699
24789
  }], dataset: [{
@@ -25026,6 +25116,10 @@ class SpatialControlsComponent {
25026
25116
  get hasTranscripts() {
25027
25117
  return !!this.dataset?.transcriptTiles || !!this.dataset?.density;
25028
25118
  }
25119
+ /** Whether the 3D cloud can draw the transcripts too: they carry z (a MERFISH volume's do). */
25120
+ get hasTranscripts3d() {
25121
+ return !!this.dataset?.transcriptTiles?.hasZ;
25122
+ }
25029
25123
  onShowImage(on) {
25030
25124
  this.controls?.setViewState({ showImage: on });
25031
25125
  }
@@ -25061,7 +25155,7 @@ class SpatialControlsComponent {
25061
25155
  return this.key.refresh(this.controls, this.dataset, this.view, this.colormap, this.reverse);
25062
25156
  }
25063
25157
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialControlsComponent, deps: [{ token: VISUALIZER }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); }
25064
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialControlsComponent, isStandalone: true, selector: "spatial-controls", inputs: { visible: "visible", is3d: "is3d" }, outputs: { visibleChange: "visibleChange" }, viewQueries: [{ propertyName: "charts", first: true, predicate: SpatialChartsComponent, descendants: true }], ngImport: i0, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n [loading]=\"selectingFromRegions\"\n (onClick)=\"selectFromRegions()\"\n [pTooltip]=\"\n selectsClassVolumes\n ? 'Select every observation inside the volumes of the classes picked in the Region Editor'\n : 'Select every observation inside the drawn regions'\n \"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\">\n No observations fell inside the\n {{ missedMode === 'class-volumes' ? 'picked classes' : 'drawn regions' }}.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectsClassVolumes\">\n Selects inside the volume of {{ pickedClasses[0]\n }}{{ pickedClasses.length > 1 ? ' and ' + (pickedClasses.length - 1 | number) + ' more classes' : '' }},\n picked in the Region Editor. Draw a region to select by it instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes && pickedClasses.length\">\n The classes picked in the Region Editor are not on the reference volume shown here, so this selects by\n the regions drawn over the cloud instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n <ng-container *ngIf=\"!pickedClasses.length\">\n Pick classes in the Region Editor's class tree to select inside their volumes.\n </ng-container>\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i5.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialCellsPanelComponent, selector: "spatial-cells-panel", inputs: ["controls", "dataset", "view", "genes", "colorBarCss", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialTranscriptsPanelComponent, selector: "spatial-transcripts-panel", inputs: ["controls", "dataset", "view", "genes", "estimate", "densityStats", "geneCounts", "colormapOptions", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialObservationsPanelComponent, selector: "spatial-observations-panel", inputs: ["controls", "dataset", "view", "genes", "legend", "colorBarCss", "selection", "selectedCategory", "colormapOptions", "is3d", "open"], outputs: ["openChange", "categoryClicked"] }, { kind: "component", type: SpatialChartsComponent, selector: "spatial-charts", inputs: ["active"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
25158
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.3.9", type: SpatialControlsComponent, isStandalone: true, selector: "spatial-controls", inputs: { visible: "visible", is3d: "is3d" }, outputs: { visibleChange: "visibleChange" }, viewQueries: [{ propertyName: "charts", first: true, predicate: SpatialChartsComponent, descendants: true }], ngImport: i0, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && (!is3d || hasTranscripts3d)\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n [loading]=\"selectingFromRegions\"\n (onClick)=\"selectFromRegions()\"\n [pTooltip]=\"\n selectsClassVolumes\n ? 'Select every observation inside the volumes of the classes picked in the Region Editor'\n : 'Select every observation inside the drawn regions'\n \"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\">\n No observations fell inside the\n {{ missedMode === 'class-volumes' ? 'picked classes' : 'drawn regions' }}.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectsClassVolumes\">\n Selects inside the volume of {{ pickedClasses[0]\n }}{{ pickedClasses.length > 1 ? ' and ' + (pickedClasses.length - 1 | number) + ' more classes' : '' }},\n picked in the Region Editor. Draw a region to select by it instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes && pickedClasses.length\">\n The classes picked in the Region Editor are not on the reference volume shown here, so this selects by\n the regions drawn over the cloud instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n <ng-container *ngIf=\"!pickedClasses.length\">\n Pick classes in the Region Editor's class tree to select inside their volumes.\n </ng-container>\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "pipe", type: i1$1.DecimalPipe, name: "number" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i3.Button, selector: "p-button", inputs: ["type", "iconPos", "icon", "badge", "label", "disabled", "loading", "loadingIcon", "raised", "rounded", "text", "plain", "severity", "outlined", "link", "tabindex", "size", "style", "styleClass", "badgeClass", "ariaLabel", "autofocus"], outputs: ["onClick", "onFocus", "onBlur"] }, { kind: "ngmodule", type: CheckboxModule }, { kind: "component", type: i4$2.Checkbox, selector: "p-checkbox", inputs: ["value", "name", "disabled", "binary", "label", "ariaLabelledBy", "ariaLabel", "tabindex", "inputId", "style", "styleClass", "labelStyleClass", "formControl", "checkboxIcon", "readonly", "required", "autofocus", "trueValue", "falseValue", "variant"], outputs: ["onChange", "onFocus", "onBlur"] }, { kind: "ngmodule", type: DialogModule }, { kind: "component", type: i5.Dialog, selector: "p-dialog", inputs: ["header", "draggable", "resizable", "positionLeft", "positionTop", "contentStyle", "contentStyleClass", "modal", "closeOnEscape", "dismissableMask", "rtl", "closable", "responsive", "appendTo", "breakpoints", "styleClass", "maskStyleClass", "maskStyle", "showHeader", "breakpoint", "blockScroll", "autoZIndex", "baseZIndex", "minX", "minY", "focusOnShow", "maximizable", "keepInViewport", "focusTrap", "transitionOptions", "closeIcon", "closeAriaLabel", "closeTabindex", "minimizeIcon", "maximizeIcon", "visible", "style", "position"], outputs: ["onShow", "onHide", "visibleChange", "onResizeInit", "onResizeEnd", "onDragEnd", "onMaximize"] }, { kind: "ngmodule", type: TooltipModule }, { kind: "directive", type: i5$1.Tooltip, selector: "[pTooltip]", inputs: ["tooltipPosition", "tooltipEvent", "appendTo", "positionStyle", "tooltipStyleClass", "tooltipZIndex", "escape", "showDelay", "hideDelay", "life", "positionTop", "positionLeft", "autoHide", "fitContent", "hideOnEscape", "pTooltip", "tooltipDisabled", "tooltipOptions"] }, { kind: "component", type: SpatialCellsPanelComponent, selector: "spatial-cells-panel", inputs: ["controls", "dataset", "view", "genes", "colorBarCss", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialTranscriptsPanelComponent, selector: "spatial-transcripts-panel", inputs: ["controls", "dataset", "view", "genes", "estimate", "densityStats", "geneCounts", "colormapOptions", "open"], outputs: ["openChange"] }, { kind: "component", type: SpatialObservationsPanelComponent, selector: "spatial-observations-panel", inputs: ["controls", "dataset", "view", "genes", "legend", "colorBarCss", "selection", "selectedCategory", "colormapOptions", "is3d", "open"], outputs: ["openChange", "categoryClicked"] }, { kind: "component", type: SpatialChartsComponent, selector: "spatial-charts", inputs: ["active"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
25065
25159
  }
25066
25160
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImport: i0, type: SpatialControlsComponent, decorators: [{
25067
25161
  type: Component,
@@ -25076,7 +25170,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
25076
25170
  SpatialTranscriptsPanelComponent,
25077
25171
  SpatialObservationsPanelComponent,
25078
25172
  SpatialChartsComponent,
25079
- ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n [loading]=\"selectingFromRegions\"\n (onClick)=\"selectFromRegions()\"\n [pTooltip]=\"\n selectsClassVolumes\n ? 'Select every observation inside the volumes of the classes picked in the Region Editor'\n : 'Select every observation inside the drawn regions'\n \"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\">\n No observations fell inside the\n {{ missedMode === 'class-volumes' ? 'picked classes' : 'drawn regions' }}.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectsClassVolumes\">\n Selects inside the volume of {{ pickedClasses[0]\n }}{{ pickedClasses.length > 1 ? ' and ' + (pickedClasses.length - 1 | number) + ' more classes' : '' }},\n picked in the Region Editor. Draw a region to select by it instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes && pickedClasses.length\">\n The classes picked in the Region Editor are not on the reference volume shown here, so this selects by\n the regions drawn over the cloud instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n <ng-container *ngIf=\"!pickedClasses.length\">\n Pick classes in the Region Editor's class tree to select inside their volumes.\n </ng-container>\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"] }]
25173
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "<p-dialog\n header=\"Spatial omics\"\n [visible]=\"visible\"\n (visibleChange)=\"onVisibleChange($event)\"\n [modal]=\"false\"\n [resizable]=\"true\"\n [draggable]=\"true\"\n position=\"left\"\n (onResizeEnd)=\"onResizeEnd()\"\n [style]=\"{ width: '30rem' }\"\n [breakpoints]=\"{ '960px': '90vw' }\"\n styleClass=\"spatial-controls-dialog\"\n>\n <!-- No port bound: say why rather than showing controls that do nothing. -->\n <div class=\"sc-empty\" *ngIf=\"!controls\">\n No spatial-omics data source is configured. A host provides one by binding\n <code>SPATIAL_DATA_PORT</code>.\n </div>\n\n <div class=\"sc-empty\" *ngIf=\"controls && !dataset\">No dataset is loaded.</div>\n\n <div class=\"sc-body\" *ngIf=\"controls && dataset as ds\">\n <div class=\"sc-dataset\">\n <span class=\"sc-dataset-name\">{{ ds.name }}</span>\n <span class=\"sc-dataset-count\">{{ ds.observations.count | number }} observations</span>\n </div>\n\n <!-- Organised as Xenium Explorer's side panel: one collapsible section per layer,\n each with its own on/off switch in the header. -->\n\n <!-- Images -->\n <div class=\"sc-panel\" *ngIf=\"ds.imageRef && !is3d\" [class.open]=\"open.images\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('images')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showImage\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowImage($event.checked)\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Images</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.images\" [class.pi-chevron-right]=\"!open.images\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.images\">\n <small class=\"sc-hint\"> Channels, colours and contrast are in the Channels &amp; Histogram dialog. </small>\n </div>\n </div>\n\n <!-- Cells -->\n <spatial-cells-panel\n *ngIf=\"hasCells && !is3d\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [colorBarCss]=\"colorBarCss\"\n [open]=\"open.cells\"\n (openChange)=\"setOpen('cells', $event)\"\n ></spatial-cells-panel>\n\n <!-- Transcripts -->\n <spatial-transcripts-panel\n *ngIf=\"hasTranscripts && (!is3d || hasTranscripts3d)\"\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [estimate]=\"estimate\"\n [densityStats]=\"densityStats\"\n [geneCounts]=\"geneCounts\"\n [colormapOptions]=\"colormapOptions\"\n [open]=\"open.transcripts\"\n (openChange)=\"setOpen('transcripts', $event)\"\n >\n </spatial-transcripts-panel>\n\n <!-- Annotations: the drawn regions, and selecting what they enclose. -->\n <div class=\"sc-panel\" [class.open]=\"open.annotations\">\n <div class=\"sc-panel-head\" (click)=\"toggleSection('annotations')\">\n <p-checkbox\n [binary]=\"true\"\n [ngModel]=\"view.showAnnotations !== false\"\n (click)=\"$event.stopPropagation()\"\n (onChange)=\"onShowAnnotations($event.checked)\"\n pTooltip=\"Show the drawn regions\"\n tooltipPosition=\"top\"\n ></p-checkbox>\n <span class=\"sc-panel-title\">Annotations</span>\n <i class=\"pi\" [class.pi-chevron-down]=\"open.annotations\" [class.pi-chevron-right]=\"!open.annotations\"></i>\n </div>\n <div class=\"sc-panel-body\" *ngIf=\"open.annotations\">\n <div class=\"sc-row\">\n <p-button\n label=\"Select from ROIs\"\n icon=\"pi pi-object-group\"\n size=\"small\"\n [outlined]=\"true\"\n [loading]=\"selectingFromRegions\"\n (onClick)=\"selectFromRegions()\"\n [pTooltip]=\"\n selectsClassVolumes\n ? 'Select every observation inside the volumes of the classes picked in the Region Editor'\n : 'Select every observation inside the drawn regions'\n \"\n tooltipPosition=\"top\"\n ></p-button>\n <p-button\n *ngIf=\"hasSelection\"\n label=\"Clear\"\n size=\"small\"\n [text]=\"true\"\n (onClick)=\"clearSelection()\"\n ></p-button>\n </div>\n <small class=\"sc-hint\" *ngIf=\"hasSelection\">\n {{ selection.count | number }} of {{ ds.observations.count | number }} selected \u2014 the rest are muted.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectionMissed\">\n No observations fell inside the\n {{ missedMode === 'class-volumes' ? 'picked classes' : 'drawn regions' }}.\n </small>\n <small class=\"sc-hint\" *ngIf=\"selectsClassVolumes\">\n Selects inside the volume of {{ pickedClasses[0]\n }}{{ pickedClasses.length > 1 ? ' and ' + (pickedClasses.length - 1 | number) + ' more classes' : '' }},\n picked in the Region Editor. Draw a region to select by it instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes && pickedClasses.length\">\n The classes picked in the Region Editor are not on the reference volume shown here, so this selects by\n the regions drawn over the cloud instead.\n </small>\n <small class=\"sc-hint\" *ngIf=\"is3d && !selectsClassVolumes\">\n In 3D a region cuts through the cloud's full depth. Orbit and draw again to cut from another angle.\n <ng-container *ngIf=\"!pickedClasses.length\">\n Pick classes in the Region Editor's class tree to select inside their volumes.\n </ng-container>\n </small>\n </div>\n </div>\n\n <!-- Observations, then what else draws them: the gene map, the 3D scene, and the\n continuous-only knobs. -->\n <spatial-observations-panel\n [controls]=\"controls\"\n [dataset]=\"ds\"\n [view]=\"view\"\n [genes]=\"genes\"\n [legend]=\"legend\"\n [colorBarCss]=\"colorBarCss\"\n [selection]=\"selection\"\n [selectedCategory]=\"selectedCategory\"\n [colormapOptions]=\"colormapOptions\"\n [is3d]=\"is3d\"\n [open]=\"open.observations\"\n (openChange)=\"setOpen('observations', $event)\"\n (categoryClicked)=\"selectCategory($event)\"\n ></spatial-observations-panel>\n\n <!-- Distributions of whatever the map is coloured by. In the SAME dialog as\n the colour controls on purpose: changing the gene and watching the\n distribution move is one action, and two floating windows made that\n harder to see rather than easier. -->\n <div class=\"sc-section sc-charts\">\n <button\n type=\"button\"\n class=\"sc-disclosure\"\n (click)=\"toggleCharts()\"\n [attr.aria-expanded]=\"chartsOpen\"\n [attr.aria-controls]=\"chartsBodyId\"\n >\n <i class=\"pi\" [class.pi-chevron-right]=\"!chartsOpen\" [class.pi-chevron-down]=\"chartsOpen\"></i>\n <span>Distribution</span>\n <small *ngIf=\"!chartsOpen && view.colorBy\">of {{ colorByLabel }}</small>\n </button>\n <!-- Kept MOUNTED when collapsed, only hidden: destroying it would throw\n away the chart kind and grouping the user picked. `active` is what\n stops it drawing while it has no size. -->\n <div [id]=\"chartsBodyId\" [hidden]=\"!chartsOpen\">\n <spatial-charts [active]=\"visible && chartsOpen\"></spatial-charts>\n </div>\n </div>\n\n <div class=\"sc-btns\">\n <p-button label=\"Reset\" size=\"small\" [text]=\"true\" (onClick)=\"reset()\"></p-button>\n </div>\n </div>\n</p-dialog>\n", styles: [".sc-hint{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-error{color:var(--red-600, #c62828)}.sc-section{display:flex;flex-direction:column;gap:.35rem}.sc-lbl{font-size:.78rem;color:var(--text-color-secondary, #6c757d);white-space:nowrap}.sc-row{display:flex;align-items:center;gap:.6rem}.sc-row p-slider,.sc-row p-dropdown,.sc-row p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row p-slider .p-slider{width:100%}:host ::ng-deep .sc-row p-dropdown .p-dropdown,:host ::ng-deep .sc-row p-treeSelect .p-treeselect{width:100%}.sc-row .sc-lbl{flex:0 0 calc(50% - .6rem);min-width:0;overflow:hidden;text-overflow:ellipsis}.sc-cmap-option{display:inline-flex;align-items:center;gap:.4rem}.sc-cmap-swatch{width:3.5rem;height:.7rem;border-radius:2px;display:block}.sc-val{flex:0 0 4rem;text-align:right;font-size:.78rem;font-variant-numeric:tabular-nums;white-space:nowrap}:host ::ng-deep .sc-full,:host ::ng-deep .sc-full .p-dropdown,:host ::ng-deep .sc-full .p-autocomplete,:host ::ng-deep .sc-full input{width:100%}.sc-colorbar{height:.75rem;border-radius:2px;border:1px solid rgba(0,0,0,.2)}.sc-colorbar-ends{display:flex;justify-content:space-between;font-size:.7rem;color:var(--text-color-secondary, #6c757d);margin-top:.2rem}.sc-panel{border:1px solid var(--surface-border, #dee2e6);border-radius:6px;margin-bottom:.5rem;overflow:hidden}.sc-panel-head{display:flex;align-items:center;gap:.6rem;padding:.5rem .7rem;cursor:pointer;-webkit-user-select:none;user-select:none;background:var(--surface-100, #f5f6f7)}.sc-panel-head .sc-panel-title{flex:1;font-weight:600}.sc-panel.open>.sc-panel-head{background:var(--primary-color, #2196f3);color:var(--primary-color-text, #fff)}.sc-panel-body{display:flex;flex-direction:column;gap:.45rem;padding:.6rem .75rem .75rem}.sc-subhead{display:flex;align-items:center;gap:.5rem;margin-top:.4rem;font-size:.72rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--text-color-secondary, #6c757d)}.sc-subhead:after{content:\"\";flex:1;border-top:1px solid var(--surface-border, #dee2e6)}:host ::ng-deep .sc-buttons .p-button{padding:.35rem .75rem;font-size:.85rem}.sc-row-picker p-dropdown,.sc-row-picker p-multiSelect,.sc-row-picker p-treeSelect{flex:1 1 0;min-width:0}:host ::ng-deep .sc-row-picker .sc-variant{width:6.5rem}:host ::ng-deep .sc-row-picker p-multiselect .p-multiselect{width:100%}:host ::ng-deep .sc-row-picker p-dropdown:has(.sc-variant){flex:0 0 6.5rem}:host ::ng-deep .sc-row-picker p-treeselect:has(.sc-palette){flex:0 0 3rem}:host ::ng-deep .sc-palette .p-treeselect-label{width:0;padding:0}.sc-icon-btn{flex:0 0 auto;border:0;background:transparent;cursor:pointer;padding:.25rem .35rem;font:inherit;font-size:1.05rem;color:var(--text-color, #333);border-radius:4px}.sc-icon-btn:hover{background:var(--surface-200, #e9ecef)}.sc-icon-btn.active{color:var(--primary-color, #2196f3);font-weight:700}.sc-icon-btn:disabled{opacity:.5;cursor:default}.sc-small-btn{margin-left:auto;border:1px solid var(--surface-border, #ced4da);background:var(--surface-0, #fff);border-radius:4px;padding:.2rem .6rem;font:inherit;font-size:.8rem;cursor:pointer}:host ::ng-deep .sc-group-heading{font-size:.7rem;font-weight:700;letter-spacing:.05em;text-transform:uppercase;opacity:.7}.sc-group-list,.sc-gene-tree{max-height:16rem;overflow-y:auto;border:1px solid var(--surface-border, #dee2e6);border-radius:4px;padding:.25rem .4rem}.sc-group-row,.sc-gene-row{display:flex;align-items:center;gap:.45rem;padding:.15rem 0;font-size:.88rem}.sc-group-row .sc-group-label,.sc-gene-row .sc-group-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-group-all,.sc-gene-root,.sc-gene-group{font-weight:600}.sc-group-all .sc-icon-btn,.sc-gene-root .sc-icon-btn,.sc-gene-group .sc-icon-btn{margin-left:auto;font-size:.85rem}.sc-count{flex:0 0 auto;font-size:.78rem;color:var(--text-color-secondary, #6c757d)}.sc-glyph-option{display:inline-flex;align-items:center;gap:.4rem}.sc-glyph-svg{width:1rem;height:1rem;fill:currentColor}:host ::ng-deep .sc-glyph-pick .p-dropdown-label{padding:.2rem .4rem}.sc-color-input{width:2.5rem;height:1.8rem;border:1px solid var(--surface-border, #ced4da);border-radius:4px;padding:0;background:none}.sc-row-number p-slider,.sc-row-range p-slider{flex:1 1 0;min-width:0}:host ::ng-deep .sc-num{width:4.5rem;padding:.3rem .4rem;text-align:center}:host ::ng-deep .sc-row-number p-inputnumber,:host ::ng-deep .sc-row-range p-inputnumber{flex:0 0 auto}.sc-gene-icon{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:1.4rem;height:1.4rem;padding:0;border:1px solid transparent;border-radius:4px;background:transparent;cursor:pointer}.sc-gene-icon:hover,.sc-gene-icon:focus-visible{border-color:var(--surface-border, rgba(0, 0, 0, .2))}:host ::ng-deep .spatial-controls-dialog .p-dialog-content{padding:.75rem 1rem 1rem}.sc-empty{padding:.5rem 0;font-size:.85rem;color:var(--text-color-secondary, #6c757d);line-height:1.4}.sc-empty code{font-size:.8rem}.sc-body{display:flex;flex-direction:column;gap:.85rem}.sc-dataset{display:flex;flex-direction:column;gap:.1rem;padding-bottom:.5rem;border-bottom:1px solid var(--surface-border, #dee2e6)}.sc-dataset-name{font-weight:600;font-size:.9rem}.sc-dataset-count{font-size:.75rem;color:var(--text-color-secondary, #6c757d)}.sc-btns{display:flex;justify-content:flex-end}.sc-charts{padding-top:.5rem;border-top:1px solid var(--surface-border, #dee2e6);gap:0}.sc-disclosure{display:flex;align-items:center;gap:.4rem;width:100%;padding:.2rem 0;font:inherit;font-size:.82rem;font-weight:600;text-align:left;background:none;border:0;cursor:pointer;color:inherit}.sc-disclosure i{font-size:.7rem;color:var(--text-color-secondary, #6c757d)}.sc-disclosure small{font-weight:400;color:var(--text-color-secondary, #6c757d);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sc-disclosure:hover{color:var(--primary-color, #3b82f6)}\n"] }]
25080
25174
  }], ctorParameters: () => [{ type: undefined, decorators: [{
25081
25175
  type: Inject,
25082
25176
  args: [VISUALIZER]
@@ -39289,6 +39383,9 @@ class SpatialControlsFacade {
39289
39383
  // The gene filter names the old dataset's genes: kept, it could hide every observation.
39290
39384
  if (current.observationGenes?.length)
39291
39385
  ctx.store.setSpatialView({ observationGenes: [] });
39386
+ // Group colours are chosen for this dataset's groupings; another's same-named column is not them.
39387
+ if (Object.keys(current.groupColors ?? {}).length)
39388
+ ctx.store.setSpatialView({ groupColors: {} });
39292
39389
  });
39293
39390
  this.controls = this.build();
39294
39391
  }
@@ -39371,7 +39468,7 @@ class SpatialControlsFacade {
39371
39468
  }
39372
39469
  // Resolved by the SAME function the renderer uses, so a legend swatch
39373
39470
  // can never disagree with the colour on screen.
39374
- return resolveCategoryColors(column.meta);
39471
+ return categoryColorsFor(name, column.meta, this.ctx.store.currentSpatialView().groupColors);
39375
39472
  },
39376
39473
  continuousValues: async (source) => {
39377
39474
  if (source.kind === 'feature')
@@ -39390,7 +39487,7 @@ class SpatialControlsFacade {
39390
39487
  return {
39391
39488
  name,
39392
39489
  categories: column.meta.categories,
39393
- colors: resolveCategoryColors(column.meta),
39490
+ colors: categoryColorsFor(name, column.meta, this.ctx.store.currentSpatialView().groupColors),
39394
39491
  codes: column.codes,
39395
39492
  };
39396
39493
  },
@@ -42482,8 +42579,10 @@ const REGION_MESH_DIMMED_OPACITY = 0.12;
42482
42579
  /**
42483
42580
  * The region reconstructions shown in a volume view: one translucent surface per region, in its
42484
42581
  * colour, placed in the volume's world box (pixel x, y and slice z mapped onto it the way the
42485
- * volume maps its voxels) and re-placed when the volume's Z is stretched. A surface is updated in
42486
- * place (`setData`, `color`) rather than added again, and picked by napari-js (`pickSurface`).
42582
+ * volume maps its voxels). The box's Z stretch is the surfaces' `scale3d` (a uniform), so a stretch
42583
+ * alone changes no geometry; a new box re-places them. A surface is updated in place (`setData`,
42584
+ * `color`, `scale3d`) rather than added again, and picked by napari-js (`pickSurface`), which
42585
+ * picks it where it is drawn.
42487
42586
  *
42488
42587
  * Highlighting is focus + context: the highlighted surfaces turn near opaque and the others fade
42489
42588
  * to a ghost, so the selection stands out while staying in place among its neighbours.
@@ -42503,13 +42602,30 @@ class VolumeRegionMeshes {
42503
42602
  this.wanted = meshes;
42504
42603
  this.sync();
42505
42604
  }
42506
- /** Follow the volume's frame (it opened, or its Z was stretched): re-place every surface. */
42605
+ /**
42606
+ * Follow the volume's frame. A new box (it opened, another volume) re-places every surface; a Z
42607
+ * stretch of the same box only sets their `scale3d`, so nothing is re-uploaded while dragging.
42608
+ */
42507
42609
  setFrame(frame) {
42610
+ const placed = this.frame ? baseFrame(this.frame) : null;
42611
+ const next = baseFrame(frame);
42612
+ const sameBox = !!placed &&
42613
+ placed.size.every((v, i) => v === next.size[i]) &&
42614
+ placed.world.every((v, i) => Math.abs(v - next.world[i]) <= 1e-9 * Math.max(1, Math.abs(v)));
42508
42615
  this.frame = frame;
42509
- for (const e of this.layers.values())
42510
- e.layer.setData(toWorld(e.mesh.vertices, frame), e.mesh.faces);
42616
+ const scale = this.scale3d();
42617
+ for (const e of this.layers.values()) {
42618
+ if (!sameBox)
42619
+ e.layer.setData(toWorld(e.mesh.vertices, next), e.mesh.faces);
42620
+ e.layer.scale3d = scale;
42621
+ }
42511
42622
  this.sync();
42512
42623
  }
42624
+ /** The surfaces' GPU scale: the frame's Z stretch. */
42625
+ scale3d() {
42626
+ const s = this.frame?.zScale;
42627
+ return [1, 1, s && s > 0 ? s : 1];
42628
+ }
42513
42629
  /** Highlight the surfaces of these keys (none: every surface as usual). */
42514
42630
  setHighlight(keys) {
42515
42631
  this.highlighted = new Set(keys);
@@ -42561,7 +42677,7 @@ class VolumeRegionMeshes {
42561
42677
  }
42562
42678
  // Rebuilt or recoloured: the same surface, updated in place.
42563
42679
  if (want.vertices !== e.mesh.vertices && this.frame) {
42564
- e.layer.setData(toWorld(want.vertices, this.frame), want.faces);
42680
+ e.layer.setData(toWorld(want.vertices, baseFrame(this.frame)), want.faces);
42565
42681
  }
42566
42682
  if (want.color !== e.mesh.color)
42567
42683
  e.layer.color = want.color;
@@ -42576,8 +42692,9 @@ class VolumeRegionMeshes {
42576
42692
  }
42577
42693
  add(mesh) {
42578
42694
  const frame = this.frame;
42579
- const layer = this.viewer.addSurface(toWorld(mesh.vertices, frame), mesh.faces, undefined, {
42695
+ const layer = this.viewer.addSurface(toWorld(mesh.vertices, baseFrame(frame)), mesh.faces, undefined, {
42580
42696
  name: `region ${mesh.key}`,
42697
+ scale3d: this.scale3d(),
42581
42698
  color: mesh.color,
42582
42699
  opacity: this.opacityOf(mesh.key),
42583
42700
  blending: 'translucent',
@@ -42596,6 +42713,11 @@ class VolumeRegionMeshes {
42596
42713
  this.layers.delete(key);
42597
42714
  }
42598
42715
  }
42716
+ /** `frame` without its Z stretch: the box the surfaces' geometry is placed in. */
42717
+ function baseFrame(frame) {
42718
+ const s = frame.zScale && frame.zScale > 0 ? frame.zScale : 1;
42719
+ return s === 1 ? frame : { size: frame.size, world: [frame.world[0], frame.world[1], frame.world[2] / s] };
42720
+ }
42599
42721
  /**
42600
42722
  * Image px / slice coordinates → the volume's centred world box: pixel x of a `w`-wide image
42601
42723
  * spans the box's width, slice k sits at the middle of its share of the box's depth.
@@ -42842,7 +42964,11 @@ class VolumeScene {
42842
42964
  if (!base || !this.fullSize)
42843
42965
  return;
42844
42966
  const depth = base.depth * this.ctx.settings.volumeZScale;
42845
- const frame = { size: this.fullSize, world: [base.width, base.height, depth] };
42967
+ const frame = {
42968
+ size: this.fullSize,
42969
+ world: [base.width, base.height, depth],
42970
+ zScale: this.ctx.settings.volumeZScale,
42971
+ };
42846
42972
  this.regionMeshes.setFrame(frame);
42847
42973
  this.slicePlanes.setFrame(frame);
42848
42974
  }
@@ -43533,6 +43659,13 @@ const SPATIAL_FALLBACK_RADIUS = 4;
43533
43659
  * 40 µm/px template, so drawing them to scale would put every cell a fifth of a pixel wide
43534
43660
  * and the section would come up empty. The point-size control scales up from this floor. */
43535
43661
  const SPATIAL_SLICE_MIN_DIAMETER_PX = 1.5;
43662
+ /**
43663
+ * Smallest drawn marker diameter in the 2D view, in SCREEN pixels, at any zoom. A marker's size is
43664
+ * the cell's own (data units), so zoomed out over a whole Cytos plate or Xenium section every cell
43665
+ * fell below a pixel and the observations vanished; with the floor they stay a dense field of
43666
+ * dots, coloured as they are, at every zoom (the GPU applies it per frame: no rebuild on zoom).
43667
+ */
43668
+ const SPATIAL_MARKER_MIN_SCREEN_PX = 2;
43536
43669
  /** Longest side of the gene-map raster, in field pixels. A gene map is a smooth
43537
43670
  * field read as territory, so it gains nothing from matching a 2 Gpx slide's
43538
43671
  * resolution — and the estimate costs one pass over this many pixels. */
@@ -43700,9 +43833,9 @@ function totalDensityGroup(selection) {
43700
43833
  * {@link DENSITY_MAX_CLUSTERS}, each with its legend colour — restricted to the selection when
43701
43834
  * there is one, so "select a region, check the box" answers which clusters live there.
43702
43835
  */
43703
- function rankDensityGroups(columnName, column, count, selection) {
43836
+ function rankDensityGroups(columnName, column, count, selection, groupColors) {
43704
43837
  const inSelection = selection.count > 0 ? selection.mask : null;
43705
- const colors = resolveCategoryColors(column.meta);
43838
+ const colors = categoryColorsFor(columnName, column.meta, groupColors);
43706
43839
  const counts = new Uint32Array(column.meta.categories.length);
43707
43840
  for (let i = 0; i < count; i++) {
43708
43841
  if (inSelection && !inSelection[i])
@@ -44229,7 +44362,7 @@ class CellLayers {
44229
44362
  * value, and a selection or opacity change recolours with the vector unchanged (SPATIAL-12). */
44230
44363
  this.contrastWindows = new ContrastWindowCache();
44231
44364
  }
44232
- /** True while outlines are on screen — the service hides its dots then. */
44365
+ /** True while outlines are on screen. */
44233
44366
  get outlinesShown() {
44234
44367
  return this.polygonsShown;
44235
44368
  }
@@ -44279,6 +44412,8 @@ class CellLayers {
44279
44412
  JSON.stringify(view.continuousColormap),
44280
44413
  view.cellDraw,
44281
44414
  view.cellOpacity,
44415
+ // Any grouping's: the cells may be coloured by segmentation method rather than the cell type.
44416
+ JSON.stringify(view.groupColors),
44282
44417
  ].join('|');
44283
44418
  const present = ['cellFill', 'cellOutline', 'nucleusOutline'].some((g) => this.groups.shown(g));
44284
44419
  if (!geometryChanged && styleKey === this.cellStyleKey && present) {
@@ -44376,8 +44511,8 @@ class CellLayers {
44376
44511
  ? 'segmentation_method'
44377
44512
  : cellTypeColumnFor(dataset, view);
44378
44513
  const codes = name ? await this.lookup.codes(name) : null;
44379
- const palette = categoryRgba(codes?.meta ?? null);
44380
- const missing = rgba01(MISSING_COLOR);
44514
+ const palette = categoryRgba(name, codes?.meta ?? null, view.groupColors);
44515
+ const missing = rgba01$1(MISSING_COLOR);
44381
44516
  // One RGBA per shape (napari Shapes' `face_color`): any number of categories keeps its own
44382
44517
  // colour, where a colormap band per category ran out of LUT entries past ~127.
44383
44518
  const faceColor = new Float32Array(owners.length * 4);
@@ -44457,13 +44592,15 @@ class CellLayers {
44457
44592
  }
44458
44593
  }
44459
44594
  /** An 8-bit RGB as an opaque 0..1 RGBA. */
44460
- function rgba01([r, g, b]) {
44595
+ function rgba01$1([r, g, b]) {
44461
44596
  return [r / 255, g / 255, b / 255, 1];
44462
44597
  }
44463
44598
  /** A grouping's category colours, as 0..1 RGBA by category code (one default without a grouping). */
44464
- function categoryRgba(meta) {
44465
- const hex = meta && meta.kind === 'categorical' ? resolveCategoryColors(meta) : [DEFAULT_CATEGORICAL_PALETTE[0]];
44466
- return hex.map((h) => rgba01(parseHex(h)));
44599
+ function categoryRgba(name, meta, overrides) {
44600
+ const hex = name && meta && meta.kind === 'categorical'
44601
+ ? categoryColorsFor(name, meta, overrides)
44602
+ : [DEFAULT_CATEGORICAL_PALETTE[0]];
44603
+ return hex.map((h) => rgba01$1(parseHex(h)));
44467
44604
  }
44468
44605
 
44469
44606
  /**
@@ -45458,6 +45595,7 @@ function transcriptPlanKey(dataset, view, job, pxPerUnit) {
45458
45595
  view.transcriptAllGenesHidden.join(','),
45459
45596
  JSON.stringify(view.transcriptGeneColors),
45460
45597
  JSON.stringify(view.transcriptGeneGroups),
45598
+ groupColorsKey(cellTypeColumnFor(dataset, view), view.groupColors),
45461
45599
  ].join('|');
45462
45600
  }
45463
45601
  /** The view the markers are coloured by, for what was loaded. */
@@ -45476,7 +45614,7 @@ function colorViewFor(view, kind, clustered, hasCellTypes) {
45476
45614
  }
45477
45615
  return view;
45478
45616
  }
45479
- const UNASSIGNED_RGBA = [0.62, 0.62, 0.62, 0.55];
45617
+ const UNASSIGNED_RGBA$1 = [0.62, 0.62, 0.62, 0.55];
45480
45618
  /**
45481
45619
  * The transcript markers of the 2D spatial view: runs the planner's job, drops hidden groups
45482
45620
  * and genes, groups a selection under the marker budget, colours the entries and draws them as
@@ -45678,8 +45816,8 @@ class TranscriptLayers {
45678
45816
  const name = cellTypeColumnFor(dataset, view);
45679
45817
  const codes = name ? await this.lookup.codes(name).catch(() => null) : null;
45680
45818
  const cellColor = new Map();
45681
- if (codes?.meta.kind === 'categorical') {
45682
- const colors = resolveCategoryColors(codes.meta);
45819
+ if (name && codes?.meta.kind === 'categorical') {
45820
+ const colors = categoryColorsFor(name, codes.meta, view.groupColors);
45683
45821
  codes.meta.categories.forEach((c, k) => cellColor.set(c, colors[k]));
45684
45822
  }
45685
45823
  const colors = clusterColorMap(view.transcriptGenes, view.transcriptGeneGroups, cellColor, DEFAULT_CATEGORICAL_PALETTE);
@@ -45714,7 +45852,10 @@ class TranscriptLayers {
45714
45852
  else {
45715
45853
  const name = cellTypeColumnFor(dataset, view);
45716
45854
  const codes = name ? await this.lookup.codes(name) : null;
45717
- rgb = codes?.meta.kind === 'categorical' ? resolveCategoryColors(codes.meta).map(parseHex) : [];
45855
+ rgb =
45856
+ name && codes?.meta.kind === 'categorical'
45857
+ ? categoryColorsFor(name, codes.meta, view.groupColors).map(parseHex)
45858
+ : [];
45718
45859
  codeOf = (i) => {
45719
45860
  const o = t.observation[i];
45720
45861
  if (!codes || o === NO_OBSERVATION)
@@ -45727,7 +45868,7 @@ class TranscriptLayers {
45727
45868
  const tuples = rgb.map(([r, g, b]) => [r / 255, g / 255, b / 255, 1]);
45728
45869
  for (let i = 0; i < t.count; i++) {
45729
45870
  const c = codeOf(i);
45730
- rgba.set(c < 0 ? UNASSIGNED_RGBA : tuples[c], i * 4);
45871
+ rgba.set(c < 0 ? UNASSIGNED_RGBA$1 : tuples[c], i * 4);
45731
45872
  }
45732
45873
  return rgba;
45733
45874
  }
@@ -45760,11 +45901,10 @@ const MAX_TILE_RETRIES = 3;
45760
45901
  * -----------
45761
45902
  * Cell fill, cell outline, nucleus outline, the transcript density, then transcripts on top
45762
45903
  * ({@link TILE_LAYER_ORDER}); a plan restores the order once, after all its groups are in
45763
- * place. The service's observation markers sit under all of them; while outlines are drawn the
45764
- * markers are hidden (a cell is its outline then, not a dot), so the cells never have to
45765
- * be ordered against them. The density goes OVER the markers on purpose: under 10^5
45766
- * dots it would only show in the gaps between cells. {@link afterObservations} restores
45767
- * the order after the service re-adds its markers.
45904
+ * place. The service's observation markers sit under all of them, drawn at every zoom: once
45905
+ * cells are big enough to outline, their outlines go over their markers. The density goes
45906
+ * OVER the markers on purpose: under 10^5 dots it would only show in the gaps between cells.
45907
+ * {@link afterObservations} restores the order after the service re-adds its markers.
45768
45908
  *
45769
45909
  * ONE LAYER PER GROUP
45770
45910
  * -------------------
@@ -45836,7 +45976,7 @@ class NapariSpatialTileLayers {
45836
45976
  owns(layer) {
45837
45977
  return this.groups.owns(layer);
45838
45978
  }
45839
- /** True while outlines are on screen — the service hides its dots then. */
45979
+ /** True while outlines are on screen. */
45840
45980
  get outlinesShown() {
45841
45981
  return this.cells.outlinesShown;
45842
45982
  }
@@ -46242,13 +46382,13 @@ class Spatial2dScene extends SpatialSceneBase {
46242
46382
  return this.drawn ? this.drawn[k] : k;
46243
46383
  }
46244
46384
  /**
46245
- * Whether the markers are drawn: when the user wants them, and not while cell outlines are on
46246
- * screen — then a cell IS its outline, and a circle on top of it is noise. Zoomed out past the
46247
- * outline threshold, the circles stand in for the cells.
46385
+ * Whether the markers are drawn: whenever the user wants them, at every zoom. Cell outlines come
46386
+ * and go with the zoom (their level of detail follows it) and are drawn over the markers; the
46387
+ * observations themselves never disappear with them.
46248
46388
  */
46249
46389
  pointsVisible() {
46250
46390
  const view = this.session.latest?.[1];
46251
- return (view?.showPoints ?? true) && !this.session.tiles?.outlinesShown;
46391
+ return view?.showPoints ?? true;
46252
46392
  }
46253
46393
  /**
46254
46394
  * Redraw the markers over a freshly rendered image. Two reasons, both invisible from the
@@ -46397,6 +46537,8 @@ class Spatial2dScene extends SpatialSceneBase {
46397
46537
  scale: ref?.scale ?? [1, 1],
46398
46538
  translate: ref?.translate ?? [0, 0],
46399
46539
  visible: this.pointsVisible(),
46540
+ // Never under a couple of screen pixels, so the observations stay visible zoomed out.
46541
+ minScreenSize: SPATIAL_MARKER_MIN_SCREEN_PX,
46400
46542
  });
46401
46543
  // Outlines, density and transcripts go back over the markers just added.
46402
46544
  this.session.tiles?.afterObservations();
@@ -46716,7 +46858,7 @@ class Spatial2dScene extends SpatialSceneBase {
46716
46858
  const column = await port.getColumn(colorBy.name);
46717
46859
  if (isCategoricalColumn(column)) {
46718
46860
  const rgba = encodeCategorical(column.codes, {
46719
- colors: resolveCategoryColors(column.meta),
46861
+ colors: categoryColorsFor(colorBy.name, column.meta, view.groupColors),
46720
46862
  opacity: view.opacity,
46721
46863
  muted,
46722
46864
  });
@@ -46801,6 +46943,267 @@ function gatherSymbols(symbols, indices) {
46801
46943
  return Uint8Array.from(indices, (i) => symbols[i]);
46802
46944
  }
46803
46945
 
46946
+ /** Most transcripts drawn in the 3D view: past it, every k-th one is drawn (and the console says so). */
46947
+ const TRANSCRIPTS_3D_MAX = 2_000_000;
46948
+ /** Most level-0 tiles read for the 3D view: a dataset needing more is too large to draw whole. */
46949
+ const TRANSCRIPTS_3D_MAX_TILES = 4096;
46950
+ /** Tile reads in flight at once. */
46951
+ const CONCURRENCY = 8;
46952
+ /** A transcript's marker in the 3D view, in screen pixels at a transcript scale of 1. */
46953
+ const TRANSCRIPT_3D_BASE_SIZE = 2;
46954
+ /** A transcript with no colour by cell type (in no cell, or a cell in no group), as the 2D markers. */
46955
+ const UNASSIGNED_RGBA = [0.62, 0.62, 0.62, 0.55];
46956
+ /** The genes a view draws in 3D, as the tile query takes them, or null when it draws none. */
46957
+ function transcripts3dGenes(dataset, view) {
46958
+ if (view.transcriptMode === 'off')
46959
+ return null;
46960
+ if (view.transcriptAllGenes) {
46961
+ if (allGenesHidden(view))
46962
+ return null;
46963
+ return { query: [ALL_GENES], allGenes: true };
46964
+ }
46965
+ const hidden = new Set(view.transcriptHiddenGenes);
46966
+ const genes = view.transcriptGenes.filter((g) => !hidden.has(g));
46967
+ return genes.length ? { query: genes, allGenes: false } : null;
46968
+ }
46969
+ /**
46970
+ * Each loaded transcript's colour, packed RGBA, as the 2D markers colour them by gene: a gene's
46971
+ * chosen colour, else the palette's by its slot (the selection's order, or with "All genes" the
46972
+ * dataset's gene code).
46973
+ */
46974
+ function transcripts3dColors(view, names, gene, count) {
46975
+ const n = names ? names.length : DEFAULT_CATEGORICAL_PALETTE.length;
46976
+ const table = Array.from({ length: n }, (_g, i) => rgba01((names && view.transcriptGeneColors[names[i]]) ||
46977
+ DEFAULT_CATEGORICAL_PALETTE[i % DEFAULT_CATEGORICAL_PALETTE.length]));
46978
+ return packed(count, (i) => table[names ? gene[i] : gene[i] % n]);
46979
+ }
46980
+ /**
46981
+ * How the panel's "Colour by" applies in 3D, as it does to the 2D markers (`colorViewFor`): by
46982
+ * cell type when the dataset has one (else by gene), by cluster for the selected genes (with "All
46983
+ * genes" a cluster is its gene), by gene otherwise.
46984
+ */
46985
+ function transcripts3dColorBy(dataset, view, allGenes) {
46986
+ const by = view.transcriptColorBy;
46987
+ if (by === 'cellType')
46988
+ return cellTypeColumnFor(dataset, view) ? 'cellType' : 'gene';
46989
+ if (by === 'cluster' && !allGenes)
46990
+ return 'cluster';
46991
+ return 'gene';
46992
+ }
46993
+ /** The colours the cells' grouping gives its groups, by name (none without a grouping). */
46994
+ async function cellGroupColors(dataset, view, port) {
46995
+ const name = cellTypeColumnFor(dataset, view);
46996
+ if (!name)
46997
+ return null;
46998
+ const column = await port.getColumn(name);
46999
+ if (!isCategoricalColumn(column))
47000
+ return null;
47001
+ const hex = categoryColorsFor(name, column.meta, view.groupColors);
47002
+ const byName = new Map(column.meta.categories.map((c, k) => [c, hex[k]]));
47003
+ return { codes: column.codes, colors: hex.map(rgba01), byName };
47004
+ }
47005
+ /** `#rrggbb` as an opaque 0..1 RGBA. */
47006
+ function rgba01(hex) {
47007
+ const [r, g, b] = parseHex(hex);
47008
+ return [r / 255, g / 255, b / 255, 1];
47009
+ }
47010
+ /** `count` colours packed RGBA, unassigned where `at` has none. */
47011
+ function packed(count, at) {
47012
+ const out = new Float32Array(count * 4);
47013
+ for (let i = 0; i < count; i++)
47014
+ out.set(at(i) ?? UNASSIGNED_RGBA, i * 4);
47015
+ return out;
47016
+ }
47017
+ /**
47018
+ * The transcripts of the 3D spatial view: every molecule of the genes the Transcripts panel draws
47019
+ * (its selected genes, or "All genes"), at its own x, y and z, in the cloud's box and Z stretch.
47020
+ *
47021
+ * The 2D view reads transcripts by level of detail for the camera; a 3D orbit has no such window,
47022
+ * so this reads the whole level 0 once per gene selection (a few hundred thousand molecules for a
47023
+ * MERFISH section) and only restyles after that. Drawn only for a dataset whose transcripts carry z.
47024
+ */
47025
+ class Spatial3dTranscripts {
47026
+ constructor(viewer, begin) {
47027
+ this.viewer = viewer;
47028
+ this.begin = begin;
47029
+ this.layer = null;
47030
+ /** What {@link layer} was loaded from (dataset, genes, quality), and styled with. */
47031
+ this.loadKey = null;
47032
+ this.styleKey = null;
47033
+ this.loaded = null;
47034
+ this.loads = new Supersede();
47035
+ }
47036
+ /** The layer drawn, if any (for tests and the scene's Z stretch). */
47037
+ get drawn() {
47038
+ return this.layer;
47039
+ }
47040
+ /** Draw what `view` asks for, loading the molecules when the genes changed. */
47041
+ async sync(dataset, view, port, frame) {
47042
+ const meta = dataset.transcriptTiles;
47043
+ const genes = meta?.hasZ && port?.getTranscriptTile ? transcripts3dGenes(dataset, view) : null;
47044
+ if (!meta || !genes || !port) {
47045
+ this.remove();
47046
+ return;
47047
+ }
47048
+ if (genes.allGenes && (meta.count ?? 0) > TRANSCRIPTS_3D_MAX) {
47049
+ console.warn(`[napari-js] ${meta.count} transcripts are too many to draw in 3D: select genes to see theirs`);
47050
+ this.remove();
47051
+ return;
47052
+ }
47053
+ const quality = view.transcriptQuality === 'all' ? 'all' : 'high';
47054
+ const loadKey = `${dataset.id}|${genes.query.join('\u0001')}|${quality}`;
47055
+ if (loadKey !== this.loadKey) {
47056
+ const task = this.loads.next();
47057
+ const [x0, y0, x1, y1] = meta.bounds;
47058
+ const keys = tilesInRect({ x0, y0, x1: x1 + 1e-6, y1: y1 + 1e-6 }, 0, meta.levels, meta.bounds);
47059
+ if (keys.length > TRANSCRIPTS_3D_MAX_TILES) {
47060
+ console.warn(`[napari-js] ${keys.length} transcript tiles are too many to read for the 3D view`);
47061
+ this.remove();
47062
+ return;
47063
+ }
47064
+ const end = this.begin('Transcripts');
47065
+ let tiles;
47066
+ try {
47067
+ tiles = await pooled(keys, CONCURRENCY, (k) => port.getTranscriptTile(0, k.gx, k.gy, { genes: genes.query, quality }));
47068
+ }
47069
+ catch (err) {
47070
+ console.warn('[napari-js] transcripts for the 3D view could not be read', err);
47071
+ // The panel asked for other transcripts than those drawn: take them down rather than leave
47072
+ // the old selection up as if it were the new one; the next change reads again. Only the
47073
+ // current load may: a superseded one must not tear down what a newer one drew.
47074
+ if (task())
47075
+ this.clearLayer();
47076
+ return;
47077
+ }
47078
+ finally {
47079
+ end();
47080
+ }
47081
+ if (!task())
47082
+ return;
47083
+ this.loadKey = loadKey;
47084
+ this.styleKey = null;
47085
+ this.loaded = { tiles, names: genes.allGenes ? (meta.genes ?? null) : genes.query };
47086
+ }
47087
+ await this.draw(dataset, view, port, frame, genes.allGenes);
47088
+ }
47089
+ /** Follow the scene's Z stretch (nothing is re-uploaded). */
47090
+ setZScale(s) {
47091
+ if (this.layer)
47092
+ this.layer.scale3d = [1, 1, s];
47093
+ }
47094
+ /** Take the layer down and forget what it showed. */
47095
+ remove() {
47096
+ this.loads.cancel();
47097
+ this.clearLayer();
47098
+ }
47099
+ clearLayer() {
47100
+ if (this.layer)
47101
+ this.viewer.layers.remove(this.layer);
47102
+ this.layer = null;
47103
+ this.loadKey = null;
47104
+ this.styleKey = null;
47105
+ this.loaded = null;
47106
+ }
47107
+ async draw(dataset, view, port, frame, allGenes) {
47108
+ const loaded = this.loaded;
47109
+ if (!loaded)
47110
+ return;
47111
+ const by = transcripts3dColorBy(dataset, view, allGenes);
47112
+ const styleKey = [
47113
+ frame.origin.join(','),
47114
+ by,
47115
+ by === 'gene' ? JSON.stringify(view.transcriptGeneColors) : '',
47116
+ by !== 'gene' ? `${cellTypeColumnFor(dataset, view)}|${JSON.stringify(view.groupColors)}` : '',
47117
+ by === 'cluster' ? JSON.stringify(view.transcriptGeneGroups) : '',
47118
+ allGenes ? view.transcriptAllGenesHidden.join(',') : '',
47119
+ ].join('|');
47120
+ const size = TRANSCRIPT_3D_BASE_SIZE * (view.transcriptScale > 0 ? view.transcriptScale : 1);
47121
+ if (this.layer && styleKey === this.styleKey) {
47122
+ this.layer.size = size;
47123
+ this.layer.opacity = view.transcriptOpacity;
47124
+ this.layer.scale3d = [1, 1, frame.zScale];
47125
+ return;
47126
+ }
47127
+ const { names } = loaded;
47128
+ const shown = allGenes && names ? allGeneShownBy(view.transcriptAllGenesHidden) : null;
47129
+ const keep = shown && names ? (g) => !!names[g] && shown(names[g]) : () => true;
47130
+ const total = loaded.tiles.reduce((n, t) => n + t.count, 0);
47131
+ const stride = Math.max(1, Math.ceil(total / TRANSCRIPTS_3D_MAX));
47132
+ if (stride > 1)
47133
+ console.warn(`[napari-js] drawing every ${stride}th of ${total} transcripts in 3D`);
47134
+ const [ox, oy, oz] = frame.origin;
47135
+ const positions = [];
47136
+ const genes = [];
47137
+ const cells = [];
47138
+ let seen = 0;
47139
+ for (const t of loaded.tiles) {
47140
+ for (let i = 0; i < t.count; i++) {
47141
+ if (!keep(t.gene[i]) || seen++ % stride)
47142
+ continue;
47143
+ positions.push(t.x[i] + ox, t.y[i] + oy, t.z[i] + oz);
47144
+ genes.push(t.gene[i]);
47145
+ cells.push(t.observation[i]);
47146
+ }
47147
+ }
47148
+ const count = genes.length;
47149
+ let colors;
47150
+ if (by === 'gene') {
47151
+ colors = transcripts3dColors(view, names, Uint16Array.from(genes), count);
47152
+ }
47153
+ else {
47154
+ const groups = await cellGroupColors(dataset, view, port).catch((err) => {
47155
+ console.warn('[napari-js] the cells grouping for the 3D transcripts could not be read', err);
47156
+ return null;
47157
+ });
47158
+ // A newer draw (another style, or the layer taken down) won while the column loaded.
47159
+ if (this.loaded !== loaded)
47160
+ return;
47161
+ if (by === 'cellType') {
47162
+ colors = packed(count, (i) => {
47163
+ const o = cells[i];
47164
+ const code = !groups || o === NO_OBSERVATION ? NO_CATEGORY : groups.codes[o];
47165
+ return code === NO_CATEGORY ? undefined : groups.colors[code];
47166
+ });
47167
+ }
47168
+ else {
47169
+ // A cluster named like a group of the cells' grouping takes that group's colour, as in 2D.
47170
+ const byCluster = clusterColorMap(view.transcriptGenes, view.transcriptGeneGroups, groups?.byName ?? new Map(), DEFAULT_CATEGORICAL_PALETTE);
47171
+ const table = (names ?? []).map((gene) => {
47172
+ const hex = byCluster.get(clusterOfGene(gene, view.transcriptGeneGroups));
47173
+ return hex ? rgba01(hex) : undefined;
47174
+ });
47175
+ colors = packed(count, (i) => table[genes[i]]);
47176
+ }
47177
+ }
47178
+ if (this.layer)
47179
+ this.viewer.layers.remove(this.layer);
47180
+ this.layer = null;
47181
+ this.styleKey = styleKey;
47182
+ if (!count)
47183
+ return;
47184
+ this.layer = this.viewer.addPoints3D(Float32Array.from(positions), new Float32Array(count), {
47185
+ name: 'transcripts',
47186
+ size,
47187
+ opacity: view.transcriptOpacity,
47188
+ scale3d: [1, 1, frame.zScale],
47189
+ colors,
47190
+ });
47191
+ }
47192
+ }
47193
+ /** `work` over `items`, at most `limit` at once, results in order. */
47194
+ async function pooled(items, limit, work) {
47195
+ const out = new Array(items.length);
47196
+ let next = 0;
47197
+ const run = async () => {
47198
+ while (next < items.length) {
47199
+ const i = next++;
47200
+ out[i] = await work(items[i]);
47201
+ }
47202
+ };
47203
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, run));
47204
+ return out;
47205
+ }
47206
+
46804
47207
  /** The least Z stretch: a tenth of the true depth. */
46805
47208
  const SPATIAL_Z_SCALE_MIN = 0.1;
46806
47209
  /** The most Z stretch a cloud allows, at least: the Volume view's. */
@@ -46920,6 +47323,7 @@ class Spatial3dScene extends SpatialSceneBase {
46920
47323
  * this scene carries it). */
46921
47324
  this.invertSub = null;
46922
47325
  this.regionMeshes = new VolumeRegionMeshes(ctx.viewer);
47326
+ this.transcripts3d = new Spatial3dTranscripts(ctx.viewer, (label) => ctx.badge.begin(label));
46923
47327
  }
46924
47328
  /** Show these region reconstructions with the cloud (replacing the ones shown). */
46925
47329
  setRegionMeshes(meshes) {
@@ -46956,6 +47360,7 @@ class Spatial3dScene extends SpatialSceneBase {
46956
47360
  this.regionMeshes.setFrame({
46957
47361
  size: [meta.width, meta.height, meta.depth],
46958
47362
  world: [meta.width * vx, meta.height * vy, meta.depth * vz * this.zScale()],
47363
+ zScale: this.zScale(),
46959
47364
  });
46960
47365
  }
46961
47366
  /**
@@ -47018,6 +47423,7 @@ class Spatial3dScene extends SpatialSceneBase {
47018
47423
  const s = this.zScale();
47019
47424
  if (this.points)
47020
47425
  this.points.scale3d = [1, 1, s];
47426
+ this.transcripts3d.setZScale(s);
47021
47427
  for (const layer of [this.volume, this.geneMapLayer, ...this.densityLayers]) {
47022
47428
  const base = layer && this.baseVoxels.get(layer);
47023
47429
  if (layer && base)
@@ -47080,6 +47486,7 @@ class Spatial3dScene extends SpatialSceneBase {
47080
47486
  }
47081
47487
  dispose() {
47082
47488
  this.regionMeshes.clear();
47489
+ this.transcripts3d.remove();
47083
47490
  this.disposeSpatial();
47084
47491
  this.invertSub?.unsubscribe();
47085
47492
  this.invertSub = null;
@@ -47225,6 +47632,11 @@ class Spatial3dScene extends SpatialSceneBase {
47225
47632
  this.volume.opacity = view.volumeOpacity;
47226
47633
  }
47227
47634
  this.drawCloud(viewer, dataset, view, selection, enc, geneShown);
47635
+ // The transcripts last: they sit in the box the cloud fixed, and their first read can take a while.
47636
+ await this.transcripts3d.sync(dataset, view, this.session.port, {
47637
+ origin: this.origin,
47638
+ zScale: this.zScale(),
47639
+ });
47228
47640
  }
47229
47641
  /**
47230
47642
  * Draw the point cloud: a new layer for new geometry (a dataset or a section), the colour
@@ -47515,7 +47927,13 @@ class Spatial3dScene extends SpatialSceneBase {
47515
47927
  // holding the same number of cells would otherwise look like the same key and
47516
47928
  // leave the previous ROI's fields on screen.
47517
47929
  const key = on
47518
- ? [dataset.id, column ?? 'all', smoothing, this.session.selectionRev(selection)].join('|')
47930
+ ? [
47931
+ dataset.id,
47932
+ column ?? 'all',
47933
+ smoothing,
47934
+ this.session.selectionRev(selection),
47935
+ groupColorsKey(column, view.groupColors),
47936
+ ].join('|')
47519
47937
  : null;
47520
47938
  if (key === this.densityKey)
47521
47939
  return;
@@ -47532,7 +47950,7 @@ class Spatial3dScene extends SpatialSceneBase {
47532
47950
  return;
47533
47951
  let groups;
47534
47952
  try {
47535
- groups = await this.densityGroups(dataset, column, selection);
47953
+ groups = await this.densityGroups(dataset, column, selection, view);
47536
47954
  }
47537
47955
  catch (err) {
47538
47956
  console.warn('[napari-js] density volumes: column unavailable', err);
@@ -47591,7 +48009,7 @@ class Spatial3dScene extends SpatialSceneBase {
47591
48009
  * which is a real question on its own ("where is the tissue dense?") and the honest thing to
47592
48010
  * show when the view is not encoding a taxonomy.
47593
48011
  */
47594
- async densityGroups(dataset, column, selection) {
48012
+ async densityGroups(dataset, column, selection, view) {
47595
48013
  const port = this.session.port;
47596
48014
  const meta = column ? findColumnMeta(dataset, column) : undefined;
47597
48015
  if (!port || !column || !meta || meta.kind !== 'categorical')
@@ -47599,7 +48017,7 @@ class Spatial3dScene extends SpatialSceneBase {
47599
48017
  const loaded = await port.getColumn(column);
47600
48018
  if (!isCategoricalColumn(loaded))
47601
48019
  return [];
47602
- return rankDensityGroups(column, loaded, dataset.observations.count, selection);
48020
+ return rankDensityGroups(column, loaded, dataset.observations.count, selection, view.groupColors);
47603
48021
  }
47604
48022
  /**
47605
48023
  * Add (or keep) the dataset's reference volume. The offset that sits the observations inside
@@ -47726,6 +48144,7 @@ class Spatial3dScene extends SpatialSceneBase {
47726
48144
  /** Drop every layer of the cloud (the dataset went away, or has no z). */
47727
48145
  removeLayers(viewer) {
47728
48146
  this.regionMeshes.clearFrame();
48147
+ this.transcripts3d.remove();
47729
48148
  for (const layer of [this.points, this.volume, this.geneMapLayer, ...this.densityLayers]) {
47730
48149
  if (layer)
47731
48150
  viewer.layers.remove(layer);
@@ -47778,7 +48197,7 @@ class Spatial3dScene extends SpatialSceneBase {
47778
48197
  if (colorBy.kind === 'column') {
47779
48198
  const column = await port.getColumn(colorBy.name);
47780
48199
  if (isCategoricalColumn(column)) {
47781
- return encodeSpatial3dCategorical(column.codes, resolveCategoryColors(column.meta));
48200
+ return encodeSpatial3dCategorical(column.codes, categoryColorsFor(colorBy.name, column.meta, view.groupColors));
47782
48201
  }
47783
48202
  return this.encodeContinuous(column.values, view);
47784
48203
  }
@@ -47815,6 +48234,7 @@ function cloudSubset(dataset, view, geneShown = null) {
47815
48234
  const scalarKey = [
47816
48235
  key,
47817
48236
  view.colorBy ? `${view.colorBy.kind}:${view.colorBy.name}` : 'flat',
48237
+ groupColorsKey(view.colorBy?.kind === 'column' ? view.colorBy.name : null, view.groupColors),
47818
48238
  observationGeneStyleKey(view),
47819
48239
  view.logScale ? 'log' : 'lin',
47820
48240
  clip.join(','),
@@ -50422,4 +50842,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.9", ngImpor
50422
50842
  */
50423
50843
 
50424
50844
  export { SupersededError as $, ALL_GENES as A, Polygon as B, CANVAS_TOOL_IDS as C, DEFAULT_CATEGORICAL_PALETTE as D, ProcessingImage as E, REGION_IO_PORT as F, REGION_SOURCE_MIN_CHARS as G, HexColorPickerComponent as H, IMAGE_STATE_PORT as I, Rectangle as J, Region as K, LUT_COLORS as L, MISSING_COLOR as M, NAPARI_DECIMATE_OPTIONS as N, RegionEditorComponent as O, PLOT_MODE_CONTEXT as P, RoutingVisualizerService as Q, REGION_EDITOR_API as R, SAM_MODELS as S, SEMANTIC_SEGMENTER as T, SPATIAL_DATA_PORT as U, SPATIAL_WIRE_VERSION as V, ShapeSelection as W, SpatialChartsComponent as X, SpatialControlsComponent as Y, SpatialDataHttpService as Z, SpatialSelectionStore as _, runDecoder as a, omicsLayout as a$, TILE_ACCESS_PORT as a0, TOOLBAR_TOOLS as a1, VISUALIZER as a2, VIZ_ALERT_TOAST_KEY as a3, VIZ_CONFIG as a4, VIZ_TOAST_KEY as a5, ViewerFeature as a6, VisualizationModule as a7, VisualizerComponent as a8, assertManifestVersion as a9, hydrateBounds as aA, importFile as aB, isBuiltinPlotType as aC, isCanvasToolId as aD, isCategoricalColumn as aE, isContinuousColumn as aF, isDialogToolContribution as aG, isEmptyPresetSet as aH, isLabelVolumeName as aI, isLittleEndian as aJ, isNapari3d as aK, isNapariIsosurface as aL, isNapariScatter as aM, isNapariScatter3d as aN, isNapariSurface as aO, isNapariVolume as aP, isSamModelReady as aQ, isSpatialOmics as aR, isSpatialOmics3d as aS, isThreeDimensional as aT, isZipName as aU, labelClasses as aV, labelSliceGeoJson as aW, lutFor as aX, markerDiameters as aY, maskToIndices as aZ, mutedFromSelection as a_, benefitsFromGrouping as aa, buildOmicsTraces as ab, capabilitiesOf as ac, clearSamModelCache as ad, contrastWindow as ae, contributedPlotTypeOption as af, countMask as ag, cropImage as ah, datasetFromManifest as ai, decodeColumn as aj, decodeCoords as ak, decodeFeatureVector as al, decodePolygons as am, decodeRadius as an, defaultChannelVisible as ao, defaultPresetSet as ap, dialogToolContributions as aq, emptySelection as ar, encodeCategorical as as, encodeContinuous as at, expandZips as au, findColumnMeta as av, getDefaultSamModelId as aw, getPlotTypeDescriptor as ax, getSamModel as ay, hasZarrMetadata as az, ALL_TOOLBAR_TOOLS as b, openAnnotations as b0, openGeoJsonRegionSource as b1, openHttpRegionSource as b2, parsePresetSet as b3, planRegionImport as b4, pointInRing as b5, provideVisualization as b6, readAnnotationSlices as b7, readLabelVolume as b8, readOmeZarrLabels as b9, readZip as ba, rendererOwnsWheel as bb, resolveCategoryColors as bc, selectByCategory as bd, selectInRegions as be, setDefaultSamModel as bf, setOrtWasmBase as bg, setSamModelUrls as bh, sortToolContributions as bi, visibleToolContributions as bj, ANNOTATION_IMAGE_MAX_DIM as c, CELL_SEGMENTER as d, CHANNEL_HISTOGRAM_API as e, fetchModel as f, getOrtWasmBase as g, CellposeSegmenterService as h, ChannelHistogramComponent as i, DEFAULT_FALLBACK_PALETTE as j, DEFAULT_MUTED_OPACITY as k, DEFAULT_SAM_MODEL_ID as l, DEFAULT_SPATIAL_VIEW as m, DEFAULT_VISIBLE_CHANNELS as n, INSTANCE_SEGMENTER as o, ImageConverterService as p, MultiPolygon as q, runEncoder as r, NAPARI_DEFAULT_DECIMATE as s, NO_CATEGORY as t, NO_OBSERVATION as u, PLOT_MODE_SESSION as v, PLOT_TYPE_CONTRIBUTIONS as w, PLOT_TYPE_DESCRIPTORS as x, PREFERENCES_PORT as y, PlotType as z };
50425
- //# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-CYsPzX3J.mjs.map
50845
+ //# sourceMappingURL=jax-data-science-sci-image-visualizer-jax-data-science-sci-image-visualizer-DHrHO2ay.mjs.map