Network viewer UI minor fixes (#16880)
* new viewer * minor fixes * update on resize * send custom charts
Costa Tsaousis committed
Jan 31, 2024 at 11:01 UTC
056e88335f8f0268c27d42c063e2938c0f0c2b14
3 files changed
+392
-251
collectors/apps.plugin/apps_plugin.c
+1
@@ -5264,6 +5264,7 @@ static bool apps_plugin_exit = false;
5264
int main(int argc, char **argv) {
5265
clocks_init();
5266
nd_log_initialize_for_external_plugins("apps.plugin");
5267
+ for_each_open_fd(OPEN_FD_ACTION_CLOSE, OPEN_FD_EXCLUDE_STDIN|OPEN_FD_EXCLUDE_STDOUT|OPEN_FD_EXCLUDE_STDERR);
5268
5269
pagesize = (size_t)sysconf(_SC_PAGESIZE);
5270
collectors/network-viewer.plugin/network-viewer.c
+17
-4
@@ -77,8 +77,14 @@ static void local_socket_to_array(struct local_socket_state *ls, struct local_so
77
buffer_json_add_array_item_string(wb, TCP_STATE_2str(n->state));
78
else
79
buffer_json_add_array_item_string(wb, "stateless");
80
+
81
buffer_json_add_array_item_uint64(wb, n->pid);
81
- buffer_json_add_array_item_string(wb, n->comm);
82
+
83
+ if(!n->comm[0])
84
+ buffer_json_add_array_item_string(wb, "[unknown]");
85
+ else
86
+ buffer_json_add_array_item_string(wb, n->comm);
87
+
88
buffer_json_add_array_item_string(wb, n->cmdline);
89
buffer_json_add_array_item_string(wb, local_address);
90
buffer_json_add_array_item_uint64(wb, n->local.port);
@@ -272,12 +278,21 @@ void network_viewer_function(const char *transaction, char *function __maybe_unu
278
buffer_json_object_close(wb); // columns
279
buffer_json_member_add_string(wb, "default_sort_column", "Direction");
280
281
+ buffer_json_member_add_object(wb, "custom_charts");
282
+ {
283
+ buffer_json_member_add_object(wb, "Network Map");
284
+ {
285
+ buffer_json_member_add_string(wb, "type", "network-viewer");
286
+ }
287
+ buffer_json_object_close(wb);
288
+ }
289
+ buffer_json_object_close(wb); // custom_charts
290
+
291
buffer_json_member_add_object(wb, "charts");
292
{
293
// Data Collection Age chart
294
buffer_json_member_add_object(wb, "Count");
295
{
280
- buffer_json_member_add_string(wb, "name", "Connections");
296
buffer_json_member_add_string(wb, "type", "stacked-bar");
297
buffer_json_member_add_array(wb, "columns");
298
{
@@ -290,7 +305,6 @@ void network_viewer_function(const char *transaction, char *function __maybe_unu
305
// Streaming Age chart
306
buffer_json_member_add_object(wb, "Count");
307
{
293
- buffer_json_member_add_string(wb, "name", "Connections");
308
buffer_json_member_add_string(wb, "type", "stacked-bar");
309
buffer_json_member_add_array(wb, "columns");
310
{
@@ -303,7 +317,6 @@ void network_viewer_function(const char *transaction, char *function __maybe_unu
317
// DB Duration
318
buffer_json_member_add_object(wb, "Count");
319
{
306
- buffer_json_member_add_string(wb, "name", "Connections");
320
buffer_json_member_add_string(wb, "type", "stacked-bar");
321
buffer_json_member_add_array(wb, "columns");
322
{
collectors/network-viewer.plugin/viewer.html
+374
-247
@@ -12,6 +12,7 @@
12
height: 100%;
13
width: 100%;
14
}
15
+
16
#d3-canvas {
17
height: 100%;
18
width: 100%;
@@ -20,352 +21,478 @@
21
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400&display=swap" rel="stylesheet">
22
<!-- Include D3.js -->
23
<script src="https://d3js.org/d3.v7.min.js"></script>
23
- <script>
24
-
25
- // The transformData function
26
- function transformData(dataPayload, desiredColumnNames, columns) {
27
- console.log(dataPayload);
28
-
29
- const transformedData = [];
30
-
31
- // Create a map to store data per application
32
- const appMap = new Map();
33
-
34
- dataPayload.data.forEach(row => {
35
- const rowData = {};
36
- desiredColumnNames.forEach(columnName => {
37
- const columnIndex = columns[columnName].index;
38
- rowData[columnName] = row[columnIndex];
39
- });
40
-
41
- const appName = rowData['Process'];
42
- if (!appMap.has(appName)) {
43
- appMap.set(appName, {
44
- listenCount: 0,
45
- inboundCount: 0,
46
- outboundCount: 0,
47
- localCount: 0,
48
- privateCount: 0,
49
- publicCount: 0,
50
- totalCount: 0
51
- });
52
- }
53
-
54
- const appData = appMap.get(appName);
55
- appData.totalCount++;
56
-
57
- if (rowData['Direction'] === 'listen') {
58
- appData.listenCount++;
59
- }
60
- else if (rowData['Direction'] === 'local') {
61
- appData.localCount++;
62
- }
63
- else if (rowData['Direction'] === 'inbound') {
64
- appData.inboundCount++;
65
- }
66
- else if (rowData['Direction'] === 'outbound') {
67
- appData.outboundCount++;
68
- }
69
-
70
- if (rowData['RemoteAddressSpace'] === 'public') {
71
- appData.publicCount++;
72
- }
73
- else if (rowData['RemoteAddressSpace'] === 'private') {
74
- appData.privateCount++;
75
- }
76
- });
77
-
78
- // Convert the map to an array format
79
- for (let [appName, appData] of appMap) {
80
- transformedData.push({
81
- name: appName,
82
- ...appData
83
- });
84
- }
85
-
86
- console.log(transformedData);
87
-
88
- return transformedData;
89
- }
90
- </script>
24
</head>
25
<body>
26
<div id="d3-canvas"></div> <!-- Div for D3 rendering -->
27
28
<script>
96
- // Function to draw the circles and labels for each application with border forces
97
- function drawApplications(data) {
98
- console.log(data);
99
- console.log(data.length)
100
- const maxTotalCount = d3.max(data, d => d.totalCount);
101
- const maxXCount = d3.max(data, d => Math.abs(Math.max(d.publicCount, d.privateCount)));
102
- const maxStrength = 0.005;
103
- const borderPadding = 40;
104
- console.log("maxTotalCount", maxTotalCount);
105
- console.log("maxXCount", maxXCount);
106
-
107
- const max = {
108
- totalCount: d3.max(data, d => d.totalCount),
109
- localCount: d3.max(data, d => d.localCount),
110
- listenCount: d3.max(data, d => d.listenCount),
111
- privateCount: d3.max(data, d => d.privateCount),
112
- publicCount: d3.max(data, d => d.publicCount),
113
- inboundCount: d3.max(data, d => d.inboundCount),
114
- outboundCount: d3.max(data, d => d.outboundCount),
115
- }
116
-
117
- const w = window.innerWidth;
118
- const h = window.innerHeight;
119
- const cw = w / 2;
120
- const ch = h / 2;
121
- console.log(w, h, cw, ch);
122
- console.log(w, h, cw, ch);
123
-
124
- const minSize = 13;
125
- const maxSize = Math.max(5, Math.min(w, h) / data.length) + minSize; // Avoid division by zero or too large sizes
126
-
127
- const publicColor = "#bbaa00";
128
- const privateColor = "#5555ff";
129
- const serverColor = "#009900";
130
- const clientColor = "#990000";
29
+ function transformData(dataPayload) {
30
+ // console.log("dataPayload", dataPayload);
31
+ const desiredColumns = ["Direction", "Protocol", "Namespace", "Process", "CommandLine", "LocalIP", "LocalPort", "RemoteIP", "RemotePort", "LocalAddressSpace", "RemoteAddressSpace"];
32
+
33
+ const transformedData = [];
34
+ const appMap = new Map();
35
+
36
+ dataPayload.data.forEach(row => {
37
+ const rowData = {};
38
+ desiredColumns.forEach(columnName => {
39
+ const columnIndex = dataPayload.columns[columnName].index;
40
+ rowData[columnName] = row[columnIndex];
41
+ });
42
132
- function hexToHalfOpacityRGBA(hex) {
133
- // Ensure the hex color is valid
134
- if (hex.length !== 7 || hex[0] !== '#') {
135
- throw new Error('Invalid hex color format');
43
+ const appName = rowData['Process'];
44
+ if (!appMap.has(appName)) {
45
+ appMap.set(appName, {
46
+ counts: {
47
+ listen: 0,
48
+ inbound: 0,
49
+ outbound: 0,
50
+ local: 0,
51
+ private: 0,
52
+ public: 0,
53
+ total: 0
54
+ }
55
+ });
56
}
57
138
- // Extract the red, green, and blue components
139
- const r = parseInt(hex.slice(1, 3), 16);
140
- const g = parseInt(hex.slice(3, 5), 16);
141
- const b = parseInt(hex.slice(5, 7), 16);
58
+ const appData = appMap.get(appName);
59
+ appData.counts.total++;
60
+
61
+ if (rowData['Direction'] === 'listen')
62
+ appData.counts.listen++;
63
+ else if (rowData['Direction'] === 'local')
64
+ appData.counts.local++;
65
+ else if (rowData['Direction'] === 'inbound')
66
+ appData.counts.inbound++;
67
+ else if (rowData['Direction'] === 'outbound')
68
+ appData.counts.outbound++;
69
+
70
+ if (rowData['RemoteAddressSpace'] === 'public')
71
+ appData.counts.public++;
72
+ else if (rowData['RemoteAddressSpace'] === 'private')
73
+ appData.counts.private++;
74
+ });
75
143
- // Return the color in RGBA format with half opacity
144
- return `rgba(${r}, ${g}, ${b}, 0.5)`;
76
+ // Convert the map to an array format
77
+ for (let [appName, appData] of appMap) {
78
+ transformedData.push({
79
+ name: appName,
80
+ ...appData
81
+ });
82
}
83
147
- const pieColors = d3.scaleOrdinal()
148
- .domain(["publicCount", "privateCount", "listenInboundCount", "outboundCount", "others"])
149
- .range([publicColor, privateColor, serverColor, clientColor, "#666666"]); // Example colors
84
+ // console.log("transformedData", transformedData);
85
+ return transformedData;
86
+ }
87
151
- const pie = d3.pie().value(d => d.value);
152
- const arc = d3.arc();
88
+ function normalizeData(data, w, h, borderPadding) {
89
+ const cw = w / 2 - borderPadding;
90
+ const ch = h / 2 - borderPadding;
91
154
- function getPieData(d) {
155
- const others = d.totalCount - (d.publicCount + d.privateCount + d.listenCount + d.inboundCount + d.outboundCount);
156
- return [
157
- {value: d.publicCount},
158
- {value: d.privateCount},
159
- {value: d.listenCount + d.inboundCount},
160
- {value: d.outboundCount},
161
- {value: others > 0 ? others : 0}
162
- ];
163
- }
92
+ const minSize = 13;
93
+ const maxSize = Math.max(5, Math.min(w, h) / data.length) + minSize;
94
165
- const forceStrengthScale = d3.scaleLinear()
166
- .domain([0, maxTotalCount])
167
- .range([0, maxStrength]);
95
+ const max = {
96
+ total: d3.max(data, d => d.counts.total),
97
+ local: d3.max(data, d => d.counts.local),
98
+ listen: d3.max(data, d => d.counts.listen),
99
+ private: d3.max(data, d => d.counts.private),
100
+ public: d3.max(data, d => d.counts.public),
101
+ inbound: d3.max(data, d => d.counts.inbound),
102
+ outbound: d3.max(data, d => d.counts.outbound),
103
+ }
104
105
const circleSize = d3.scaleLog()
170
- .domain([1, maxTotalCount]) // Assuming maxTotalCount is the maximum value in your data
106
+ .domain([1, max.total])
107
.range([minSize, maxSize])
108
.clamp(true); // Clamps the output so that it stays within the range
109
174
- const logScaleRight = d3.scaleLog().domain([1, max.publicCount + 1]).range([0, cw - borderPadding]);
175
- const logScaleLeft = d3.scaleLog().domain([1, max.privateCount + 1]).range([0, cw - borderPadding]);
176
- const logScaleTop = d3.scaleLog().domain([1, max.outboundCount + 1]).range([0, ch - borderPadding]);
177
- const logScaleBottom = d3.scaleLog().domain([1, (max.listenCount + max.inboundCount) / 2 + 1]).range([0, ch - borderPadding]);
110
+ const logScaleRight = d3.scaleLog().domain([1, max.public + 1]).range([0, cw]);
111
+ const logScaleLeft = d3.scaleLog().domain([1, max.private + 1]).range([0, cw]);
112
+ const logScaleTop = d3.scaleLog().domain([1, max.outbound + 1]).range([0, ch]);
113
+ const logScaleBottom = d3.scaleLog().domain([1, (max.listen + max.inbound) / 2 + 1]).range([0, ch]);
114
115
data.forEach((d, i) => {
180
- const forces = {
181
- total: d.totalCount / max.totalCount,
182
- local: d.localCount / max.localCount,
183
- listen: d.listenCount / max.listenCount,
184
- private: d.privateCount / max.privateCount,
185
- public: d.publicCount / max.publicCount,
186
- inbound: d.inboundCount / max.inboundCount,
187
- outbound: d.outboundCount / max.outboundCount,
116
+ d.forces = {
117
+ total: d.counts.total / max.total,
118
+ local: d.counts.local / max.local,
119
+ listen: d.counts.listen / max.listen,
120
+ private: d.counts.private / max.private,
121
+ public: d.counts.public / max.public,
122
+ inbound: d.counts.inbound / max.inbound,
123
+ outbound: d.counts.outbound / max.outbound,
124
}
125
190
- const pos = {
191
- right: logScaleRight(d.publicCount + 1),
192
- left: logScaleLeft(d.privateCount + 1),
193
- top: logScaleTop(d.outboundCount + 1),
194
- bottom: logScaleBottom((d.listenCount + d.inboundCount) / 2 + 1),
126
+ d.pos = {
127
+ // we add 1 to avoid log(0)
128
+ right: logScaleRight(d.counts.public + 1),
129
+ left: logScaleLeft(d.counts.private + 1),
130
+ top: logScaleTop(d.counts.outbound + 1),
131
+ bottom: logScaleBottom((d.counts.listen + d.counts.inbound) / 2 + 1),
132
};
133
197
- d.targetX = cw + pos.right - pos.left;
198
- d.targetY = ch + pos.bottom - pos.top;
134
+ const others = d.counts.total - (d.counts.public + d.counts.private + d.counts.listen + d.counts.inbound + d.counts.outbound);
135
+ d.d3 = {
136
+ x: borderPadding + cw + d.pos.right - d.pos.left,
137
+ y: borderPadding + ch + d.pos.bottom - d.pos.top,
138
+ size: circleSize(d.counts.total),
139
+ pie: [
140
+ { value: d.counts.public },
141
+ { value: d.counts.private },
142
+ { value: d.counts.listen + d.counts.inbound },
143
+ { value: d.counts.outbound },
144
+ { value: others > 0 ? others : 0 },
145
+ ]
146
+ }
147
+
148
+ if(d.d3.x - d.d3.size / 2 < borderPadding)
149
+ d.d3.x = borderPadding + d.d3.size * 2;
150
+
151
+ if(d.d3.x + d.d3.size / 2 > w)
152
+ d.d3.x = w - d.d3.size * 2;
153
+
154
+ if(d.d3.y - d.d3.size / 2 < borderPadding)
155
+ d.d3.y = borderPadding + d.d3.size * 2;
156
200
- if(d.name === 'deluged')
201
- console.log("object", d, "forces", forces, "pos", pos);
157
+ if(d.d3.y + d.d3.size / 2 > h)
158
+ d.d3.y = h - d.d3.size * 2;
159
+
160
+ // if (d.name === 'telnet')
161
+ // console.log("object", d, "cw", cw, "ch", ch);
162
});
163
164
+ return data;
165
+ }
166
205
- console.log(data);
167
+ const themes = {
168
+ dark: {
169
+ publicColor: "#bb9900",
170
+ privateColor: "#323299",
171
+ serverColor: "#008800",
172
+ clientColor: "#994433",
173
+ otherColor: "#454545",
174
+ backgroundColor: "black",
175
+ appFontColor: "#bbbbbb",
176
+ appFontFamily: 'IBM Plex Sans',
177
+ appFontSize: "12px",
178
+ appFontWeight: "regular",
179
+ borderFontColor: "#aaaaaa",
180
+ borderFontFamily: 'IBM Plex Sans',
181
+ borderFontSize: "14px",
182
+ borderFontWeight: "bold",
183
+ },
184
+ light: {
185
+ publicColor: "#bbaa00",
186
+ privateColor: "#5555ff",
187
+ serverColor: "#009900",
188
+ clientColor: "#990000",
189
+ otherColor: "#666666",
190
+ backgroundColor: "white",
191
+ appFontColor: "black",
192
+ appFontFamily: 'IBM Plex Sans',
193
+ appFontSize: "12px",
194
+ appFontWeight: "bold",
195
+ borderFontColor: "white",
196
+ borderFontFamily: 'IBM Plex Sans',
197
+ borderFontSize: "14px",
198
+ borderFontWeight: "bold",
199
+ }
200
+ }
201
207
- const svg = d3.select('#d3-canvas').append('svg')
208
- .attr('width', '100%')
209
- .attr('height', '100%');
202
+ function hexToHalfOpacityRGBA(hex) {
203
+ if (hex.length !== 7 || hex[0] !== '#')
204
+ throw new Error('Invalid hex color format');
205
+
206
+ const r = parseInt(hex.slice(1, 3), 16);
207
+ const g = parseInt(hex.slice(3, 5), 16);
208
+ const b = parseInt(hex.slice(5, 7), 16);
209
+ return `rgba(${r}, ${g}, ${b}, 0.5)`;
210
+ }
211
+
212
+ function drawInitialChart(svg, data, w, h, borderPadding, theme) {
213
+ const cw = w / 2;
214
+ const ch = h / 2;
215
+
216
+ document.body.style.backgroundColor = theme.backgroundColor;
217
211
- // Top area - Clients
218
svg.append('rect')
219
.attr('x', 0)
220
.attr('y', 0)
221
.attr('width', '100%')
216
- .attr('height', borderPadding / 2) // Adjust height as needed
217
- .style('fill', hexToHalfOpacityRGBA(clientColor));
222
+ .attr('height', borderPadding / 2)
223
+ .style('fill', hexToHalfOpacityRGBA(theme.clientColor));
224
225
svg.append('text')
226
.text('Clients')
227
.attr('x', '50%')
222
- .attr('y', borderPadding / 2 - 4) // Adjust y position based on the rectangle's height
228
+ .attr('y', borderPadding / 2 - 4)
229
.attr('text-anchor', 'middle')
224
- .style('font-family', 'IBM Plex Sans')
225
- .style('font-size', '14px')
226
- .style('font-weight', 'bold'); // Make the font bold
230
+ .style('font-family', theme.borderFontFamily)
231
+ .style('font-size', theme.borderFontSize)
232
+ .style('font-weight', theme.borderFontWeight)
233
+ .style('fill', theme.borderFontColor);
234
228
- // Bottom area - Servers
235
svg.append('rect')
236
.attr('x', 0)
237
.attr('y', h - borderPadding / 2)
238
.attr('width', '100%')
233
- .attr('height', borderPadding / 2) // Adjust height as needed
234
- .style('fill', hexToHalfOpacityRGBA(serverColor));
239
+ .attr('height', borderPadding / 2)
240
+ .style('fill', hexToHalfOpacityRGBA(theme.serverColor));
241
242
svg.append('text')
243
.text('Servers')
244
.attr('x', '50%')
239
- .attr('y', h - borderPadding / 2 + 16) // Adjust y position based on the rectangle's height
245
+ .attr('y', h - borderPadding / 2 + 16)
246
.attr('text-anchor', 'middle')
241
- .style('font-family', 'IBM Plex Sans')
242
- .style('font-size', '14px')
243
- .style('font-weight', 'bold'); // Make the font bold
247
+ .style('font-family', theme.borderFontFamily)
248
+ .style('font-size', theme.borderFontSize)
249
+ .style('font-weight', theme.borderFontWeight)
250
+ .style('fill', theme.borderFontColor);
251
252
svg.append('rect')
246
- .attr('x', w - borderPadding / 2) // Position it close to the right edge
253
+ .attr('x', w - borderPadding / 2)
254
.attr('y', 0)
248
- .attr('width', borderPadding / 2) // Width of the border area
255
+ .attr('width', borderPadding / 2)
256
.attr('height', '100%')
250
- .style('fill', hexToHalfOpacityRGBA(publicColor));
257
+ .style('fill', hexToHalfOpacityRGBA(theme.publicColor));
258
259
svg.append('text')
260
.text('Public')
254
- .attr('x', w - (borderPadding / 2)) // Position close to the right edge
255
- .attr('y', ch - 10) // Vertically centered
261
+ .attr('x', w - (borderPadding / 2))
262
+ .attr('y', ch - 10)
263
.attr('text-anchor', 'middle')
257
- .attr('dominant-baseline', 'middle') // Center alignment of the text
258
- .attr('transform', `rotate(90, ${w - (borderPadding / 2)}, ${ch})`) // Rotate around the text's center
259
- .style('font-family', 'IBM Plex Sans')
260
- .style('font-size', '14px')
261
- .style('font-weight', 'bold'); // Make the font bold
264
+ .attr('dominant-baseline', 'middle')
265
+ .attr('transform', `rotate(90, ${w - (borderPadding / 2)}, ${ch})`)
266
+ .style('font-family', theme.borderFontFamily)
267
+ .style('font-size', theme.borderFontSize)
268
+ .style('font-weight', theme.borderFontWeight)
269
+ .style('fill', theme.borderFontColor);
270
271
svg.append('rect')
264
- .attr('x', 0) // Positioned at the left edge
272
+ .attr('x', 0)
273
.attr('y', 0)
266
- .attr('width', borderPadding / 2) // Width of the border area
274
+ .attr('width', borderPadding / 2)
275
.attr('height', '100%')
268
- .style('fill', hexToHalfOpacityRGBA(privateColor));
276
+ .style('fill', hexToHalfOpacityRGBA(theme.privateColor));
277
278
svg.append('text')
279
.text('Private')
272
- .attr('x', borderPadding / 2) // Position close to the left edge
273
- .attr('y', ch) // Vertically centered
280
+ .attr('x', borderPadding / 2)
281
+ .attr('y', ch)
282
.attr('text-anchor', 'middle')
275
- .attr('dominant-baseline', 'middle') // Center alignment of the text
276
- .attr('transform', `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`) // Rotate around the text's center
277
- .style('font-family', 'IBM Plex Sans')
278
- .style('font-size', '14px')
279
- .style('font-weight', 'bold'); // Make the font bold
280
-
281
- function boundaryForce(alpha) {
282
- return function(d) {
283
- const nodeRadius = circleSize(d.totalCount) / 2;
284
- d.x = Math.max(borderPadding + nodeRadius, Math.min(w - borderPadding - nodeRadius, d.x));
285
- d.y = Math.max(borderPadding + nodeRadius, Math.min(h - borderPadding - nodeRadius, d.y));
286
- };
287
- }
283
+ .attr('dominant-baseline', 'middle')
284
+ .attr('transform', `rotate(-90, ${borderPadding / 2 - 10}, ${ch})`)
285
+ .style('font-family', theme.borderFontFamily)
286
+ .style('font-size', theme.borderFontSize)
287
+ .style('font-weight', theme.borderFontWeight)
288
+ .style('fill', theme.borderFontColor);
289
+ }
290
+
291
+ let positionsMap = new Map();
292
+ function saveCurrentPositions(svg) {
293
+ svg.selectAll('.app').each(function(d) {
294
+ if (d) {
295
+ positionsMap.set(d.name, { x: d.x, y: d.y });
296
+ }
297
+ });
298
+ }
299
+
300
+ function updateApps(svg, data, w, h, borderPadding, theme) {
301
+ const cw = w / 2;
302
+ const ch = h / 2;
303
289
- const simulation = d3.forceSimulation(data)
290
- .force('center', d3.forceCenter(window.innerWidth / 2, window.innerHeight / 2).strength(1)) // Scale center force strength
291
- .force("x", d3.forceX(d => d.targetX).strength(0.3))
292
- .force("y", d3.forceY(d => d.targetY).strength(0.3))
293
- .force("charge", d3.forceManyBody().strength(-0.5))
294
- .force("collide", d3.forceCollide(d => circleSize(d.totalCount) * 1.1 + 15).strength(1))
295
- .force("boundary", boundaryForce(0.5))
296
- .on('tick', ticked);
304
+ saveCurrentPositions(svg);
305
+ //svg.selectAll('.app').remove();
306
+
307
+ const pieColors = d3.scaleOrdinal()
308
+ .domain(["public", "private", "listenInbound", "outbound", "others"])
309
+ .range([theme.publicColor, theme.privateColor, theme.serverColor, theme.clientColor, theme.otherColor]);
310
+
311
+ const pie = d3.pie().value(d => d.value);
312
+ const arc = d3.arc();
313
314
+ // Binding data with key function
315
const app = svg.selectAll('.app')
299
- .data(data)
300
- .enter().append('g')
316
+ .data(data, d => d.name);
317
+
318
+ // Remove any elements that no longer have data associated with them
319
+ app.exit()
320
+ .transition()
321
+ .style("opacity", 0)
322
+ .remove();
323
+
324
+ // Enter selection for new data points
325
+ const appEnter = app.enter()
326
+ .append('g')
327
.attr('class', 'app')
302
- .call(d3.drag()
303
- .on('start', dragstarted)
304
- .on('drag', dragged)
305
- .on('end', dragended));
328
+ .attr('transform', `translate(${cw}, ${ch})`); // Start from center
329
307
- app.each(function(d) {
330
+ // Initialize new elements
331
+ appEnter.each(function (d) {
332
const group = d3.select(this);
309
- const pieData = pie(getPieData(d));
310
- const radius = circleSize(d.totalCount);
333
+ const oldPos = positionsMap.get(d.name) || { x: cw, y: ch };
334
+
335
+ d.x = oldPos.x;
336
+ d.y = oldPos.y;
337
+
338
+ const pieData = pie(d.d3.pie);
339
+ const radius = d.d3.size;
340
341
group.selectAll('path')
342
.data(pieData)
343
.enter().append('path')
344
+ .transition()
345
.attr('d', arc.innerRadius(0).outerRadius(radius))
346
.attr('fill', (d, i) => pieColors(i));
347
+
348
+ group.append('text')
349
+ .text(d => d.name)
350
+ .attr('text-anchor', 'middle')
351
+ .attr('y', radius + 10)
352
+ .style('font-family', theme.appFontFamily)
353
+ .style('font-size', theme.appFontSize)
354
+ .style('font-weight', theme.appFontWeight)
355
+ .style('fill', theme.appFontColor);
356
});
357
319
- app.append('text')
320
- .text(d => d.name)
321
- .attr('text-anchor', 'middle')
322
- .attr('y', d => circleSize(d.totalCount) + 10)
323
- .style('font-family', 'IBM Plex Sans') // Set the font family
324
- .style('font-size', '12px') // Set the font size
325
- .style('font-weight', 'bold'); // Make the font bold
358
+ // Update selection
359
+ app.each(function (d) {
360
+ const group = d3.select(this);
361
+ const oldPos = positionsMap.get(d.name) || { x: cw, y: ch };
362
327
- // Initialize app positions at the center
328
- app.attr('transform', `translate(${window.innerWidth / 2}, ${window.innerHeight / 2})`);
363
+ d.x = oldPos.x;
364
+ d.y = oldPos.y;
365
330
- function ticked() {
331
- app.attr('transform', d => `translate(${d.x}, ${d.y})`);
332
- }
366
+ group.selectAll('path')
367
+ .data(pie(d.d3.pie))
368
+ .transition()
369
+ .attr('d', arc.innerRadius(0).outerRadius(d.d3.size));
370
+
371
+ group.select('text')
372
+ .attr('y', d.d3.size + 10);
373
+ });
374
+
375
+ // Transition for new elements
376
+ appEnter.transition()
377
+ .attr('transform', d => `translate(${d.d3.x}, ${d.d3.y})`);
378
334
- function dragstarted(event, d) {
335
- if (!event.active) simulation.alphaTarget(1).restart();
336
- d.fx = d.x;
337
- d.fy = d.y;
379
+ // Merge the enter and update selections
380
+ const mergedApp = appEnter.merge(app);
381
+
382
+ // Apply the drag behavior to the merged selection
383
+ mergedApp.call(d3.drag()
384
+ .on('start', dragstarted)
385
+ .on('drag', dragged)
386
+ .on('end', dragended));
387
+
388
+ return mergedApp;
389
+ }
390
+
391
+ let initial = true;
392
+ let simulation;
393
+
394
+ function dragstarted(event, d) {
395
+ if (!event.active) simulation.alphaTarget(1).restart();
396
+ d.fx = d.x;
397
+ d.fy = d.y;
398
+ }
399
+
400
+ function dragged(event, d) {
401
+ d.fx = event.x;
402
+ d.fy = event.y;
403
+ }
404
+
405
+ function dragended(event, d) {
406
+ if (!event.active) simulation.alphaTarget(0);
407
+ d.fx = null;
408
+ d.fy = null;
409
+ }
410
+
411
+ // Function to draw the circles and labels for each application with border forces
412
+ function drawApplications(data, w, h, borderPadding, theme) {
413
+ let svg = d3.select('#d3-canvas').select('svg');
414
+ if(svg.empty()) {
415
+ svg = d3.select('#d3-canvas').append('svg')
416
+ .attr('width', '100%')
417
+ .attr('height', '100%');
418
+
419
+ drawInitialChart(svg, data, w, h, borderPadding, theme);
420
}
421
340
- function dragged(event, d) {
341
- d.fx = event.x;
342
- d.fy = event.y;
422
+ const app = updateApps(svg, data, w, h, borderPadding, theme);
423
+
424
+ if(initial) {
425
+ simulation = d3.forceSimulation(data)
426
+ //.force('center', d3.forceCenter(cw, ch).strength(1))
427
+ .force("x", d3.forceX(d => d.d3.x).strength(0.05))
428
+ .force("y", d3.forceY(d => d.d3.y).strength(0.05))
429
+ //.force("charge", d3.forceManyBody().strength(-0.05))
430
+ .force("collide", d3.forceCollide(d => d.d3.size * 1.1 + 15).strength(1))
431
+ .on('tick', ticked);
432
+ }
433
+ else {
434
+ simulation.nodes(data)
435
+ .force("x", d3.forceX(d => d.d3.x).strength(0.02))
436
+ .force("y", d3.forceY(d => d.d3.y).strength(0.02))
437
+ .alpha(1).restart();
438
}
439
345
- function dragended(event, d) {
346
- if (!event.active) simulation.alphaTarget(0);
347
- d.fx = null;
348
- d.fy = null;
440
+ function ticked() {
441
+ app.attr('transform', d => `translate(${d.x}, ${d.y})`);
442
}
443
+
444
+ initial = false;
445
}
446
447
+ function redrawChart() {
448
+ const w = window.innerWidth;
449
+ const h = window.innerHeight;
450
+
451
+ // Update SVG dimensions
452
+ const svg = d3.select('#d3-canvas').select('svg')
453
+ .attr('width', w)
454
+ .attr('height', h);
455
+
456
+ // Redraw the chart with the new dimensions
457
+ fetchDataAndUpdateChart(); // This function should use the new 'w' and 'h'
458
+ }
459
+
460
+ // Debounce function to optimize performance
461
+ function debounce(func, timeout = 300) {
462
+ let timer;
463
+ return (...args) => {
464
+ clearTimeout(timer);
465
+ timer = setTimeout(() => { func.apply(this, args); }, timeout);
466
+ };
467
+ }
468
+
469
+ // Attach the event listener to the window resize event
470
+ window.addEventListener('resize', debounce(() => {
471
+ redrawChart();
472
+ }));
473
+
474
// Modify your fetchData function to call drawApplications after data transformation
353
- function fetchData() {
475
+ function fetchDataAndUpdateChart() {
476
fetch('http://localhost:19999/api/v1/function?function=network-viewer')
477
.then(response => response.json())
478
.then(data => {
357
- // Your existing code
358
- const desiredColumns = ["Direction", "Protocol", "Namespace", "Process", "CommandLine", "LocalIP", "LocalPort", "RemoteIP", "RemotePort", "LocalAddressSpace", "RemoteAddressSpace"];
359
- const transformed = transformData(data, desiredColumns, data.columns);
479
+ const transformed = transformData(data);
480
+
481
+ const w = window.innerWidth;
482
+ const h = window.innerHeight;
483
+ const borderPadding = 40;
484
361
- // Now draw the applications with border forces
362
- drawApplications(transformed);
485
+ const normalized = normalizeData(transformed, w, h, borderPadding);
486
+ drawApplications(normalized, w, h, borderPadding, themes.dark);
487
})
488
.catch(error => console.error('Error fetching data:', error));
489
}
490
367
- // Load data on start
368
- window.onload = fetchData;
491
+ // Initial load
492
+ window.onload = () => {
493
+ redrawChart();
494
+ setInterval(fetchDataAndUpdateChart, 2000); // You may need to adjust this part
495
+ };
496
</script>
497
</body>
498
</html>