Completed
Push — master ( eca4bd...8c9b02 )
by
unknown
03:47
created

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