All Downloads are FREE. Search and download functionalities are using the official Maven repository.

rs.scripts.diagram-viewer.0.9.2.source-code.dependsGraph.renderer.ts Maven / Gradle / Ivy

// SPDX-License-Identifier: Apache-2.0
// Copyright 2019 XLRIT (https://www.xlrit.com/)
import $          from 'jquery'
import * as joint from 'jointjs'
import dagre      from 'dagre'
import graphlib   from 'graphlib'

import * as util  from './util'
import * as dg    from './dependsGraph.shapes'

interface IDependsGraph {
  steps: Array
  deps: Array
}

interface IStep {
  id:        string
  label:     string
  highlight: boolean
}

interface IDep {
  source:    string
  target:    string
  kind:      string
  detail:    string
  reduced:   boolean
  highlight: boolean
}

// from https://www.graphviz.org/doc/info/colors.html - paired12
const componentColors = [
  '#e31a1c', '#a6cee3', '#1f78b4', '#b2df8a',
  '#33a02c', '#fb9a99', '#fdbf6f', '#ff7f00',
  '#cab2d6', '#6a3d9a', '#ffff99', '#b15928']

export default class DependsGraphRenderer {
  initialized: boolean = false
  linkColorizer: (link: joint.dia.Link) => string = _l => 'black'
  readonly dependsGraph: IDependsGraph
  readonly graph: joint.dia.Graph
  readonly paper: joint.dia.Paper
  readonly stepShapes: dg.StepShape[]
  readonly depShapes: dg.DepShape[]

