Skip to content
This repository has been archived by the owner on Jan 21, 2024. It is now read-only.

Commit

Permalink
Implemented graph filtering
Browse files Browse the repository at this point in the history
  • Loading branch information
spoenemann committed May 17, 2018
1 parent b55fa77 commit d148eb8
Show file tree
Hide file tree
Showing 9 changed files with 237 additions and 41 deletions.
98 changes: 98 additions & 0 deletions depgraph-navigator/src/browser/graph/graph-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright (C) 2018 TypeFox
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*/

import { injectable } from "inversify";
import { SGraphSchema, SModelIndex, SModelElementSchema } from "sprotty/lib";
import { DependencyGraphNodeSchema, DependencyGraphEdgeSchema, isNode, isEdge } from "./graph-model";

@injectable()
export class DependencyGraphFilter {

protected nameFilter: (name: string) => boolean = node => true;

setFilter(text: string) {
const textTrim = text.trim();
if (textTrim.length === 0)
this.nameFilter = name => true;
else if (text.startsWith(' ') && text.endsWith(' '))
this.nameFilter = name => name === textTrim;
else if (text.startsWith(' '))
this.nameFilter = name => name.startsWith(textTrim);
else if (text.endsWith(' '))
this.nameFilter = name => name.endsWith(textTrim);
else
this.nameFilter = name => name.indexOf(textTrim) >= 0;
}

refresh(graph: SGraphSchema, index: SModelIndex<SModelElementSchema>): void {
let nodeCount = 0;
let visibleCount = 0;

// Count the nodes and apply the name filter
for (const element of graph.children) {
if (isNode(element)) {
const visible = this.nameFilter(element.name);
element.hidden = !visible;
nodeCount++;
if (visible)
visibleCount++;
}
}
if (visibleCount === nodeCount)
return;

// Construct a map of incoming edges
const incoming = this.createIncomingMap(graph, index);
const dfsMark: { [id: string]: boolean } = {};

// Perform a depth-first-search to find the nodes from which the name-filtered nodes are reachable
for (const element of graph.children) {
if (isNode(element) && !element.hidden) {
this.dfs(element, incoming, dfsMark, index);
}
}
}

protected createIncomingMap(graph: SGraphSchema, index: SModelIndex<SModelElementSchema>):
Map<DependencyGraphNodeSchema, DependencyGraphEdgeSchema[]> {
const incoming = new Map<DependencyGraphNodeSchema, DependencyGraphEdgeSchema[]>();
for (const element of graph.children) {
if (isEdge(element)) {
const target = index.getById(element.targetId);
if (isNode(target)) {
let arr = incoming.get(target);
if (arr) {
arr.push(element);
} else {
arr = [element];
incoming.set(target, arr);
}
}
}
}
return incoming;
}

protected dfs(node: DependencyGraphNodeSchema,
incoming: Map<DependencyGraphNodeSchema, DependencyGraphEdgeSchema[]>,
mark: { [id: string]: boolean },
index: SModelIndex<SModelElementSchema>): void {
if (mark[node.id])
return;
mark[node.id] = true;
for (const edge of incoming.get(node) || []) {
const source = index.getById(edge.sourceId);
if (isNode(source)) {
source.hidden = false;
this.dfs(source, incoming, mark, index);
}
}
}

}
43 changes: 35 additions & 8 deletions depgraph-navigator/src/browser/graph/graph-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
SGraphSchema, SModelIndex, SModelElementSchema, SNodeSchema, SShapeElementSchema, SEdgeSchema,
SLabelSchema, Point
} from 'sprotty/lib';
import { isNode } from './graph-model';

export type ElkFactory = () => ELK;

Expand All @@ -31,28 +32,38 @@ export class ElkGraphLayout {
}

layout(graph: SGraphSchema, index: SModelIndex<SModelElementSchema>): Promise<void> {
const elkGraph = this.transformToElk(graph) as ElkNode;
const elkGraph = this.transformToElk(graph, index) as ElkNode;
return this.elk.layout(elkGraph).then(result => this.applyLayout(result, index));
}

