Passed
Push — master ( 7b3678...eca4bd )
by
unknown
02:40
created

GraphHelpers.ts ➔ getTextDimensions   A

Complexity

Conditions 3

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 5
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 3
1
import { DependencyLink, DependencyNode, NodeSelection, RenderedDependencyNode } 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 { removeTooltipOnNodeHoverSubscription, subscribeToShowTooltipOnNodeHover, ZoomScaleStorage } from './UserEventHelpers';
8
import {
9
    selectAllLinks,
10
    selectAllNodes,
11
    selectById,
12
    selectOverviewContainer,
13
    selectDetailsButtonRect,
14
    selectDetailsButtonText,
15
    selectDetailsButtonWrapper,
16
    selectHighlightBackground,
17
    selectHighLightedNodes,
18
} from './Selectors';
19
20
export function getTextDimensions(textElement: SVGTextElement | null) {
21
    if (textElement) {
22
        return select<SVGTextElement, DependencyNode>(textElement)
23
            .node()
24
            ?.getBBox();
25
    }
26
}
27
28
export function getNodeDimensions(selectedNode: DependencyNode): { width: number; height: number } {
29
    const foundNode = selectAllNodes()
30
        .filter((node: DependencyNode) => node.x === selectedNode.x && node.y === selectedNode.y)
31
        .node();
32
    return foundNode ? foundNode.getBBox() : { width: 200, height: 25 };
33
}
34
35
export function findMaxDependencyLevel(labelNodesGroup: NodeSelection<SVGGElement>) {
36
    return (
37
        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
        ) - 1
44
    );
