<!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>