Passed
Push — master ( 5db84c...83abc2 )
by
unknown
03:00 queued 14s
created

GraphHelpers.ts ➔ selectAllLinks   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
1
import { DependencyLink, DependencyNode, NodeSelection } from '../../components/types';
2
import { event, select, selectAll } from 'd3-selection';
3
import { Simulation } from 'd3-force';
4
import { drag } from 'd3-drag';
5
import { zoom, zoomIdentity } from 'd3-zoom';
6
import { BACKGROUND_HIGHLIGHT_OPACITY, BASE_FONT_SIZE, LabelColors, Selectors, TextColors, TRANSITION_DURATION } from '../AppConsts';
7
8
export function getLabelTextDimensions(node: Node) {
9
    const textNode = select<SVGGElement, DependencyNode>(node.previousSibling as SVGGElement).node();
10
11
    if (!textNode) {
12
        return undefined;
13
    }
14
15
    return textNode.getBBox();
16
}
17
18
export function getNodeDimensions(selectedNode: DependencyNode): { width: number; height: number } {
19
    const foundNode = select<SVGGElement, DependencyNode>('#labels')
20
        .selectAll<SVGGElement, DependencyNode>('g')
21
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
22
        .node();
23
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
24
}
25
26
export function findMaxDependencyLevel(labelNodesGroup: NodeSelection<SVGGElement>) {
27
    return (
28
        Math.max(
29
            ...labelNodesGroup
30
                .selectAll<HTMLElement, DependencyNode>('g')
31
                .filter((node: DependencyNode) => node.level > 0)
32
                .data()
33
                .map((node: DependencyNode) => node.level)
34
        ) - 1
35
    );
36
}
37
38
export function highlight(clickedNode: DependencyNode) {
39
    const linksData = selectAllLinks().data();
40
    const labelNodes = selectAllNodes();
41
42
    const visitedNodes = setDependencyLevelOnEachNode(clickedNode, labelNodes.data());
43
44
    if (visitedNodes.length === 1) {
45
        return;
46
    }
47
48
    labelNodes.each(function(this: SVGGElement, node: DependencyNode) {
49
        const areNodesDirectlyConnected = areNodesConnected(clickedNode, node, linksData);
50
        const labelElement = this.firstElementChild;
51
        const textElement = this.lastElementChild;
52
53
        if (!labelElement || !textElement) {
54
            return;
55
        }
56
57
        if (areNodesDirectlyConnected) {
58
            select<Element, DependencyNode>(labelElement).attr('fill', getHighLightedLabelColor);
59
            select<Element, DependencyNode>(textElement).style('fill', TextColors.HIGHLIGHTED);
60
        } else {
61
            select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
62
            select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
63
        }
64
    });
65
}
66
67
export function selectHighLightedNodes() {
68
    return selectAllNodes().filter(function(this: SVGGElement) {
69
        return this.firstElementChild ? this.firstElementChild.getAttribute('fill') !== LabelColors.DEFAULT : false;
70
    });
71
}
72
73
export function selectAllNodes() {
74
    return select(Selectors.LABELS).selectAll<SVGGElement, DependencyNode>('g');
75
}
76
77
export function selectAllLinks() {
78
    return select(Selectors.LINKS).selectAll<SVGPathElement, DependencyLink>('path');
79
}
80
81
export function selectHighlightBackground() {
82
    return select(Selectors.HIGHLIGHT_BACKGROUND);
83
}
84
85
function selectDetailsButtonWrapper() {
86
    return select(Selectors.DETAILS_BUTTON);
87
}
88
89
export function selectDetailsButtonRect() {
90
    return selectDetailsButtonWrapper().select('rect');
91
}
92
93
export function selectDetailsButtonText() {
94
    return selectDetailsButtonWrapper().select('text');
95
}
96
97
export function centerScreenToDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale?: number) {
98
    if (!dimension) {
99
        return;
100
    }
101
    const svgContainer = select(Selectors.CONTAINER);
102
103
    const width = Number(svgContainer.attr('width'));
104
    const height = Number(svgContainer.attr('height'));
105
106
    const scaleValue = scale || Math.min(1.3, 0.9 / Math.max(dimension.width / width, dimension.height / height));
107
108
    svgContainer
109
        .attr('data-scale', scaleValue)
110
        .transition()
111
        .duration(TRANSITION_DURATION)
112
        .call(
113
            zoom<any, any>().on('zoom', changeZoom).transform,
114
            zoomIdentity
115
                .translate(width / 2, height / 2)
116
                .scale(scaleValue)
117
                .translate(-dimension.x - dimension.width / 2, -dimension.y - dimension.height / 2)
118
        );
119
}
120
121
function getScaleValue() {
122
    return select(Selectors.CONTAINER).attr('data-scale');
123
}
124
125
export function hideHighlightBackground() {
126
    const detailsButtonRectSelection = selectDetailsButtonRect();
127
    const detailsButtonTextSelection = selectDetailsButtonText();
128
    selectAll([selectHighlightBackground().node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
129
        .transition()
130
        .duration(TRANSITION_DURATION)
131
        .style('opacity', 0)
132
        .end()
133
        .then(() => {
134
            selectDetailsButtonWrapper().lower();
135
        });
136
}
137
138
function showHighlightBackground(dimension: ReturnType<typeof findGroupBackgroundDimension>) {
139
    if (!dimension) {
140
        return;
141
    }
142
    const highlightBackground = selectHighlightBackground();
143
    const detailsButtonRectSelection = selectDetailsButtonRect();
144
    const detailsButtonTextSelection = selectDetailsButtonText();
145
146
    const scaleValue = Number(getScaleValue());
147
148
    const isBackgroundActive = highlightBackground.style('opacity') === String(BACKGROUND_HIGHLIGHT_OPACITY);
149
150
    const scaleMultiplier = 1 / scaleValue;
151
152
    const buttonWidth = 100 * scaleMultiplier;
153
    const buttonHeight = 60 * scaleMultiplier;
154
    const buttonMarginBottom = 10 * scaleMultiplier;
155
    const buttonMarginRight = 40 * scaleMultiplier;
156
    const buttonX = dimension.x + dimension.width - buttonWidth - buttonMarginRight;
157
    const buttonY = dimension.y + dimension.height - buttonHeight - buttonMarginBottom;
158
    const buttonRadius = 5 * scaleMultiplier;
159
    const buttonTextFontSize = BASE_FONT_SIZE * scaleMultiplier;
160
    const buttonTextPositionX = dimension.x + dimension.width - buttonWidth / 2 - buttonMarginRight;
161
    const buttonTextPositionY = dimension.y + dimension.height - buttonHeight / 2 + 6 * scaleMultiplier - buttonMarginBottom;
162
163
    const elementsNextAttributes = [
164
        {
165
            x: dimension.x,
166
            y: dimension.y,
167
            width: dimension.width,
168
            height: dimension.height,
169
            opacity: BACKGROUND_HIGHLIGHT_OPACITY,
170
        },
171
        {
172
            x: buttonX,
173
            y: buttonY,
174
            rx: buttonRadius,
175
            ry: buttonRadius,
176
            width: buttonWidth,
177
            height: buttonHeight,
178
            opacity: 1,
179
        },
180
        {
181
            fontSize: buttonTextFontSize,
182
            x: buttonTextPositionX,
183
            y: buttonTextPositionY,
184
            opacity: 1,
185
        },
186
    ];
187
188
    if (isBackgroundActive) {
189
        selectAll([highlightBackground.node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
190
            .data(elementsNextAttributes)
191
            .transition()
192
            .duration(TRANSITION_DURATION)
193
            .attr('x', data => data.x)
194
            .attr('y', data => data.y)
195
            .attr('rx', data => data.rx || 0)
196
            .attr('ry', data => data.ry || 0)
197
            .attr('width', data => data.width || 0)
198
            .attr('height', data => data.height || 0)
199
            .attr('font-size', data => data.fontSize || 0);
200
    } else {
201
        selectDetailsButtonWrapper().raise();
202
        selectAll([highlightBackground.node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
203
            .data(elementsNextAttributes)
204
            .attr('x', data => data.x)
205
            .attr('y', data => data.y)
206
            .attr('rx', data => data.rx || 0)
207
            .attr('ry', data => data.ry || 0)
208
            .attr('width', data => data.width || 0)
209
            .attr('height', data => data.height || 0)
210
            .attr('font-size', data => data.fontSize || 0)
211
            .transition()
212
            .duration(TRANSITION_DURATION)
213
            .style('opacity', data => data.opacity);
214
    }
215
}
216
217
export function zoomToHighLightedNodes() {
218
    const highlightedNodes = selectHighLightedNodes();
219
    const dimension = findGroupBackgroundDimension(highlightedNodes.data());
220
221
    centerScreenToDimension(dimension);
222
    showHighlightBackground(dimension);
223
}
224
225
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
226
    nodes.forEach((node: DependencyNode) => (node.level = 0));
227
228
    const visitedNodes: DependencyNode[] = [];
229
    const nodesToVisit: DependencyNode[] = [];
230
231
    nodesToVisit.push({ ...clickedNode, level: 1 });
232
233
    while (nodesToVisit.length > 0) {
234
        const currentNode = nodesToVisit.shift();
235
236
        if (!currentNode) {
237
            return [];
238
        }
239
240
        currentNode.links.forEach((node: DependencyNode) => {
241
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
242
                node.level = currentNode.level + 1;
243
                nodesToVisit.push(node);
244
            }
245
        });
246
247
        visitedNodes.push(currentNode);
248
    }
249
250
    return visitedNodes;
251
}
252
253
function containsNode(arr: DependencyNode[], node: DependencyNode) {
254
    return arr.findIndex((el: DependencyNode) => compareNodes(el, node)) > -1;
255
}
256
257
export function compareNodes<T extends { name: string; version: string }, K extends { name: string; version: string }>(
258
    node1: T,
259
    node2: K
260
): Boolean {
261
    return node1.name === node2.name && node1.version === node2.version;
262
}
263
264
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
265
    return (
266
        a.index === b.index ||
267
        links.some(
268
            link =>
269
                (link.source.index === a.index && link.target.index === b.index) ||
270
                (link.source.index === b.index && link.target.index === a.index)
271
        )
272
    );
273
}
274
275
export function getHighLightedLabelColor(node: DependencyNode) {
276
    const { isConsumer, isProvider } = node;
277
278
    if (isConsumer && isProvider) {
279
        return LabelColors.PROVIDER_CONSUMER;
280
    }
281
282
    if (isProvider) {
283
        return LabelColors.PROVIDER;
284
    }
285
286
    if (isConsumer) {
287
        return LabelColors.CONSUMER;
288
    }
289
290
    return LabelColors.DEFAULT;
291
}
292
293
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
294
    let isDragStarted = false;
295
    return drag<SVGGElement, DependencyNode>()
296
        .on('start', (node: DependencyNode) => {
297
            if (!selectHighLightedNodes().data().length) {
298
                dragStarted(node, simulation);
299
                isDragStarted = true;
300
            }
301
        })
302
        .on('drag', (node: DependencyNode) => {
303
            if (isDragStarted) {
304
                dragged(node);
305
            }
306
        })
307
        .on('end', (node: DependencyNode) => {
308
            dragEnded(node, simulation);
309
            isDragStarted = false;
310
        });
311
}
312
313
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
314
    if (!event.active) {
315
        simulation.alphaTarget(0.3).restart();
316
    }
317
    node.fx = node.x;
318
    node.fy = node.y;
319
}
320
321
function dragged(node: DependencyNode) {
322
    node.fx = event.x;
323
    node.fy = event.y;
324
}
325
326
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
327
    if (!event.active) {
328
        simulation.alphaTarget(0);
329
    }
330
    node.fx = null;
331
    node.fy = null;
332
}
333
334
export function changeZoom() {
335
    const { transform } = event;
336
    const zoomLayer = select(Selectors.ZOOM);
337
    zoomLayer.attr('transform', transform);
338
    zoomLayer.attr('stroke-width', 1 / transform.k);
339
}
340
341
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
342
    if (nodesGroup.length === 0) {
343
        return;
344
    }