  constructor($paper: JQuery, dependsGraph: IDependsGraph) {
    this.dependsGraph = dependsGraph;
    this.graph = new joint.dia.Graph
    this.paper = new joint.dia.Paper({
      el: $paper,
      model: this.graph,
      width: 300,
      height: 200,
      gridSize: 10,
      drawGrid: false,
      background: {
        color: 'rgba(255, 253, 235, 0.5)'
      },
    })
    this.stepShapes = this.dependsGraph.steps.map(this.createStepShape)
    this.depShapes = this.dependsGraph.deps.map(this.createDepShape)

    const $toolbar = $('
').appendTo($paper); this.addToggleButton($toolbar, 'Reduced', state => this.toggleReduced(state)) this.addToggleButton($toolbar, 'Elided', state => this.toggleElided(state)) this.addCommandButton($toolbar, 'Layout', () => this.layout()) this.addCommandButton($toolbar, 'Cycles', () => { this.refreshComponents(); this.setElementColors(); }) // log link info on click this.paper.on('link:pointerclick', (linkView: joint.dia.LinkView, evt, x, y) => { const linkModel: any = linkView.model console.log(linkModel.data) }) this.paper.on('link:pointerdblclick', (linkView: joint.dia.LinkView, evt) => { this.greyoutStyle() const currentLink = linkView.model this.resetLinkStyle(currentLink, true) this.resetNodeStyle(this.getElement(currentLink.source()), false) this.resetNodeStyle(this.getElement(currentLink.target()), false) }) this.paper.on('element:pointerdblclick', (elementView: joint.dia.ElementView, evt) => { this.greyoutStyle() const currentElement = elementView.model this.resetNodeStyle(currentElement, true) }) this.paper.on('blank:pointerclick', (_evt) => this.resetStyle()) $paper.on('diagram:toggle', (_evt: any, state: boolean) => { this.updateVisibility(state) }) } private addCommandButton($toolbar: JQuery, label: string, action: () => any): void { $(``) .appendTo($toolbar) .on('click', _evt => action()) } private addToggleButton($toolbar: JQuery, label: string, update: (state: boolean) => any): void { const $button = $(``).appendTo($toolbar) let state = false $button.on('click', _evt => { state = !state $button.toggleClass('on', state) update(state) }) } private createStepShape(step: IStep) { const label = step.label.replace(/: /g, ':\n') const isInput = label.includes('Input from') return new dg.StepShape(step.id) .set('shown', true) .attr('body/fill', isInput ? '#D6B700' : '#009ED0') .attr('body/strokeWidth', step.highlight ? '3' : '0.5') .attr('label/text', label) .attr('step', step) } private createDepShape(dep: IDep) { const reduced = dep.reduced const elided = dep.kind.includes('Elided') const shown = !reduced && !elided return new dg.DepShape(dep) .source({ id: dep.source }) .target({ id: dep.target }) .set('kind', dep.kind) .set('reduced', reduced) .set('elided', elided) .set('shown', shown) .attr('line/strokeWidth', dep.highlight ? 3 : 1) .attr('line/strokeDasharray', shown ? null : '2') //.attr('line/visibility', hide ? 'hidden' : 'visible') } private resetNodeStyle(node: joint.dia.Element, reach: boolean) { this.resetElementStyle(node) const component = this.getComponent(node) if (component.length > 1) this.resetComponentStyle(component) else reach ? this.resetNeighborsStyle(node) : this.resetElementStyle(node) } private resetNeighborsStyle(element: joint.dia.Element) { this.graph.getNeighbors(element).forEach(this.resetElementStyle) this.graph.getConnectedLinks(element, { inbound: true, outbound: true }).forEach(li => this.resetLinkStyle(li, true)) } private resetComponentStyle(component: joint.dia.Element[]) { component.forEach(el => { this.resetElementStyle(el) this.graph.getConnectedLinks(el).forEach(li => { const sourceComponent = this.getElement(li.source()).attr('component') const targetComponent = this.getElement(li.target()).attr('component') if (sourceComponent == targetComponent) this.resetLinkStyle(li, true) }) //this.graph.getNeighbors(el).forEach(this.resetElementStyle) }) } private getComponent(element: joint.dia.Element): joint.dia.Element[] { const componentNeighbors = this.graph.getNeighbors(element).filter(el => el.attr('component') === element.attr('component')) const component: joint.dia.Element[] = [element] while (true) { const current = componentNeighbors.pop() if (current === undefined) break; component.push(current) for (var compneigh of this.graph.getNeighbors(current).filter(el => el.attr('component') === element.attr('component') && !component.includes(el))) { componentNeighbors.push(compneigh) } } return component } private isInput(element: joint.dia.Element) { return element.attr('label/text').includes('Input from') } private resetElementStyle(element: joint.dia.Element) { element.attr('body/stroke', 'black'); element.attr('body/fill', element.attr('originalColor')) element.attr('body/filter', dg.dropShadow) element.attr('label/fill', 'white') } private resetLinkStyle(link: joint.dia.Link, highlight: boolean = false) { link.attr('line/stroke', this.linkColorizer(link)) link.attr('line/strokeWidth', highlight ? 2.5 : 1) } private greyoutStyle() { this.paper.model.getElements().forEach(el => { el.attr('body/stroke', '#8888887f') el.attr('body/fill', '#cfcfcf7f') el.attr('label/fill', '#8484847f') el.removeAttr('body/filter') }) this.paper.model.getLinks().forEach(li => { li.attr('line/stroke', '#888888') li.attr('line/strokeWidth', 1) }) } private resetStyle() { this.paper.model.getElements().forEach(this.resetElementStyle) this.paper.model.getLinks().forEach(li => this.resetLinkStyle(li)) } private updateVisibility(state: boolean) { this.paper.$el.toggleClass('hidden', !state) if (!this.initialized && state) { this.initialize() } } async initialize() { if (this.initialized) { console.warn('Already initialized: {}', this) return } console.log('Initializing depends graph with %d nodes and %d edges...', this.dependsGraph.steps.length, this.dependsGraph.deps.length) util.time('depends graph addShownCells', () => this.addShownCells()) util.time('depends graph refreshComponents', () => this.refreshComponents()) util.time('depends graph setElementColors', () => this.setElementColors()) util.time('depends graph layout', () => this.layout()) this.initialized = true } private addShownCells() { const shownStepShapes = this.stepShapes.filter(stepShape => stepShape.get('shown')) this.graph.addCells(shownStepShapes) shownStepShapes.forEach(shape => util.autoSize(shape, this.paper)) const shownDepShapes = this.depShapes.filter(depShape => depShape.get('shown')) this.graph.addCells(shownDepShapes) } private toggleReduced(show: boolean) { const reducedDepShapes = this.depShapes.filter(depShape => depShape.get('reduced')) if (show) this.graph.addCells(reducedDepShapes) else this.graph.removeCells(reducedDepShapes) } private toggleElided(show: boolean) { const elidedDepShapes = this.depShapes.filter(depShape => depShape.get('elided')) if (show) this.graph.addCells(elidedDepShapes) else this.graph.removeCells(elidedDepShapes) } private refreshComponents() { const components = util.tarjan(this.graph) components.sort((a, b) => b.length - a.length) components.forEach((component, index) => { component.forEach(element => element.attr('component', index)) }) const getLinkComponentColor = (link: joint.dia.Link) => { const sourceComp = this.getElement(link.source()).attr('component') const targetComp = this.getElement(link.target()).attr('component') return (sourceComp == targetComp ? componentColors[sourceComp % componentColors.length] : 'black') } const getLinkKindColor = (link: joint.dia.Link) => { switch (link.attr('kind')) { case 'Step': return 'Blue'; case 'Wrapper': return 'Gray'; case 'Multiple': return 'DarkGreen'; case 'After': return 'Purple'; default: return 'Black'; } } const hasCycle = components.some(component => component.length > 1) this.linkColorizer = hasCycle ? getLinkComponentColor : getLinkKindColor this.graph.getLinks().forEach(link => { link.attr('line/stroke', this.linkColorizer(link)) }) } private setElementColors() { this.graph.getElements().forEach(element => { const component = element.attr('component') const neighborComponents = this.graph.getNeighbors(element).map(neighbor => neighbor.attr('component')) const isInCycle = neighborComponents.includes(component) const color = isInCycle ? componentColors[component % componentColors.length] : (this.isInput(element) ? '#D6B700' : '#009ED0') element.attr('body/fill', color) element.attr('originalColor', color) }) } private layout() { const bbox = joint.layout.DirectedGraph.layout(this.graph, { dagre, graphlib, nodeSep: 50, edgeSep: 10, rankDir: 'TB', }) const margin = 100 this.paper.setDimensions(bbox.width + margin * 2, bbox.height + margin * 2) this.paper.translate(margin, margin) } private getElement(arg: joint.dia.Link.EndJSON): joint.dia.Element { return this.graph.getCell(arg.id!) } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy