Passed
Pull Request — master (#15)
by
unknown
02:40
created

GraphHelpers.ts ➔ selectDetailsButtonRect   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
1
import { DependencyLink, DependencyNode, LinkSelection, NodeSelection } from '../../components/types';
2
import { select, event, 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, LabelColors, Selectors, TextColors, TRANSITION_DURATION } from '../AppConsts';
7
8
export function getLabelTextDimensions(node: Node) {
9
    const textNode = select<SVGGElement, DependencyNode>(node.previousSibling as SVGGElement).node();
10
11
    if (!textNode) {
12
        return undefined;
13
    }
14
15
    return textNode.getBBox();
16
}
17
18
export function getNodeDimensions(selectedNode: DependencyNode): { width: number; height: number } {
19
    const foundNode = select<SVGGElement, DependencyNode>('#labels')
20
        .selectAll<SVGGElement, DependencyNode>('g')
21
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
22
        .node();
23
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
24
}
25
26
export function findMaxDependencyLevel(labelNodesGroup: NodeSelection<SVGGElement>) {
27
    return (
28
        Math.max(
29
            ...labelNodesGroup
30
                .selectAll<HTMLElement, DependencyNode>('g')
31
                .filter((node: DependencyNode) => node.level > 0)
32
                .data()
33
                .map((node: DependencyNode) => node.level)
34
        ) - 1
35
    );
36
}
37
38
export function highlight(clickedNode: DependencyNode, links: LinkSelection) {
39
    const linksData = links.data();
40
    const labelNodes = selectAllNodes();
41
42
    const visitedNodes = setDependencyLevelOnEachNode(clickedNode, labelNodes.data());
43
44
    if (visitedNodes.length === 1) {
45
        return;
46
    }
47
48
    labelNodes.each(function(this: SVGGElement, node: DependencyNode) {
49
        const areNodesDirectlyConnected = areNodesConnected(clickedNode, node, linksData);
50
        const labelElement = this.firstElementChild;
51
        const textElement = this.lastElementChild;
52
53
        if (!labelElement || !textElement) {
54
            return;
55
        }
56
57
        if (areNodesDirectlyConnected) {
58
            select<Element, DependencyNode>(labelElement).attr('fill', getHighLightedLabelColor);
59
            select<Element, DependencyNode>(textElement).style('fill', TextColors.HIGHLIGHTED);
60
        } else {
61
            select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
62
            select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
63
        }
64
    });
65
}
66
67
export function selectHighLightedNodes() {
68
    return selectAllNodes().filter(function(this: SVGGElement) {
69
        return this.firstElementChild ? this.firstElementChild.getAttribute('fill') !== LabelColors.DEFAULT : false;
70
    });
71
}
72
73
export function selectAllNodes() {
74
    return select(Selectors.LABELS).selectAll<SVGGElement, DependencyNode>('g');
75
}
76
77
export function selectHighlightBackground() {
78
    return select(Selectors.HIGHLIGHT_BACKGROUND);
79
}
80
81
function selectDetailsButtonWrapper() {
82
    return select(Selectors.DETAILS_BUTTON);
83
}
84
85
export function selectDetailsButtonRect() {
86
    return selectDetailsButtonWrapper().select('rect');
87
}
88
89
export function selectDetailsButtonText() {
90
    return selectDetailsButtonWrapper().select('text');
91
}
92
93
export function centerScreenToDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale?: number) {
94
    if (dimension) {
95
        const svgContainer = select(Selectors.CONTAINER);
96
97
        const width = Number(svgContainer.attr('width'));
98
        const height = Number(svgContainer.attr('height'));
99
100
        const scaleValue = scale || Math.min(1.3, 0.9 / Math.max(dimension.width / width, dimension.height / height));
101
102
        svgContainer
103
            .attr('data-scale', scaleValue)
104
            .transition()
105
            .duration(TRANSITION_DURATION)
106
            .call(
107
                zoom<any, any>().on('zoom', zoomed).transform,
108
                zoomIdentity
109
                    .translate(width / 2, height / 2)
110
                    .scale(scaleValue)
111
                    .translate(-dimension.x - dimension.width / 2, -dimension.y - dimension.height / 2)
112
            );
113
    }
114
}
115
116
function getScaleValue() {
117
    return select(Selectors.CONTAINER).attr('data-scale');
118
}
119
120
function removeHighlightBackground() {
121
    const detailsButtonRectSelection = selectDetailsButtonRect();
122
    const detailsButtonTextSelection = selectDetailsButtonText();
123
    selectAll([selectHighlightBackground().node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
124
        .transition()
125
        .duration(TRANSITION_DURATION)
126
        .style('opacity', 0);
127
}
128
129
function showHighlightBackground(dimension: ReturnType<typeof findGroupBackgroundDimension>) {
130
    if (dimension) {
131
        const highlightBackground = selectHighlightBackground();
132
        const detailsButtonRectSelection = selectDetailsButtonRect();
133
        const detailsButtonTextSelection = selectDetailsButtonText();
134
135
        const scaleValue = Number(getScaleValue());
136
137
        const isBackgroundNotActive = highlightBackground.style('opacity') !== String(BACKGROUND_HIGHLIGHT_OPACITY);
138
139
        const scaleMultiplier = 1 / scaleValue;
140
141
        const buttonWidth = 100 * scaleMultiplier;
142
        const buttonHeight = 40 * scaleMultiplier;
143
        const buttonMargin = 20 * scaleMultiplier;
144
        const buttonX = dimension.x + dimension.width - buttonWidth - buttonMargin;
145
        const buttonY = dimension.y + dimension.height - buttonHeight - buttonMargin;
146
        const buttonTextFontSize = 20 * scaleMultiplier;
147
        const buttonTextPositionX = dimension.x + dimension.width - buttonWidth / 2 - buttonMargin;
148
        const buttonTextPositionY = dimension.y + dimension.height - buttonHeight / 2 + 7 * scaleMultiplier - buttonMargin;
149
150
        if (isBackgroundNotActive) {
151
            highlightBackground
152
                .attr('x', dimension.x)
153
                .attr('y', dimension.y)
154
                .attr('width', dimension.width)
155
                .attr('height', dimension.height)
156
                .transition()
157
                .duration(TRANSITION_DURATION)
158
                .style('opacity', BACKGROUND_HIGHLIGHT_OPACITY);
159
160
            detailsButtonRectSelection
161
                .attr('width', buttonWidth)
162
                .attr('height', buttonHeight)
163
                .attr('x', buttonX)
164
                .attr('y', buttonY)
165
                .transition()
166
                .duration(TRANSITION_DURATION)
167
                .style('opacity', 1);
168
169
            detailsButtonTextSelection
170
                .attr('font-size', buttonTextFontSize)
171
                .style('text-anchor', 'middle')
172
                .attr('x', buttonTextPositionX)
173
                .attr('y', buttonTextPositionY)
174
                .transition()
175
                .duration(TRANSITION_DURATION)
176
                .style('opacity', 1);
177
        } else {
178
            const elementsNextAttributes = [
179
                {
180
                    x: dimension.x,
181
                    y: dimension.y,
182
                    width: dimension.width,
183
                    height: dimension.height,
184
                },
185
                {
186
                    x: buttonX,
187
                    y: buttonY,
188
                    width: buttonWidth,
189
                    height: buttonHeight,
190
                },
191
                {
192
                    fontSize: buttonTextFontSize,
193
                    x: buttonTextPositionX,
194
                    y: buttonTextPositionY,
195
                },
196
            ];
197
198
            selectAll([highlightBackground.node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
199
                .data(elementsNextAttributes)
200
                .transition()
201
                .duration(TRANSITION_DURATION)
202
                .attr('x', data => data.x)
203
                .attr('y', data => data.y)
204
                .attr('width', data => data.width || 0)
205
                .attr('height', data => data.height || 0)
206
                .attr('font-size', data => data.fontSize || 0);
207
        }
208
    }
209
}
210
211
export function zoomToHighLightedNodes() {
212
    const highlightedNodes = selectHighLightedNodes();
213
    const dimension = findGroupBackgroundDimension(highlightedNodes.data());
214
215
    centerScreenToDimension(dimension);
216
    showHighlightBackground(dimension);
217
}
218
219
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
220
    nodes.forEach((node: DependencyNode) => (node.level = 0));
221
222
    const visitedNodes: DependencyNode[] = [];
223
    const nodesToVisit: DependencyNode[] = [];
224
225
    nodesToVisit.push({ ...clickedNode, level: 1 });
226
227
    while (nodesToVisit.length > 0) {
228
        const currentNode = nodesToVisit.shift();
229
230
        if (!currentNode) {
231
            return [];
232
        }
233
234
        currentNode.links.forEach((node: DependencyNode) => {
235
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
236
                node.level = currentNode.level + 1;
237
                nodesToVisit.push(node);
238
            }
239
        });
240
241
        visitedNodes.push(currentNode);
242
    }
243
244
    return visitedNodes;
245
}
246
247
function containsNode(arr: DependencyNode[], node: DependencyNode) {
248
    return arr.findIndex((el: DependencyNode) => compareNodes(el, node)) > -1;
249
}
250
251
export function compareNodes<T extends { name: string; version: string }, K extends { name: string; version: string }>(
252
    node1: T,
253
    node2: K
254
): Boolean {
255
    return node1.name === node2.name && node1.version === node2.version;
256
}
257
258
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
259
    return (
260
        a.index === b.index ||
261
        links.some(
262
            link =>
263
                (link.source.index === a.index && link.target.index === b.index) ||
264
                (link.source.index === b.index && link.target.index === a.index)
265
        )
266
    );
267
}
268
269
export function getHighLightedLabelColor(node: DependencyNode) {
270
    const { isConsumer, isProvider } = node;
271
272
    if (isConsumer && isProvider) {
273
        return LabelColors.PROVIDER_CONSUMER;
274
    }
275
276
    if (isProvider) {
277
        return LabelColors.PROVIDER;
278
    }
279
280
    if (isConsumer) {
281
        return LabelColors.CONSUMER;
282
    }
283
284
    return LabelColors.DEFAULT;
285
}
286
287
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
288
    return drag<SVGGElement, DependencyNode>()
289
        .on('start', (node: DependencyNode) => {
290
            if (!selectHighLightedNodes().data().length) {
291
                dragStarted(node, simulation);
292
            }
293
        })
294
        .on('drag', (node: DependencyNode) => {
295
            if (!selectHighLightedNodes().data().length) {
296
                dragged(node);
297
            }
298
        })
299
        .on('end', (node: DependencyNode) => {
300
            if (!selectHighLightedNodes().data().length) {
301
                dragEnded(node, simulation);
302
            }
303
        });
304
}
305
306
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
307
    if (!event.active) {
308
        simulation.alphaTarget(0.3).restart();
309
    }
