Passed
Pull Request — master (#17)
by
unknown
02:50
created

src/utils/helpers/DetailsDrawHelpers.ts   A

Complexity

Total Complexity 20
Complexity/F 2

Size

Lines of Code 224
Function Count 10

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 20
eloc 192
mnd 10
bc 10
fnc 10
dl 0
loc 224
rs 10
bpm 1
cpm 2
noi 0
c 0
b 0
f 0

10 Functions

Rating   Name   Duplication   Size   Complexity  
A DetailsDrawHelpers.ts ➔ initializeDetailsView 0 13 1
B DetailsDrawHelpers.ts ➔ createDetailsViewContainer 0 43 1
A DetailsDrawHelpers.ts ➔ switchDetailsVisibility 0 17 2
A DetailsDrawHelpers.ts ➔ createDiagrams 0 25 3
A DetailsDrawHelpers.ts ➔ mapNodeToTreeStructure 0 7 2
A DetailsDrawHelpers.ts ➔ createTree 0 8 2
A DetailsDrawHelpers.ts ➔ shutdownDetailsView 0 4 1
A DetailsDrawHelpers.ts ➔ deleteDiagrams 0 5 1
B DetailsDrawHelpers.ts ➔ createDiagram 0 58 4
A DetailsDrawHelpers.ts ➔ getRootYPosition 0 9 3
1
import { TreeNode } from '../../components/types';
2
import { selectDetailsContainerDiv, selectDetailsExitButtonWrapper, selectDetailsViewContainer, selectDetailsZoom } from './Selectors';
3
import { ElementColors, FAST_TRANSITION_DURATION, Selectors } from '../AppConsts';
4
import { hierarchy, HierarchyPointNode, tree } from 'd3-hierarchy';
5
import { create, linkHorizontal, zoom, zoomIdentity, symbol, symbolCross } from 'd3';
6
import { createZoom } from './DrawHelpers';
7
import { changeZoom } from './GraphHelpers';
8
9
export function createDetailsViewContainer(width: number, height: number) {
10
    const containerWidth = width * 4;
11
    const containerHeight = height * 4;
12
    const container = selectDetailsContainerDiv()
13
        .style('display', 'none')
14
        .style('opacity', 0)
15
        .append('svg')
16
        .attr('id', 'details-view-container')
17
        .attr('x', 0)
18
        .attr('y', 0)
19
        .attr('width', containerWidth)
20
        .attr('height', containerHeight)
21
        .attr('viewBox', `0 0 ${width} ${height}`)
22
        .attr('preserveAspectRatio', 'xMidYMid meet');
23
    container
24
        .append('rect')
25
        .attr('width', width)
26
        .attr('height', height)
27
        .attr('fill', ElementColors.DETAILS_BACKGROUND);
28
    container
29
        .append('g')
30
        .attr('id', 'details-exit-button')
31
        .attr('cursor', 'pointer')
32
        .append('rect')
33
        .attr('transform', 'translate(5,5)')
34
        .attr('width', 10)
35
        .attr('height', 10)
36
        .attr('fill', ElementColors.DETAILS_BACKGROUND);
37
    selectDetailsExitButtonWrapper()
38
        .append('path')
39
        .attr('transform', 'translate(10,10) rotate(45)')
40
        .attr(
41
            'd',
42
            symbol()
43
                .type(symbolCross)
44
                .size(20)
45
        );
46
    createZoom(container, Selectors.ZOOM_DETAILS);
47
    container.call(
48
        zoom<any, any>().on('zoom', changeZoom(Selectors.ZOOM_DETAILS)).transform,
49
        // rough center screen on diagram's root node
50
        zoomIdentity.translate(-width / 5.3, -height / 2.65)
51
    );
52
}
53
54
interface TreeStructure {
55
    name: string;
56
    children?: TreeStructure[];
57
}
58
59
type TreeNodeWithVisitedFlag = TreeNode & { isVisited?: boolean };
60
61
function mapNodeToTreeStructure(node: TreeNodeWithVisitedFlag, links: (keyof TreeNode & 'consumers') | 'providers'): TreeStructure {
62
    node.isVisited = true;
63
    const UnvisitedLinks = node[links].filter((node: TreeNodeWithVisitedFlag) => !node.isVisited && node[links]);
64
    const children = UnvisitedLinks.length > 0 ? UnvisitedLinks.map(nestedNode => mapNodeToTreeStructure(nestedNode, links)) : [];
65
    node.isVisited = undefined;
66
    return { name: node.name, children };
67
}
68
69
const VERTICAL_DISTANCE_BETWEEN_NODES = 40;
70
const HORIZONTAL_DISTANCE_BETWEEN_NODES = 300;
71
72
const NODE_NEIGHBOURS_SEPARATION_MULTIPLIER = 1;
73
const NODE_NORMAL_SEPARATION_MULTIPLIER = 4;
74
75
function createTree(data: TreeStructure) {
76
    const node = hierarchy(data);
77
    return tree<TreeStructure>()
78
        .nodeSize([VERTICAL_DISTANCE_BETWEEN_NODES, HORIZONTAL_DISTANCE_BETWEEN_NODES])
79
        .separation((node1, node2) =>
80
            node1.parent === node2.parent ? NODE_NEIGHBOURS_SEPARATION_MULTIPLIER : NODE_NORMAL_SEPARATION_MULTIPLIER
81
        )(node);
82
}
83
84
function getRootYPosition(data: HierarchyPointNode<TreeStructure>) {
85
    let x0 = Infinity;
86
    let x1 = -x0;
87
    data.each(node => {
88
        if (node.x > x1) x1 = node.x;
89
        if (node.x < x0) x0 = node.x;
90
    });
91
    return VERTICAL_DISTANCE_BETWEEN_NODES - x0;
92
}
93
94
function createDiagram(
95
    tree: HierarchyPointNode<TreeStructure>,
96
    containerWidth: number,
97
    containerHeight: number,
98
    rootNodeXOffset: number,
99
    drawToLeft?: boolean
100
) {
101
    if (!tree.children || !tree.children.length) {
102
        return null;
103
    }
104
105
    const diagramWidth = containerWidth / 2;
106
    const diagramXOffset = -diagramWidth / 8;
107
    const diagramYOffset = -containerHeight / 2;
108
109
    const svg = create('svg').attr('viewBox', `${diagramXOffset} ${diagramYOffset + rootNodeXOffset} ${diagramWidth} ${containerHeight}`);
110
111
    const g = svg
112
        .append('g')
113
        .attr('font-size', 15)
114
        .attr('transform', `translate(0,${rootNodeXOffset}) ${drawToLeft ? 'rotate(180)' : ''}`);
115
116
    g.append('g')
117
        .attr('fill', 'none')
118
        .attr('stroke', ElementColors.DETAILS_LINK)
119
        .attr('stroke-width', 2)
120
        .selectAll('path')
121
        .data(tree.links())
122
        .join('path')
123
        .attr(
124
            'd',
125
            linkHorizontal<any, any>()
126
                .x(d => d.y)
127
                .y(d => d.x)
128
        );
129
130
    const node = g
131
        .append('g')
132
        .attr('stroke-linejoin', 'round')
133
        .attr('stroke-width', 3)
134
        .selectAll('g')
135
        .data(tree.descendants())
136
        .join('g')
137
        .attr('transform', d => `translate(${d.y},${d.x}) ${drawToLeft ? 'rotate(180)' : ''}`);
138
139
    node.append('text')
140
        .attr('dy', '0.31em')
141
        .attr('x', 0)
142
        .attr('text-anchor', 'middle')
143
        .style('background-color', '#ffffff')
144
        .text(node => node.data.name)
145
        .clone(true)
146
        .lower()
147
        .attr('stroke-width', 4)
148
        .attr('stroke', 'white');
149
150
    return svg.node();
151
}
152
153
function createDiagrams(
154
    container: ReturnType<typeof selectDetailsViewContainer>,
155
    consumersData: TreeStructure,
156
    providersData: TreeStructure,
157
    width: number,
158
    height: number
159
) {
160
    const consumersTree = createTree(consumersData);
161
    const providersTree = createTree(providersData);
162
163
    const consumersRootYPosition = getRootYPosition(consumersTree);
164
    const providersRootYPosition = getRootYPosition(providersTree);
165
166
    const rootNodeXOffset = Math.max(consumersRootYPosition, providersRootYPosition);
167
168
    const consumersDiagram = createDiagram(consumersTree, width, height, rootNodeXOffset, true);
169
    const providersDiagram = createDiagram(providersTree, width, height, rootNodeXOffset);
170
171
    if (providersDiagram) {
172
        container.append<SVGSVGElement>(() => providersDiagram);
173
    }
174
175
    if (consumersDiagram) {
176
        container.append<SVGSVGElement>(() => consumersDiagram);
177
    }
178
}
179
180
export function initializeDetailsView(node: TreeNode) {
181
    const consumerNodes = mapNodeToTreeStructure(node, 'consumers');
182
    const providerNodes = mapNodeToTreeStructure(node, 'providers');
183
184
    const detailsViewContainer = selectDetailsViewContainer();
185
    const width = Number(detailsViewContainer.attr('width'));
186
    const height = Number(detailsViewContainer.attr('height'));
187
188
    const detailsZoom = selectDetailsZoom();
189
190
    createDiagrams(detailsZoom, consumerNodes, providerNodes, width, height);
191
    switchDetailsVisibility();
192
}
193
194
export function shutdownDetailsView() {
195
    switchDetailsVisibility();
196
    deleteDiagrams();
197
}
198
199
function deleteDiagrams() {
200
    selectDetailsZoom()
201
        .transition()
202
        .selectAll('*')
203
        .remove();
204
}
205
206
export function switchDetailsVisibility() {
207
    const container = selectDetailsContainerDiv();
208
    const isVisible = container.style('display') === 'block';
209
    if (isVisible) {
210
        container
211
            .transition()
212
            .duration(FAST_TRANSITION_DURATION)
213
            .style('opacity', 0)
214
            .transition()
215
            .style('display', 'none');
216
    } else {
217
        container
218
            .style('display', 'block')
219
            .transition()
220
            .duration(FAST_TRANSITION_DURATION)
221
            .style('opacity', 1);
222
    }
223
}
224