345
346
    let upperLimitNode = nodesGroup[0];
347
    let lowerLimitNode = nodesGroup[0];
348
    let leftLimitNode = nodesGroup[0];
349
    let rightLimitNode = nodesGroup[0];
350
351
    nodesGroup.forEach((node: DependencyNode) => {
352
        if (!node.x || !node.y || !rightLimitNode.x || !leftLimitNode.x || !upperLimitNode.y || !lowerLimitNode.y) {
353
            return;
354
        }
355
        if (node.x > rightLimitNode.x) {
356
            rightLimitNode = node;
357
        }
358
359
        if (node.x < leftLimitNode.x) {
360
            leftLimitNode = node;
361
        }
362
363
        if (node.y < upperLimitNode.y) {
364
            upperLimitNode = node;
365
        }
366
367
        if (node.y > lowerLimitNode.y) {
368
            lowerLimitNode = node;
369
        }
370
    });
371
372
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
373
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
374
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width + 50 - leftLimitWithOffset : 0;
375
    const height = lowerLimitNode.y ? lowerLimitNode.y! + 100 - upperLimitWithOffset : 0;
376
377
    return {
378
        x: leftLimitWithOffset,
379
        y: upperLimitWithOffset,
380
        width,
381
        height,
382
    };
383
}
384
385
export class LevelStorage {
386
    private static level: number = 1;
387
    private static maxLevel: number = 1;
388
389
    public static getLevel(): number {
390
        return this.level;
391
    }
392
393
    public static increase() {
394
        this.level = this.level + 1;
395
    }
396
397
    public static decrease() {
398
        this.level = this.level - 1;
399
    }
400
401
    public static isBelowMax() {
402
        return this.level < this.maxLevel;
403
    }
404
405
    static isAboveMin() {
406
        return this.level > 1;
407
    }
408
409
    static setMaxLevel(maxLevel: number) {
410
        this.maxLevel = maxLevel;
411
    }
412
413
    static getMaxLevel(): number {
414
        return this.maxLevel;
415
    }
416
417
    public static reset() {
418
        this.level = 1;
419
        this.maxLevel = 1;
420
    }
421
}
422