Passed
Pull Request — master (#7)
by Pawel
03:06
created

DrawHelpers.ts ➔ createLabelPath   B

Complexity

Conditions 5

Size

Total Lines 25
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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