From 62aef3eec6907873734d7e16fcb8b992faa1db95 Mon Sep 17 00:00:00 2001 From: Tom Gallacher Date: Tue, 6 Dec 2016 12:38:17 +0000 Subject: [PATCH] Implement multi-tier info boxes These use HTML inside of SVG, this allows us to style the SVG's much easier, it also enables us to reference the dom nodes from within react. --- spikes/architecture/d3-revamp/index.html | 330 +++++++++++++++++------ 1 file changed, 245 insertions(+), 85 deletions(-) diff --git a/spikes/architecture/d3-revamp/index.html b/spikes/architecture/d3-revamp/index.html index 208ca463..8e1d0307 100644 --- a/spikes/architecture/d3-revamp/index.html +++ b/spikes/architecture/d3-revamp/index.html @@ -7,7 +7,7 @@ stroke-opacity: 1; } -.health { +.health, .health_warn { font-family: LibreFranklin; font-size: 12px; font-weight: bold; @@ -16,6 +16,10 @@ text-align: center; } +.health_warn { + font-size: 15px; +} + .stat { font-family: LibreFranklin; font-size: 12px; @@ -25,6 +29,29 @@ 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; +} + @@ -97,6 +124,211 @@ function rect(x, y, width, height, radius) { 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') + + // An icon or label that exists within the circle, inside the infobox + stats.append('text') + .attr('class', 'cpu') + .attr('class', 'stat') + .attr('x', '12') + .attr('y', '65') + .attr('text-anchor', 'start') + .attr('fill', 'rgba(255, 255, 255, 0.8)') + .text('CPU: 63%') + + // An icon or label that exists within the circle, inside the infobox + stats.append('text') + .attr('class', 'memory') + .attr('class', 'stat') + .attr('x', '12') + .attr('y', '85') + .attr('text-anchor', 'start') + .attr('fill', 'rgba(255, 255, 255, 0.8)') + .text('Memory: 50%') + + // An icon or label that exists within the circle, inside the infobox + stats.append('text') + .attr('class', 'network') + .attr('class', 'stat') + .attr('x', '12') + .attr('y', '105') + .attr('text-anchor', 'start') + .attr('fill', 'rgba(255, 255, 255, 0.8)') + .text('Network: 1.23kb/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, 78, 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(229, 163, 57)') + .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 secondary = stats.append('g'); + + secondary.append('path') + .attr('class', 'node') + .attr('d', bottomRoundedRect('0', '-113', 170, 114, 4)) + .attr('stroke', '#343434') + .attr('stroke-width', '1px') + .attr('fill', '#464646') + + secondary.append('text') + .attr('class', 'secondary') + .attr('x', '12') + .attr('y', '150') + .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', '144') + .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', '149') + .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', 160) + .attr('width', 160) + .attr('height', 100) + // 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') @@ -112,90 +344,16 @@ d3.json('services.json', function(error, graph) { .append('g') .attr('class', 'node_group'); - // Box where label will live - node.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 = node.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 = node.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') - - // An icon or label that exists within the circle, inside the infobox - stats.append('text') - .attr('class', 'cpu') - .attr('class', 'stat') - .attr('x', '12') - .attr('y', '65') - .attr('text-anchor', 'start') - .attr('fill', 'rgba(255, 255, 255, 0.8)') - .text('CPU: 63%') - - // An icon or label that exists within the circle, inside the infobox - stats.append('text') - .attr('class', 'memory') - .attr('class', 'stat') - .attr('x', '12') - .attr('y', '85') - .attr('text-anchor', 'start') - .attr('fill', 'rgba(255, 255, 255, 0.8)') - .text('Memory: 50%') - - // An icon or label that exists within the circle, inside the infobox - stats.append('text') - .attr('class', 'network') - .attr('class', 'stat') - .attr('x', '12') - .attr('y', '105') - .attr('text-anchor', 'start') - .attr('fill', 'rgba(255, 255, 255, 0.8)') - .text('Network: 1.23kb/sec') - - - node.call(d3.drag() - .on('start', dragstarted) - .on('drag', dragged) - .on('end', dragended)); - - node.append('title') - .text(function(d) { return d.id; }); + 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) @@ -209,6 +367,8 @@ d3.json('services.json', function(error, graph) { } function ticked() { + // TODO: Remove these values and pull them out of the height of the boxes + // that the constraints belong to. r=180 r2=130