Passed
Pull Request — master (#7)
by Pawel
02:40
created

GraphHelpers.ts ➔ handleDrag   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 6
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
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 zoomToHighLightedNodes() {
93
    const highlightedNodes = selectHighLightedNodes();
94
    const svgContainer = select('#container');
95
    const svgContainerNode = select<SVGSVGElement, DependencyNode>('#container').node();
96
    const dim = findGroupBackgroundDimension(highlightedNodes.data());
97
98
    if (!svgContainerNode || !dim) {
99
        return;
100
    }
101
102
    const width = Number(svgContainerNode.getAttribute('width'));
103
    const height = Number(svgContainerNode.getAttribute('height'));
104
105
    const scaleValue = Math.min(8, 0.9 / Math.max(dim.width / width, dim.height / height));
106
107
    svgContainer
108
        .transition()
109
        .duration(750)
110
        .call(
111
            zoom<any, any>().on('zoom', zoomed).transform,
112
            zoomIdentity
113
                .translate(width / 2, height / 2)
114
                .scale(scaleValue)
115
                .translate(-dim.x - dim.width / 2, -dim.y - dim.height / 2)
116
        );
117
}
118
119
export function setDependencyLevelOnEachNode(clickedNode: DependencyNode, nodes: DependencyNode[]): DependencyNode[] {
120
    nodes.forEach((node: DependencyNode) => (node.level = 0));
121
122
    const visitedNodes: DependencyNode[] = [];
123
    const nodesToVisit: DependencyNode[] = [];
124
125
    nodesToVisit.push({ ...clickedNode, level: 1 });
126
127
    while (nodesToVisit.length > 0) {
128
        const currentNode = nodesToVisit.shift();
129
130
        if (!currentNode) {
131
            return [];
132
        }
133
134
        currentNode.links.forEach((node: DependencyNode) => {
135
            if (!containsNode(visitedNodes, node) && !containsNode(nodesToVisit, node)) {
136
                node.level = currentNode.level + 1;
137
                nodesToVisit.push({ ...node, level: currentNode.level + 1 });
138
            }
139
        });
140
141
        visitedNodes.push(currentNode);
142
    }
143
144
    return visitedNodes;
145
}
146
147
function containsNode(arr: DependencyNode[], node: DependencyNode) {
148
    return arr.findIndex((el: DependencyNode) => Boolean(el.name === node.name && el.version === node.version)) > -1;
149
}
150
151
export function areNodesConnected(a: DependencyNode, b: DependencyNode, links: DependencyLink[]) {
152
    return (
153
        a.index === b.index ||
154
        links.some(
155
            link =>
156
                (link.source.index === a.index && link.target.index === b.index) ||
157
                (link.source.index === b.index && link.target.index === a.index)
158
        )
159
    );
160
}
161
162
export function getHighLightedLabelColor(node: DependencyNode) {
163
    const { isConsumer, isProvider } = node;
164
165
    if (isConsumer && isProvider) {
166
        return LabelColors.PROVIDER_CONSUMER;
167
    }
168
169
    if (isProvider) {
170
        return LabelColors.PROVIDER;
171
    }
172
173
    if (isConsumer) {
174
        return LabelColors.CONSUMER;
175
    }
176
177
    return LabelColors.DEFAULT;
178
}
179
180
export function handleDrag(simulation: Simulation<DependencyNode, DependencyLink>) {
181
    return drag<SVGGElement, DependencyNode>()
182
        .on('start', (node: DependencyNode) => dragStarted(node, simulation))
183
        .on('drag', dragged)
184
        .on('end', (node: DependencyNode) => dragEnded(node, simulation));
185
}
186
187
function dragStarted(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
188
    if (!event.active) {
189
        simulation.alphaTarget(0.3).restart();
190
    }
191
    node.fx = node.x;
192
    node.fy = node.y;
193
}
194
195
function dragged(node: DependencyNode) {
196
    node.fx = event.x;
197
    node.fy = event.y;
198
}
199
200
function dragEnded(node: DependencyNode, simulation: Simulation<DependencyNode, DependencyLink>) {
201
    if (!event.active) {
202
        simulation.alphaTarget(0);
203
    }
204
    node.fx = null;
205
    node.fy = null;
206
}
207
208
export function zoomed() {
209
    const { transform } = event;
210
    const zoomLayer = select('#zoom');
211
    zoomLayer.attr('transform', transform);
212
    zoomLayer.attr('stroke-width', 1 / transform.k);
213
}
214
215
export function findGroupBackgroundDimension(nodesGroup: DependencyNode[]) {
216
    if (nodesGroup.length === 0) {
217
        return undefined;
218
    }
219
220
    let upperLimitNode = nodesGroup[0];
221
    let lowerLimitNode = nodesGroup[0];
222
    let leftLimitNode = nodesGroup[0];
223
    let rightLimitNode = nodesGroup[0];
224
225
    nodesGroup.forEach((node: DependencyNode) => {
226
        if (!node.x || !node.y || !rightLimitNode.x || !leftLimitNode.x || !upperLimitNode.y || !lowerLimitNode.y) {
227
            return;
228
        }
229
        if (node.x > rightLimitNode.x) {
230
            rightLimitNode = node;
231
        }
232
233
        if (node.x < leftLimitNode.x) {
234
            leftLimitNode = node;
235
        }
236
237
        if (node.y < upperLimitNode.y) {
238
            upperLimitNode = node;
239
        }
240
241
        if (node.y > lowerLimitNode.y) {
242
            lowerLimitNode = node;
243
        }
244
    });
245
246
    const upperLimitWithOffset = upperLimitNode.y ? upperLimitNode.y - 50 : 0;
247
    const leftLimitWithOffset = leftLimitNode.x ? leftLimitNode.x - 100 : 0;
248
    const width = rightLimitNode.x && rightLimitNode.width ? rightLimitNode.x + rightLimitNode.width + 50 - leftLimitWithOffset : 0;
249
    const height = lowerLimitNode.y ? lowerLimitNode.y! + 100 - upperLimitWithOffset : 0;
250
251
    return {
252
        x: leftLimitWithOffset,
253
        y: upperLimitWithOffset,
254
        width,
255
        height,
256
    };
257
}
258
259
export function setResetViewHandler() {
260
    LevelStorage.reset();
261
    const svgContainer = select('#container');
262
    const zoomLayer = select('#zoom');
263
    svgContainer.on('dblclick', () => {
264
        selectAllNodes().each((node: DependencyNode) => (node.level = 0));
265
266
        selectHighLightedNodes().each(function(this: SVGGElement) {
267
            const labelElement = this.firstElementChild;
268
            const textElement = this.lastElementChild;
269
270
            if (!labelElement || !textElement) {
271
                return;
272
            }
273
274
            select<Element, DependencyNode>(labelElement).attr('fill', LabelColors.DEFAULT);
275
            select<Element, DependencyNode>(textElement).style('fill', TextColors.DEFAULT);
276
        });
277
278
        svgContainer
279
            .transition()
280
            .duration(750)
281
            .call(zoom<any, any>().on('zoom', () => zoomLayer.attr('transform', event.transform)).transform, zoomIdentity);
282
    });
283
}
284
285
export class LevelStorage {
286
    private static level: number = 1;
287
    private static maxLevel: number = 1;
288
289
    public static getLevel(): number {
290
        return this.level;
291
    }
292
293
    public static increase() {
294
        this.level = this.level + 1;
295
    }
296
297
    public static decrease() {
298
        this.level = this.level - 1;
299
    }
300
301
    public static isMax() {
302
        return this.level < this.maxLevel;
303
    }
304
305
    static isMin() {
306
        return this.level > 1;
307
    }
308
309
    static setMaxLevel(maxLevel: number) {
310
        this.maxLevel = maxLevel;
311
    }
312
313
    static getMaxLevel(): number {
314
        return this.maxLevel;
315
    }
316
317
    public static reset() {
318
        this.level = 1;
319
        this.maxLevel = 1;
320
    }
321
}
322