File

src/app/app.component.ts

Implements

AfterViewInit

Metadata

Index

Properties
Methods
Outputs

Constructor

constructor(lookup: OrganLookupService, ga: GoogleAnalyticsService, configState: GlobalConfigState<GlobalConfig>)
Parameters :
Name Type Optional
lookup OrganLookupService No
ga GoogleAnalyticsService No
configState GlobalConfigState<GlobalConfig> No

Outputs

nodeClicked
Type : EventEmitter
sexChange
Type : EventEmitter
sideChange
Type : EventEmitter

Methods

updateInput
updateInput(key: string, value)
Parameters :
Name Type Optional
key string No
value No
Returns : void

Properties

Readonly asctbUrl$
Default value : this.configState.getOption('asctbUrl')
Readonly blocks$
Type : Observable<TissueBlockResult[]>
Readonly donorLabel$
Default value : this.configState.getOption('donorLabel')
Readonly euiUrl$
Default value : this.configState.getOption('euiUrl')
Readonly filter$
Default value : this.configState .getOption('highlightProviders') .pipe(map((providers) => ({ tmc: providers ?? [] })))
Readonly hraPortalUrl$
Default value : this.configState.getOption('hraPortalUrl')
left
Type : ElementRef<HTMLElement>
Decorators :
@ViewChild('left', {read: ElementRef, static: true})
Readonly onlineCourseUrl$
Default value : this.configState.getOption('onlineCourseUrl')
Readonly organ$
Type : Observable<SpatialEntity | undefined>
Readonly organInfo$
Type : Observable<OrganInfo | undefined>
Readonly paperUrl$
Default value : this.configState.getOption('paperUrl')
right
Type : ElementRef<HTMLElement>
Decorators :
@ViewChild('right', {read: ElementRef, static: true})
Readonly ruiUrl$
Default value : this.configState.getOption('ruiUrl')
Readonly scene$
Type : Observable<SpatialSceneNode[]>
Readonly sex$
Default value : this.configState.getOption('sex')
Readonly side$
Default value : this.configState.getOption('side')
stats
Type : AggregateResult[]
Default value : []
Readonly stats$
Type : Observable<AggregateResult[]>
Readonly statsLabel$
Type : Observable<string>
import { Immutable } from '@angular-ru/common/typings';
import {
  AfterViewInit,
  ChangeDetectionStrategy,
  Component,
  ElementRef,
  EventEmitter,
  Output,
  ViewChild,
} from '@angular/core';
import { SpatialSceneNode } from 'ccf-body-ui';
import { AggregateResult, SpatialEntity, TissueBlockResult } from 'ccf-database';
import { GlobalConfigState, OrganInfo } from 'ccf-shared';
import { GoogleAnalyticsService } from 'ngx-google-analytics';
import { Observable, combineLatest, of } from 'rxjs';
import { map, shareReplay, startWith, switchMap, tap } from 'rxjs/operators';

import { OrganLookupService } from './core/services/organ-lookup/organ-lookup.service';

interface GlobalConfig {
  organIri?: string;
  side?: string;
  sex?: 'Both' | 'Male' | 'Female';
  highlightProviders?: string[];
  donorLabel?: string;
  ruiUrl?: string;
  euiUrl?: string;
  asctbUrl?: string;
  hraPortalUrl?: string;
  onlineCourseUrl?: string;
  paperUrl?: string;
}

const EMPTY_SCENE = [{ color: [0, 0, 0, 0], opacity: 0.001 }];

