From e1c0808a9119e63e871522e6dbcbf24c61532b64 Mon Sep 17 00:00:00 2001 From: Xiao Gui <xgui3783@gmail.com> Date: Wed, 12 Apr 2023 16:54:14 +0200 Subject: [PATCH] refactor: connectivity component --- .storybook/preview-head.html | 2 +- deploy/csp/index.js | 2 +- package.json | 2 +- src/atlasComponents/sapi/schemaV3.ts | 31 +- src/atlasComponents/sapi/sxplrTypes.ts | 4 +- .../connectivityBrowser.component.ts | 847 +++++++++++------- .../connectivityBrowser.template.html | 210 +++-- src/features/connectivity/module.ts | 28 +- .../feature-view/feature-view.component.ts | 4 +- src/features/filterCategories.pipe.ts | 2 +- src/features/transform-pd-to-ds.pipe.ts | 20 +- src/index.html | 2 +- src/viewerModule/nehuba/store/store.ts | 1 - src/viewerModule/nehuba/store/type.ts | 3 - 14 files changed, 705 insertions(+), 453 deletions(-) diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html index 1095b2dd6..4fc7d43c7 100644 --- a/.storybook/preview-head.html +++ b/.storybook/preview-head.html @@ -15,5 +15,5 @@ } </style> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous"> -<script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.5/dist/connectivity-component/connectivity-component.js" defer></script> +<script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.6/dist/connectivity-component/connectivity-component.js" defer></script> <link rel="stylesheet" href="icons/iav-icons.css"> diff --git a/deploy/csp/index.js b/deploy/csp/index.js index 4905289c3..1d67da419 100644 --- a/deploy/csp/index.js +++ b/deploy/csp/index.js @@ -116,7 +116,7 @@ module.exports = { 'https://unpkg.com/mathjax@3.1.2/', // math jax 'https://unpkg.com/three-surfer@0.0.13/dist/bundle.js', // for threeSurfer (freesurfer support in browser) 'https://unpkg.com/ng-layer-tune@0.0.6/dist/ng-layer-tune/', // needed for ng layer control - 'https://unpkg.com/hbp-connectivity-component@0.6.5/', // needed for connectivity component + 'https://unpkg.com/hbp-connectivity-component@0.6.6/', // needed for connectivity component (req, res) => res.locals.nonce ? `'nonce-${res.locals.nonce}'` : null, ...SCRIPT_SRC, ...WHITE_LIST_SRC, diff --git a/package.json b/package.json index 5766b9546..2ee728b99 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "watch": "ng build --watch --configuration development", "test": "ng test", "test-ci": "ng test --progress false --watch false --browsers=ChromeHeadless", - "sapi-schema": "npx openapi-typescript@6.1.0 https://siibra-api-latest.apps-dev.hbp.eu/v3_0/openapi.json --output ./src/atlasComponents/sapi/schemaV3.ts && eslint ./src/atlasComponents/sapi/schemaV3.ts --no-ignore --fix", + "sapi-schema": "npx openapi-typescript@6.1.0 https://siibra-api-stable.apps-dev.hbp.eu/v3_0/openapi.json --output ./src/atlasComponents/sapi/schemaV3.ts && eslint ./src/atlasComponents/sapi/schemaV3.ts --no-ignore --fix", "api-schema": "node src/plugin/generateTypes.js", "docs:json": "compodoc -p ./tsconfig.json -e json -d .", "storybook": "npm run docs:json && start-storybook -p 6006", diff --git a/src/atlasComponents/sapi/schemaV3.ts b/src/atlasComponents/sapi/schemaV3.ts index 7c926b526..c6740c619 100644 --- a/src/atlasComponents/sapi/schemaV3.ts +++ b/src/atlasComponents/sapi/schemaV3.ts @@ -489,11 +489,11 @@ export interface components { /** @Type */ "@type": string /** Index */ - index: string[] + index: unknown[] /** Dtype */ dtype: string /** Columns */ - columns: string[] + columns: unknown[] /** Ndim */ ndim: number /** Data */ @@ -510,7 +510,7 @@ export interface components { /** Urls */ urls: (components["schemas"]["EbrainsDsUrl"])[] /** Description */ - description: string + description?: string /** Contributors */ contributors: (components["schemas"]["EbrainsDsPerson"])[] /** Ebrains Page */ @@ -722,6 +722,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** Page[FeatureMetaModel] */ Page_FeatureMetaModel_: { @@ -733,6 +735,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** Page[ParcellationEntityVersionModel] */ Page_ParcellationEntityVersionModel_: { @@ -744,6 +748,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** Page[SiibraAtlasModel] */ Page_SiibraAtlasModel_: { @@ -755,6 +761,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** Page[SiibraCorticalProfileModel] */ Page_SiibraCorticalProfileModel_: { @@ -766,6 +774,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** Page[SiibraEbrainsDataFeatureModel] */ Page_SiibraEbrainsDataFeatureModel_: { @@ -777,6 +787,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** Page[SiibraParcellationModel] */ Page_SiibraParcellationModel_: { @@ -788,6 +800,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** Page[SiibraRegionalConnectivityModel] */ Page_SiibraRegionalConnectivityModel_: { @@ -799,6 +813,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** Page[SiibraTabularModel] */ Page_SiibraTabularModel_: { @@ -810,6 +826,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** Page[SiibraVoiModel] */ Page_SiibraVoiModel_: { @@ -821,6 +839,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** Page[Union[SiibraCorticalProfileModel, SiibraReceptorDensityFp, SiibraTabularModel]] */ Page_Union_SiibraCorticalProfileModel__SiibraReceptorDensityFp__SiibraTabularModel__: { @@ -832,6 +852,8 @@ export interface components { page: number /** Size */ size: number + /** Pages */ + pages?: number } /** ParcellationEntityVersionModel */ ParcellationEntityVersionModel: { @@ -985,7 +1007,8 @@ export interface components { SiibraAnchorModel: { /** @Type */ "@type": string - location?: components["schemas"]["LocationModel"] + /** Location */ + location?: components["schemas"]["LocationModel"] | components["schemas"]["CoordinatePointModel"] /** Regions */ regions: (components["schemas"]["SiibraRegionAssignmentQual"])[] } diff --git a/src/atlasComponents/sapi/sxplrTypes.ts b/src/atlasComponents/sapi/sxplrTypes.ts index 0fcf7abfe..ef556f25e 100644 --- a/src/atlasComponents/sapi/sxplrTypes.ts +++ b/src/atlasComponents/sapi/sxplrTypes.ts @@ -125,8 +125,8 @@ export type CorticalFeature<T extends CorticalDataType, IndexType extends string type TabularDataType = number | string | number[] export type TabularFeature<T extends TabularDataType> = { - index: string[] - columns: string[] + index: unknown[] + columns: unknown[] data?: T[][] } & Feature diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts b/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts index 05ffb3f2c..dbc546a7e 100644 --- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts +++ b/src/features/connectivity/connectivityBrowser/connectivityBrowser.component.ts @@ -1,387 +1,554 @@ -import {AfterViewInit, Component, ElementRef, OnDestroy, ViewChild, Input, ChangeDetectorRef} from "@angular/core"; -import {select, Store} from "@ngrx/store"; -import {fromEvent, Subscription, BehaviorSubject, Observable} from "rxjs"; -import {catchError, take, switchMap} from "rxjs/operators"; +import { Component, ElementRef, OnDestroy, ViewChild, Input, SimpleChanges, HostListener, OnChanges } from "@angular/core"; +import { Store, select} from "@ngrx/store"; +import { Subscription, BehaviorSubject, combineLatest, merge, concat, NEVER} from "rxjs"; +import { switchMap, map, tap, shareReplay, distinctUntilChanged, withLatestFrom, filter, finalize } from "rxjs/operators"; -import { atlasAppearance } from "src/state"; -import {SAPI} from "src/atlasComponents/sapi/sapi.service"; +import { atlasAppearance, atlasSelection } from "src/state"; +import { SAPI } from "src/atlasComponents/sapi/sapi.service"; import { of } from "rxjs"; -import { HttpClient } from "@angular/common/http"; import { SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate } from "src/atlasComponents/sapi/sxplrTypes"; -import { actions, selectors } from "src/state/atlasSelection"; +import { actions } from "src/state/atlasSelection"; import { translateV3Entities } from "src/atlasComponents/sapi/translateV3"; +import { DS } from "src/features/filterCategories.pipe"; +import { FormControl, FormGroup } from "@angular/forms"; +import { PathReturn } from "src/atlasComponents/sapi/typeV3"; +import { arrayEqual } from "src/util/array"; +import { switchMapWaitFor } from "src/util/fn"; + +type PathParam = DS['value'][number] +type ConnFeat = PathReturn<"/feature/RegionalConnectivity/{feature_id}"> @Component({ selector: 'sxplr-features-connectivity-browser', templateUrl: './connectivityBrowser.template.html', styleUrls: ['./connectivityBrowser.style.scss'] }) -export class ConnectivityBrowserComponent implements AfterViewInit, OnDestroy { +export class ConnectivityBrowserComponent implements OnChanges, OnDestroy { + @Input('sxplr-features-connectivity-browser-atlas') + atlas: SxplrAtlas - @Input('sxplr-features-connectivity-browser-atlas') - atlas: SxplrAtlas + @Input('sxplr-features-connectivity-browser-template') + template: SxplrTemplate - @Input('sxplr-features-connectivity-browser-template') - template: SxplrTemplate + @Input('sxplr-features-connectivity-browser-parcellation') + parcellation: SxplrParcellation - @Input('sxplr-features-connectivity-browser-parcellation') - parcellation: SxplrParcellation + parcellation$ = new BehaviorSubject<SxplrParcellation>(null) - /** - * accordion expansion should only toggle the clearviewqueue state - * which should be the single source of truth - * setcolormaps$ is set by the presence/absence of clearviewqueue[CONNECTIVITY_NAME_PLATE] - */ - private _isFirstUpdate = true - @Input() - set accordionExpanded(flag: boolean) { - /** - * ignore first update - */ - if (this._isFirstUpdate) { - this._isFirstUpdate = false - return - } + #accordionExpanded$ = new BehaviorSubject<boolean>(null) + @Input() + set accordionExpanded(flag: boolean) { + this.#accordionExpanded$.next(flag) + } + + region$ = new BehaviorSubject<SxplrRegion>(null) + @Input() + set region(region: SxplrRegion) { + this.region$.next(region) + } - if (this.types.length && !this.selectedType) this.selectType(this._types[0]) + types$ = new BehaviorSubject<PathParam[]>(null) + @Input() + types: PathParam[] - if (flag) { - if (this.selectedSubjectIndex >= 0 && this.allRegions.length) { - this.setCustomLayer() - } else { - this.setCustomLayerOnLoad = true - } - } else { - this.removeCustomLayer() - } + connectivityFilterForm = new FormGroup({ + selectedType: new FormControl<PathParam>(null), + selectedView: new FormControl<'average'|'subject'>('subject'), + selectedCohort: new FormControl<string>(null), + selectedDatasetIndex: new FormControl<number>(0), + selectedSubjectIndex: new FormControl<number>(0), + }) - } - - private _region: SxplrRegion - @Input() - set region(region) { - this._region = region - this.regionName = region.name - } + displayForm = new FormGroup({ + logChecked: new FormControl<boolean>(false) + }) - get region() { - return this._region - } + formValue$ = this.connectivityFilterForm.valueChanges.pipe( + shareReplay(1), + ) - - private _types: any[] = [] - @Input() - set types(val) { - this._types = val.map(t => ({...t, shortName: t.name.split('.').pop()})) - } - get types() { - return this._types - } + private subscriptions: Subscription[] = [] + static LayerId = 'connectivity-colormap-id' - public selectedType: any - public selectedCohort: string - - public cohortDatasets: any[] - - public selectedSubjectIndex = null - public selectedCohortDatasetIndex: any - public selectedCohortSubjects: any - public fetchedItems: any[] = [] - public cohorts: string[] - public selectedView: 'subject' | 'average' | null - public averageDisabled: boolean = true - public subjectsDisabled: boolean = true - - public regionName: string - - public selectedDataset: any - public connectionsString: string - public pureConnections: { [key: string]: number } - public connectedAreas: BehaviorSubject<ConnectedArea[]> = new BehaviorSubject([]) - public noConnectivityForRegion: boolean - private subscriptions: Subscription[] = [] - public allRegions: SxplrRegion[] = [] - private regionIndexInMatrix = -1 - public defaultColorMap: Map<string, Map<number, { red: number, green: number, blue: number }>> - public matrixString: string - public fetchingPreData: boolean - public fetching: boolean - public connectivityLayerId = 'connectivity-colormap-id' - private setCustomLayerOnLoad = false - private customLayerEnabled: boolean - - public logDisabled: boolean = true - public logChecked: boolean = true - - private endpoint: string - - - @ViewChild('connectivityComponent') public connectivityComponentElement: ElementRef<any> - @ViewChild('fullConnectivityGrid') public fullConnectivityGridElement: ElementRef<any> - - constructor( - private store$: Store, - private http: HttpClient, - private changeDetectionRef: ChangeDetectorRef, - protected sapi: SAPI - ) { - SAPI.BsEndpoint$.pipe(take(1)).subscribe(en => this.endpoint = `${en}/feature/RegionalConnectivity`) - } + @ViewChild('connectivityComponent') public connectivityComponentElement: ElementRef<any> + @ViewChild('fullConnectivityGrid') public fullConnectivityGridElement: ElementRef<any> - public ngAfterViewInit(): void { - this.subscriptions.push( - - this.store$.pipe( - select(selectors.selectedParcAllRegions) - ).subscribe(flattenedRegions => { - this.defaultColorMap = null - this.allRegions = flattenedRegions - if (this.setCustomLayerOnLoad) { - this.setCustomLayer() - this.setCustomLayerOnLoad = false - } - }), - ) + constructor( + private store$: Store, + protected sapi: SAPI + ) { - this.subscriptions.push( - fromEvent(this.connectivityComponentElement.nativeElement, 'connectedRegionClicked', {capture: true}) - .subscribe((e: CustomEvent) => { - this.navigateToRegion(e.detail.name) - }), - ) - } - - setCustomLayer() { - if (this.customLayerEnabled) { - this.removeCustomLayer() - } - const map = new Map<SxplrRegion, number[]>() - const areas = this.connectedAreas.value - for (const region of this.allRegions) { - const area = areas.find(a => a.name === region.name) - if (area) { - map.set(region, Object.values(area.color)) - } else { - map.set(region, [255,255,255,0.1]) + this.subscriptions.push( + /** + * on accordion expansion, if nothing is selected, select default (0) type + */ + combineLatest([ + this.#accordionExpanded$, + this.types$, + concat( + of(null as PathParam), + this.formValue$.pipe( + map(v => v.selectedType), + distinctUntilChanged((n, o) => n.name === o.name) + ) + ), + ]).pipe( + ).subscribe(([flag, types, selectedType]) => { + if (flag && !selectedType) { + this.connectivityFilterForm.patchValue({ + selectedType: types[0] + }) } - } - this.customLayerEnabled = true - const customLayer: atlasAppearance.const.CustomLayer = { - clType: 'customlayer/colormap', - id: this.connectivityLayerId, - colormap: map - } - - this.store$.dispatch( - atlasAppearance.actions.addCustomLayer({customLayer}) - ) - } - - removeCustomLayer() { - this.store$.dispatch( - atlasAppearance.actions.removeCustomLayer({ - id: this.connectivityLayerId + }), + /** + * on set log + */ + this.displayForm.valueChanges.pipe( + map(v => v.logChecked), + switchMap(switchMapWaitFor({ + condition: () => !!this.connectivityComponentElement, + leading: true + })) + ).subscribe(flag => { + const el = this.connectivityComponentElement + el.nativeElement.setShowLog(flag) + }), + /** + * on type selection, select first cohort + */ + this.formValue$.pipe( + map(v => v.selectedType), + distinctUntilChanged((n, o) => n.name === o.name), + switchMap(() => + this.cohorts$.pipe( + /** + * it's important to not use distinctUntilChanged + * new corhots emit should always trigger this flow + */ + ) + ) + ).subscribe(cohorts => { + if (cohorts.length > 0) { + this.connectivityFilterForm.patchValue({ + selectedCohort: cohorts[0] + }) + } + }), + /** + * on select cohort + */ + + this.selectedCohort$.pipe( + switchMap(() => this.cohortDatasets$.pipe( + map(dss => dss.length), + distinctUntilChanged(), + filter(length => length > 0), + )) + ).subscribe(() => { + this.connectivityFilterForm.patchValue({ + selectedDatasetIndex: 0, + selectedSubjectIndex: 0, }) - ) - } - - clearCohortSelection() { - this.fetchedItems = [] - this.selectedCohort = null - this.cohorts = [] - this.selectedCohort = null - this.selectedCohortDatasetIndex = null - this.selectedCohortSubjects = null - - this.selectedSubjectIndex = null - } - - selectType(type) { - this.clearCohortSelection() - this.selectedType = type - this.removeCustomLayer() - this.getModality() - } - - - getModality() { - this.fetchingPreData = true - this.fetchModality().subscribe((res: any) => { + }), + /** + * on + * - accordion update + * - colormap change + * - fetching matrix + * remove custom layer + */ + merge( + this.#accordionExpanded$, + this.colormap$, + this.#fetchingMatrix$, + ).subscribe(() => { + this.removeCustomLayer() + }), + /** + * on update colormap, add new custom layer + */ + combineLatest([ + this.#accordionExpanded$, + this.colormap$, + ]).pipe( + withLatestFrom( + this.store$.pipe( + select(atlasSelection.selectors.selectedParcAllRegions) + ) + ) + ).subscribe(([[accordionExpanded, conn], allregions]) => { + if (!accordionExpanded || !conn) { + return + } - this.fetchedItems.push(...res.items) + const map = new Map<SxplrRegion, number[]>() + for (const region of allregions) { + const area = conn.find(a => a.name === region.name) + if (area) { + map.set(region, Object.values(area.color)) + } else { + map.set(region, [255,255,255,0.1]) + } + } - this.cohorts = [...new Set(this.fetchedItems.map(item => item.cohort))] - this.fetchingPreData = false - this.changeDetectionRef.detectChanges() - this.selectCohort(this.cohorts[0]) - + this.store$.dispatch( + atlasAppearance.actions.addCustomLayer({ + customLayer: { + clType: 'customlayer/colormap', + id: ConnectivityBrowserComponent.LayerId, + colormap: map + } + }) + ) + }), + /** + * on pure connection update, update logchecked box + */ + this.#pureConnections$.subscribe(v => { + for (const val of Object.values(v)) { + if (val > 1) { + this.displayForm.get("logChecked").enable() + return + } + } + this.displayForm.get("logChecked").patchValue(false) + this.displayForm.get("logChecked").disable() }) - } - - public fetchModality = (): Observable<any> => { - const url = `${this.endpoint}?parcellation_id=${encodeURIComponent(this.parcellation.id)}&type=${encodeURIComponent(this.selectedType.shortName)}` - return this.http.get(url) - } - - selectCohort(cohort: string) { - this.selectedCohort = cohort - this.averageDisabled = !this.fetchedItems.find(s => s.cohort === this.selectedCohort && !s.subjects.length) - this.subjectsDisabled = !this.fetchedItems.find(s => s.cohort === this.selectedCohort && s.subjects.length > 0) - this.selectedView = !this.averageDisabled? 'average' : 'subject' - - this.cohortDatasets = this.fetchedItems.filter(i => this.selectedCohort === i.cohort) - - this.selectedCohortDatasetChanged(0) - } - - selectedCohortDatasetChanged(index) { - this.selectedCohortDatasetIndex = index - this.selectedCohortSubjects = this.cohortDatasets[index].subjects + ) + } - this.selectedDataset = this.cohortDatasets[index].datasets[0] - - - const keepSubject = this.selectedSubjectIndex >= 0 && this.cohortDatasets[this.selectedCohortDatasetIndex].subjects - .includes(this.selectedCohortSubjects[this.selectedSubjectIndex]) - - this.subjectSliderChanged(keepSubject? this.selectedSubjectIndex : 0) - } - - subjectSliderChanged(index: number) { - this.selectedSubjectIndex = index - this.fetchConnectivity() + public ngOnChanges(changes: SimpleChanges): void { + const { parcellation, types } = changes + if (parcellation) { + this.parcellation$.next(parcellation.currentValue) } - - fetchConnectivity() { - const subject = this.selectedCohortSubjects[this.selectedSubjectIndex] - const dataset = this.cohortDatasets[this.selectedCohortDatasetIndex] - - this.fetching = true - const url = `${this.endpoint}/${dataset.id}?parcellation_id=${this.parcellation.id}&subject=${subject}&type=${this.selectedType.shortName}` - - this.http.get(url).pipe(catchError(() => { - this.fetching = false - return of(null) - })).subscribe(ds => { - this.fetching = false - this.setMatrixData(ds.matrices[subject]) + if (types) [ + this.types$.next(types.currentValue) + ] + } + + removeCustomLayer() { + this.store$.dispatch( + atlasAppearance.actions.removeCustomLayer({ + id: ConnectivityBrowserComponent.LayerId }) - } - - setMatrixData(data) { - this.regionIndexInMatrix = data.columns.findIndex(re => this.region.id === re['@id']) - - if (this.regionIndexInMatrix < 0) { - this.noConnectivityForRegion = true - this.changeDetectionRef.detectChanges() - return - } else if (this.noConnectivityForRegion) { - this.noConnectivityForRegion = false - } - - const regionProfile = data.data[this.regionIndexInMatrix] - - const maxStrength = Math.max(...regionProfile) - - this.logChecked = maxStrength > 1 - this.logDisabled = maxStrength <= 1 - const areas = regionProfile.reduce((p, c, i) => { - return { - ...p, - [data.columns[i].name]: c + ) + } + + busy$ = new BehaviorSubject<boolean>(false) + + #selectedType$ = this.formValue$.pipe( + map(v => v.selectedType), + distinctUntilChanged((o, n) => o?.name === n?.name), + shareReplay(1), + ) + + #connFeatures$ = this.parcellation$.pipe( + switchMap(parc => concat( + of(null as PathParam), + this.#selectedType$, + ).pipe( + switchMap(selectedType => { + if (!selectedType || !parc) { + return of([] as ConnFeat[]) } - }, {}) - this.pureConnections = areas - this.connectionsString = JSON.stringify(areas) - this.connectedAreas.next(this.formatConnections(areas)) - - this.setCustomLayer() - this.matrixString = JSON.stringify(data.columns.map((mc, i) => ([mc.name, ...data.data[i]]))) - this.changeDetectionRef.detectChanges() - - - return data - } - - - changeLog(checked: boolean) { - this.logChecked = checked - this.connectedAreas.next(this.formatConnections(this.pureConnections)) - this.connectivityComponentElement.nativeElement.toggleShowLog() - this.setCustomLayer() - } + const typedName = getType(selectedType.name) + if (!guardType(typedName)) { + throw new Error(`type ${typedName} is not in ${validTypes.join(',')}`) + } + const query = { + parcellation_id: parc.id, + type: typedName + } + this.busy$.next(true) + return this.sapi.v3Get( + "/feature/RegionalConnectivity", + { query } + ).pipe( + switchMap(resp => + this.sapi.iteratePages( + resp, + page => this.sapi.v3Get( + "/feature/RegionalConnectivity", + { query: { ...query, page } } + ) + ) + ), + finalize(() => { + this.busy$.next(false) + }) + ) + }) + )), + ) + + cohorts$ = this.#connFeatures$.pipe( + map(v => { + const cohorts: string[] = [] + for (const item of v) { + if (!cohorts.includes(item.cohort)) { + cohorts.push(item.cohort) + } + } + return cohorts + }) + ) + + selectedCohort$ = this.formValue$.pipe( + map(v => v.selectedCohort), + distinctUntilChanged() + ) + + cohortDatasets$ = combineLatest([ + this.#connFeatures$, + this.formValue$.pipe( + map(v => v.selectedCohort), + distinctUntilChanged() + ), + ]).pipe( + map(([ features, selectedCohort ]) => features.filter(f => f.cohort === selectedCohort)), + distinctUntilChanged( + arrayEqual((o, n) => o?.id === n?.id) + ), + shareReplay(1), + ) + + selectedDataset$ = this.cohortDatasets$.pipe( + switchMap(features => this.formValue$.pipe( + map(v => v.selectedDatasetIndex), + distinctUntilChanged(), + map(dsIdx => features[dsIdx]), + shareReplay(1), + )), + ) + + displaySubject$ = this.selectedDataset$.pipe( + distinctUntilChanged((o, n) => o?.id === n?.id), + map(ds => { + return (idx: number) => ds.subjects[idx] + }) + ) + + selectedDatasetAdditionalInfos$ = this.selectedDataset$.pipe( + map(ds => ds ? ds.datasets : []) + ) + + #fetchingMatrix$ = new BehaviorSubject<boolean>(true) + + #matrixInput$ = combineLatest([ + this.parcellation$, + this.formValue$, + this.cohortDatasets$, + ]).pipe( + map(([ parcellation, form, dss ]) => { + const { + selectedDatasetIndex: dsIdx, + selectedSubjectIndex: subIdx + } = form + const ds = dss[dsIdx] + if (!ds) { + return null + } - navigateToRegion(regionName: string) { - this.sapi.v3Get("/regions/{region_id}", { - path: {region_id: regionName}, + const subject = ds.subjects[subIdx] + if (!subject) { + return null + } + return { + parcellation, + feature_id: ds.id, + subject + } + }), + shareReplay(1), + ) + + #selectedMatrix$ = this.#matrixInput$.pipe( + switchMap(input => { + if (!input) { + return NEVER + } + const { parcellation, feature_id, subject } = input + + this.#fetchingMatrix$.next(true) + return this.sapi.v3Get( + "/feature/RegionalConnectivity/{feature_id}", + { query: { - parcellation_id: this.parcellation.id, - space_id: this.template.id + parcellation_id: parcellation.id, + subject, + }, + path: { + feature_id } - }).pipe( - switchMap(r => translateV3Entities.translateRegion(r)) - ).subscribe(region => { - const centroid = region.centroid?.loc - if (centroid) { - this.store$.dispatch( - actions.navigateTo({ - navigation: { - position: centroid.map(v => v*1e6), - }, - animation: true - }) - ) + } + ) + }), + tap(() => this.#fetchingMatrix$.next(false)), + shareReplay(1), + ) + + #pureConnections$ = this.#matrixInput$.pipe( + filter(v => !!v), + switchMap(({ subject }) => + this.#selectedMatrix$.pipe( + filter(v => !!v.matrices[subject]), + withLatestFrom(this.region$), + map(([ v, region ]) => { + const b = v.matrices[subject] + const foundIdx = b.columns.findIndex(v => v['name'] === region.name) + if (typeof foundIdx !== 'number') { + return null } + const profile = b.data[foundIdx] + if (!profile) { + return null + } + const rObj: Record<string, number> = {} + b.columns.reduce((acc, curr, idx) => { + const rName = curr['name'] as string + acc[rName] = profile[idx] as number + return acc + }, rObj) + return rObj }) - } - - getRegionWithName(region: string) { - return this.allRegions.find(r => r.name === region) - } + ), + ), + ) + + colormap$ = this.#matrixInput$.pipe( + switchMap(() => concat( + of(null as ConnectedArea[]), + combineLatest([ + this.#pureConnections$, + this.displayForm.valueChanges.pipe( + map(v => v.logChecked), + distinctUntilChanged() + ) + ]).pipe( + map(([ conn, flag ]) => processProfile(conn, flag)) + ) + )) + ) + + view$ = combineLatest([ + this.selectedDataset$, + this.formValue$, + this.#fetchingMatrix$, + concat( + of(null as Record<string, number>), + this.#pureConnections$, + ), + this.region$, + ]).pipe( + map(([sDs, form, fetchingMatrix, pureConnections, region]) => { + return { + showSubject: sDs && form.selectedView === "subject", + numSubjects: sDs?.subjects.length, + fetchingMatrix, + connections: pureConnections, + region, + } + }), + shareReplay(1), + ) + + @HostListener('connectedRegionClicked', ['$event']) + onRegionClicked(event: CustomEvent) { + const regionName = event.detail.name as string + this.sapi.v3Get("/regions/{region_id}", { + path: {region_id: regionName}, + query: { + parcellation_id: this.parcellation.id, + space_id: this.template.id + } + }).pipe( + switchMap(r => translateV3Entities.translateRegion(r)) + ).subscribe(region => { + const centroid = region.centroid?.loc + if (centroid) { + this.store$.dispatch( + actions.navigateTo({ + navigation: { + position: centroid.map(v => v*1e6), + }, + animation: true + }) + ) + } + }) + } + + exportConnectivityProfile() { + const a = document.querySelector('hbp-connectivity-matrix-row'); + (a as any).downloadCSV() + } + + public exportFullConnectivity() { + this.fullConnectivityGridElement?.nativeElement['downloadCSV']() + } + + public ngOnDestroy(): void { + this.removeCustomLayer() + this.subscriptions.forEach(s => s.unsubscribe()) + } +} - exportConnectivityProfile() { - const a = document.querySelector('hbp-connectivity-matrix-row'); - (a as any).downloadCSV() - } +function clamp(min: number, max: number) { + return function(val: number) { + return Math.max(min, Math.min(max, val)) + } +} +const clamp01 = clamp(0, 1) +function interpolate255(val: number) { + return Math.round(clamp01(val) * 255) +} +function jet(val: number) { + return { + r: val < 0.7 + ? interpolate255(4 * val - 1.5) + : interpolate255(-4.0 * val + 4.5), + g: val < 0.5 + ? interpolate255(4.0 * val - 0.5) + : interpolate255(-4.0 * val + 3.5), + b: val < 0.3 + ? interpolate255(4.0 * val + 0.5) + : interpolate255(-4.0 * val + 2.5) + } +} - public exportFullConnectivity() { - this.fullConnectivityGridElement?.nativeElement['downloadCSV']() - } +function processProfile(areas: Record<string, number>, logFlag=false): ConnectedArea[] { + const returnValue: Omit<ConnectedArea, "color">[] = [] + for (const areaname in areas) { + returnValue.push({ + name: areaname, + numberOfConnections: areas[areaname], + }) + } + returnValue.sort((a, b) => b.numberOfConnections - a.numberOfConnections) + if (returnValue.length === 0) { + return [] + } + const preprocess = (v: number) => logFlag ? Math.log10(v) : v + return returnValue.map(v => ({ + ...v, + color: jet( + preprocess(v.numberOfConnections) / preprocess(returnValue[0].numberOfConnections) + ) + })) +} - public ngOnDestroy(): void { - this.removeCustomLayer() - this.subscriptions.forEach(s => s.unsubscribe()) - } +function getType(name: string) { + return name.split(".").slice(-1)[0] +} - private formatConnections(areas: { [key: string]: number }) { - const cleanedObj = Object.keys(areas) - .map(key => ({name: key, numberOfConnections: areas[key]})) - .filter(f => f.numberOfConnections > 0) - .sort((a, b) => +b.numberOfConnections - +a.numberOfConnections) - - const logMax = this.logChecked ? Math.log(cleanedObj[0].numberOfConnections) : cleanedObj[0].numberOfConnections - const colorAreas = [] - - cleanedObj.forEach((a) => { - colorAreas.push({ - ...a, - color: { - r: this.colormap_red((this.logChecked ? Math.log(a.numberOfConnections) : a.numberOfConnections) / logMax ), - g: this.colormap_green((this.logChecked ? Math.log(a.numberOfConnections) : a.numberOfConnections) / logMax ), - b: this.colormap_blue((this.logChecked ? Math.log(a.numberOfConnections) : a.numberOfConnections) / logMax ) - }, - }) - }) - return colorAreas - } - private clamp = val => Math.round(Math.max(0, Math.min(1.0, val)) * 255) - private colormap_red = x => x < 0.7? this.clamp(4.0 * x - 1.5) : this.clamp(-4.0 * x + 4.5) - private colormap_green = x => x < 0.5? this.clamp(4.0 * x - 0.5) : this.clamp(-4.0 * x + 3.5) - private colormap_blue = x => x < 0.3? this.clamp(4.0 * x + 0.5) : this.clamp(-4.0 * x + 2.5) +const validTypes = ["FunctionalConnectivity", "StreamlineCounts", "StreamlineLengths"] +function guardType(name: unknown): name is "FunctionalConnectivity" | "StreamlineCounts" | "StreamlineLengths" { + return typeof name === "string" && validTypes.includes(name) } type ConnectedArea = { diff --git a/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html b/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html index 87e51584a..a7061fe7a 100644 --- a/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html +++ b/src/features/connectivity/connectivityBrowser/connectivityBrowser.template.html @@ -1,104 +1,142 @@ <div class="w-100 h-100 d-block d-flex flex-column sxplr-pb-2"> - <div> - <div *ngIf="types && types.length" - class="flex-grow-0 flex-shrink-0 d-flex flex-row flex-nowrap d-flex flex-column"> - <mat-form-field class="flex-grow-1 flex-shrink-1 w-100"> - <mat-label> - Modality - </mat-label> - <mat-select [value]="selectedType" (selectionChange)="selectType($event.value)"> - <mat-option *ngFor="let type of types" [value]="type"> - {{ type.shortName }} - </mat-option> - </mat-select> - </mat-form-field> + <form [formGroup]="connectivityFilterForm"> + + <ng-template [ngIf]="types$ | async" let-types> - <mat-form-field *ngIf="!fetchingPreData && selectedType" class="flex-grow-1 flex-shrink-1 w-100"> - <mat-label> - Cohort - </mat-label> - <mat-select [value]="selectedCohort" (selectionChange)="selectCohort($event.value)"> - <mat-option *ngFor="let cohort of cohorts" [value]="cohort"> - {{ cohort }} - </mat-option> - </mat-select> - </mat-form-field> - </div> + <div + class="flex-grow-0 flex-shrink-0 d-flex flex-row flex-nowrap d-flex flex-column"> + <mat-form-field class="flex-grow-1 flex-shrink-1 w-100"> + <mat-label> + Modality + </mat-label> + <mat-select formControlName="selectedType"> + <mat-option *ngFor="let type of types" [value]="type"> + {{ type.display_name }} + </mat-option> + </mat-select> + </mat-form-field> - <mat-radio-group *ngIf="selectedCohort" [(ngModel)]="selectedView"> - <mat-radio-button value="average" class="m-2" [disabled]="averageDisabled" color="primary"> - Average - </mat-radio-button> - <mat-radio-button value="subject" class="m-2" [disabled]="subjectsDisabled" color="primary"> - Subject - </mat-radio-button> - </mat-radio-group> - - <div *ngIf="cohortDatasets && cohortDatasets.length > 1" - class="flex-grow-0 flex-shrink-0 d-flex flex-nowrap align-items-center"> - <div class="flex-grow-1 flex-shrink-1 w-100"> - <mat-label> - Dataset - </mat-label> - <mat-slider [min]="0" [max]="cohortDatasets.length - 1" (change)="selectedCohortDatasetChanged($event.value)" - [value]="selectedCohortDatasetIndex" thumbLabel step="1" class="w-100"> - </mat-slider> + <mat-form-field *ngIf="!(busy$ | async) && (formValue$ | async | getProperty : 'selectedType')" class="flex-grow-1 flex-shrink-1 w-100"> + <mat-label> + Cohort + </mat-label> + <mat-select formControlName="selectedCohort"> + <mat-option *ngFor="let cohort of cohorts$ | async" + [value]="cohort"> + {{ cohort }} + </mat-option> + </mat-select> + </mat-form-field> </div> - </div> + </ng-template> + + <ng-template [ngIf]="formValue$ | async | getProperty : 'selectedCohort'"> + <mat-radio-group formControlName="selectedView"> + <mat-radio-button value="average" class="m-2" color="primary"> + Average + </mat-radio-button> + <mat-radio-button value="subject" class="m-2" color="primary"> + Subject + </mat-radio-button> + </mat-radio-group> + </ng-template> + + <ng-template [ngIf]="cohortDatasets$ | async" let-cohortDatasets> + <ng-template [ngIf]="cohortDatasets.length > 1"> + <div class="flex-grow-0 flex-shrink-0 d-flex flex-nowrap align-items-center"> + <div class="flex-grow-1 flex-shrink-1 w-100"> + <mat-label> + Dataset + </mat-label> + <mat-slider [min]="0" + [max]="cohortDatasets.length - 1" + step="1" + formControlName="selectedDatasetIndex" + thumbLabel + class="w-100"> + </mat-slider> + </div> + </div> + </ng-template> + </ng-template> - <div *ngIf="selectedCohortDatasetIndex >= 0 && selectedCohortSubjects" + <div *ngIf="view$ | async | getProperty : 'showSubject'" class="flex-grow-0 flex-shrink-0 d-flex flex-nowrap align-items-center"> <div class="flex-grow-1 flex-shrink-1 w-100"> <mat-label> Subject </mat-label> - <mat-slider [min]="0" [max]="selectedCohortSubjects.length - 1" - (change)="subjectSliderChanged($event.value)" [value]="selectedSubjectIndex" - thumbLabel step="1" class="w-100"> + <mat-slider [min]="0" + [max]="(view$ | async | getProperty : 'numSubjects') - 1" + thumbLabel + [displayWith]="displaySubject$ | async" + step="1" + formControlName="selectedSubjectIndex" + class="w-100"> </mat-slider> </div> </div> - </div> - <div class="d-flex justify-content-center"> - <mat-spinner *ngIf="fetching"></mat-spinner> - </div> + </form> - <div *ngIf="regionName && !fetching" - [style.visibility]="selectedCohort && (selectedSubjectIndex >= 0 || !averageDisabled)? 'visible' : 'hidden'" - class="d-flex align-items-center"> - <mat-checkbox class="mr-2" [checked]="logChecked" (change)="changeLog($event.checked)" - [disabled]="logDisabled || noConnectivityForRegion">Log 10</mat-checkbox> - <button mat-button [matMenuTriggerFor]="exportMenu" [disabled]="!connectedAreas.value"> - <i class="fas fa-download mr-2"></i> - <span>Export</span> - </button> - <button *ngIf="selectedDataset" iav-stop="mousedown click" class="icons" mat-icon-button sxplr-dialog - [sxplr-dialog-size]="null" [sxplr-dialog-data]="{ - title: selectedDataset?.name, - descMd: selectedDataset?.description + '' + (selectedDataset?.authors && selectedDataset?.authors.join()), - actions: [selectedDataset.ebrains_page] - }"> - <i class="fas fa-info"></i> - </button> - </div> + <ng-template [ngIf]="view$ | async | getProperty : 'region'" let-region> + + <!-- loading spinner --> + <ng-template [ngIf]="view$ | async | getProperty : 'fetchingMatrix'" + [ngIfElse]="profileTmpl"> + <div class="d-flex justify-content-center" id = 'blabla'> + <mat-spinner></mat-spinner> + </div> + </ng-template> - <hbp-connectivity-matrix-row #connectivityComponent - [style.visibility]="regionName && !fetching && !noConnectivityForRegion && selectedCohort - && (selectedSubjectIndex >= 0 || !averageDisabled)? 'visible' : 'hidden'" - [region]="regionName" - [connections]="connectionsString" - show-export="true" hide-export-view="true" theme="dark"> - </hbp-connectivity-matrix-row> - <div *ngIf="noConnectivityForRegion">No connectivity for the region.</div> + <!-- profile --> + <!-- <pre>{{ view$ | async | json }}</pre> --> + <ng-template #profileTmpl> + <ng-template [ngIf]="view$ | async | getProperty : 'connections'" let-conn> + <div class="d-flex align-items-center"> + <form [formGroup]="displayForm"> + <mat-checkbox + class="mr-2" + formControlName="logChecked"> + Log 10 + </mat-checkbox> + </form> + <button mat-button [matMenuTriggerFor]="exportMenu"> + <i class="fas fa-download mr-2"></i> + <span>Export</span> + </button> + <ng-template ngFor [ngForOf]="selectedDatasetAdditionalInfos$ | async" let-info> + <button class="icons" + mat-icon-button + sxplr-dialog + [sxplr-dialog-size]="null" + [sxplr-dialog-data]="{ + title: info?.name, + descMd: info?.description, + actions: [info.ebrains_page] + }"> + <i class="fas fa-info"></i> + </button> + </ng-template> + </div> + + <hbp-connectivity-matrix-row #connectivityComponent + [region]="region.name" + [connections]="conn | json" + show-export="true" + hide-export-view="true" + theme="dark"> + </hbp-connectivity-matrix-row> + </ng-template> + </ng-template> + </ng-template> +</div> - <full-connectivity-grid #fullConnectivityGrid [matrix]="matrixString" [datasetName]="selectedDataset?.name" - [datasetDescription]="selectedDataset?.description" only-export="true"> - </full-connectivity-grid> - <mat-menu #exportMenu="matMenu"> - <button mat-menu-item [disabled]="noConnectivityForRegion" - (click)="exportConnectivityProfile()">Regional</button> - <button mat-menu-item (click)="exportFullConnectivity()">Dataset</button> - </mat-menu> -</div> \ No newline at end of file +<mat-menu #exportMenu="matMenu"> + <button mat-menu-item + (click)="exportConnectivityProfile()"> + Regional + </button> + <button mat-menu-item (click)="exportFullConnectivity()">Dataset</button> +</mat-menu> diff --git a/src/features/connectivity/module.ts b/src/features/connectivity/module.ts index 97a7793ff..221ad467e 100644 --- a/src/features/connectivity/module.ts +++ b/src/features/connectivity/module.ts @@ -1,19 +1,31 @@ import { CommonModule } from "@angular/common"; import {CUSTOM_ELEMENTS_SCHEMA, NgModule} from "@angular/core"; import { SAPI } from "src/atlasComponents/sapi"; - -import {AngularMaterialModule} from "src/sharedModules"; -import {FormsModule} from "@angular/forms"; -import { DialogModule } from "src/ui/dialogInfo"; - import { ConnectivityBrowserComponent } from "./connectivityBrowser/connectivityBrowser.component"; +import { ReactiveFormsModule } from "@angular/forms"; +import { DialogModule } from "src/ui/dialogInfo"; +import { MatSelectModule } from "@angular/material/select"; +import { MatRadioModule } from "@angular/material/radio"; +import { MatSliderModule } from "@angular/material/slider"; +import { MatMenuModule } from "@angular/material/menu"; +import { UtilModule } from "src/util"; +import { MatCheckboxModule } from "@angular/material/checkbox"; +import { MatButtonModule } from "@angular/material/button"; +import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; @NgModule({ imports: [ CommonModule, - FormsModule, - AngularMaterialModule, - DialogModule + ReactiveFormsModule, + MatSelectModule, + MatRadioModule, + MatSliderModule, + MatMenuModule, + DialogModule, + UtilModule, + MatCheckboxModule, + MatButtonModule, + MatProgressSpinnerModule, ], declarations: [ ConnectivityBrowserComponent, diff --git a/src/features/feature-view/feature-view.component.ts b/src/features/feature-view/feature-view.component.ts index 400644f14..3d9f1cdc9 100644 --- a/src/features/feature-view/feature-view.component.ts +++ b/src/features/feature-view/feature-view.component.ts @@ -43,7 +43,7 @@ export class FeatureViewComponent implements OnChanges { voi$ = new BehaviorSubject<VoiFeature>(null) columns$: Observable<string[]> = this.tabular$.pipe( map(data => data - ? ['index', ...data.columns] + ? ['index', ...data.columns] as string[] : []), ) @@ -52,7 +52,7 @@ export class FeatureViewComponent implements OnChanges { map(v => { return v.index.map((receptor, idx) => ({ receptor: { - label: receptor + label: receptor as string }, density: { mean: v.data[idx][0] as number, diff --git a/src/features/filterCategories.pipe.ts b/src/features/filterCategories.pipe.ts index 8f1033287..8521c7454 100644 --- a/src/features/filterCategories.pipe.ts +++ b/src/features/filterCategories.pipe.ts @@ -2,7 +2,7 @@ import { KeyValue } from "@angular/common" import { Pipe, PipeTransform } from "@angular/core" import { PathReturn } from "src/atlasComponents/sapi/typeV3" -type DS = KeyValue<string, PathReturn<"/feature/_types">["items"]> +export type DS = KeyValue<string, PathReturn<"/feature/_types">["items"]> @Pipe({ name: 'filterCategory', diff --git a/src/features/transform-pd-to-ds.pipe.ts b/src/features/transform-pd-to-ds.pipe.ts index 09f598d9d..c0cc94e29 100644 --- a/src/features/transform-pd-to-ds.pipe.ts +++ b/src/features/transform-pd-to-ds.pipe.ts @@ -2,6 +2,14 @@ import { CdkTableDataSourceInput } from '@angular/cdk/table'; import { Pipe, PipeTransform } from '@angular/core'; import { TabularFeature } from 'src/atlasComponents/sapi/sxplrTypes'; +function typeGuard(input: unknown): input is string | number | number[]{ + return typeof input === "string" || typeof input === "number" || (Array.isArray(input) && input.every(v => typeof v === "number")) +} + +function isString(input: unknown): input is string { + return typeof input === "string" +} + @Pipe({ name: 'transformPdToDs', pure: true @@ -10,11 +18,19 @@ export class TransformPdToDsPipe implements PipeTransform { transform(pd: TabularFeature<string|number|number[]>): CdkTableDataSourceInput<unknown> { return pd.data.map((arr, idx) => { + const val = pd.index[idx] + if (!typeGuard(val)) { + throw new Error(`Expected val to be of type string, number or number[], but was none.`) + } const returnVal: Record<string, string|number|number[]> = { - index: pd.index[idx], + index: val, } arr.forEach((val, colIdx) => { - returnVal[pd.columns[colIdx]] = val + const key = pd.columns[colIdx] + if (!isString(key)) { + throw new Error(`Expected key to be of type string, number or number[], but was not`) + } + returnVal[key] = val }) return returnVal }) diff --git a/src/index.html b/src/index.html index 8b04e68c0..0f009f4f2 100644 --- a/src/index.html +++ b/src/index.html @@ -15,7 +15,7 @@ <script src="https://unpkg.com/kg-dataset-previewer@1.2.0/dist/kg-dataset-previewer/kg-dataset-previewer.js" defer></script> <script src="https://unpkg.com/three-surfer@0.0.13/dist/bundle.js" defer></script> <script type="module" src="https://unpkg.com/ng-layer-tune@0.0.6/dist/ng-layer-tune/ng-layer-tune.esm.js"></script> - <script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.5/dist/connectivity-component/connectivity-component.js" ></script> + <script type="module" src="https://unpkg.com/hbp-connectivity-component@0.6.6/dist/connectivity-component/connectivity-component.js" ></script> <script defer src="https://unpkg.com/mathjax@3.1.2/es5/tex-svg.js"></script> <script defer src="https://unpkg.com/d3@6.2.0/dist/d3.min.js"></script> <title>Siibra Explorer</title> diff --git a/src/viewerModule/nehuba/store/store.ts b/src/viewerModule/nehuba/store/store.ts index 0dd7043f3..33f4275eb 100644 --- a/src/viewerModule/nehuba/store/store.ts +++ b/src/viewerModule/nehuba/store/store.ts @@ -20,7 +20,6 @@ const defaultState: INehubaFeature = { panelMode: EnumPanelMode.FOUR_PANEL, panelOrder: '0123', octantRemoval: true, - clearViewQueue: {}, auxMeshes: [] } diff --git a/src/viewerModule/nehuba/store/type.ts b/src/viewerModule/nehuba/store/type.ts index a55be8ced..7908af13a 100644 --- a/src/viewerModule/nehuba/store/type.ts +++ b/src/viewerModule/nehuba/store/type.ts @@ -16,8 +16,5 @@ export interface INehubaFeature { panelMode: string panelOrder: string octantRemoval: boolean - clearViewQueue: { - [key: string]: boolean - } auxMeshes: IAuxMesh[] } -- GitLab