Passed
Push — master ( 58a9e6...52bfcc )
by
unknown
02:37
created

src/utils/helpers/GraphHelpers.ts   B

Complexity

Total Complexity 46
Complexity/F 2.56

Size

Lines of Code 365
Function Count 18

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 46
eloc 297
mnd 28
bc 28
fnc 18
dl 0
loc 365
rs 8.72
bpm 1.5555
cpm 2.5555
noi 0
c 0
b 0
f 0

18 Functions

Rating   Name   Duplication   Size   Complexity  
A GraphHelpers.ts ➔ dragged 0 4 1
A GraphHelpers.ts ➔ getLabelTextDimensions 0 9 2
A GraphHelpers.ts ➔ dragEnded 0 7 2
A GraphHelpers.ts ➔ areNodesConnected 0 8 1
B GraphHelpers.ts ➔ showHighlightBackground 0 77 3
A GraphHelpers.ts ➔ getHighLightedLabelColor 0 17 4
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 42 11
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 } 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
export function centerScreenToDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale?: number) {
79
    if (!dimension) {
80
        return;
81
    }
82
83
    const svgContainer = selectContainer();
84
85
    const width = Number(svgContainer.attr('width'));
86
    const height = Number(svgContainer.attr('height'));
87
88
    const scaleValue = scale || Math.min(1.3, 0.9 / Math.max(dimension.width / width, dimension.height / height));
89
90
    ZoomScaleStorage.setScale(scaleValue);
91
    svgContainer
92
        .transition()
93
        .duration(TRANSITION_DURATION)
94
        .call(
95
            zoom<any, any>().on('zoom', changeZoom(ElementIds.ZOOM_OVERVIEW)).transform,
96
            zoomIdentity
97
                .translate(width / 2, height / 2)
98
                .scale(scaleValue)
99
                .translate(-dimension.x - dimension.width / 2, -dimension.y - dimension.height / 2)
100
        );
101
}
102
103
export function hideHighlightBackground() {
104
    const detailsButtonRectSelection = selectDetailsButtonRect();
105
    const detailsButtonTextSelection = selectDetailsButtonText();
106
    selectAll([selectHighlightBackground().node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
107
        .transition()
108
        .duration(TRANSITION_DURATION)
109
        .style('opacity', 0)
110
        .end()
111
        .then(() => {
112
            selectDetailsButtonWrapper().lower();
113
        });
114
}
115
116
function showHighlightBackground(dimension: ReturnType<typeof findGroupBackgroundDimension>) {
117
    if (!dimension) {
118
        return;
119
    }
120
    const highlightBackground = selectHighlightBackground();
121
    const detailsButtonRectSelection = selectDetailsButtonRect();
122
    const detailsButtonTextSelection = selectDetailsButtonText();
123
124
    const scaleValue = ZoomScaleStorage.getScale();
125
126
    const isBackgroundActive = highlightBackground.style('opacity') === String(BACKGROUND_HIGHLIGHT_OPACITY);
127
128
    const scaleMultiplier = 1 / scaleValue;
129
130
    const buttonWidth = 100 * scaleMultiplier;
131
    const buttonHeight = 60 * scaleMultiplier;
132
    const buttonMarginBottom = 10 * scaleMultiplier;
133
    const buttonMarginRight = 40 * scaleMultiplier;
134
    const buttonX = dimension.x + dimension.width - buttonWidth - buttonMarginRight;
135
    const buttonY = dimension.y + dimension.height - buttonHeight - buttonMarginBottom;
136
    const buttonRadius = 5 * scaleMultiplier;
137
    const buttonTextFontSize = BASE_FONT_SIZE * scaleMultiplier;
138
    const buttonTextPositionX = dimension.x + dimension.width - buttonWidth / 2 - buttonMarginRight;
139
    const buttonTextPositionY = dimension.y + dimension.height - buttonHeight / 2 + 6 * scaleMultiplier - buttonMarginBottom;
140
141
    const elementsNextAttributes = [
142
        {
143
            x: dimension.x,
144
            y: dimension.y,
145
            width: dimension.width,
146
            height: dimension.height,
147
            opacity: BACKGROUND_HIGHLIGHT_OPACITY,
148
        },
149
        {
150
            x: buttonX,
151
            y: buttonY,
152
            rx: buttonRadius,
153
            ry: buttonRadius,
154
            width: buttonWidth,
155
            height: buttonHeight,
156
            opacity: 1,
157
        },
158
        {
159
            fontSize: buttonTextFontSize,
160
            x: buttonTextPositionX,
161
            y: buttonTextPositionY,
162
            opacity: 1,
163
        },
164
    ];
165
166
    if (isBackgroundActive) {
167
        selectAll([highlightBackground.node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
168
            .data(elementsNextAttributes)
169
            .transition()
170
            .duration(TRANSITION_DURATION)
171
            .attr('x', data => data.x)
172
            .attr('y', data => data.y)
173
            .attr('rx', data => data.rx || 0)
174
            .attr('ry', data => data.ry || 0)
175
            .attr('width', data => data.width || 0)
176
            .attr('height', data => data.height || 0)
177
            .attr('font-size', data => data.fontSize || 0);
178
    } else {
179
        selectDetailsButtonWrapper().raise();
180
        selectAll([highlightBackground.node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
181
            .data(elementsNextAttributes)
182
            .attr('x', data => data.x)
183
            .attr('y', data => data.y)
184
            .attr('rx', data => data.rx || 0)
185
            .attr('ry', data => data.ry || 0)
186
            .attr('width', data => data.width || 0)
187
            .attr('height', data => data.height || 0)
188
            .attr('font-size', data => data.fontSize || 0)
189
            .transition()
190
            .duration(TRANSITION_DURATION)
191
            .style('opacity', data => data.opacity);
192
    }
193
}
194
195
export function zoomToHighLightedNodes() {
196
    const highlightedNodes = selectHighLightedNodes();
197
    const dimension = findGroupBackgroundDimension(highlightedNodes.data());
198
199
    centerScreenToDimension(dimension);
200
    showHighlightBackground(dimension);
201
}
202
203
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
204
    nodes.forEach((node: DependencyNode) => (node.level = 0));
205
206
    const visitedNodes: DependencyNode[] = [];
207
    const nodesToVisit: DependencyNode[] = [];
208
209
    clickedNode.level = 1;
210
211
    nodesToVisit.push(clickedNode);
212
213
    while (nodesToVisit.length > 0) {
214
        const currentNode = nodesToVisit.shift();
215
216
        if (!currentNode) {
217
            return [];
218
        }
219
220
        currentNode.links.forEach((node: DependencyNode) => {
221
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
222
                node.level = currentNode.level + 1;
223
                nodesToVisit.push(node);
224
            }
225
        });
226
227
        visitedNodes.push(currentNode);
228
    }
229
230
    return visitedNodes;
231
}
232
233
function containsNode(arr: DependencyNode[], node: DependencyNode) {
234
    return arr.findIndex((el: DependencyNode) => compareNodes(el, node)) > -1;
235
}
236
237
export function compareNodes<T extends { name: string; version: string }, K extends { name: string; version: string }>(
238
    node1: T,
239
    node2: K
240
): Boolean {
241
    return node1.name === node2.name && node1.version === node2.version;
242
}
243
244
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
245
    return (
246
        a.index === b.index ||
247
        links.some(
248
            link =>
249
                (link.source.index === a.index && link.target.index === b.index) ||
250
                (link.source.index === b.index && link.target.index === a.index)
251
        )
252
    );
253
}
254
255
export function getHighLightedLabelColor(node: DependencyNode) {
256
    const { isConsumer, isProvider } = node;
257
258
    if (isConsumer && isProvider) {
259
        return LabelColors.PROVIDER_CONSUMER;
260
    }
261
262
    if (isProvider) {
263
        return LabelColors.PROVIDER;
264
    }
265
266
    if (isConsumer) {
267
        return LabelColors.CONSUMER;
268
    }
269
270
    return LabelColors.DEFAULT;
271
}
272
273
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
274
    let isDragStarted = false;
275
    return drag<SVGGElement, DependencyNode>()
276
        .on('start', (node: DependencyNode) => {
277
            if (!selectHighLightedNodes().data().length) {
278
                dragStarted(node, simulation);
279
                isDragStarted = true;
280
            }
281
        })
282
        .on('drag', (node: DependencyNode) => {
283
            if (isDragStarted) {
284
                dragged(node);
285
            }
286
        })
287
        .on('end', (node: DependencyNode) => {
288
            dragEnded(node, simulation);
289
            isDragStarted = false;
290
        });
291
}
292
293
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
294
    if (!event.active) {
295
        simulation.alphaTarget(0.3).restart();
296
    }
297
    node.fx = node.x;
298
    node.fy = node.y;
299
}
300
301
function dragged(node: DependencyNode) {
302
    node.fx = event.x;
303
    node.fy = event.y;
304
}
305
306
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
307
    if (!event.active) {
308
        simulation.alphaTarget(0);
309
    }
310
    node.fx = null;
311
    node.fy = null;
312
}
313
314
export const changeZoom = (zoomSelector: ElementIds.ZOOM_OVERVIEW | ElementIds.ZOOM_DETAILS) => () => {
315
    const { transform } = event;
316
    const zoomLayer = selectById(zoomSelector);
317
    zoomLayer.attr('transform', transform);
318
    zoomLayer.attr('stroke-width', 1 / transform.k);
319
    ZoomScaleStorage.setScale(transform.k);
320
};
321
322
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
323
    if (nodesGroup.length === 0) {
324
        return;
325
    }
