Passed
Push — master ( bb6643...7b3678 )
by
unknown
04:13 queued 01:34
created

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