<!DOCTYPE html> <meta charset='utf-8'> <style> .links line { stroke: #343434; stroke-opacity: 1; } .health, .health_warn { font-family: LibreFranklin; font-size: 12px; font-weight: bold; font-style: normal; font-stretch: normal; text-align: center; } .health_warn { font-size: 15px; } .stat { font-family: LibreFranklin; font-size: 12px; font-weight: normal; font-style: normal; font-stretch: normal; line-height: 1.5; } .node_statistics { font-family: LibreFranklin; font-size: 12px; font-weight: normal; font-style: normal; font-stretch: normal; line-height: 1.5; } .node_statistics p { margin: 0 0 0 0; color: rgba(255, 255, 255, 0.8); } .primary, .secondary { font-family: LibreFranklin; font-size: 12px; font-weight: normal; font-style: normal; font-stretch: normal; line-height: 1.5; } </style> <svg width='960' height='700'></svg> <script src='https://d3js.org/d3.v4.min.js'></script> <script> var svg = d3.select('svg'), width = +svg.attr('width'), height = +svg.attr('height'); var color = d3.scaleOrdinal(d3.schemeCategory20); var simulation = d3.forceSimulation() .force('charge', d3.forceManyBody().strength(() => -50).distanceMin(() => 30)) .force('link', d3.forceLink().distance(() => 200).id(function(d) { return d.id; })) .force('collide', d3.forceCollide().radius(function(d) { return 200 + 0.5; }).iterations(2)) .force('center', d3.forceCenter(width * 1/3, height * 1/3)) function rightRoundedRect(x, y, width, height, radius) { return 'M' + x + ',' + y // Move to top left (absolute) + 'h ' + (width - radius) // Horizontal line to (relative) + 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + radius // Relative arc + 'v ' + (height - 2 * radius) // Vertical line to (relative) + 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + radius // Relative arch + 'h ' + (radius - width) // Horizontal lint to (relative) + 'z '; // path back to start } function leftRoundedRect(x, y, width, height, radius) { return 'M' + (x + width) + ',' + y // Move to (absolute) start at top-right + 'v ' + height // Vertical line to (relative) + 'h ' + (radius - width) // Horizontal line to (relative) + 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + -radius // Relative arc + 'v ' + -(height - 2 * radius) // Vertical line to (relative) + 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + -radius // Relative arch + 'z '; // path back to start } function topRoundedRect(x, y, width, height, radius) { return 'M' + x + ',' + -(y - height) // Move to (absolute) start at bottom-right + 'v ' + -(height - 2 * radius) // Vertical line to (relative) + 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + -radius // Relative arc + 'h ' + -(radius - width) // Horizontal line to (relative) + 'a ' + radius + ',' + radius + ' 0 0 1 ' + radius + ',' + radius // Relative arc + 'v ' + (height - 2 * radius) // Vertical line to (relative) + 'h ' + (radius - width) // Horizontal line to (relative) + 'z '; // path back to start } function bottomRoundedRect(x, y, width, height, radius) { return 'M' + x + ',' + -(y - height) // Move to (absolute) start at bottom-right + 'v ' + -(height - 2 * radius) // Vertical line to (relative) + 'h ' + (radius + width) // Horizontal line to (relative) + 'v ' + (height - 2 * radius) // Vertical line to (relative) + 'a ' + -radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + radius // Relative arc + 'h ' + (radius - width) // Horizontal line to (relative) + 'a ' + radius + ',' + radius + ' 0 0 1 ' + -radius + ',' + -radius // Relative arc + 'z '; // path back to start } function rect(x, y, width, height, radius) { return 'M' + x + ',' + -(y - height - 2 * radius) // Move to (absolute) start at bottom-right + 'v ' + -(height - 2 * radius) // Vertical line to (relative) + 'h ' + (width + radius + 2) // Horizontal line to (relative) + 'v ' + (height - 2 * radius) // Vertical line to (relative) + 'h ' + (radius - width) // Horizontal line to (relative) + 'z '; // path back to start } d3.json('services.json', function(error, graph) { if (error) throw error; function createNode(elm) { // Box where label will live elm.append('path') .attr('class', 'node') .attr('d', topRoundedRect('0', '0', 170, 47, 4)) .attr('stroke', '#343434') .attr('stroke-width', '1px') .attr('fill', '#464646') // Hover-over text for a node's label. var text = elm.append('g') text.append('text') .attr('class', 'info_text') .attr('x', '12') .attr('y', '30') .attr('text-anchor', 'start') .attr('fill', '#fff') .text(d => d.id) text.append('circle') .attr('class', 'alert') .attr('cx', function () { return d3.select(this.parentNode).select('.info_text').node().getBBox().width + 30 }) .attr('cy', '24') .attr('stroke-width', '0px') .attr('fill', (d) => d.id == 'Memcached' ? 'rgb(217, 77, 68)' : 'rgb(0,175,102)') .attr('r', '9px') // An icon or label that exists within the circle, inside the infobox text.append('text') .attr('class', 'health') .attr('x', function () { return d3.select(this.parentNode).select('.info_text').node().getBBox().width + 30 }) .attr('y', '29') .attr('text-anchor', 'middle') .attr('fill', '#fff') .text((d) => d.id == 'Memcached' ? '!' : '❤') // Box where stats will live var stats = elm.append('g'); stats.append('path') .attr('class', 'node') .attr('d', (d) => d.id == 'Percona' ? rect('0', '-39', 170, 78, 2) : bottomRoundedRect('0', '-39', 170, 78, 4)) .attr('stroke', '#343434') .attr('stroke-width', '1px') .attr('fill', '#464646') var html = stats .append('switch') .append('foreignObject') .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility') .attr('x', 12) .attr('y', 57) .attr('width', 160) .attr('height', 70) // From here everything will be rendered with react using a ref. // However for now these values are hard-coded. .append('xhtml:div') .attr('class', 'node_statistics') // Remove with react + dyanmic data. html.append('p') .text('CPU: 48%') html.append('p') .text('Memory: 54%') html.append('p') .text('Network: 1.75kb/sec') elm.call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)); elm.append('title') .text(function(d) { return d.id; }); } function createExtendedNode(elm) { elm.append('path') .attr('class', 'node') .attr('d', topRoundedRect('0', '0', 170, 47, 4)) .attr('stroke', '#343434') .attr('stroke-width', '1px') .attr('fill', '#464646') // Hover-over text for a node's label. var text = elm.append('g') text.append('text') .attr('class', 'info_text') .attr('x', '12') .attr('y', '30') .attr('text-anchor', 'start') .attr('fill', '#fff') .text(d => d.id) // Box where stats will live var stats = elm.append('g'); var primary = stats.append('g') primary.append('path') .attr('class', 'node') .attr('d', rect('0', '-39', 170, 114, 2)) .attr('stroke', '#343434') .attr('stroke-width', '1px') .attr('fill', '#464646') primary.append('text') .attr('class', 'primary') .attr('x', '12') .attr('y', '70') .attr('text-anchor', 'start') .attr('fill', '#fff') .text('Primary') primary.append('circle') .attr('class', 'alert') .attr('cx', function () { return d3.select(this.parentNode).select('.primary').node().getBBox().width + 30 }) .attr('cy', '64') .attr('stroke-width', '0px') .attr('fill', 'rgb(227, 130, 0)') .attr('r', '9px') // An icon or label that exists within the circle, inside the infobox primary.append('text') .attr('class', 'health_warn') .attr('x', function () { return d3.select(this.parentNode).select('.primary').node().getBBox().width + 30 }) .attr('y', '69') .attr('text-anchor', 'middle') .attr('fill', '#fff') .text('☇') var html = primary .append('switch') .append('foreignObject') .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility') .attr('x', 12) .attr('y', 87) .attr('width', 160) .attr('height', 70) // From here everything will be rendered with react using a ref. // However for now these values are hard-coded. .append('xhtml:div') .attr('class', 'node_statistics') // Remove with react + dyanmic data. html.append('p') .text('CPU: 48%') html.append('p') .text('Memory: 54%') html.append('p') .text('Network: 1.75kb/sec') var secondary = stats.append('g'); secondary.append('path') .attr('class', 'node') .attr('d', bottomRoundedRect('0', '-149', 170, 114, 4)) .attr('stroke', '#343434') .attr('stroke-width', '1px') .attr('fill', '#464646') secondary.append('text') .attr('class', 'secondary') .attr('x', '12') .attr('y', '183') .attr('text-anchor', 'start') .attr('fill', '#fff') .text('Secondary') secondary.append('circle') .attr('class', 'alert') .attr('cx', function () { return d3.select(this.parentNode).select('.secondary').node().getBBox().width + 30 }) .attr('cy', '177') .attr('stroke-width', '0px') .attr('fill', 'rgb(0,175,102)') .attr('r', '9px') // An icon or label that exists within the circle, inside the infobox secondary.append('text') .attr('class', 'health') .attr('x', function () { return d3.select(this.parentNode).select('.secondary').node().getBBox().width + 30 }) .attr('y', '182') .attr('text-anchor', 'middle') .attr('fill', '#fff') .text('❤') var html = secondary .append('switch') .append('foreignObject') .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility') .attr('x', 12) .attr('y', 200) .attr('width', 160) .attr('height', 70) // From here everything will be rendered with react using a ref. // However for now these values are hard-coded. .append('xhtml:div') .attr('class', 'node_statistics') // Remove with react + dyanmic data. html.append('p') .text('CPU: 48%') html.append('p') .text('Memory: 54%') html.append('p') .text('Network: 1.75kb/sec') elm.call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)); } // Drawing the links between nodes var link = svg.append('g') .attr('class', 'links') .selectAll('line') .data(graph.links) .enter().append('line') .attr('stroke-width', '2px') // And svg group, to contain all of the attributes in @antonas' first prototype var node = svg.selectAll('.node') .data(graph.nodes) .enter() .append('g') .attr('class', 'node_group'); svg.selectAll('.node_group').each(function (d) { // Create different type of node for services with Primaries + Secondaries // We could extend this further to allow us to have as many nested services // as wanted. if (d.id === 'Percona') { createExtendedNode(d3.select(this)); } else { createNode(d3.select(this)); } }); simulation .nodes(graph.nodes) .on('tick', ticked); simulation.force('link') .links(graph.links); function contrain(dimension, r, z) { return Math.max(0, Math.min(dimension - r, z)) } function ticked() { // TODO: Remove these values and pull them out of the height of the boxes // that the constraints belong to. r=174 r2=270 link .attr('x1', function(d) { let x; svg.selectAll('.node_group').each(function (_, i) { if (i !== d.source.index) return; x = d3.select(this).node().getBBox().width; }); return contrain(width, x, d.source.x) + 80; }) .attr('y1', function(d) { let y; svg.selectAll('.node_group').each(function (_, i) { if (i !== d.source.index) return; y = d3.select(this).node().getBBox().height; }); return contrain(height, y, d.source.y) + 24; }) .attr('x2', function(d) { let x; svg.selectAll('.node_group').each(function (_, i) { if (i !== d.target.index) return; x = d3.select(this).node().getBBox().width; }); return contrain(width, x, d.target.x) + 80; }) .attr('y2', function(d) { let y; svg.selectAll('.node_group').each(function (_, i) { if (i !== d.target.index) return; y = d3.select(this).node().getBBox().height; }); return contrain(height, y, d.target.y) + 24; }); svg.selectAll('.node_group') .attr('transform', function(d) { let x = d3.select(this).node().getBBox().width; let y = d3.select(this).node().getBBox().height; return 'translate(' + contrain(width, x, d.x) + ',' + contrain(height, y, d.y) + ')'; }); } }); function dragstarted(d) { if (!d3.event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(d) { d.fx = d3.event.x; d.fy = d3.event.y; } function dragended(d) { if (!d3.event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } </script>