File

projects/web-mev/src/app/d3/components/deseq2/deseq2.component.ts

Index

Properties

Properties

Control
Control: number
Type : number
Experimental
Experimental: number
Type : number
lfcSE
lfcSE: number
Type : number
log2FoldChange
log2FoldChange: number
Type : number
name
name: string
Type : string
overall_mean
overall_mean: number
Type : number
padj
padj: number
Type : number
pvalue
pvalue: number
Type : number
stat
stat: number
Type : number
import {
  Component,
  OnInit,
  ChangeDetectionStrategy,
  ViewChild,
  AfterViewInit,
  Input,
  ElementRef
} from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { AnalysesService } from '@app/features/analysis/services/analysis.service';
import { merge, BehaviorSubject, Observable } from 'rxjs';
import { tap, finalize } from 'rxjs/operators';
import { DataSource } from '@angular/cdk/table';
import * as d3 from 'd3';
import d3Tip from 'd3-tip';
import { FormGroup, FormControl } from '@angular/forms';
import { CustomSetType } from '@app/_models/metadata';
import { MatDialog } from '@angular/material/dialog';
import { AddSampleSetComponent } from '../dialogs/add-sample-set/add-sample-set.component';
import { MetadataService } from '@app/core/metadata/metadata.service';
import { Utils } from '@app/shared/utils/utils';

/**
 * Deseq2 Component
 *
 * Used for Deseq2 analysis
 */
@Component({
  selector: 'mev-deseq2',
  templateUrl: './deseq2.component.html',
  styleUrls: ['./deseq2.component.scss'],
  changeDetection: ChangeDetectionStrategy.Default
})
export class Deseq2Component implements OnInit, AfterViewInit {
  @Input() outputs;
  dataSource: FeaturesDataSource; // datasource for MatTable
  boxPlotData; // data retrieved from the dgeResourceId resource, pre-processed for D3 box plot visualization
  dgeResourceId;

  @ViewChild(MatPaginator) paginator: MatPaginator;
  @ViewChild(MatSort) sort: MatSort;
  @ViewChild('boxPlot') svgElement: ElementRef;

  /* Table settings */
  displayedColumns = [
    'name',
    'overall_mean',
    'log2FoldChange',
    'pvalue',
    'padj',
    'lfcSE',
    'stat'
  ];
  operators = [
    { id: 'eq', name: ' = ' },
    { id: 'gte', name: ' >=' },
    { id: 'gt', name: ' > ' },
    { id: 'lte', name: ' <=' },
    { id: 'lt', name: ' < ' },
    { id: 'absgt', name: 'ABS(x) > ' },
    { id: 'abslt', name: 'ABS(x) < ' }
  ];

  defaultPageIndex = 0;
  defaultPageSize = 10;
  defaultSorting = { field: 'log2FoldChange', direction: 'asc' };

  /* Table filters */
  allowedFilters = {
    /*name: { defaultValue: '', hasOperator: false },*/
    padj: {
      defaultValue: '',
      hasOperator: true,
      operatorDefaultValue: 'lte'
    },
    log2FoldChange: {
      defaultValue: '',
      hasOperator: true,
      operatorDefaultValue: 'lte'
    }
  };

  filterForm = new FormGroup({});

  /* D3 Chart settings */
  containerId = '#boxPlot';
  imageName = 'DESeq2'; // file name for downloaded SVG image
  margin = { top: 50, right: 300, bottom: 100, left: 50 };
  outerHeight = 700;
  precision = 2;
  delta = 0.1; // used for X and Y axis ranges (we add delta to avoid bug when both max and min are zeros)
  boxWidth = 20; // the width of rectangular box
  jitterWidth = 10;
  tooltipOffsetX = 10; // to position the tooltip on the right side of the triggering element