@Component({
  selector: 'ccf-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements AfterViewInit {
  @ViewChild('left', { read: ElementRef, static: true }) left!: ElementRef<HTMLElement>;
  @ViewChild('right', { read: ElementRef, static: true }) right!: ElementRef<HTMLElement>;

  @Output() readonly sexChange = new EventEmitter<'Male' | 'Female'>();
  @Output() readonly sideChange = new EventEmitter<'Left' | 'Right'>();
  @Output() readonly nodeClicked = new EventEmitter();
  readonly sex$ = this.configState.getOption('sex');
  readonly side$ = this.configState.getOption('side');
  readonly filter$ = this.configState
    .getOption('highlightProviders')
    .pipe(map((providers) => ({ tmc: providers ?? [] })));
  readonly donorLabel$ = this.configState.getOption('donorLabel');
  readonly ruiUrl$ = this.configState.getOption('ruiUrl');
  readonly euiUrl$ = this.configState.getOption('euiUrl');
  readonly asctbUrl$ = this.configState.getOption('asctbUrl');
  readonly hraPortalUrl$ = this.configState.getOption('hraPortalUrl');
  readonly onlineCourseUrl$ = this.configState.getOption('onlineCourseUrl');
  readonly paperUrl$ = this.configState.getOption('paperUrl');

  readonly organInfo$: Observable<OrganInfo | undefined>;
  readonly organ$: Observable<SpatialEntity | undefined>;
  readonly scene$: Observable<SpatialSceneNode[]>;
  readonly stats$: Observable<AggregateResult[]>;
  readonly statsLabel$: Observable<string>;
  readonly blocks$: Observable<TissueBlockResult[]>;

  stats: AggregateResult[] = [];

  private latestConfig: Immutable<GlobalConfig> = {};
  private latestOrganInfo?: OrganInfo;

  constructor(
    lookup: OrganLookupService,
    private readonly ga: GoogleAnalyticsService,
    private readonly configState: GlobalConfigState<GlobalConfig>,
  ) {
    this.organInfo$ = configState.config$.pipe(
      tap((config) => (this.latestConfig = config)),
      switchMap((config) =>
        lookup.getOrganInfo(config.organIri ?? '', config.side?.toLowerCase?.() as OrganInfo['side'], config.sex),
      ),
      tap((info) => this.logOrganLookup(info)),
      tap((info) => (this.latestOrganInfo = info)),
      shareReplay(1),
    );

    this.organ$ = this.organInfo$.pipe(
      switchMap((info) =>
        info ? lookup.getOrgan(info, info.hasSex ? this.latestConfig.sex : undefined) : of(undefined),
      ),
      tap((organ) => {
        if (organ && this.latestOrganInfo) {
          const newSex = this.latestOrganInfo?.hasSex ? organ.sex : undefined;
          if (newSex !== this.latestConfig.sex) {
            this.updateInput('sex', newSex);
          }
          if (organ.side !== this.latestConfig.side) {
            this.updateInput('side', organ.side);
          }
        }
      }),
      shareReplay(1),
    );

    this.scene$ = this.organ$.pipe(
      switchMap((organ) =>
        organ && this.latestOrganInfo
          ? lookup.getOrganScene(this.latestOrganInfo, organ.sex)
          : of(EMPTY_SCENE as SpatialSceneNode[]),
      ),
    );

    this.stats$ = combineLatest([this.organ$, this.donorLabel$]).pipe(
      switchMap(([organ, donorLabel]) =>
        organ && this.latestOrganInfo
          ? lookup
              .getOrganStats(this.latestOrganInfo, organ.sex)
              .pipe(
                map((agg) =>
                  agg.map((result) =>
                    donorLabel && result.label === 'Donors' ? { ...result, label: donorLabel } : result,
                  ),
                ),
              )
          : of([]),
      ),
    );

    this.statsLabel$ = this.organ$.pipe(
      map((organ) => this.makeStatsLabel(this.latestOrganInfo, organ?.sex)),
      startWith('Loading...'),
    );

    this.blocks$ = this.organ$.pipe(
      switchMap((organ) =>
        organ && this.latestOrganInfo ? lookup.getBlocks(this.latestOrganInfo, organ.sex) : of([]),
      ),
    );
  }

  ngAfterViewInit(): void {
    const { left, right } = this;
    const rightHeight = right.nativeElement.offsetHeight;
    left.nativeElement.style.height = `${rightHeight}px`;
  }

  updateInput(key: string, value: unknown): void {
    this.configState.patchConfig({ [key]: value });
  }

  private makeStatsLabel(info: OrganInfo | undefined, sex?: string): string {
    let parts: (string | undefined)[] = [`Unknown IRI: ${this.latestConfig.organIri}`];
    if (info) {
      // Use title cased side for a cleaner display
      const side = info.side ? info.side.charAt(0).toUpperCase() + info.side.slice(1) : undefined;
      parts = [sex, info.organ, side];
    }
    return parts.filter((seg) => !!seg).join(', ');
  }

  private logOrganLookup(info: OrganInfo | undefined): void {
    const event = info ? 'organ_lookup_success' : 'organ_lookup_failure';
    const inputs = `Iri: ${this.latestConfig.organIri} - Sex: ${this.latestConfig.sex} - Side: ${this.latestConfig.side}`;
    this.ga.event(event, 'organ', inputs);
  }
}
<div class="container">
  <div class="left" #left>
    <ccf-organ
      [blocks]="(blocks$ | async) ?? []"
      [filter]="$any(filter$ | async)"
      [sex]="(sex$ | async)!"
      [side]="$any(side$ | async)"
      [organ]="(organ$ | async) ?? undefined"
      [scene]="(scene$ | async) ?? []"
      (sexChange)="updateInput('sex', $event); sexChange.emit($event)"
      (nodeClick)="nodeClicked.emit($event)"
      (sideChange)="updateInput('side', $event); sideChange.emit($event)"
    >
    </ccf-organ>
  </div>
  <div class="right" #right>
    <ccf-stats-list [statsLabel]="(statsLabel$ | async) ?? ''" [stats]="(stats$ | async) ?? []"> </ccf-stats-list>
    <ccf-link-cards
      [ruiUrl]="(ruiUrl$ | async) ?? ''"
      [euiUrl]="(euiUrl$ | async) ?? ''"
      [asctbUrl]="(asctbUrl$ | async) ?? ''"
      [hraPortalUrl]="(hraPortalUrl$ | async) ?? ''"
      [onlineCourseUrl]="(onlineCourseUrl$ | async) ?? ''"
      [paperUrl]="(paperUrl$ | async) ?? ''"
    >
    </ccf-link-cards>
  </div>
</div>

./app.component.scss

.container {
  height: fit-content;
  display: flex;
  flex-direction: row;
  padding: 1rem;
  font-family:
    var(--ccf-ui-font, ''),
    Inter,
    Inter Variable,
    sans-serif;
  font-size: 0.95rem;
  line-height: 1.5;
  text-align: left;
  position: relative;
  background-color: white;
  color: black;

  .left {
    width: auto;
    flex-grow: 1;
  }

  .right {
    margin-left: 2rem;
    height: fit-content;
    width: 29rem;
  }
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""