Completed
Pull Request — master (#20)
by
unknown
10:49
created

src/utils/helpers/DetailsDrawHelpers.ts   A

Complexity

Total Complexity 20
Complexity/F 1.67

Size

Lines of Code 237
Function Count 12

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
wmc 20
eloc 202
mnd 8
bc 8
fnc 12
dl 0
loc 237
rs 10
bpm 0.6666
cpm 1.6666
noi 0
c 0
b 0
f 0

12 Functions

Rating   Name   Duplication   Size   Complexity  
A DetailsDrawHelpers.ts ➔ initializeDetailsView 0 18 2
B DetailsDrawHelpers.ts ➔ createDetailsViewContainer 0 40 1
A DetailsDrawHelpers.ts ➔ switchDetailsVisibility 0 17 2
A DetailsDrawHelpers.ts ➔ createDiagrams 0 25 3
A DetailsDrawHelpers.ts ➔ transformDiagramElement 0 3 2
A DetailsDrawHelpers.ts ➔ resetZoomPosition 0 8 1
A DetailsDrawHelpers.ts ➔ mapNodeToTreeStructure 0 7 1
A DetailsDrawHelpers.ts ➔ shutdownDetailsView 0 4 1
A DetailsDrawHelpers.ts ➔ createTree 0 8 2
A DetailsDrawHelpers.ts ➔ deleteDiagrams 0 5 1
B DetailsDrawHelpers.ts ➔ createDiagram 0 58 2
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, HierarchyPointLink } 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
}
49
50
function resetZoomPosition(width: number, height: number) {
51
    const container = selectDetailsViewContainer();
52
53
    container.call(
54
        zoom<SVGSVGElement, unknown>().on('zoom', changeZoom(ElementIds.ZOOM_DETAILS)).transform,
55
        // rough center screen on diagram's root node
56
        zoomIdentity.translate(-width / 5.3, -height / 2.65)
57
    );
58
}
59
60
interface TreeStructure {
61
    name: string;
62
    children?: TreeStructure[];
63
}
64
65
type TreeNodeWithVisitedFlag = TreeNode & { isVisited?: boolean };
66
67
function mapNodeToTreeStructure(node: TreeNodeWithVisitedFlag, linksType: 'consumers' | 'providers'): TreeStructure {
68
    node.isVisited = true;
69
    const unvisitedLinks = node[linksType].filter((linkedNode: TreeNodeWithVisitedFlag) => !linkedNode.isVisited && linkedNode[linksType]);
70
    const children = unvisitedLinks.map(nestedNode => mapNodeToTreeStructure(nestedNode, linksType));
71
    node.isVisited = undefined;
72
    return { name: node.name, children };
73
}
74
75
const VERTICAL_DISTANCE_BETWEEN_NODES = 40;
76
const HORIZONTAL_DISTANCE_BETWEEN_NODES = 300;
77
78
const NODE_NEIGHBOURS_SEPARATION_MULTIPLIER = 1;
79
const NODE_NORMAL_SEPARATION_MULTIPLIER = 4;
80
81
function createTree(data: TreeStructure) {
82
    const node = hierarchy(data);
83
    return tree<TreeStructure>()
84
        .nodeSize([VERTICAL_DISTANCE_BETWEEN_NODES, HORIZONTAL_DISTANCE_BETWEEN_NODES])
85
        .separation((node1, node2) =>
86
            node1.parent === node2.parent ? NODE_NEIGHBOURS_SEPARATION_MULTIPLIER : NODE_NORMAL_SEPARATION_MULTIPLIER
87
        )(node);
88
}
89
90
function getRootYPosition(data: HierarchyPointNode<TreeStructure>) {
91
    let x0 = Infinity;
92
    data.each(node => {
93
        if (node.x < x0) x0 = node.x;
94
    });
95
    return VERTICAL_DISTANCE_BETWEEN_NODES - x0;
96
}
97
98
function createDiagram(
99
    tree: HierarchyPointNode<TreeStructure>,
100
    containerWidth: number,
101
    containerHeight: number,
102
    rootNodeYOffset: number,
103
    drawToLeft: boolean = false
104
) {
105
    if (!tree.children || !tree.children.length) {
106
        return null;
107
    }
108
109
    const diagramWidth = containerWidth / 2;
110
    const diagramXOffset = -diagramWidth / 8;
111
    const diagramYOffset = -containerHeight / 2;
112
113
    const svg = create('svg').attr('viewBox', `${diagramXOffset} ${diagramYOffset + rootNodeYOffset} ${diagramWidth} ${containerHeight}`);
114
115
    const g = svg
116
        .append('g')
117
        .attr('font-size', 15)
118
        .attr('transform', transformDiagramElement(0, rootNodeYOffset, drawToLeft));
119
120
    g.append('g')
121
        .attr('fill', 'none')
122
        .attr('stroke', ElementColors.DETAILS_LINK)
123
        .attr('stroke-width', 2)
124
        .selectAll('path')
125
        .data(tree.links())
126
        .join('path')
127
        .attr(
128
            'd',
129
            linkHorizontal<HierarchyPointLink<TreeStructure>, HierarchyPointNode<TreeStructure>>()
130
                .x(d => d.y)
131
                .y(d => d.x)
132
        );
133
134
    const node = g
135
        .append('g')
136
        .attr('stroke-linejoin', 'round')
137
        .attr('stroke-width', 3)
138
        .selectAll('g')
139
        .data(tree.descendants())
140
        .join('g')
141
        .attr('transform', d => transformDiagramElement(d.y, d.x, drawToLeft));
142
143
    node.append('text')
144
        .attr('dy', '0.31em')
145
        .attr('x', 0)
146
        .attr('text-anchor', 'middle')
147
        .style('background-color', '#ffffff')
148
        .text(node => node.data.name)
149
        .clone(true)
150
        .lower()
151
        .attr('stroke-width', 4)
152
        .attr('stroke', 'white');
153
154
    return svg.node();
155
}
156
157
function transformDiagramElement(xOffset: number, yOffset: number, drawToLeft: boolean) {
158
    return `translate(${xOffset},${yOffset}) ${drawToLeft ? 'rotate(180)' : ''}`;
159
}
160
161
function createDiagrams(
162
    container: ReturnType<typeof selectDetailsZoom>,
163
    consumersData: TreeStructure,
164
    providersData: TreeStructure,
165
    width: number,
166
    height: number
167
) {
168
    const consumersTree = createTree(consumersData);
169
    const providersTree = createTree(providersData);
170
171
    const consumersRootYPosition = getRootYPosition(consumersTree);
172
    const providersRootYPosition = getRootYPosition(providersTree);
173
174
    const rootNodeYOffset = Math.max(consumersRootYPosition, providersRootYPosition);
175
176
    const consumersDiagram = createDiagram(consumersTree, width, height, rootNodeYOffset, true);
177
    const providersDiagram = createDiagram(providersTree, width, height, rootNodeYOffset);
178
179
    if (providersDiagram) {
180
        container.append<SVGSVGElement>(() => providersDiagram);
181
    }
182
183
    if (consumersDiagram) {
184
        container.append<SVGSVGElement>(() => consumersDiagram);
185
    }
186
}
187
188
export function initializeDetailsView(node: TreeNode) {
189
    const consumerNodes = mapNodeToTreeStructure(node, 'consumers');
190
    const providerNodes = mapNodeToTreeStructure(node, 'providers');
191
192
    const detailsViewContainer = selectDetailsViewContainer();
193
    const width = Number(detailsViewContainer.attr('width'));
194
    const height = Number(detailsViewContainer.attr('height'));
195
196
    const detailsContainerDiv = selectDetailsContainerDiv().node();
197
198
    const detailsZoom = selectDetailsZoom();
199
    createDiagrams(detailsZoom, consumerNodes, providerNodes, width, height);
200
    switchDetailsVisibility();
201
    const { width: containerWidth, height: containerHeight } = detailsContainerDiv
202
        ? detailsContainerDiv.getBoundingClientRect()
203
        : { width: 0, height: 0 };
204
    resetZoomPosition(containerWidth, containerHeight);
205
}
206
207
export function shutdownDetailsView() {
208
    switchDetailsVisibility();
209
    deleteDiagrams();
210
}
211
212
function deleteDiagrams() {
213
    selectDetailsZoom()
214
        .transition()
215
        .selectAll('*')
216
        .remove();
217
}
218
219
export function switchDetailsVisibility() {
220
    const container = selectDetailsContainerDiv();
221
    const isVisible = container.style('display') === 'block';
222
    if (isVisible) {
223
        container
224
            .transition()
225
            .duration(FAST_TRANSITION_DURATION)
226
            .style('opacity', 0)
227
            .transition()
228
            .style('display', 'none');
229
    } else {
230
        container
231
            .style('display', 'block')
232
            .transition()
233
            .duration(FAST_TRANSITION_DURATION)
234
            .style('opacity', 1);
235
    }
236
}
237