  boxPlotTypes = {
    Experimental: {
      label: 'Treated/Experimental',
      yCat: 'experValues',
      yPoints: 'experPoints',
      color: '#f40357'
    },
    Base: {
      label: 'Baseline/Control',
      yCat: 'baseValues',
      yPoints: 'basePoints',
      color: '#f4cc03'
    }
  };
  xCat = 'key'; // field name in data for X axis
  yExperCat = this.boxPlotTypes.Experimental.yCat; // field name in data for Y axis (used to build experimental box plots)
  yBaseCat = this.boxPlotTypes.Base.yCat; // field name in data for Y axis (used to build baseline box plots)
  yExperPoints = this.boxPlotTypes.Experimental.yPoints; // field name in data for Y axis (used to draw individual points for experimental samples)
  yBasePoints = this.boxPlotTypes.Base.yPoints; // field name in data for Y axis (used to draw individual points for baseline samples)
  xScale; // scale functions to transform data values into the the range
  yScale;

  constructor(
    private analysesService: AnalysesService,
    public dialog: MatDialog,
    private metadataService: MetadataService
  ) {
    this.dataSource = new FeaturesDataSource(this.analysesService);

    // adding form controls depending on the tables settings (the allowedFilters property)
    for (const key in this.allowedFilters) {
      if (this.allowedFilters.hasOwnProperty(key)) {
        // TSLint rule
        const defaultValue = this.allowedFilters[key].defaultValue;
        this.filterForm.addControl(key, new FormControl(defaultValue));
        if (this.allowedFilters[key].hasOperator) {
          const operatorDefaultValue = this.allowedFilters[key]
            .operatorDefaultValue;
          this.filterForm.addControl(
            key + '_operator',
            new FormControl(operatorDefaultValue)
          );
        }
      }
    }
  }

  ngOnInit() {
    this.initializeFeatureResource();
  }

  ngAfterViewInit() {
    this.sort.sortChange.subscribe(
      () => (this.paginator.pageIndex = this.defaultPageIndex)
    );
    this.dataSource.connect().subscribe(featureData => {
      this.boxPlotData = featureData;
      this.preprocessBoxPlotData();
      this.createChart();
    });
    merge(this.sort.sortChange, this.paginator.page)
      .pipe(
        tap(() => {
          this.loadFeaturesPage();
          this.preprocessBoxPlotData();
          this.createChart();
        })
      )
      .subscribe();
  }

  ngOnChanges(): void {
    this.initializeFeatureResource();
  }

  initializeFeatureResource(): void {
    this.dgeResourceId = this.outputs.dge_results;
    const sorting = {
      sortField: this.defaultSorting.field,
      sortDirection: this.defaultSorting.direction
    };
    this.dataSource.loadFeatures(
      this.dgeResourceId,
      {},
      sorting,
      this.defaultPageIndex,
      this.defaultPageSize
    );
  }

  /**
   * Function is triggered when submitting the form with table filters
   */
  onSubmit() {
    this.paginator.pageIndex = this.defaultPageIndex;
    this.loadFeaturesPage();
  }

  /**
   * Function is triggered when resizing the chart
   */
  onResize(event) {
    this.createChart();
  }

  /**
   * Function to prepape the outputs data for D3 box plot visualization
   */
  preprocessBoxPlotData() {
    const baseSamples = this.outputs.base_condition_samples.elements.map(
      elem => elem.id
    );
    const experSamples = this.outputs.experimental_condition_samples.elements.map(
      elem => elem.id
    );

    const countsFormatted = this.boxPlotData.map(elem => {
      const baseNumbers = baseSamples.reduce(
        (acc, cur) => [...acc, elem[cur]],
        []
      );
      const experNumbers = experSamples.reduce(
        (acc, cur) => [...acc, elem[cur]],
        []
      );
      const newElem = { key: elem.name };
      newElem[this.yExperCat] = Utils.getBoxPlotStatistics(experNumbers);
      newElem[this.yBaseCat] = Utils.getBoxPlotStatistics(baseNumbers);
      newElem[this.yExperPoints] = experNumbers;
      newElem[this.yBasePoints] = baseNumbers;
      return newElem;
    });
    this.boxPlotData = countsFormatted;

    // overwrite labels if there are custom names defined by the user
    if (this.outputs.base_condition_name) {
      this.boxPlotTypes.Base.label = this.outputs.base_condition_name;
    }

    if (this.outputs.experimental_condition_name) {
      this.boxPlotTypes.Experimental.label = this.outputs.experimental_condition_name;
    }
  }