protected transformToElk(smodel: SModelElementSchema): ElkGraphElement {
protected transformToElk(smodel: SModelElementSchema, index: SModelIndex<SModelElementSchema>): ElkGraphElement {
switch (smodel.type) {
case 'graph': {
const sgraph = smodel as SGraphSchema;
return <ElkNode>{
id: sgraph.id,
layoutOptions: this.graphOptions(sgraph),
children: sgraph.children.filter(c => c.type === 'node').map(c => this.transformToElk(c)) as ElkNode[],
edges: sgraph.children.filter(c => c.type === 'edge').map(c => this.transformToElk(c)) as ElkEdge[]
children: sgraph.children
.filter(c => c.type === 'node' && this.filterNode(c as SNodeSchema))
.map(c => this.transformToElk(c, index)) as ElkNode[],
edges: sgraph.children
.filter(c => c.type === 'edge' && this.filterEdge(c as SEdgeSchema, index))
.map(c => this.transformToElk(c, index)) as ElkEdge[]
};
}
case 'node': {
const snode = smodel as SNodeSchema;
const elkNode: ElkNode = { id: snode.id };
if (snode.children) {
elkNode.children = snode.children.filter(c => c.type === 'node').map(c => this.transformToElk(c)) as ElkNode[];
elkNode.edges = snode.children.filter(c => c.type === 'edge').map(c => this.transformToElk(c)) as ElkEdge[];
elkNode.labels = snode.children.filter(c => c.type === 'label').map(c => this.transformToElk(c)) as ElkLabel[];
elkNode.children = snode.children
.filter(c => c.type === 'node' && this.filterNode(c as SNodeSchema))
.map(c => this.transformToElk(c, index)) as ElkNode[];
elkNode.edges = snode.children
.filter(c => c.type === 'edge' && this.filterEdge(c as SEdgeSchema, index))
.map(c => this.transformToElk(c, index)) as ElkEdge[];
elkNode.labels = snode.children
.filter(c => c.type === 'label')
.map(c => this.transformToElk(c, index)) as ElkLabel[];
}
this.transformShape(elkNode, snode);
return elkNode;
Expand All @@ -65,7 +76,9 @@ export class ElkGraphLayout {
target: sedge.targetId
};
if (sedge.children) {
elkEdge.labels = sedge.children.filter(c => c.type === 'label').map(c => this.transformToElk(c)) as ElkLabel[];
elkEdge.labels = sedge.children
.filter(c => c.type === 'label')
.map(c => this.transformToElk(c, index)) as ElkLabel[];
}
const points = sedge.routingPoints;
if (points && points.length >= 2) {
Expand All @@ -86,6 +99,20 @@ export class ElkGraphLayout {
}
}

protected filterNode(node: SNodeSchema): boolean {
return !(node as any).hidden;
}

protected filterEdge(edge: SEdgeSchema, index: SModelIndex<SModelElementSchema>): boolean {
const source = index.getById(edge.sourceId);
if (!source || isNode(source) && !this.filterNode(source))
return false;
const target = index.getById(edge.targetId);
if (!target || isNode(target) && !this.filterNode(target))
return false;
return true;
}

protected graphOptions(sgraph: SGraphSchema): LayoutOptions {
return {
'elk.algorithm': 'layered',
Expand Down
14 changes: 13 additions & 1 deletion depgraph-navigator/src/browser/graph/graph-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,29 @@
* http://www.apache.org/licenses/LICENSE-2.0
*/

import { RectangularNode, moveFeature, SEdge, editFeature, SNodeSchema, SEdgeSchema } from "sprotty/lib";
import {
RectangularNode, moveFeature, SEdge, editFeature, SNodeSchema, SEdgeSchema, SModelElementSchema
} from "sprotty/lib";

export interface DependencyGraphNodeSchema extends SNodeSchema {
name: string
versions: string[]
resolved?: boolean
hidden?: boolean
description?: string
url?: string
error?: string
}

export function isNode(element?: SModelElementSchema): element is DependencyGraphNodeSchema {
return element !== undefined && element.type === 'node';
}

export class DependencyGraphNode extends RectangularNode {
name: string = '';
versions: string[] = [];
resolved: boolean = false;
hidden: boolean = false;
description?: string;
url?: string;
error?: string;
Expand All @@ -38,6 +46,10 @@ export interface DependencyGraphEdgeSchema extends SEdgeSchema {
optional?: boolean
}

export function isEdge(element?: SModelElementSchema): element is DependencyGraphEdgeSchema {
return element !== undefined && element.type === 'edge';
}

export class DependencyGraphEdge extends SEdge {
optional: boolean = false;

Expand Down
2 changes: 2 additions & 0 deletions depgraph-navigator/src/browser/graph/graph-sprotty-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { ResolveNodesHandler } from './resolve-nodes';
import { DependencyNodeView, DependencyEdgeView } from './graph-views';
import { popupModelFactory } from './popup-info';
import { ElkGraphLayout, ElkFactory } from './graph-layout';
import { DependencyGraphFilter } from './graph-filter';

export interface ContainerFactoryArguments {
elkFactory: ElkFactory
Expand All @@ -31,6 +32,7 @@ export interface ContainerFactoryArguments {

export default (args: ContainerFactoryArguments) => {
const depGraphModule = new ContainerModule((bind, unbind, isBound, rebind) => {
bind(DependencyGraphFilter).toSelf();
bind(ResolveNodesHandler).toSelf();
bind(ElkFactory).toConstantValue(args.elkFactory);
bind(ElkGraphLayout).toSelf();
Expand Down
8 changes: 8 additions & 0 deletions depgraph-navigator/src/browser/graph/graph-views.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export class DependencyNodeView implements IView {
cornerRadius = 5;

render(node: Readonly<DependencyGraphNode>, context: RenderingContext): VNode {
if (node.hidden) {
return <g class-depgraph-hidden={true}></g>
}

return <g>
<rect class-sprotty-node={true}
class-mouseover={node.hoverFeedback} class-selected={node.selected}
Expand All @@ -31,6 +35,10 @@ export class DependencyNodeView implements IView {

export class DependencyEdgeView extends PolylineEdgeView {
render(edge: Readonly<DependencyGraphEdge>, context: RenderingContext): VNode {
if ((edge.source as any).hidden || (edge.target as any).hidden) {
return <g class-depgraph-hidden={true}></g>
}

const route = edge.route();
if (route.length === 0)
return this.renderDanglingEdge("Cannot compute route", edge, context);
Expand Down
47 changes: 34 additions & 13 deletions depgraph-navigator/src/browser/graph/model-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import { injectable, inject, optional } from "inversify";
import {
LocalModelSource, ComputedBoundsAction, TYPES, IActionDispatcher, ActionHandlerRegistry, ViewerOptions,
PopupModelFactory, IStateAwareModelProvider, SGraphSchema, ILogger, SelectAction, FitToScreenAction
PopupModelFactory, IStateAwareModelProvider, SGraphSchema, ILogger, SelectAction, FitToScreenAction,
SelectAllAction
} from "sprotty/lib";
import { IGraphGenerator } from "./graph-generator";
import { ElkGraphLayout } from "./graph-layout";
import { DependencyGraphNodeSchema } from "./graph-model";
import { DependencyGraphNodeSchema, isNode } from "./graph-model";
import { DependencyGraphFilter } from "./graph-filter";

@injectable()
export class DepGraphModelSource extends LocalModelSource {
Expand All @@ -28,6 +30,7 @@ export class DepGraphModelSource extends LocalModelSource {
@inject(TYPES.ActionHandlerRegistry) actionHandlerRegistry: ActionHandlerRegistry,
@inject(TYPES.ViewerOptions) viewerOptions: ViewerOptions,
@inject(IGraphGenerator) public readonly graphGenerator: IGraphGenerator,
@inject(DependencyGraphFilter) protected readonly graphFilter: DependencyGraphFilter,
@inject(ElkGraphLayout) protected readonly elk: ElkGraphLayout,
@inject(TYPES.ILogger) protected readonly logger: ILogger,
@inject(TYPES.PopupModelFactory)@optional() popupModelFactory?: PopupModelFactory,
Expand All @@ -53,23 +56,33 @@ export class DepGraphModelSource extends LocalModelSource {
};
}

start(): void {
this.setModel(this.graphGenerator.graph);
}

select(elementIds: string[]): void {
this.actionDispatcher.dispatch(new SelectAction(elementIds));
this.actionDispatcher.dispatch(new SelectAction(elementIds.filter(id => {
const element = this.graphGenerator.index.getById(id);
return isNode(element) && !element.hidden;
})));
}

selectAfterUpdate(elementId: string): void {
this.pendingSelection.push(elementId);
}

selectAll(): void {
const elementIds = this.model.children!.filter(c => c.type === 'node').map(c => c.id);
const elementIds = this.model.children!.map(c => c.id);
this.select(elementIds);
}

center(elementIds: string[]): void {
this.actionDispatcher.dispatch(<FitToScreenAction>{
elementIds,
kind: 'fit',
elementIds: elementIds.filter(id => {
const element = this.graphGenerator.index.getById(id);
return isNode(element) && !element.hidden;
}),
padding: 20,
maxZoom: 1,
animate: true
Expand All @@ -80,8 +93,12 @@ export class DepGraphModelSource extends LocalModelSource {
this.pendingCenter.push(elementId);
}

start(): void {
this.setModel(this.graphGenerator.graph);
filter(text: string): void {
this.graphFilter.setFilter(text);
this.graphFilter.refresh(this.graphGenerator.graph, this.graphGenerator.index);
this.actionDispatcher.dispatch(new SelectAllAction(false));
this.pendingCenter = this.model.children!.filter(c => isNode(c) && !c.hidden).map(c => c.id);
this.updateModel();
}

createNode(name: string, version?: string): void {
Expand All @@ -94,20 +111,23 @@ export class DepGraphModelSource extends LocalModelSource {
}

async resolveNodes(nodes: DependencyGraphNodeSchema[]): Promise<void> {
if (nodes.every(n => !!n.resolved)) {
if (nodes.every(n => !!n.hidden || !!n.resolved)) {
this.center(nodes.map(n => n.id));
} else {
if (this.loadIndicator) {
this.loadIndicator(true);
}
for (const node of nodes) {
try {
await this.graphGenerator.resolveNode(node);
} catch (error) {
node.error = error.toString();
if (!node.hidden) {
try {
await this.graphGenerator.resolveNode(node);
} catch (error) {
node.error = error.toString();
}
this.pendingCenter.push(node.id);
}
this.pendingCenter.push(node.id);
}
this.graphFilter.refresh(this.graphGenerator.graph, this.graphGenerator.index);
this.updateModel();
}
}
Expand All @@ -117,6 +137,7 @@ export class DepGraphModelSource extends LocalModelSource {
this.graphGenerator.index.remove(element);
}
this.model.children = [];
this.graphFilter.setFilter('');
this.updateModel();
}

Expand Down
11 changes: 8 additions & 3 deletions standalone-app/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@
<div class="col-md-12">
<h2>Package Dependency Graph for npm</h2>
<input type="text" id="package-input" class="form-control" placeholder="Type a package name">
<div id="loading-indicator" style="visibility: hidden;"><i class="fas fa-spinner fa-spin fa-2x"></i></div>
<div id="error-indicator" style="visibility: hidden;"><i class="fas fa-exclamation-circle fa-2x"></i></div>
<div id="loading-indicator" class="indicator-icon" style="visibility: hidden;">
<i class="fas fa-spinner fa-spin fa-2x"></i>
</div>
<div id="error-indicator" class="indicator-icon" style="visibility: hidden;">
<i class="fas fa-exclamation-circle fa-2x"></i>
</div>
<div id="sprotty" class="content-widget"></div>
<div id="button-bar">
<div class="button-bar">
<button id="button-clear">Clear</button>
<button id="button-select-all">Select All</button>
<input type="text" id="filter-input" class="form-control" placeholder="Filter dependencies">
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit d148eb8

Please sign in to comment.