Passed
Pull Request — master (#14)
by
unknown
03:00
created

GraphHelpers.ts ➔ centerScreenForDimension   A

Complexity

Conditions 2

Size

Total Lines 17
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 17
rs 9.75
c 0
b 0
f 0
cc 2
1
import { DependencyLink, DependencyNode } from '../../components/types';
2
import { select, Selection, event, BaseType } from 'd3-selection';
3
import { Simulation } from 'd3-force';
4
import { drag } from 'd3-drag';
5
import { zoom, zoomIdentity } from 'd3-zoom';
6
7
export type NodeSelection<T extends BaseType> = Selection<T, DependencyNode, Element, HTMLElement>;
8
9
export type LinkSelection = Selection<SVGPathElement, DependencyLink, SVGGElement, DependencyNode>;
10
11
export enum LabelColors {
12
    PROVIDER = '#00BFC2',
13
    CONSUMER = '#039881',
14
    PROVIDER_CONSUMER = '#03939F',
15
    DEFAULT = '#dcdee0',
16
}
17
18
export enum TextColors {
19
    HIGHLIGHTED = 'WHITE',
20
    DEFAULT = '#5E6063',
21
}
22
23
export function getLabelTextDimensions(node: Node) {
24
    const textNode = select<SVGGElement, DependencyNode>(node.previousSibling as SVGGElement).node();
25
26
    if (!textNode) {
27
        return undefined;
28
    }
29
30
    return textNode.getBBox();
31
}
32
33
export function getNodeDimensions(selectedNode: DependencyNode): { width: number; height: number } {
34
    const foundNode = select<SVGGElement, DependencyNode>('#labels')
35
        .selectAll<SVGGElement, DependencyNode>('g')
36
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
37
        .node();
38
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
39
}
40
41
export function findMaxDependencyLevel(labelNodesGroup: NodeSelection<SVGGElement>) {
42
    return (
43
        Math.max(
44
            ...labelNodesGroup
45
                .selectAll<HTMLElement, DependencyNode>('g')
46
                .filter((node: DependencyNode) => node.level > 0)
47
                .data()
48
                .map((node: DependencyNode) => node.level)
49
        ) - 1
50
    );
51
}
52
53
export function highlight(clickedNode: DependencyNode, links: LinkSelection) {
54
    const linksData = links.data();
55
    const labelNodes = selectAllNodes();
56
57
    const visitedNodes = setDependencyLevelOnEachNode(clickedNode, labelNodes.data());
58
59
    if (visitedNodes.length === 1) {
60
        return;
61
    }
62
63
    labelNodes.each(function(this: SVGGElement, node: DependencyNode) {
64
        const areNodesDirectlyConnected = areNodesConnected(clickedNode, node, linksData);
65
        const labelElement = this.firstElementChild;
66
        const textElement = this.lastElementChild;
67
68
        if (!labelElement || !textElement) {
69
            return;
70
        }
71
72
        if (areNodesDirectlyConnected) {
73
            select<Element, DependencyNode>(labelElement).attr('fill', getHighLightedLabelColor);
74
            select<Element, DependencyNode>(textElement).style('fill', TextColors.HIGHLIGHTED);
75
        } else {
76
            select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
77
            select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
78
        }
79
    });
80
}
81
82
export function selectHighLightedNodes() {
83
    return selectAllNodes().filter(function(this: SVGGElement) {
84
        return this.firstElementChild ? this.firstElementChild.getAttribute('fill') !== LabelColors.DEFAULT : false;
85
    });
86
}
87
88
export function selectAllNodes() {
89
    return select('#labels').selectAll<SVGGElement, DependencyNode>('g');
90
}
91
92
function centerScreenForDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale: number = 1) {
93
    if (dimension) {
94
        const svgContainer = select('#container');
95
96
        const width = Number(svgContainer.attr('width'));
97
        const height = Number(svgContainer.attr('height'));
98
99
        svgContainer
100
            .transition()
101
            .duration(750)
102
            .call(
103
                zoom<any, any>().on('zoom', zoomed).transform,
104
                zoomIdentity
105
                    .translate(width / 2, height / 2)
106
                    .scale(scale)
107
                    .translate(-dimension.x - dimension.width / 2, -dimension.y - dimension.height / 2)
108
            );
109
    }
110
}
111
112
export function zoomToHighLightedNodes() {
113
    const highlightedNodes = selectHighLightedNodes();
114
    const svgContainerNode = select<SVGSVGElement, DependencyNode>('#container').node();
115
    const dimension = findGroupBackgroundDimension(highlightedNodes.data());
116
117
    if (!svgContainerNode || !dimension) {
118
        return;
119
    }
120
121
    const width = Number(svgContainerNode.getAttribute('width'));
122
    const height = Number(svgContainerNode.getAttribute('height'));
123
124
    const scaleValue = Math.min(1.3, 0.9 / Math.max(dimension.width / width, dimension.height / height));
125
126
    centerScreenForDimension(dimension, scaleValue);
127
}
128
129
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
130
    nodes.forEach((node: DependencyNode) => (node.level = 0));
131
132
    const visitedNodes: DependencyNode[] = [];
133
    const nodesToVisit: DependencyNode[] = [];
134
135
    nodesToVisit.push({ ...clickedNode, level: 1 });
136
137
    while (nodesToVisit.length > 0) {
138
        const currentNode = nodesToVisit.shift();
139
140
        if (!currentNode) {
141
            return [];
142
        }
143
144
        currentNode.links.forEach((node: DependencyNode) => {
145
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
146
                node.level = currentNode.level + 1;
147
                nodesToVisit.push(node);
148
            }
149
        });