  /**
   * Function that is triggered when the user clicks the "Create a custom sample" button
   */
  onCreateCustomFeatureSet() {
    const features = this.dataSource.featuresSubject.value.map(elem => ({
      id: elem.name
    }));
    const dialogRef = this.dialog.open(AddSampleSetComponent, {
      data: { type: CustomSetType.FeatureSet }
    });

    dialogRef.afterClosed().subscribe(customSetData => {
      if (customSetData) {
        const customSet = {
          name: customSetData.name,
          type: CustomSetType.FeatureSet,
          elements: features,
          multiple: true
        };

        this.metadataService.addCustomSet(customSet);
      }
    });
  }

  /**
   * Function to generate D3 box plot
   */
  createChart(): void {
    const outerWidth = this.svgElement.nativeElement.offsetWidth;
    const outerHeight = this.outerHeight;
    const width = outerWidth - this.margin.left - this.margin.right;
    const height = outerHeight - this.margin.top - this.margin.bottom;

    const data = this.boxPlotData;
    d3.select(this.containerId)
      .selectAll('svg')
      .remove();

    const svg = d3
      .select(this.containerId)
      .append('svg')
      .attr('width', outerWidth)
      .attr('height', outerHeight)
      .append('g')
      .attr(
        'transform',
        'translate(' + this.margin.left + ',' + this.margin.top + ')'
      )
      .style('fill', 'none');

    // Tooltip
    const tooltipOffsetX = this.tooltipOffsetX;
    const tip = d3Tip()
      .attr('class', 'd3-tip')
      .offset([-10, 0])
      .html((event, d) => {
        // if it is a hover over an individual point, show the value
        if (d !== Object(d)) return 'Value: ' + d.toFixed(this.precision);

        // if it is a hover over a box plot, show table with basic statistic values
        const htmlTable =
          '<table><thead><th></th><th>' +
          this.boxPlotTypes.Experimental.label +
          '</th><th>' +
          this.boxPlotTypes.Base.label +
          '</th><thead>' +
          '<tr><td>Q1</td><td>' +
          d[this.yExperCat].q1.toFixed(this.precision) +
          '</td><td>' +
          d[this.yBaseCat].q1.toFixed(this.precision) +
          '</td></tr>' +
          '<tr><td>Q2</td><td>' +
          d[this.yExperCat].median.toFixed(this.precision) +
          '</td><td>' +
          d[this.yBaseCat].median.toFixed(this.precision) +
          '</td></tr>' +
          '<tr><td>Q3</td><td>' +
          d[this.yExperCat].q3.toFixed(this.precision) +
          '</td><td>' +
          d[this.yBaseCat].q3.toFixed(this.precision) +
          '</td></tr>' +
          '<tr><td>IQR</td><td>' +
          d[this.yExperCat].iqr.toFixed(this.precision) +
          '</td><td>' +
          d[this.yBaseCat].iqr.toFixed(this.precision) +
          '</td></tr>' +
          '<tr><td>MIN</td><td>' +
          d[this.yExperCat].min.toFixed(this.precision) +
          '</td><td>' +
          d[this.yBaseCat].min.toFixed(this.precision) +
          '</td></tr>' +
          '<tr><td>MAX</td><td>' +
          d[this.yExperCat].max.toFixed(this.precision) +
          '</td><td>' +
          d[this.yBaseCat].max.toFixed(this.precision) +
          '</td></tr>' +
          '</table>';
        return '<b>' + d[this.xCat] + '</b><br>' + htmlTable;
      });
    svg.call(tip);

    svg
      .append('rect')
      .attr('width', width)
      .attr('height', height)
      .style('fill', 'transparent');

    /* Setting up X-axis and Y-axis*/
    this.xScale = d3
      .scaleBand()
      .rangeRound([0, width])
      .domain(data.map(d => d.key))
      .paddingInner(1)
      .paddingOuter(0.5);

    this.yScale = d3.scaleLinear().rangeRound([height, 0]);

    const experMaxVal = d3.max(data, d => <number>d[this.yExperCat].max);
    const experMinVal = d3.min(data, d => <number>d[this.yExperCat].min);
    const baseMaxVal = d3.max(data, d => <number>d[this.yBaseCat].max);
    const baseMinVal = d3.min(data, d => <number>d[this.yBaseCat].min);
    const yMax = Math.max(baseMaxVal, experMaxVal);
    const yMin = Math.min(baseMinVal, experMinVal);
    const yRange = yMax - yMin + this.delta; // add delta to avoid bug when both max and min are zeros

    this.yScale.domain([
      yMin - yRange * this.delta,
      yMax + yRange * this.delta
    ]);

    svg
      .append('g')
      .attr('transform', 'translate(0,' + height + ')')
      .attr('class', 'x-axis')
      .call(d3.axisBottom(this.xScale))
      .selectAll('text')
      .style('text-anchor', 'end')
      .attr('dx', '-.8em')
      .attr('dy', '.15em')
      .attr('transform', 'rotate(-45)');

    svg.append('g').call(d3.axisLeft(this.yScale));

    // Box plots
    Object.keys(this.boxPlotTypes).forEach((key, i) => {
      const yCatProp = this.boxPlotTypes[key].yCat;
      const yPointsProp = this.boxPlotTypes[key].yPoints;
      const color = this.boxPlotTypes[key].color;

      // Main vertical line
      svg
        .selectAll('.vertLines')
        .data(data)
        .enter()
        .append('line')
        .attr(
          'x1',
          (d: any) =>
            this.xScale(d[this.xCat]) + (1.2 * i - 0.6) * this.boxWidth
        )
        .attr(
          'x2',
          (d: any) =>
            this.xScale(d[this.xCat]) + (1.2 * i - 0.6) * this.boxWidth
        )
        .attr('y1', (d: any) => this.yScale(d[yCatProp].min))
        .attr('y2', (d: any) => this.yScale(d[yCatProp].max))
        .attr('stroke', 'black')
        .style('width', 10);

      svg
        .selectAll('.boxes')
        .data(data)
        .enter()
        .append('rect')
        .attr(
          'x',
          d => this.xScale(d[this.xCat]) + (1.2 * i - 1.1) * this.boxWidth
        )
        .attr('y', d => this.yScale(d[yCatProp].q3))
        .attr(
          'height',
          d => this.yScale(d[yCatProp].q1) - this.yScale(d[yCatProp].q3)
        )
        .attr('width', this.boxWidth)
        .attr('stroke', 'black')
        .style('fill', color)
        .attr('pointer-events', 'all')
        .on('mouseover', function(mouseEvent: any, d) {
          tip.show(mouseEvent, d, this);
          tip.style('left', mouseEvent.x + tooltipOffsetX + 'px');
        })
        .on('mouseout', tip.hide);

      // Medians
      svg
        .selectAll('.medianLines')
        .data(data)
        .enter()
        .append('line')
        .attr(
          'x1',
          d => this.xScale(d[this.xCat]) + (1.2 * i - 1.1) * this.boxWidth
        )
        .attr(
          'x2',
          d => this.xScale(d[this.xCat]) + (1.2 * i - 0.1) * this.boxWidth
        )
        .attr('y1', d => this.yScale(d[yCatProp].median))
        .attr('y2', d => this.yScale(d[yCatProp].median))
        .attr('stroke', 'black')
        .style('width', 80);

      // Add individual points with jitter
      svg
        .selectAll('genePoints')
        .data(data)
        .enter()
        .each((d, ix, nodes) => {
          d3.select(nodes[ix])
            .selectAll('.individualPoints')
            .data(d[yPointsProp])
            .enter()
            .append('circle')
            .attr(
              'cx',
              this.xScale(data[ix][this.xCat]) +
                (1.2 * i - 0.6) * this.boxWidth -
                this.jitterWidth / 2 +
                Math.random() * this.jitterWidth
            )
            .attr('cy', d => this.yScale(d))
            .attr('r', 3)
            .style('fill', color)
            .attr('stroke', '#000000')
            .attr('pointer-events', 'all')
            .on('mouseover', function(mouseEvent: any, d) {
              tip.show(mouseEvent, d, this);
              tip.style('left', mouseEvent.x + tooltipOffsetX + 'px');
            })
            .on('mouseout', tip.hide);
        });

      // Legend
      const boxPlotColors = Object.keys(this.boxPlotTypes).map(key => ({
        label: this.boxPlotTypes[key].label,
        color: this.boxPlotTypes[key].color
      }));
      const legend = svg
        .selectAll('.legend')
        .data(boxPlotColors)
        .enter()
        .append('g')
        .classed('legend', true)
        .attr('transform', function(d, i) {
          return 'translate(0,' + i * 20 + ')';
        });

      legend
        .append('circle')
        .attr('r', 5)
        .attr('cx', width + 20)
        .attr('fill', d => d.color);

      legend
        .append('text')
        .attr('x', width + 30)
        .attr('dy', '.35em')
        .style('fill', '#000')
        .attr('class', 'legend-label')
        .text(d => d.label);
    });
  }