326
327
    let upperLimitNode = nodesGroup[0];
328
    let lowerLimitNode = nodesGroup[0];
329
    let leftLimitNode = nodesGroup[0];
330
    let rightLimitNode = nodesGroup[0];
331
332
    nodesGroup.forEach((node: DependencyNode) => {
333
        if (!node.x || !node.y || !rightLimitNode.x || !leftLimitNode.x || !upperLimitNode.y || !lowerLimitNode.y) {
334
            return;
335
        }
336
        if (node.x > rightLimitNode.x) {
337
            rightLimitNode = node;
338
        }
339
340
        if (node.x < leftLimitNode.x) {
341
            leftLimitNode = node;
342
        }
343
344
        if (node.y < upperLimitNode.y) {
345
            upperLimitNode = node;
346
        }
347
348
        if (node.y > lowerLimitNode.y) {
349
            lowerLimitNode = node;
350
        }
351
    });
352
353
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
354
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
355
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width + 50 - leftLimitWithOffset : 0;
356
    const height = lowerLimitNode.y ? lowerLimitNode.y! + 100 - upperLimitWithOffset : 0;
357
358
    return {
359
        x: leftLimitWithOffset,
360
        y: upperLimitWithOffset,
361
        width,
362
        height,
363
    };
364
}
365