Passed
Push — master ( e14e22...172c7d )
by
unknown
03:44
created

overview.helpers.ts ➔ findGroupBackgroundDimension   C

Complexity

Conditions 10

Size

Total Lines 51
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 44
dl 0
loc 51
rs 5.9999
c 0
b 0
f 0
cc 10

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like overview.helpers.ts ➔ findGroupBackgroundDimension 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 { DependencyNode, Network, NodeSelection } from '../components/types';
2
import { selectAllLinks, selectAllNodes, selectOverviewContainer } from '../utils/helpers/Selectors';
3
import { areNodesConnected, createNodeLabels, createNodes, getHighLightedLabelColor, getRenderedNodes } from './graph-nodes';
4
import { BaseType, select } from 'd3-selection';
5
import { ElementIds, Colors, TRANSITION_DURATION } from '../utils/AppConsts';
6
import { getButtonDimension } from './highlight-background/highlight-background.helpers';
7
import { ZoomScaleStorage } from '../utils/helpers/UserEventHelpers';
8
import { zoom, zoomIdentity } from 'd3-zoom';
9
import { changeZoom, createZoom } from '../zoom/zoom';
10
import { addNodesDrag, createSimulation } from './simulation/simulation';
11
import { createSVGContainer } from './overview-container';
12
import { createLinkMarkersDefinition, createLinkPath, createLinks } from './graph-links';
13
import { createHighlightBackground } from './highlight-background/highlight-background';
14
import { createDetailsButton } from './highlight-background/details-button';
15
import { createTooltip } from './tooltip/tooltip';
16
17
export function createOverview(container: HTMLDivElement, network: Network) {
18
    const { nodes, links } = network;
19
    const width = container.clientWidth;
20
    const height = container.clientHeight;
21
22
    const simulation = createSimulation(nodes, links, width, height);
23
    const svgContainer = createSVGContainer(width, height);
24
    const zoomLayer = createZoom(svgContainer, ElementIds.OVERVIEW_ZOOM);
25
26
    createLinkMarkersDefinition(svgContainer);
27
28
    createHighlightBackground(zoomLayer);
29
30
    const labelNodesGroup = zoomLayer.append('g').attr('id', ElementIds.LABELS);
31
    const linkElements = createLinks(zoomLayer, links);
32
33
    createNodes(labelNodesGroup, nodes);
34
    createNodeLabels(labelNodesGroup, nodes);
35
    createDetailsButton(zoomLayer);
36
    createTooltip(zoomLayer);
37
38
    labelNodesGroup.selectAll<SVGGElement, DependencyNode>('g').call(addNodesDrag(simulation));
39
40
    simulation.on('tick', () => {
41
        linkElements.each(createLinkPath);
42
43
        labelNodesGroup
44
            .selectAll('text')
45
            .data(nodes)
46
            .attr('x', (node: DependencyNode) => (node.x ? node.x : null))
47
            .attr('y', (node: DependencyNode) => (node.y ? node.y : null));
48
49
        labelNodesGroup
50
            .selectAll<Element, DependencyNode>('path')
51
            .attr('transform', (node: DependencyNode) =>
52
                node.x && node.y ? 'translate(' + (node.x - 30) + ',' + (node.y - 55) + ')' : null
53
            )
54
            .lower();
55
    });
56
}
57
58
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
59
    nodes.forEach(node => {
60
        node.level = 0;
61
    });
62
63
    const visitedNodes: DependencyNode[] = [];
64
    const nodesToVisit: DependencyNode[] = [];
65
66
    clickedNode.level = 1;
67
68
    nodesToVisit.push(clickedNode);
69
70
    while (nodesToVisit.length > 0) {
71
        const currentNode = nodesToVisit.shift();
72
73
        if (!currentNode) {
74
            return [];
75
        }
76
77
        currentNode.links.forEach((node: DependencyNode) => {
78
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
79
                node.level = currentNode.level + 1;
80
                nodesToVisit.push(node);
81
            }
82
        });
83
84
        visitedNodes.push(currentNode);
85
    }
86
87
    return visitedNodes;
