Completed
Pull Request — master (#20)
by
unknown
10:49
created

src/utils/helpers/GraphHelpers.ts   B

Complexity

Total Complexity 51
Complexity/F 2.55

Size

Lines of Code 437
Function Count 20

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 51
eloc 354
mnd 31
bc 31
fnc 20
dl 0
loc 437
rs 7.92
bpm 1.55
cpm 2.55
noi 0
c 0
b 0
f 0

20 Functions

Rating   Name   Duplication   Size   Complexity  
A GraphHelpers.ts ➔ getLabelTextDimensions 0 9 2
A GraphHelpers.ts ➔ findMaxDependencyLevel 0 10 1
A GraphHelpers.ts ➔ highlight 0 26 4
A GraphHelpers.ts ➔ getNodeDimensions 0 6 2
A GraphHelpers.ts ➔ dragged 0 4 1
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 ➔ compareNodes 0 6 1
C GraphHelpers.ts ➔ findGroupBackgroundDimension 0 62 11
A GraphHelpers.ts ➔ getDefaultScaleValue 0 9 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 ➔ hideHighlightBackground 0 11 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 } 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) {
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 width = Number(container.attr('width'));
83
    const height = Number(container.attr('height'));
84
85
    return Math.min(1.3, 0.9 / Math.max(dimension.width / width, dimension.height / height));
86
}
87
88
export function centerScreenToDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale?: number) {
89
    if (!dimension) {
90
        return;
91
    }
92
93
    const svgContainer = selectContainer();
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.ZOOM_OVERVIEW)).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 LabelColors.FOCUSED;
309
    }
310
311
    if (isConsumer && isProvider) {
312
        return LabelColors.PROVIDER_CONSUMER;
313
    }
314
315
    if (isProvider) {
316
        return LabelColors.PROVIDER;
317
    }
318
319
    if (isConsumer) {
320
        return LabelColors.CONSUMER;
321
    }
322
323
    return LabelColors.DEFAULT;
324
}
325
326
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
327
    let isDragStarted = false;
328
    return drag<SVGGElement, DependencyNode>()
329
        .on('start', (node: DependencyNode) => {
330
            if (!selectHighLightedNodes().data().length) {
331
                dragStarted(node, simulation);
332
                isDragStarted = true;
333
            }
334
        })
335
        .on('drag', (node: DependencyNode) => {
336
            if (isDragStarted) {
337
                dragged(node);
338
            }
339
        })
340
        .on('end', (node: DependencyNode) => {
341
            dragEnded(node, simulation);
342
            isDragStarted = false;
343
        });
344
}
345
346
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
347
    if (!event.active) {
348
        simulation.alphaTarget(0.3).restart();
349
    }
350
    node.fx = node.x;
351
    node.fy = node.y;
352
}
353
354
function dragged(node: DependencyNode) {
355
    node.fx = event.x;
356
    node.fy = event.y;
357
}
358
359
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
360
    if (!event.active) {
361
        simulation.alphaTarget(0);
362
    }
363
    node.fx = null;
364
    node.fy = null;
365
}
366
367
export const changeZoom = (zoomSelector: ElementIds.ZOOM_OVERVIEW | ElementIds.ZOOM_DETAILS) => () => {
368
    const { transform } = event;
369
    const zoomLayer = selectById(zoomSelector);
370
    zoomLayer.attr('transform', transform);
371
    zoomLayer.attr('stroke-width', 1 / transform.k);
372
    ZoomScaleStorage.setScale(transform.k);
373
};
374
375
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
376
    if (nodesGroup.length === 0) {
377
        return;
378
    }
379
380
    let upperLimitNode = nodesGroup[0];
381
    let lowerLimitNode = nodesGroup[0];
382
    let leftLimitNode = nodesGroup[0];
383
    let rightLimitNode = nodesGroup[0];
384
385
    nodesGroup.forEach((node: DependencyNode) => {
386
        if (
387
            !node.x ||
388
            !node.y ||
389
            !node.width ||
390
            !rightLimitNode.x ||
391
            !rightLimitNode.width ||
392
            !leftLimitNode.x ||
393
            !upperLimitNode.y ||
394
            !lowerLimitNode.y
395
        ) {
396
            return;
397
        }
398
        if (node.x + node.width > rightLimitNode.x + rightLimitNode.width) {
399
            rightLimitNode = node;
400
        }
401
402
        if (node.x < leftLimitNode.x) {
403
            leftLimitNode = node;
404
        }
405
406
        if (node.y < upperLimitNode.y) {
407
            upperLimitNode = node;
408
        }
409
410
        if (node.y > lowerLimitNode.y) {
411
            lowerLimitNode = node;
412
        }
413
    });
414
415
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
416
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
417
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width - leftLimitWithOffset : 0;
418
    const height = lowerLimitNode.y ? lowerLimitNode.y - upperLimitWithOffset : 0;
419
420
    const dimension = {
421
        x: leftLimitWithOffset,
422
        y: upperLimitWithOffset,
423
        width,
424
        height,
425
    };
426
427
    const container = selectContainer();
428
429
    const scale = getDefaultScaleValue(container, dimension);
430
431
    const { buttonHeight, buttonMarginBottom } = getButtonDimension(dimension, scale);
432
433
    dimension.height += buttonHeight + buttonMarginBottom * 4;
434
435
    return dimension;
436
}
437