Passed
Push — master ( 83654d...5db84c )
by
unknown
03:04
created

DrawHelpers.ts ➔ createDetailsButton   A

Complexity

Conditions 1

Size

Total Lines 20
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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