Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
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 {
$(`${label} `)
.appendTo($toolbar)
.on('click', _evt => action())
}
private addToggleButton($toolbar: JQuery, label: string, update: (state: boolean) => any): void {
const $button = $(`${label} `).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!)
}
}