Passed
Push — master ( 1aa986...83654d )
by
unknown
08:16
created

GraphHelpers.ts ➔ centerScreenToDimension   A

Complexity

Conditions 2

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 19
rs 9.65
c 0
b 0
f 0
cc 2
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 LabelColors {
12
    PROVIDER = '#00BFC2',
13
    CONSUMER = '#039881',
14
    PROVIDER_CONSUMER = '#03939F',
15
    DEFAULT = '#dcdee0',
16
}
17
18
export enum TextColors {
19
    HIGHLIGHTED = 'WHITE',
20
    DEFAULT = '#5E6063',
21
}
22
23
export function getLabelTextDimensions(node: Node) {
24
    const textNode = select<SVGGElement, DependencyNode>(node.previousSibling as SVGGElement).node();
25
26
    if (!textNode) {
27
        return undefined;
28
    }
29
30
    return textNode.getBBox();
31
}
32
33
export function getNodeDimensions(selectedNode: DependencyNode): { width: number; height: number } {
34
    const foundNode = select<SVGGElement, DependencyNode>('#labels')
35
        .selectAll<SVGGElement, DependencyNode>('g')
36
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
37
        .node();
38
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
39
}
40
41
export function findMaxDependencyLevel(labelNodesGroup: NodeSelection<SVGGElement>) {
42
    return (
43
        Math.max(
44
            ...labelNodesGroup
45
                .selectAll<HTMLElement, DependencyNode>('g')
46
                .filter((node: DependencyNode) => node.level > 0)
47
                .data()
48
                .map((node: DependencyNode) => node.level)
49
        ) - 1
50
    );
51
}
52
53
export function highlight(clickedNode: DependencyNode, links: LinkSelection) {
54
    const linksData = links.data();
55
    const labelNodes = selectAllNodes();
56
57
    const visitedNodes = setDependencyLevelOnEachNode(clickedNode, labelNodes.data());
58
59
    if (visitedNodes.length === 1) {
60
        return;
61
    }
62
63
    labelNodes.each(function(this: SVGGElement, node: DependencyNode) {
64
        const areNodesDirectlyConnected = areNodesConnected(clickedNode, node, linksData);
65
        const labelElement = this.firstElementChild;
66
        const textElement = this.lastElementChild;
67
68
        if (!labelElement || !textElement) {
69
            return;
70
        }
71
72
        if (areNodesDirectlyConnected) {
73
            select<Element, DependencyNode>(labelElement).attr('fill', getHighLightedLabelColor);
74
            select<Element, DependencyNode>(textElement).style('fill', TextColors.HIGHLIGHTED);
75
        } else {
76
            select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
77
            select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
78
        }
79
    });
80
}
81
82
export function selectHighLightedNodes() {
83
    return selectAllNodes().filter(function(this: SVGGElement) {
84
        return this.firstElementChild ? this.firstElementChild.getAttribute('fill') !== LabelColors.DEFAULT : false;
85
    });
86
}
87
88
export function selectAllNodes() {
89
    return select('#labels').selectAll<SVGGElement, DependencyNode>('g');
90
}
91
92
export function centerScreenToDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale?: number) {
93
    if (dimension) {
94
        const svgContainer = select('#container');
95
96
        const width = Number(svgContainer.attr('width'));
97
        const height = Number(svgContainer.attr('height'));
98
99
        const scaleValue = Math.min(1.3, 0.9 / Math.max(dimension.width / width, dimension.height / height));
100
101
        svgContainer
102
            .transition()
103
            .duration(750)
104
            .call(
105
                zoom<any, any>().on('zoom', zoomed).transform,
106
                zoomIdentity
107
                    .translate(width / 2, height / 2)
108
                    .scale(scale || scaleValue)
109
                    .translate(-dimension.x - dimension.width / 2, -dimension.y - dimension.height / 2)
110
            );
111
    }
112
}
113
114
export function zoomToHighLightedNodes() {
115
    const highlightedNodes = selectHighLightedNodes();
116
    const dimension = findGroupBackgroundDimension(highlightedNodes.data());
117
118
    centerScreenToDimension(dimension);
119
}
120
121
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
122
    nodes.forEach((node: DependencyNode) => (node.level = 0));
123
124
    const visitedNodes: DependencyNode[] = [];
125
    const nodesToVisit: DependencyNode[] = [];
126
127
    nodesToVisit.push({ ...clickedNode, level: 1 });
128
129
    while (nodesToVisit.length > 0) {
130
        const currentNode = nodesToVisit.shift();
131
132
        if (!currentNode) {
133
            return [];
134
        }
135
136
        currentNode.links.forEach((node: DependencyNode) => {
137
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
138
                node.level = currentNode.level + 1;
139
                nodesToVisit.push(node);
140
            }
141
        });
142
143
        visitedNodes.push(currentNode);
144
    }
145
146
    return visitedNodes;