310
    node.fx = node.x;
311
    node.fy = node.y;
312
}
313
314
function dragged(node: DependencyNode) {
315
    node.fx = event.x;
316
    node.fy = event.y;
317
}
318
319
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
320
    if (!event.active) {
321
        simulation.alphaTarget(0);
322
    }
323
    node.fx = null;
324
    node.fy = null;
325
}
326
327
export function zoomed() {
328
    const { transform } = event;
329
    const zoomLayer = select(Selectors.ZOOM);
330
    zoomLayer.attr('transform', transform);
331
    zoomLayer.attr('stroke-width', 1 / transform.k);
332
}
333
334
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
335
    if (nodesGroup.length === 0) {
336
        return undefined;
337
    }
338
339
    let upperLimitNode = nodesGroup[0];
340
    let lowerLimitNode = nodesGroup[0];
341
    let leftLimitNode = nodesGroup[0];
342
    let rightLimitNode = nodesGroup[0];
343
344
    nodesGroup.forEach((node: DependencyNode) => {
345
        if (!node.x || !node.y || !rightLimitNode.x || !leftLimitNode.x || !upperLimitNode.y || !lowerLimitNode.y) {
346
            return;
347
        }
348
        if (node.x > rightLimitNode.x) {
349
            rightLimitNode = node;
350
        }
351
352
        if (node.x < leftLimitNode.x) {
353
            leftLimitNode = node;
354
        }
355
356
        if (node.y < upperLimitNode.y) {
357
            upperLimitNode = node;
358
        }
359
360
        if (node.y > lowerLimitNode.y) {
361
            lowerLimitNode = node;
362
        }
363
    });
