Passed
Pull Request — master (#7)
by Pawel
03:06
created

GraphHelpers.ts ➔ findGroupBackgroundDimension   C

Complexity

Conditions 11

Size

Total Lines 42
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 35
dl 0
loc 42
rs 5.4
c 0
b 0
f 0
cc 11

How to fix   Complexity   

Complexity

Complex classes like GraphHelpers.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 { DependencyLink, DependencyNode } from '../../components/types';
2
import { select, Selection, event, BaseType } from 'd3-selection';
3
import { Simulation } from 'd3-force';
4
import { drag } from 'd3-drag';
5
import { zoom, zoomIdentity } from 'd3-zoom';
6
7
export type NodeSelection<T extends BaseType> = Selection<T, DependencyNode, Element, HTMLElement>;
8
9
export type LinkSelection = Selection<SVGPathElement, DependencyLink, SVGGElement, DependencyNode>;
10
11
export enum Colors {
12
    PROVIDER = '#00BFC2',
13
    CONSUMER = '#039881',
14
    PROVIDER_CONSUMER = '#03939F',
15
    DEFAULT = '#dcdee0',
16
}
17
18
export function getLabelTextDimensions(node: Node) {
19
    const textNode = select<SVGGElement, DependencyNode>(node.previousSibling as SVGGElement).node();
20
21
    if (!textNode) {
22
        return undefined;
23
    }
24
25
    return textNode.getBBox();
26
}
27
28
export function getNodeDimensions(selectedNode: DependencyNode): { width: number; height: number } {
29
    const foundNode = select<SVGGElement, DependencyNode>('#labels')
30
        .selectAll<SVGGElement, DependencyNode>('g')
31
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
32
        .node();
33
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
34
}
35
36
export function findMaxDependencyLevel(labelNodesGroup: NodeSelection<SVGGElement>) {
37
    return Math.max(
38
        ...labelNodesGroup
39
            .selectAll<HTMLElement, DependencyNode>('g')
40
            .filter((node: DependencyNode) => node.level > 0)
41
            .data()
42
            .map((node: DependencyNode) => node.level)
43
    );
44
}
45
46
export function highlight(clickedNode: DependencyNode, linkNodes: LinkSelection) {
47
    const linksData = linkNodes.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', 'white');
68
        } else {
69
            select<Element, DependencyNode>(labelElement).attr('fill', Colors.DEFAULT);
70
            select<Element, DependencyNode>(textElement).style('fill', 'black');
71
        }
72
    });
73
}
74
75
export function selectHighLightedNodes() {
76
    return selectAllNodes().filter(function(this: SVGGElement) {
77
        return this.firstElementChild ? this.firstElementChild.getAttribute('fill') !== Colors.DEFAULT : false;
78
    });
79
}
80
81
export function selectAllNodes() {
82
    return select('#labels').selectAll<SVGGElement, DependencyNode>('g');
83
}
84
85
export function zoomToHighLightedNodes() {
86
    const highlightedNodes = selectHighLightedNodes();
87
    const svgContainer = select('#container');
88
    const svgContainerNode = select<SVGSVGElement, DependencyNode>('#container').node();
89
    const dim = findGroupBackgroundDimension(highlightedNodes.data());
90
91
    if (!svgContainerNode || !dim) {
92
        return;
93
    }
94
95
    const width = Number(svgContainerNode.getAttribute('width'));
96
    const height = Number(svgContainerNode.getAttribute('height'));
97
98
    const scaleValue = Math.min(8, 0.9 / Math.max(dim.width / width, dim.height / height));
99
100
    svgContainer
101
        .transition()
102
        .duration(750)
103
        .call(
104
            zoom<any, any>().on('zoom', zoomed).transform,
105
            zoomIdentity
106
                .translate(width / 2, height / 2)
107
                .scale(scaleValue)
108
                .translate(-dim.x - dim.width / 2 - 250, -dim.y - dim.height / 2)
109
        );
110
}
111
112
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
113
    nodes.forEach((node: DependencyNode) => (node.level = 0));
114
115
    const visitedNodes: DependencyNode[] = [];
116
    const nodesToVisit: DependencyNode[] = [];
117
118
    nodesToVisit.push({ ...clickedNode, level: 1 });
119
120
    while (nodesToVisit.length > 0) {
121
        const currentNode = nodesToVisit.shift();
122
123
        if (!currentNode) {
124
            return [];
125
        }
126
127
        currentNode.links.forEach((node: DependencyNode) => {
128
            if (!contains(visitedNodes, node) && !contains(nodesToVisit, node)) {
129
                node.level = currentNode.level + 1;
130
                nodesToVisit.push({ ...node, level: currentNode.level + 1 });
131
            }
132
        });
133
134
        visitedNodes.push(currentNode);
135
    }
136
137
    return visitedNodes;
