Passed
Pull Request — master (#17)
by
unknown
03:15
created

src/utils/helpers/DetailsDrawHelpers.ts   A

Complexity

Total Complexity 19
Complexity/F 1.9

Size

Lines of Code 223
Function Count 10

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

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