88
}
89
90
export function containsNode(arr: DependencyNode[], node: DependencyNode) {
91
    return arr.findIndex((el: DependencyNode) => compareNodes(el, node)) > -1;
92
}
93
94
export function compareNodes<T extends { name: string; version: string }, K extends { name: string; version: string }>(
95
    node1: T,
96
    node2: K
97
): Boolean {
98
    return node1.name === node2.name && node1.version === node2.version;
99
}
100
101
export function highlightNodes(clickedNode: DependencyNode) {
102
    const linksData = selectAllLinks().data();
103
    const labelNodes = selectAllNodes();
104
105
    const visitedNodes = setDependencyLevelOnEachNode(clickedNode, labelNodes.data());
106
107
    if (visitedNodes.length === 1) {
108
        return;
109
    }
110
111
    labelNodes.each(function(this: SVGGElement, node: DependencyNode) {
112
        const areNodesDirectlyConnected = areNodesConnected(clickedNode, node, linksData);
113
        const labelElement = this.firstElementChild;
114
        const textElement = this.lastElementChild;
115
116
        if (!labelElement || !textElement) {
117
            return;
118
        }
119
120
        if (areNodesDirectlyConnected) {
121
            select<Element, DependencyNode>(labelElement).attr('fill', getHighLightedLabelColor);
122
            select<Element, DependencyNode>(textElement).style('fill', Colors.WHITE);
123
        } else {
124
            select<Element, DependencyNode>(labelElement).attr('fill', Colors.LIGHT_GREY);
125
            select<Element, DependencyNode>(textElement).style('fill', Colors.BASIC_TEXT);
126
        }
127
    });
128
}
129
130
function getDefaultScaleValue<T extends BaseType>(container: NodeSelection<T>, dimension: ReturnType<typeof findGroupBackgroundDimension>) {
131
    if (!dimension) {
132
        return 1;
133
    }
134
    const containerWidth = Number(container.attr('width'));
135
    const containerHeight = Number(container.attr('height'));
136
137
    const maxScaleValue = 1.3;
138
    const dimensionToContainerSizeRatio = Math.max(dimension.width / containerWidth, dimension.height / containerHeight);
139
140
    return Math.min(maxScaleValue, 0.9 / dimensionToContainerSizeRatio);
141
}
142
143
export function centerScreenToDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale?: number) {
144
    if (!dimension) {
145
        return;
146
    }
147
148
    const svgContainer = selectOverviewContainer();
149
150
    const width = Number(svgContainer.attr('width'));
151
    const height = Number(svgContainer.attr('height'));
152
153
    const scaleValue = scale || getDefaultScaleValue(svgContainer, dimension);
154
155
    ZoomScaleStorage.setScale(scaleValue);
156
    svgContainer
157
        .transition()
158
        .duration(TRANSITION_DURATION)
159
        .call(
160
            zoom<any, any>().on('zoom', changeZoom(ElementIds.OVERVIEW_ZOOM)).transform,
161
            zoomIdentity
162
                .translate(width / 2, height / 2)
163
                .scale(scaleValue)
164
                .translate(-dimension.x - dimension.width / 2, -dimension.y - dimension.height / 2)
165
        );
166
}
167
168
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
169
    const renderedNodes = getRenderedNodes(nodesGroup);
170
    if (renderedNodes.length === 0) {
171
        return;
172
    }
173
174
    let upperLimitNode = renderedNodes[0];
175
    let lowerLimitNode = renderedNodes[0];
176
    let leftLimitNode = renderedNodes[0];
177
    let rightLimitNode = renderedNodes[0];
178
179
    renderedNodes.forEach(node => {
180
        if (node.x + node.width > rightLimitNode.x + rightLimitNode.width) {
181
            rightLimitNode = node;
182
        }
183
184
        if (node.x < leftLimitNode.x) {
185
            leftLimitNode = node;
186
        }
187
188
        if (node.y < upperLimitNode.y) {
189
            upperLimitNode = node;
190
        }
191
192
        if (node.y > lowerLimitNode.y) {
193
            lowerLimitNode = node;
194
        }
195
    });
196
197
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
198
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
199
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width - leftLimitWithOffset : 0;
200
    const height = lowerLimitNode.y ? lowerLimitNode.y - upperLimitWithOffset : 0;
201
202
    const dimension = {
203
        x: leftLimitWithOffset,
204
        y: upperLimitWithOffset,
205
        width,
206
        height,
207
    };
208
209
    const container = selectOverviewContainer();
210
211
    const scale = getDefaultScaleValue(container, dimension);
212
213
    const { buttonHeight, buttonMarginBottom } = getButtonDimension(dimension, scale);
214
215
    dimension.height += buttonHeight + buttonMarginBottom * 4;
216
217
    return dimension;
218
}
219