Passed
Push — master ( 58a9e6...52bfcc )
by
unknown
02:37
created

DetailsDrawHelpers.ts ➔ createDiagram   B

Complexity

Conditions 2

Size

Total Lines 58
Code Lines 53

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 53
dl 0
loc 58
rs 8.5381
c 0
b 0
f 0
cc 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    container.call(
49
        zoom<SVGSVGElement, unknown>().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, linksType: 'consumers' | 'providers'): TreeStructure {
63
    node.isVisited = true;
64
    const unvisitedLinks = node[linksType].filter((linkedNode: TreeNodeWithVisitedFlag) => !linkedNode.isVisited && linkedNode[linksType]);
65
    const children = unvisitedLinks.map(nestedNode => mapNodeToTreeStructure(nestedNode, linksType));
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
    rootNodeYOffset: number,
98
    drawToLeft: boolean = false
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 + rootNodeYOffset} ${diagramWidth} ${containerHeight}`);
109
110
    const g = svg
111
        .append('g')
112
        .attr('font-size', 15)
113
        .attr('transform', transformDiagramElement(0, rootNodeYOffset, drawToLeft));
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<HierarchyPointLink<TreeStructure>, HierarchyPointNode<TreeStructure>>()
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 => transformDiagramElement(d.y, d.x, drawToLeft));
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 transformDiagramElement(xOffset: number, yOffset: number, drawToLeft: boolean) {
153
    return `translate(${xOffset},${yOffset}) ${drawToLeft ? 'rotate(180)' : ''}`;
154
}
155
156
function createDiagrams(
157
    container: ReturnType<typeof selectDetailsViewContainer>,
158
    consumersData: TreeStructure,
159
    providersData: TreeStructure,
160
    width: number,
161
    height: number
162
) {
163
    const consumersTree = createTree(consumersData);
164
    const providersTree = createTree(providersData);
165
166
    const consumersRootYPosition = getRootYPosition(consumersTree);
167
    const providersRootYPosition = getRootYPosition(providersTree);
168
169
    const rootNodeYOffset = Math.max(consumersRootYPosition, providersRootYPosition);
170
171
    const consumersDiagram = createDiagram(consumersTree, width, height, rootNodeYOffset, true);
172
    const providersDiagram = createDiagram(providersTree, width, height, rootNodeYOffset);
173
174
    if (providersDiagram) {
175
        container.append<SVGSVGElement>(() => providersDiagram);
176
    }
177
178
    if (consumersDiagram) {
179
        container.append<SVGSVGElement>(() => consumersDiagram);
180
    }
181
}
182
183
export function initializeDetailsView(node: TreeNode) {
184
    const consumerNodes = mapNodeToTreeStructure(node, 'consumers');
185
    const providerNodes = mapNodeToTreeStructure(node, 'providers');
186
187
    const detailsViewContainer = selectDetailsViewContainer();
188
    const width = Number(detailsViewContainer.attr('width'));
189
    const height = Number(detailsViewContainer.attr('height'));
190
191
    const detailsZoom = selectDetailsZoom();
192
193
    createDiagrams(detailsZoom, consumerNodes, providerNodes, width, height);
194
    switchDetailsVisibility();
195
}
196
197
export function shutdownDetailsView() {
198
    switchDetailsVisibility();
199
    deleteDiagrams();
200
}
201
202
function deleteDiagrams() {
203
    selectDetailsZoom()
204
        .transition()
205
        .selectAll('*')
206
        .remove();
207
}
208
209
export function switchDetailsVisibility() {
210
    const container = selectDetailsContainerDiv();
211
    const isVisible = container.style('display') === 'block';
212
    if (isVisible) {
213
        container
214
            .transition()
215
            .duration(FAST_TRANSITION_DURATION)
216
            .style('opacity', 0)
217
            .transition()
218
            .style('display', 'none');
219
    } else {
220
        container
221
            .style('display', 'block')
222
            .transition()
223
            .duration(FAST_TRANSITION_DURATION)
224
            .style('opacity', 1);
225
    }
226
}
227