File

projects/web-mev/src/app/features/analysis/components/analysis-flow/analysis-flow.component.ts

Description

Analysis Flow Component. Used to display the visual representation of all performed analyses as DAG graph

Implements

OnInit

Metadata

changeDetection ChangeDetectionStrategy.Default
selector mev-analysis-flow
styleUrls ./analysis-flow.component.scss
templateUrl ./analysis-flow.component.html

Index

Properties
Methods
Outputs

Constructor

constructor(route: ActivatedRoute, analysesService: AnalysesService, workspaceDetailService: WorkspaceDetailService, dialog: MatDialog)
Parameters :
Name Type Optional
route ActivatedRoute No
analysesService AnalysesService No
workspaceDetailService WorkspaceDetailService No
dialog MatDialog No

Outputs

executedOperationId
Type : EventEmitter<any>

Methods

buildDAG
buildDAG(data)

Function to render DAG (Directed Acyclic Graph)

Parameters :
Name Optional
data No
Returns : void
ngOnInit
ngOnInit()
Returns : void
onNodeClick
onNodeClick(d)

Function is triggered when a tree node is clicked. Clicking on operation nodes redirects to the Analysis Result. Clicking on Resource nodes opens the Preview file pop-up.

Parameters :
Name Optional
d No
Returns : void
previewItem
previewItem(resourceId)

Function is used to preview the content of the workspace resource (file)

Parameters :
Name Optional
resourceId No
Returns : void
saveAnalysisHistory
saveAnalysisHistory()

Function performs a ‘workspace export’ which takes the graph and make some ‘full record’ including the operation versions, etc.

Returns : void

Properties

