Passed
Push — master ( fc2a4a...e14e22 )
by
unknown
03:40
created

src/utils/helpers/GraphHelpers.ts   B

Complexity

Total Complexity 51
Complexity/F 2.22

Size

Lines of Code 431
Function Count 23

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 51
eloc 348
mnd 28
bc 28
fnc 23
dl 0
loc 431
rs 7.92
bpm 1.2173
cpm 2.2173
noi 0
c 0
b 0
f 0

23 Functions

Rating   Name   Duplication   Size   Complexity  
A GraphHelpers.ts ➔ highlight 0 26 4
A GraphHelpers.ts ➔ areNodesConnected 0 8 1
B GraphHelpers.ts ➔ getButtonDimension 0 39 2
B GraphHelpers.ts ➔ showHighlightBackground 0 75 3
A GraphHelpers.ts ➔ centerScreenToDimension 0 23 2
A GraphHelpers.ts ➔ compareNodes 0 6 1
A GraphHelpers.ts ➔ setDependencyLevelOnEachNode 0 29 4
A GraphHelpers.ts ➔ containsNode 0 3 1
A GraphHelpers.ts ➔ zoomToHighLightedNodes 0 7 1
A GraphHelpers.ts ➔ hideHighlightBackground 0 11 1
A GraphHelpers.ts ➔ findMaxDependencyLevel 0 10 1
A GraphHelpers.ts ➔ getDefaultScaleValue 0 12 2
A GraphHelpers.ts ➔ getTextDimensions 0 4 2
A GraphHelpers.ts ➔ getNodeDimensions 0 6 2
A GraphHelpers.ts ➔ getRenderedNodes 0 3 1
A GraphHelpers.ts ➔ dragged 0 4 1
A GraphHelpers.ts ➔ dragEnded 0 7 2
A GraphHelpers.ts ➔ getHighLightedLabelColor 0 13 3
A GraphHelpers.ts ➔ dragStarted 0 7 2
C GraphHelpers.ts ➔ findGroupBackgroundDimension 0 51 10
A GraphHelpers.ts ➔ showTooltip 0 3 1
A GraphHelpers.ts ➔ handleDrag 0 20 3
A GraphHelpers.ts ➔ hideTooltip 0 3 1

How to fix   Complexity   

Complexity

Complex classes like src/utils/helpers/GraphHelpers.ts often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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