147
}
148
149
function containsNode(arr: DependencyNode[], node: DependencyNode) {
150
    return arr.findIndex((el: DependencyNode) => compareNodes(el, node)) > -1;
151
}
152
153
export function compareNodes<T extends { name: string; version: string }, K extends { name: string; version: string }>(
154
    node1: T,
155
    node2: K
156
): Boolean {
157
    return node1.name === node2.name && node1.version === node2.version;
158
}
159
160
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
161
    return (
162
        a.index === b.index ||
163
        links.some(
164
            link =>
165
                (link.source.index === a.index && link.target.index === b.index) ||
166
                (link.source.index === b.index && link.target.index === a.index)
167
        )
168
    );
169
}
170
171
export function getHighLightedLabelColor(node: DependencyNode) {
172
    const { isConsumer, isProvider } = node;
173
174
    if (isConsumer && isProvider) {
175
        return LabelColors.PROVIDER_CONSUMER;
176
    }
177
178
    if (isProvider) {
179
        return LabelColors.PROVIDER;
180
    }
181
182
    if (isConsumer) {
183
        return LabelColors.CONSUMER;
184
    }
185
186
    return LabelColors.DEFAULT;
187
}
188
189
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
190
    return drag<SVGGElement, DependencyNode>()
191
        .on('start', (node: DependencyNode) => dragStarted(node, simulation))
192
        .on('drag', dragged)
193
        .on('end', (node: DependencyNode) => dragEnded(node, simulation));
194
}
195
196
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
197
    if (!event.active) {
198
        simulation.alphaTarget(0.3).restart();
199
    }
200
    node.fx = node.x;
201
    node.fy = node.y;
202
}
203
204
function dragged(node: DependencyNode) {
205
    node.fx = event.x;
206
    node.fy = event.y;
207
}
208
209
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
210
    if (!event.active) {
211
        simulation.alphaTarget(0);
212
    }
213
    node.fx = null;
214
    node.fy = null;
215
}
216
217
export function zoomed() {
218
    const { transform } = event;
219
    const zoomLayer = select('#zoom');
220
    zoomLayer.attr('transform', transform);
221
    zoomLayer.attr('stroke-width', 1 / transform.k);
222
}
223
224
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
225
    if (nodesGroup.length === 0) {
226
        return undefined;
227
    }
228
229
    let upperLimitNode = nodesGroup[0];
230
    let lowerLimitNode = nodesGroup[0];
231
    let leftLimitNode = nodesGroup[0];
232
    let rightLimitNode = nodesGroup[0];
233
234
    nodesGroup.forEach((node: DependencyNode) => {
235
        if (!node.x || !node.y || !rightLimitNode.x || !leftLimitNode.x || !upperLimitNode.y || !lowerLimitNode.y) {
236
            return;
237
        }
238
        if (node.x > rightLimitNode.x) {
239
            rightLimitNode = node;
240
        }
241
242
        if (node.x < leftLimitNode.x) {
243
            leftLimitNode = node;
244
        }
245
246
        if (node.y < upperLimitNode.y) {
247
            upperLimitNode = node;
248
        }
249
250
        if (node.y > lowerLimitNode.y) {
251
            lowerLimitNode = node;
252
        }
253
    });
254
255
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
256
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
257
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width + 50 - leftLimitWithOffset : 0;
258
    const height = lowerLimitNode.y ? lowerLimitNode.y! + 100 - upperLimitWithOffset : 0;
259
260
    return {
261
        x: leftLimitWithOffset,
262
        y: upperLimitWithOffset,
263
        width,
264
        height,
265
    };
266
}
267
268
export function setResetViewHandler() {
269
    LevelStorage.reset();
270
    const svgContainer = select('#container');
271
    svgContainer.on('click', () => {
272
        const highlightedNodes = selectHighLightedNodes();
273
        if (highlightedNodes.data().length) {
274
            selectAllNodes().each((node: DependencyNode) => (node.level = 0));
275
276
            const dimension = findGroupBackgroundDimension(highlightedNodes.data());
277
278
            highlightedNodes.each(function(this: SVGGElement) {
279
                const labelElement = this.firstElementChild;
280
                const textElement = this.lastElementChild;
281
282
                if (!labelElement || !textElement) {
283
                    return;
284
                }
285
286
                select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
287
                select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
288
            });
289
290
            centerScreenToDimension(dimension, 1);
291
        }
292
    });
293
}
294
295
export class LevelStorage {
296
    private static level: number = 1;
297
    private static maxLevel: number = 1;
298
299
    public static getLevel(): number {
300
        return this.level;
301
    }
302
303
    public static increase() {
304
        this.level = this.level + 1;
305
    }
306
307
    public static decrease() {
308
        this.level = this.level - 1;
309
    }
310
311
    public static isBelowMax() {
312
        return this.level < this.maxLevel;
313
    }
314
315
    static isAboveMin() {
316
        return this.level > 1;
317
    }
318
319
    static setMaxLevel(maxLevel: number) {
320
        this.maxLevel = maxLevel;
321
    }
322
323
    static getMaxLevel(): number {
324
        return this.maxLevel;
325
    }
326
327
    public static reset() {
328
        this.level = 1;
329
        this.maxLevel = 1;
330
    }
331
}
332