@cryptotaxi247 / netdata-1 / commits / 056e88335

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>