364
365
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
366
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
367
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width + 50 - leftLimitWithOffset : 0;
368
    const height = lowerLimitNode.y ? lowerLimitNode.y! + 100 - upperLimitWithOffset : 0;
369
370
    return {
371
        x: leftLimitWithOffset,
372
        y: upperLimitWithOffset,
373
        width,
374
        height,
375
    };
376
}
377
378
export function setResetViewHandler() {
379
    LevelStorage.reset();
380
    const svgContainer = select(Selectors.CONTAINER);
381
    svgContainer.on('click', () => {
382
        const highlightedNodes = selectHighLightedNodes();
383
        if (highlightedNodes.data().length) {
384
            selectAllNodes().each((node: DependencyNode) => (node.level = 0));
385
386
            const dimension = findGroupBackgroundDimension(highlightedNodes.data());
387
388
            highlightedNodes.each(function(this: SVGGElement) {
389
                const labelElement = this.firstElementChild;
390
                const textElement = this.lastElementChild;
391
392
                if (!labelElement || !textElement) {
393
                    return;
394
                }
395
396
                select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
397
                select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
398
            });
399
400
            removeHighlightBackground();
401
402
            centerScreenToDimension(dimension, 1);
403
        }
404
    });
405
}
406
407
export class LevelStorage {
408
    private static level: number = 1;
409
    private static maxLevel: number = 1;
410
411
    public static getLevel(): number {
412
        return this.level;
413
    }
414
415
    public static increase() {
416
        this.level = this.level + 1;
417
    }
418
419
    public static decrease() {
420
        this.level = this.level - 1;
421
    }
422
423
    public static isBelowMax() {
424
        return this.level < this.maxLevel;
425
    }
426
427
    static isAboveMin() {
428
        return this.level > 1;
429
    }
430
431
    static setMaxLevel(maxLevel: number) {
432
        this.maxLevel = maxLevel;
433
    }
434
435
    static getMaxLevel(): number {
436
        return this.maxLevel;
437
    }
438
439
    public static reset() {
440
        this.level = 1;
441
        this.maxLevel = 1;
442
    }
443
}
444