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

src/utils/helpers/DrawHelpers.ts   A

Complexity

Total Complexity 24
Complexity/F 2.4

Size

Lines of Code 226
Function Count 10

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 24
eloc 189
mnd 14
bc 14
fnc 10
dl 0
loc 226
rs 10
bpm 1.4
cpm 2.4
noi 0
c 0
b 0
f 0

10 Functions

Rating   Name   Duplication   Size   Complexity  
A DrawHelpers.ts ➔ createTextElements 0 11 1
A DrawHelpers.ts ➔ createSVGContainer 0 13 1
A DrawHelpers.ts ➔ createLabels 0 9 1
A DrawHelpers.ts ➔ createSimulation 0 13 1
A DrawHelpers.ts ➔ createLinkElements 0 13 1
A DrawHelpers.ts ➔ createMarkers 0 15 1
B DrawHelpers.ts ➔ setKeyboardDependencyHighlightHandler 0 40 6
B DrawHelpers.ts ➔ createLabelPath 0 25 5
A DrawHelpers.ts ➔ createZoom 0 15 1
B DrawHelpers.ts ➔ createLinkPath 0 44 6
1
import {
2
    getLabelTextDimensions,
3
    getNodeDimensions,
4
    NodeSelection,
5
    findMaxDependencyLevel,
6
    getHighLightedLabelColor,
7
    zoomToHighLightedNodes,
8
    LevelStorage,
9
    LabelColors,
10
    TextColors,
11
} from './GraphHelpers';
12
import { DependencyLink, DependencyNode } from '../../components/types';
13
import { forceCenter, forceCollide, forceLink, forceSimulation, forceY } from 'd3-force';
14
import { event, select, Selection } from 'd3-selection';
15
import { zoom } from 'd3-zoom';
16
17
export function createLinkElements(zoomLayer: NodeSelection<SVGGElement>, links: DependencyLink[]) {
18
    return zoomLayer
19
        .append('g')
20
        .attr('id', 'links')
21
        .lower()
22
        .selectAll<HTMLElement, DependencyLink>('line.link')
23
        .data(links)
24
        .enter()
25
        .append<SVGPathElement>('svg:path')
26
        .attr('class', 'link')
27
        .attr('marker-end', 'url(#provider)')
28
        .style('stroke-width', 1);
29
}
30
31
export function createLabels(labelNodesGroup: NodeSelection<SVGGElement>, nodes: DependencyNode[]) {
32
    labelNodesGroup
33
        .selectAll('g')
34
        .data(nodes)
35
        .append<SVGPathElement>('svg:path')
36
        .attr('class', 'label')
37
        .attr('fill', LabelColors.DEFAULT)
38
        .attr('d', createLabelPath);
39
}
40
41
export function createTextElements(labelNodesGroup: NodeSelection<SVGGElement>, nodes: DependencyNode[]) {
42
    return labelNodesGroup
43
        .selectAll<HTMLElement, DependencyNode>('g#labels')
44
        .data(nodes)
45
        .enter()
46
        .append<SVGGElement>('g')
47
        .append('text')
48
        .attr('fill', TextColors.DEFAULT)
49
        .attr('cursor', 'default')
50
        .text(d => d.name);
51
}
52
53
export function createMarkers(svgContainer: NodeSelection<SVGSVGElement>): void {
54
    svgContainer
55
        .append('svg:defs')
56
        .append('svg:marker')
57
        .attr('id', 'provider')
58
        .attr('viewBox', '-5 -5 40 10')
59
        .attr('refX', 15)
60
        .attr('refY', 0)
61
        .attr('markerWidth', 40)
62
        .attr('markerHeight', 40)
63
        .attr('orient', 'auto')
64
        .append('svg:path')
65
        .attr('d', 'M0,-5L20,0L0,5,q10 -5,0 -10')
66
        .attr('fill', '#dcdee0');
67
}
68
69
export function createSimulation(nodes: DependencyNode[], links: DependencyLink[], width: number, height: number) {
70
    return forceSimulation(nodes)
71
        .force(
72
            'dependency',
73
            forceLink<DependencyNode, DependencyLink>(links)
74
                .distance(180)
75
                .id((node: DependencyNode) => node.name)
76
        )
77
        .force('center', forceCenter(width / 2, height / 2))
78
        .force('y', forceY(0.5))
79
        .force('collide', forceCollide(140))
80
        .force('nodeCollide', forceCollide(140));
81
}
82
83
export function createSVGContainer(
84
    container: HTMLDivElement,
85
    width: number,
86
    height: number
87
): Selection<SVGSVGElement, DependencyNode, Element, HTMLElement> {
88
    return select<Element, DependencyNode>(`#${container.id}`)
89
        .append('svg')
90
        .attr('id', 'container')
91
        .attr('preserveAspectRatio', 'xMidYMid meet')
92
        .attr('viewBox', `0 0 ${width} ${height}`)
93
        .attr('width', width)
94
        .attr('height', height);
95
}
96
97
export function createZoom(svgContainer: NodeSelection<SVGSVGElement>): Selection<SVGGElement, DependencyNode, Element, HTMLElement> {
98
    const zoomLayer = svgContainer.append('g').attr('id', 'zoom');
99
100
    const zoomed = () => zoomLayer.attr('transform', event.transform);
101
102
    svgContainer
103
        .call(
104
            zoom<SVGSVGElement, DependencyNode>()
105
                .scaleExtent([1 / 2, 12])
106
                .on('zoom', zoomed)
107
        )
108
        .on('dblclick.zoom', null);
109
110
    return zoomLayer;
111
}
112
113
export function createLinkPath(this: Element, link: DependencyLink): void {
114
    if (!link.source.x || !link.source.y || !link.target.x || !link.target.y) {
115
        return;
116
    }
117
118
    const xDiff = link.source.x - link.target.x;
119
    const yDiff = link.source.y - link.target.y;
120
121
    const isSourceOnTheLeft = xDiff < 0;
122
    const isSourceBelowTarget = yDiff > 0;
123
124
    const angleInRadians = Math.abs(Math.atan(yDiff / xDiff));
125
    const cosinus = Math.cos(angleInRadians);
126
    const sinus = Math.sin(angleInRadians);
127
128
    const offsetXLeft = -50 * cosinus;
129
    const offsetY = 50 * sinus;
130
    const offsetYBelow = -offsetY - 5;
131
132
    const sourceLabelWidth = getNodeDimensions(link.source).width;
133
    link.source.width = sourceLabelWidth;
134
    const targetLabelWidth = getNodeDimensions(link.target).width;
135
    link.target.width = targetLabelWidth;
136
137
    const sourceNewX = isSourceOnTheLeft ? (sourceLabelWidth + 20) * cosinus : offsetXLeft;
138
    const sourceNewY = isSourceBelowTarget ? offsetYBelow : offsetY;
139
140
    const targetNewX = isSourceOnTheLeft ? offsetXLeft : (targetLabelWidth + 20) * cosinus;
141
    const targetNewY = isSourceBelowTarget ? offsetY : offsetYBelow;
142
143
    select<Element, DependencyLink>(this)
144
        .attr(
145
            'd',
146
            'M' +
147
                (link.source.x + sourceNewX) +
148
                ',' +
149
                (link.source.y + sourceNewY) +
150
                ', ' +
151
                (link.target.x + targetNewX) +
152
                ',' +
153
                (link.target.y + targetNewY)
154
        )
155
        .attr('stroke', LabelColors.DEFAULT);
156
}
157
158
export function createLabelPath(this: Node, node: DependencyNode) {
159
    const labelTextDimensions = getLabelTextDimensions(this);
160
161
    if (!labelTextDimensions) {
162
        return '';
163
    }
164
165
    const labelTextWidth = labelTextDimensions.width;
166
167
    const { isConsumer, isProvider } = node;
168
169
    if (isConsumer && isProvider) {
170
        return 'M4.5,35l9.37,14.59L4.5,64.18h' + (labelTextWidth + 45) + 'l9-14.59L' + (labelTextWidth + 49.5) + ',35H4.5z';
171
    }
172
173
    if (isProvider) {
174
        return 'M' + (labelTextWidth + 49.5) + ',35H4.5l9.37,14.59L4.5,64.18h' + (labelTextWidth + 45);
175
    }
176
177
    if (isConsumer) {
178
        return 'M4.5,64.18h' + (labelTextWidth + 45) + 'l9.42-14.59L' + (labelTextWidth + 49.5) + ',35H4.5';
179
    }
180
181
    return 'M4.5,64.18h' + (labelTextWidth + 55) + 'L' + (labelTextWidth + 59.5) + ',35H4.5';
182
}
183
184
export function setKeyboardDependencyHighlightHandler() {
185
    select('body').on('keydown', () => {
186
        const labelNodesGroup = select<SVGGElement, DependencyNode>('g#labels');
187
        LevelStorage.setMaxLevel(findMaxDependencyLevel(labelNodesGroup));
188
189
        if (!isFinite(LevelStorage.getMaxLevel())) {
190
            return;
191
        }
192
193
        if (LevelStorage.isMax() && event.code === 'NumpadAdd') {
194
            LevelStorage.increase();
195
        }
196
197
        if (LevelStorage.isMin() && event.code === 'NumpadSubtract') {
198
            LevelStorage.decrease();
199
        }
200
201
        labelNodesGroup
202
            .selectAll<HTMLElement, DependencyNode>('g')
203
            .filter((node: DependencyNode) => node.level > 0)
204
            .each(function(this: HTMLElement, node: DependencyNode) {
205
                const labelElement = this.firstElementChild;
206
                const textElement = this.lastElementChild;
207
208
                if (!labelElement || !textElement) {
209
                    return;
210
                }
211
212
                let labelColor = LabelColors.DEFAULT;
213
                let textColor = TextColors.DEFAULT;
214
                if (node.level - 1 <= LevelStorage.getLevel()) {
215
                    labelColor = getHighLightedLabelColor(node);
216
                    textColor = TextColors.HIGHLIGHTED;
217
                }
218
219
                select<Element, DependencyNode>(labelElement).attr('fill', labelColor);
220
                select<Element, DependencyNode>(textElement).style('fill', textColor);
221
            });
222
223
        zoomToHighLightedNodes();
224
    });
225
}
226