Passed
Push — master ( 4f758b...87f5e4 )
by
unknown
02:52
created

DetailsDrawHelpers.ts ➔ resetZoomPosition   A

Complexity

Conditions 2

Size

Total Lines 11
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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