containerId
Type : string
Default value : '#dagPlot'
Public dialog
Type : MatDialog
isWait
Default value : false
margin
Type : object
Default value : { top: 50, right: 50, bottom: 50, left: 50 }
maxTextLabelLength
Type : number
Default value : 13
noDataIsAvailable
Default value : false
nodeSize
Type : number
Default value : 30
nodeTypes
Type : []
Default value : [ // settings for DAG nodes { id: 'data_resource_node', label: 'File', img: '../../../../assets/file.png' }, { id: 'op_node', label: 'Operation', img: '../../../../assets/gear.png' } ]
tooltipOffsetX
Type : number
Default value : 10
import {
  Component,
  ChangeDetectionStrategy,
  OnInit,
  EventEmitter,
  Output
} from '@angular/core';
import * as d3 from 'd3';
import * as d3dag from 'd3-dag';
import d3Tip from 'd3-tip';
import { AnalysesService } from '../../services/analysis.service';
import { ActivatedRoute } from '@angular/router';
import { MatDialog } from '@angular/material/dialog';
import { PreviewDialogComponent } from '@app/features/workspace-detail/components/dialogs/preview-dialog/preview-dialog.component';
import { WorkspaceDetailService } from '@app/features/workspace-detail/services/workspace-detail.service';

/**
 * Analysis Flow Component.
 * Used to display the visual representation of all performed analyses as DAG graph
 */
@Component({
  selector: 'mev-analysis-flow',
  templateUrl: './analysis-flow.component.html',
  styleUrls: ['./analysis-flow.component.scss'],
  changeDetection: ChangeDetectionStrategy.Default
})
export class AnalysisFlowComponent implements OnInit {
  @Output() executedOperationId: EventEmitter<any> = new EventEmitter<any>();

  noDataIsAvailable = false;
  isWait = false; // to control spinner

  /* DAG Chart settings */
  containerId = '#dagPlot';
  tooltipOffsetX = 10; // position the tooltip on the right side of the triggering element
  margin = { top: 50, right: 50, bottom: 50, left: 50 }; // chart margins
  nodeSize = 30; // tree node size
  maxTextLabelLength = 13;
  nodeTypes = [
    // settings for DAG nodes
    {
      id: 'data_resource_node',
      label: 'File',
      img: '../../../../assets/file.png'
    },
    { id: 'op_node', label: 'Operation', img: '../../../../assets/gear.png' }
  ];

  constructor(
    private route: ActivatedRoute,
    private analysesService: AnalysesService,
    private workspaceDetailService: WorkspaceDetailService,
    public dialog: MatDialog
  ) {}

  ngOnInit(): void {
    const workspaceId = this.route.snapshot.paramMap.get('workspaceId');
    this.analysesService.getExecOperationDAG(workspaceId).subscribe(data => {
      this.buildDAG(data);
    });
  }

  /**
   * Function to render DAG (Directed Acyclic Graph)
   */
  buildDAG(data): void {
    if (!data || data.length === 0) {
      this.noDataIsAvailable = true;
      return;
    }

    const dag = d3dag.dagStratify()(data);
    const layout = d3dag.sugiyama()(dag);
    const scale = x => 100 * x;

    // use computed layout and get size
    const width = scale(layout.width);
    const height = scale(layout.height);

    d3.select(this.containerId)
      .selectAll('svg')
      .remove();

    const svgSelection = d3
      .select(this.containerId)
      .append('svg')
      .attr('width', width + this.margin.right + this.margin.left)
      .attr('height', height + this.margin.top + this.margin.bottom);

    const defs = svgSelection.append('defs');

    this.nodeTypes.forEach(d => {
      defs
        .append('svg:pattern')
        .attr('id', d.id)
        .attr('width', this.nodeSize)
        .attr('height', this.nodeSize)
        .append('svg:image')
        .attr('href', d.img)
        .attr('width', this.nodeSize)
        .attr('height', this.nodeSize)
        .attr('x', this.nodeSize / 2)
        .attr('y', this.nodeSize / 2);
    });

    // Draw edges
    const line = d3
      .line()
      .curve(d3.curveCatmullRom)
      .x(d => d['x'])
      .y(d => d['y']);

    // Plot edges
    svgSelection
      .append('g')
      .selectAll('path')
      .data(dag.links())
      .enter()
      .append('path')
      .attr('d', ({ points }: any) => {
        points = points.map(el => ({ x: scale(el.x), y: scale(el.y) }));
        return line(points);
      })
      .attr('fill', 'none')
      .attr('stroke-width', 2)
      .attr('stroke', '#ccc');

    // Select nodes
    const nodes = svgSelection
      .append('g')
      .selectAll('g')
      .data(dag.descendants())
      .enter()
      .append('g')
      .attr(
        'transform',
        ({ x, y }: any) => `translate(${scale(x)}, ${scale(y)})`
      );

    // Tooltip
    const tooltipOffsetX = this.tooltipOffsetX;
    const tip = d3Tip()
      .attr('class', 'd3-tip')
      .offset([-10, 0])
      .html((event, d) => {
        const name = d.data.node_name;
        const typeLabel = this.nodeTypes.find(
          type => type.id === d.data.node_type
        ).label;
        return typeLabel + ': ' + name;
      });
    svgSelection.call(tip);

    // Plot node circles
    nodes
      .append('circle')
      .attr('r', this.nodeSize)
      .attr('fill', d => {
        return 'url(#' + d.data['node_type'] + ')';
      })
      .attr('class', 'circle')
      .on('mouseover', function(mouseEvent: any, d) {
        tip.show(mouseEvent, d, this);
        tip.style('left', mouseEvent.x + tooltipOffsetX + 'px');
      })
      .on('mouseout', tip.hide)
      .on('click', (event, d) => {
        tip.hide();
        this.onNodeClick(d);
      });

    // Add text to root nodes
    const truncate = input =>
      input.length > this.maxTextLabelLength
        ? `${input.substring(0, this.maxTextLabelLength)}...`
        : input;

    nodes
      .append('text')
      .filter(d => d.data['parentIds'].length === 0)
      .text(d => truncate(d.data['node_name']))

      .attr('dx', '-3em')
      .attr('dy', '-2em')
      .attr('class', 'text-label');
  }

  /**
   * Function is triggered when a tree node is clicked. Clicking on operation nodes
   * redirects to the Analysis Result. Clicking on Resource nodes opens the Preview file pop-up.
   */
  onNodeClick(d) {
    if (d.data.node_type === 'op_node') {
      this.executedOperationId.emit(d.data.id);
    } else {
      this.previewItem(d.data.id);
    }
  }

  /**
   * Function is used to preview the content of the workspace resource (file)
   */
  previewItem(resourceId) {
    this.isWait = true;
    this.workspaceDetailService
      .getResourcePreview(resourceId)
      .subscribe(data => {
        const previewData = {};
        if (data?.results?.length && 'rowname' in data.results[0]) {
          const minN = Math.min(data.results.length, 10);
          const slicedData = data.results.slice(0, minN);
          const columns = Object.keys(slicedData[0].values);
          const rows = slicedData.map(elem => elem.rowname);
          const values = slicedData.map(elem => {
            const rowValues = [];
            const elemValues = elem.values;
            columns.forEach(col => rowValues.push(elemValues[col]));
            return rowValues;
          });
          previewData['columns'] = columns;
          previewData['rows'] = rows;
          previewData['values'] = values;
        }

        setTimeout(() => {
          this.isWait = false;
          this.dialog.open(PreviewDialogComponent, {
            data: {
              previewData: previewData
            }
          });
        }, 1000);
      });
  }

  /**
   * Function performs a ‘workspace export’ which takes the graph
   * and make some ‘full record’ including the operation versions, etc.
   */
  saveAnalysisHistory() {}
}
<mev-spinner-overlay *ngIf="isWait"></mev-spinner-overlay>

<section class="analysis-container">
    <mat-card class="analysis-card">
        <mat-card-content class="analysis-card__main">
            <p class="analysis-card__instruction">
                Below is a visual representation of your analysis. 
                You can view which analyses were run and with which files so that analyses can be fully reproduced.
            </p>
            <mat-divider [inset]="true"></mat-divider>
            <div class="analysis-card__content">
                <div class="btn-group">
                    <button mat-raised-button color="accent" (click)="saveAnalysisHistory()" disabled=true>
                      <mat-icon aria-label="Export analysis history">save_alt</mat-icon>
                      Export analysis history
                    </button>
                </div>    
                <div *ngIf="noDataIsAvailable">No data is available</div>
                <div class="chart" #dagPlot id="dagPlot"></div>
            </div>
        </mat-card-content>
    </mat-card>
</section>

./analysis-flow.component.scss

.analysis-container {
  margin: 20px;
  width: 98%;
}

.btn-group {
  padding: 15px 0;
}

.mat-card {
  overflow-x: scroll;
}

::ng-deep {
  .d3-tip {
    line-height: 1;
    font-weight: bold;
    padding: 12px;
    background: rgba(0, 0, 0, 0.8);
    color: #fff;
    border-radius: 2px;
    z-index: 1;
  }

  .circle {
    cursor: pointer;
  }

  .text-label {
    font-weight: bold;
  }
}
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""