Passed
Push — master ( 4f758b...87f5e4 )
by
unknown
02:52
created

GraphHelpers.ts ➔ getButtonDimension   B

Complexity

Conditions 2

Size

Total Lines 39
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

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