Passed
Push — master ( 83654d...5db84c )
by
unknown
03:04
created

GraphHelpers.ts ➔ hideHighlightBackground   A

Complexity

Conditions 1

Size

Total Lines 8
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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