150
151
        visitedNodes.push(currentNode);
152
    }
153
154
    return visitedNodes;
155
}
156
157
function containsNode(arr: DependencyNode[], node: DependencyNode) {
158
    return arr.findIndex((el: DependencyNode) => compareNodes(el, node)) > -1;
159
}
160
161
export function compareNodes<T extends { name: string; version: string }, K extends { name: string; version: string }>(
162
    node1: T,
163
    node2: K
164
): Boolean {
165
    return node1.name === node2.name && node1.version === node2.version;
166
}
167
168
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
169
    return (
170
        a.index === b.index ||
171
        links.some(
172
            link =>
173
                (link.source.index === a.index && link.target.index === b.index) ||
174
                (link.source.index === b.index && link.target.index === a.index)
175
        )
176
    );
177
}
178
179
export function getHighLightedLabelColor(node: DependencyNode) {
180
    const { isConsumer, isProvider } = node;
181
182
    if (isConsumer && isProvider) {
183
        return LabelColors.PROVIDER_CONSUMER;
184
    }
185
186
    if (isProvider) {
187
        return LabelColors.PROVIDER;
188
    }
189
190
    if (isConsumer) {
191
        return LabelColors.CONSUMER;
192
    }
193
194
    return LabelColors.DEFAULT;
195
}
196
197
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
198
    return drag<SVGGElement, DependencyNode>()
199
        .on('start', (node: DependencyNode) => dragStarted(node, simulation))
200
        .on('drag', dragged)
201
        .on('end', (node: DependencyNode) => dragEnded(node, simulation));
202
}
203
204
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
205
    if (!event.active) {
206
        simulation.alphaTarget(0.3).restart();
207
    }
208
    node.fx = node.x;
209
    node.fy = node.y;
210
}
211
212
function dragged(node: DependencyNode) {
213
    node.fx = event.x;
214
    node.fy = event.y;
215
}
216
217
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
218
    if (!event.active) {
219
        simulation.alphaTarget(0);
220
    }
221
    node.fx = null;
222
    node.fy = null;
223
}
224
225
export function zoomed() {
226
    const { transform } = event;
227
    const zoomLayer = select('#zoom');
228
    zoomLayer.attr('transform', transform);
229
    zoomLayer.attr('stroke-width', 1 / transform.k);
230
}
231
232
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
233
    if (nodesGroup.length === 0) {
234
        return undefined;
235
    }
236
237
    let upperLimitNode = nodesGroup[0];
238
    let lowerLimitNode = nodesGroup[0];
239
    let leftLimitNode = nodesGroup[0];
240
    let rightLimitNode = nodesGroup[0];
241
242
    nodesGroup.forEach((node: DependencyNode) => {
243
        if (!node.x || !node.y || !rightLimitNode.x || !leftLimitNode.x || !upperLimitNode.y || !lowerLimitNode.y) {
244
            return;
245
        }
246
        if (node.x > rightLimitNode.x) {
247
            rightLimitNode = node;
248
        }
249
250
        if (node.x < leftLimitNode.x) {
251
            leftLimitNode = node;
252
        }
253
254
        if (node.y < upperLimitNode.y) {
255
            upperLimitNode = node;
256
        }
257
258
        if (node.y > lowerLimitNode.y) {
259
            lowerLimitNode = node;
260
        }
261
    });
262
263
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
264
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
265
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width + 50 - leftLimitWithOffset : 0;
266
    const height = lowerLimitNode.y ? lowerLimitNode.y! + 100 - upperLimitWithOffset : 0;
267
268
    return {
269
        x: leftLimitWithOffset,
270
        y: upperLimitWithOffset,
271
        width,
272
        height,
273
    };
274
}
275
276
export function setResetViewHandler() {
277
    LevelStorage.reset();
278
    const svgContainer = select('#container');
279
    svgContainer.on('dblclick', () => {
280
        const highlightedNodes = selectHighLightedNodes();
281
        if (highlightedNodes.data().length) {
282
            selectAllNodes().each((node: DependencyNode) => (node.level = 0));
283
284
            const dimension = findGroupBackgroundDimension(highlightedNodes.data());
285
286
            highlightedNodes.each(function(this: SVGGElement) {
287
                const labelElement = this.firstElementChild;
288
                const textElement = this.lastElementChild;
289
290
                if (!labelElement || !textElement) {
291
                    return;
292
                }
293
294
                select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
295
                select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
296
            });
297
298
            centerScreenForDimension(dimension);
299
        }
300
    });
301
}
302
303
export class LevelStorage {
304
    private static level: number = 1;
305
    private static maxLevel: number = 1;
306
307
    public static getLevel(): number {
308
        return this.level;
309
    }
310
311
    public static increase() {
312
        this.level = this.level + 1;
313
    }
314
315
    public static decrease() {
316
        this.level = this.level - 1;
317
    }
318
319
    public static isBelowMax() {
320
        return this.level < this.maxLevel;
321
    }
322
323
    static isAboveMin() {
324
        return this.level > 1;
325
    }
326
327
    static setMaxLevel(maxLevel: number) {
328
        this.maxLevel = maxLevel;
329
    }
330
331
    static getMaxLevel(): number {
332
        return this.maxLevel;
333
    }
334
335
    public static reset() {
336
        this.level = 1;
337
        this.maxLevel = 1;
338
    }
339
}
340