45
}
46
47
export function highlight(clickedNode: DependencyNode) {
48
    const linksData = selectAllLinks().data();
49
    const labelNodes = selectAllNodes();
50
51
    const visitedNodes = setDependencyLevelOnEachNode(clickedNode, labelNodes.data());
52
53
    if (visitedNodes.length === 1) {
54
        return;
55
    }
56
57
    labelNodes.each(function(this: SVGGElement, node: DependencyNode) {
58
        const areNodesDirectlyConnected = areNodesConnected(clickedNode, node, linksData);
59
        const labelElement = this.firstElementChild;
60
        const textElement = this.lastElementChild;
61
62
        if (!labelElement || !textElement) {
63
            return;
64
        }
65
66
        if (areNodesDirectlyConnected) {
67
            select<Element, DependencyNode>(labelElement).attr('fill', getHighLightedLabelColor);
68
            select<Element, DependencyNode>(textElement).style('fill', TextColors.HIGHLIGHTED);
69
        } else {
70
            select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
71
            select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
72
        }
73
    });
74
}
75
76
function getDefaultScaleValue<T extends BaseType>(container: NodeSelection<T>, dimension: ReturnType<typeof findGroupBackgroundDimension>) {
77
    if (!dimension) {
78
        return 1;
79
    }
80
    const containerWidth = Number(container.attr('width'));
81
    const containerHeight = Number(container.attr('height'));
82
83
    const maxScaleValue = 1.3;
84
    const dimensionToContainerSizeRatio = Math.max(dimension.width / containerWidth, dimension.height / containerHeight);
85
86
    return Math.min(maxScaleValue, 0.9 / dimensionToContainerSizeRatio);
87
}
88
89
export function centerScreenToDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale?: number) {
90
    if (!dimension) {
91
        return;
92
    }
93
94
    const svgContainer = selectOverviewContainer();
95
96
    const width = Number(svgContainer.attr('width'));
97
    const height = Number(svgContainer.attr('height'));
98
99
    const scaleValue = scale || getDefaultScaleValue(svgContainer, dimension);
100
101
    ZoomScaleStorage.setScale(scaleValue);
102
    svgContainer
103
        .transition()
104
        .duration(TRANSITION_DURATION)
105
        .call(
106
            zoom<any, any>().on('zoom', changeZoom(ElementIds.OVERVIEW_ZOOM)).transform,
107
            zoomIdentity
108
                .translate(width / 2, height / 2)
109
                .scale(scaleValue)
110
                .translate(-dimension.x - dimension.width / 2, -dimension.y - dimension.height / 2)
111
        );
112
}
113
114
export function hideHighlightBackground() {
115
    const detailsButtonRectSelection = selectDetailsButtonRect();
116
    const detailsButtonTextSelection = selectDetailsButtonText();
117
    selectAll([selectHighlightBackground().node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
118
        .transition()
119
        .duration(TRANSITION_DURATION)
120
        .style('opacity', 0)
121
        .end()
122
        .then(() => {
123
            selectDetailsButtonWrapper().lower();
124
        });
125
}
126
127
function getButtonDimension(dimension: ReturnType<typeof findGroupBackgroundDimension>, scale: number) {
128
    if (!dimension) {
129
        return {
130
            buttonWidth: 0,
131
            buttonHeight: 0,
132
            buttonMarginBottom: 0,
133
            buttonMarginRight: 0,
134
            buttonX: 0,
135
            buttonY: 0,
136
            buttonRadius: 0,
137
            buttonTextFontSize: 0,
138
            buttonTextPositionX: 0,
139
            buttonTextPositionY: 0,
140
        };
141
    }
142
    const scaleMultiplier = 1 / scale;
143
144
    const buttonWidth = 100 * scaleMultiplier;
145
    const buttonHeight = 60 * scaleMultiplier;
146
    const buttonMarginBottom = 10 * scaleMultiplier;
147
    const buttonMarginRight = 40 * scaleMultiplier;
148
    const buttonX = dimension.x + dimension.width - buttonWidth - buttonMarginRight;
149
    const buttonY = dimension.y + dimension.height - buttonHeight - buttonMarginBottom;
150
    const buttonRadius = 5 * scaleMultiplier;
151
    const buttonTextFontSize = BASE_FONT_SIZE * scaleMultiplier;
152
    const buttonTextPositionX = dimension.x + dimension.width - buttonWidth / 2 - buttonMarginRight;
153
    const buttonTextPositionY = dimension.y + dimension.height - buttonHeight / 2 + 6 * scaleMultiplier - buttonMarginBottom;
154
    return {
155
        buttonWidth,
156
        buttonHeight,
157
        buttonMarginBottom,
158
        buttonMarginRight,
159
        buttonX,
160
        buttonY,
161
        buttonRadius,
162
        buttonTextFontSize,
163
        buttonTextPositionX,
164
        buttonTextPositionY,
165
    };
166
}
167
168
function showHighlightBackground(dimension: ReturnType<typeof findGroupBackgroundDimension>) {
169
    if (!dimension) {
170
        return;
171
    }
172
    const highlightBackground = selectHighlightBackground();
173
    const detailsButtonRectSelection = selectDetailsButtonRect();
174
    const detailsButtonTextSelection = selectDetailsButtonText();
175
176
    const isBackgroundActive = highlightBackground.style('opacity') === String(BACKGROUND_HIGHLIGHT_OPACITY);
177
178
    const scale = ZoomScaleStorage.getScale();
179
180
    const {
181
        buttonWidth,
182
        buttonHeight,
183
        buttonX,
184
        buttonY,
185
        buttonRadius,
186
        buttonTextFontSize,
187
        buttonTextPositionX,
188
        buttonTextPositionY,
189
    } = getButtonDimension(dimension, scale);
190
191
    const elementsNextAttributes = [
192
        {
193
            x: dimension.x,
194
            y: dimension.y,
195
            width: dimension.width,
196
            height: dimension.height,
197
            opacity: BACKGROUND_HIGHLIGHT_OPACITY,
198
        },
199
        {
200
            x: buttonX,
201
            y: buttonY,
202
            rx: buttonRadius,
203
            ry: buttonRadius,
204
            width: buttonWidth,
205
            height: buttonHeight,
206
            opacity: 1,
207
        },
208
        {
209
            fontSize: buttonTextFontSize,
210
            x: buttonTextPositionX,
211
            y: buttonTextPositionY,
212
            opacity: 1,
213
        },
214
    ];
215
216
    if (isBackgroundActive) {
217
        selectAll([highlightBackground.node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
218
            .data(elementsNextAttributes)
219
            .transition()
220
            .duration(TRANSITION_DURATION)
221
            .attr('x', data => data.x)
222
            .attr('y', data => data.y)
223
            .attr('rx', data => data.rx || 0)
224
            .attr('ry', data => data.ry || 0)
225
            .attr('width', data => data.width || 0)
226
            .attr('height', data => data.height || 0)
227
            .attr('font-size', data => data.fontSize || 0);
228
    } else {
229
        selectDetailsButtonWrapper().raise();
230
        selectAll([highlightBackground.node(), detailsButtonRectSelection.node(), detailsButtonTextSelection.node()])
231
            .data(elementsNextAttributes)
232
            .attr('x', data => data.x)
233
            .attr('y', data => data.y)
234
            .attr('rx', data => data.rx || 0)
235
            .attr('ry', data => data.ry || 0)
236
            .attr('width', data => data.width || 0)
237
            .attr('height', data => data.height || 0)
238
            .attr('font-size', data => data.fontSize || 0)
239
            .transition()
240
            .duration(TRANSITION_DURATION)
241
            .style('opacity', data => data.opacity);
242
    }
243
}
244
245
export function zoomToHighLightedNodes() {
246
    const highlightedNodes = selectHighLightedNodes();
247
    const dimension = findGroupBackgroundDimension(highlightedNodes.data());
248
249
    centerScreenToDimension(dimension);
250
    showHighlightBackground(dimension);
251
}
252
253
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
254
    nodes.forEach((node: DependencyNode) => (node.level = 0));
255
256
    const visitedNodes: DependencyNode[] = [];
257
    const nodesToVisit: DependencyNode[] = [];
258
259
    clickedNode.level = 1;
260
261
    nodesToVisit.push(clickedNode);
262
263
    while (nodesToVisit.length > 0) {
264
        const currentNode = nodesToVisit.shift();
265
266
        if (!currentNode) {
267
            return [];
268
        }
269
270
        currentNode.links.forEach((node: DependencyNode) => {
271
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
272
                node.level = currentNode.level + 1;
273
                nodesToVisit.push(node);
274
            }
275
        });
276
277
        visitedNodes.push(currentNode);
278
    }
279
280
    return visitedNodes;
281
}
282
283
function containsNode(arr: DependencyNode[], node: DependencyNode) {
284
    return arr.findIndex((el: DependencyNode) => compareNodes(el, node)) > -1;
285
}
286
287
export function compareNodes<T extends { name: string; version: string }, K extends { name: string; version: string }>(
288
    node1: T,
289
    node2: K
290
): Boolean {
291
    return node1.name === node2.name && node1.version === node2.version;
292
}
293
294
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
295
    return (
296
        a.index === b.index ||
297
        links.some(
298
            link =>
299
                (link.source.index === a.index && link.target.index === b.index) ||
300
                (link.source.index === b.index && link.target.index === a.index)
301
        )
302
    );
303
}
304
305
export function getHighLightedLabelColor(node: DependencyNode) {
306
    const { isConsumer, isProvider, level } = node;
307
308
    if (level === 1) {
309
        return LabelColors.FOCUSED;
310
    }
311
312
    if (isConsumer && isProvider) {
313
        return LabelColors.PROVIDER_CONSUMER;
314
    }
315
316
    if (isProvider) {
317
        return LabelColors.PROVIDER;
318
    }
319
320
    if (isConsumer) {
321
        return LabelColors.CONSUMER;
322
    }
323
324
    return LabelColors.DEFAULT;
325
}
326
327
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
328
    let isDragStarted = false;
329
    return drag<SVGGElement, DependencyNode>()
330
        .on('start', (node: DependencyNode) => {
331
            if (!selectHighLightedNodes().data().length) {
332
                dragStarted(node, simulation);
333
                isDragStarted = true;
334
                removeTooltipOnNodeHoverSubscription();
335
            }
336
        })
337
        .on('drag', (node: DependencyNode) => {
338
            if (isDragStarted) {
339
                dragged(node);
340
            }
341
        })
342
        .on('end', (node: DependencyNode) => {
343
            dragEnded(node, simulation);
344
            isDragStarted = false;
345
            subscribeToShowTooltipOnNodeHover();
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.OVERVIEW_ZOOM | ElementIds.DETAILS_ZOOM) => () => {
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 getRenderedNodes(nodes: DependencyNode[]): RenderedDependencyNode[] {
379
    return nodes.filter((node): node is RenderedDependencyNode => node.x !== undefined && node.y !== undefined && node.width !== undefined);
380
}
381
382
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
383
    const renderedNodes = getRenderedNodes(nodesGroup);
384
    if (renderedNodes.length === 0) {
385
        return;
386
    }
387
388
    let upperLimitNode = renderedNodes[0];
389
    let lowerLimitNode = renderedNodes[0];
390
    let leftLimitNode = renderedNodes[0];
391
    let rightLimitNode = renderedNodes[0];
392
393
    renderedNodes.forEach(node => {
394
        if (node.x + node.width > rightLimitNode.x + rightLimitNode.width) {
395
            rightLimitNode = node;
396
        }
397
398
        if (node.x < leftLimitNode.x) {
399
            leftLimitNode = node;
400
        }
401
402
        if (node.y < upperLimitNode.y) {
403
            upperLimitNode = node;
404
        }
405
406
        if (node.y > lowerLimitNode.y) {
407
            lowerLimitNode = node;
408
        }
409
    });
410
411
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
412
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
413
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width - leftLimitWithOffset : 0;
414
    const height = lowerLimitNode.y ? lowerLimitNode.y - upperLimitWithOffset : 0;
415
416
    const dimension = {
417
        x: leftLimitWithOffset,
418
        y: upperLimitWithOffset,
419
        width,
420
        height,
421
    };
422
423
    const container = selectOverviewContainer();
424
425
    const scale = getDefaultScaleValue(container, dimension);
426
427
    const { buttonHeight, buttonMarginBottom } = getButtonDimension(dimension, scale);
428
429
    dimension.height += buttonHeight + buttonMarginBottom * 4;
430
431
    return dimension;
432
}
433