  /**
   * Function to load features by filters, pages and sorting settings specified by a user
   */
  loadFeaturesPage() {
    const formValues = this.filterForm.value; // i.e. {name: "asdfgh", pvalue: 3, pvalue_operator: "lte", log2FoldChange: 2, log2FoldChange_operator: "lte"}
    const paramFilter = {}; // has values {'log2FoldChange': '[absgt]:2'};
    for (const key in this.allowedFilters) {
      if (
        formValues.hasOwnProperty(key) &&
        formValues[key] !== '' &&
        formValues[key] !== null
      ) {
        if (formValues.hasOwnProperty(key + '_operator')) {
          paramFilter[key] =
            '[' + formValues[key + '_operator'] + ']:' + formValues[key];
        } else {
          paramFilter[key] = '[eq]:' + formValues[key];
        }
      }
    }

    const sorting = {
      sortField: this.sort.active,
      sortDirection: this.sort.direction
    };

    this.dataSource.loadFeatures(
      this.dgeResourceId,
      paramFilter,
      sorting,
      this.paginator.pageIndex,
      this.paginator.pageSize
    );
  }
}

export interface DESeqFeature {
  name: string;
  overall_mean: number;
  Control: number;
  Experimental: number;
  log2FoldChange: number;
  lfcSE: number;
  stat: number;
  pvalue: number;
  padj: number;
}

export class FeaturesDataSource implements DataSource<DESeqFeature> {
  public featuresSubject = new BehaviorSubject<DESeqFeature[]>([]);
  public featuresCount = 0;
  private loadingSubject = new BehaviorSubject<boolean>(false);
  public loading$ = this.loadingSubject.asObservable();

  constructor(private analysesService: AnalysesService) {}

  loadFeatures(
    resourceId: string,
    filterValues: object,
    sorting: object,
    pageIndex: number,
    pageSize: number
  ) {
    this.loadingSubject.next(true);

    this.analysesService
      .getResourceContent(
        resourceId,
        pageIndex + 1,
        pageSize,
        filterValues,
        sorting
      )
      .pipe(finalize(() => this.loadingSubject.next(false)))
      .subscribe(features => {
        this.featuresCount = features.count;
        const featuresFormatted = features.results.map(feature => {
          const newFeature = { name: feature.rowname, ...feature.values };
          return newFeature;
        });
        return this.featuresSubject.next(featuresFormatted);
      });
  }

  connect(): Observable<DESeqFeature[]> {
    return this.featuresSubject.asObservable();
  }

  disconnect(): void {
    this.featuresSubject.complete();
    this.loadingSubject.complete();
  }
}

result-matching ""

    No results matching ""