138
}
139
140
function contains(arr: DependencyNode[], node: DependencyNode) {
141
    return arr.findIndex((el: DependencyNode) => Boolean(el.name === node.name && el.version === node.version)) > -1;
142
}
143
144
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
145
    return (
146
        a.index === b.index ||
147
        links.some(
148
            link =>
149
                (link.source.index === a.index && link.target.index === b.index) ||
150
                (link.source.index === b.index && link.target.index === a.index)
151
        )
152
    );
153
}
154
155
export function getHighLightedLabelColor(node: DependencyNode) {
156
    const { isConsumer, isProvider } = node;
157
158
    if (isConsumer && isProvider) {
159
        return Colors.PROVIDER_CONSUMER;
160
    }
161
162
    if (isProvider) {
163
        return Colors.PROVIDER;
164
    }
165
166
    if (isConsumer) {
167
        return Colors.CONSUMER;
168
    }
169
170
    return Colors.DEFAULT;
171
}
172
173
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
174
    return drag<SVGGElement, DependencyNode>()
175
        .on('start', (node: DependencyNode) => dragStarted(node, simulation))
176
        .on('drag', dragged)
177
        .on('end', (node: DependencyNode) => dragEnded(node, simulation));
178
}
179
180
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
181
    if (!event.active) {
182
        simulation.alphaTarget(0.3).restart();
183
    }
184
    node.fx = node.x;
185
    node.fy = node.y;
186
}
187
188
function dragged(node: DependencyNode) {
189
    node.fx = event.x;
190
    node.fy = event.y;
191
}
192
193
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
194
    if (!event.active) {
195
        simulation.alphaTarget(0);
196
    }
197
    node.fx = null;
198
    node.fy = null;
199
}
200
201
export function zoomed() {
202
    const { transform } = event;
203
    const zoomLayer = select('#zoom');
204
    zoomLayer.attr('transform', transform);
205
    zoomLayer.attr('stroke-width', 1 / transform.k);
206
}
207
208
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
209
    if (nodesGroup.length === 0) {
210
        return undefined;
211
    }
212
213
    let upperLimitNode = nodesGroup[0];
214
    let lowerLimitNode = nodesGroup[0];
215
    let leftLimitNode = nodesGroup[0];
216
    let rightLimitNode = nodesGroup[0];
217
218
    nodesGroup.forEach((node: DependencyNode) => {
219
        if (!node.x || !node.y || !rightLimitNode.x || !leftLimitNode.x || !upperLimitNode.y || !lowerLimitNode.y) {
220
            return;
221
        }
222
        if (node.x > rightLimitNode.x) {
223
            rightLimitNode = node;
224
        }
225
226
        if (node.x < leftLimitNode.x) {
227
            leftLimitNode = node;
228
        }
229
230
        if (node.y < upperLimitNode.y) {
231
            upperLimitNode = node;
232
        }
233
234
        if (node.y > lowerLimitNode.y) {
235
            lowerLimitNode = node;
236
        }
237
    });
238
239
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
240
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
241
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width + 50 - leftLimitWithOffset : 0;
242
    const height = lowerLimitNode.y ? lowerLimitNode.y! + 100 - upperLimitWithOffset : 0;
243
244
    return {
245
        x: leftLimitWithOffset,
246
        y: upperLimitWithOffset,
247
        width,
248
        height,
249
    };
250
}
251
252
export function setResetViewHandler() {
253
    LevelStorage.reset();
254
    const svgContainer = select('#container');
255
    const zoomLayer = select('#zoom');
256
    svgContainer.on('dblclick', () => {
257
        selectAllNodes().each((node: DependencyNode) => (node.level = 0));
258
259
        selectHighLightedNodes()
260
            .each(function(this: SVGGElement) {
261
                const labelElement = this.firstElementChild;
262
                const textElement = this.lastElementChild;
263
264
                if (!labelElement || !textElement) {
265
                    return;
266
                }
267
268
                select<Element, DependencyNode>(labelElement).attr('fill', Colors.DEFAULT);
269
                select<Element, DependencyNode>(textElement).style('fill', 'black');
270
            })
271
            .attr('fill', Colors.DEFAULT);
272
273
        svgContainer
274
            .transition()
275
            .duration(750)
276
            .call(zoom<any, any>().on('zoom', () => zoomLayer.attr('transform', event.transform)).transform, zoomIdentity);
277
    });
278
}
279
280
export class LevelStorage {
281
    private static level: number = 1;
282
283
    public static getLevel(): number {
284
        return this.level;
285
    }
286
287
    public static increase() {
288
        this.level = this.level + 1;
289
    }
290
291
    public static decrease() {
292
        this.level = this.level - 1;
293
    }
294
295
    public static reset() {
296
        this.level = 1;
297
    }
298
}
299