Passed
Pull Request — master (#22)
by
unknown
03:12 queued 49s
created

src/utils/helpers/GraphHelpers.ts   C

Complexity

Total Complexity 53
Complexity/F 2.65

Size

Lines of Code 451
Function Count 20

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 53
eloc 363
mnd 33
bc 33
fnc 20
dl 0
loc 451
rs 6.96
bpm 1.65
cpm 2.65
noi 0
c 0
b 0
f 0

20 Functions

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

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