D3實現簡單業務拓撲圖

PS: 這篇文章適合有一定D3基礎的童鞋,因爲沒有多餘的註釋,只是筆者比較業餘的代碼分享,勿噴

目錄
- HTML
- JS
- CSS
- 效果圖

^ HTML

<div class="content-panel">
    <div id="object"></div>
    <div id="object-arrow"></div>
</div>

^ JS

(angularjs Controller的代碼)

var url = $location.path();
    $scope.$storage = $localStorage;
    var defaultLocation;
    if (_.has($localStorage, 'multiObject')) {
      defaultLocation = $localStorage.multiObject;
    }

    var nodeNameMap = {};
    var newNodes = _.map(abjectData.nodes, function(node) {
      nodeNameMap[node.objectId] = node.name;
      var rNode = {};
      if (_.has(defaultLocation, node.objectId)) {
        rNode.name = node.name;
        rNode.objectId = node.objectId;
        rNode.instanceCount = + node.instanceCount;
        rNode.attrs = node.attrs;
        rNode.fixed = true;
        rNode.x = defaultLocation[node.objectId].x;
        rNode.y = defaultLocation[node.objectId].y;
      } else {
        rNode = node;
      }
      return rNode;
    });

    var width = window.innerWidth - 250,
        height = window.innerHeight - 200,
        boxWidth = 160,
        countR = 15;

    var objectContainer = d3.select('#object').append('div')
        .style('position', 'absolute')
        .attr('width', width)
        .attr('height', height);

    var force = d3.layout.force();

    force
        .nodes(newNodes)
        .links(abjectData.links)
        .size([width, height])
        .linkDistance(200)
        .charge(-2000)
        .chargeDistance(250)
        .start();

    var svg = d3.select('#object-arrow').append('svg')
        .attr('width', width)
        .attr('height', height);

    svg.append('defs').append('marker')
        .attr('id', 'end-arrow')
        .attr('refX', 4)
        .attr('refY', 4)
        .attr('viewBox', '0 0 8 8')
        .attr('markerWidth', 8)
        .attr('markerHeight', 8)
        .attr('orient', 'auto')
      .append('path')
        .attr('d', 'M0,0 8 4 0 8 4 4z')
        .attr('fill', '#7FABD2')
        .attr('stroke', 'none');

    var path = svg.append('g').selectAll('path')
        .data(force.links())
      .enter().append('path')
        .attr('class', 'link')
        .style('marker-end', 'url(' + url + '#end-arrow)');

    var objectEnter = objectContainer.selectAll('div')
        .data(newNodes)
        .enter();

    var object = objectEnter.append('div')
        .attr('class', 'object')
        .attr('id', function (d) {
          return d.objectId;
        });

    var objectHeader = object.append('div')
        .attr('class', 'object-header')
          .append('i')
          .attr('class', function(d) {
            return objectIconMap(d.objectId);
          })
          .text(function (d) {
            return ' ' + d.name;
          });

    object.append('div')
        .attr('class', 'object-attrs')
        .selectAll('p')
          .data(function(d) {
            return _.uniq(d.attrs);
          })
          .enter()
          .append('p')
          .text(function(d) {
            return nodeNameMap[d];
          });

    var objectToolBar = object.append('div')
        .attr('class', 'object-settings');

    objectToolBar.append('a')
        .attr('class', 'btn btn-primary btn-link btn-xs')
        .attr('href', function (d) {
          return url + 'attr/' + d.objectId;
        })
        .append('i')
        .attr('class', 'glyphicon glyphicon-cog');

    object.on('dblclick', function(d) {
        d3.select(this).classed('fixed', d.fixed = false);
      }).call(force.drag().on('dragstart', function(d) {
        d3.select(this).classed('fixed', d.fixed = true);
      })
    );

    objectHeader.on('click', function (d) {
      if (d3.event.defaultPrevented) { return; }
      //然後根據 d 的數據,做一些個性化的跳轉判斷, Dialog或$state.go('xx')等
    });

    force.on('tick', function() {
      path.attr('d', drawPath);
      object.style('left', function (d) {
        return Math.max(Math.min(d.x, width), 0) + 'px';
      }).style('top', function (d) {
        return Math.max(Math.min(d.y, height), countR) + 'px';
      });
    });

    function objectIconMap(objectId) {
      return OBJECTICONMAP[objectId] || 'ti-layout-media-overlay-alt';
    }

    //箭頭定位算法, 明顯比較挫,看效果就知道了嘍
    function drawPath(d) {
      var dx = Math.abs(d.target.x - d.source.x),
          dy = Math.abs(d.target.y - d.source.y),
          headerHeight = 40,
          itemHeight = 25,
          arrowWidth = 5,
          itemStart = (_.indexOf(d.source.attrs, d.target.objectId) + 1) * itemHeight - itemHeight / 2,
          x1 = d.source.x + boxWidth,
          y1 = d.source.y + headerHeight + itemStart,
          x2 = x1 + dx / 4,
          y2 = y1,
          x4 = d.target.x - arrowWidth,
          y4 = d.target.y + headerHeight / 2,
          x3 = x4 - dx / 4,
          y3 = y4;

      if ((x1 - boxWidth) <= x4 && d.source.y >= d.target.y) {
        if (x1 > x4) {
          x1 -= boxWidth;
          x2 = Math.max(x1 - dx, 0);
          y2 -= dy / 4;
          x3 = Math.max(x4 - dx, 0);
          y3 = y4 + dy / 4;
        }
      } else if ((x1 - boxWidth) <= x4 && d.source.y < d.target.y) {
        if (x1 > x4) {
          x1 -= boxWidth;
          x2 = Math.max(x1 - dx, 0);
          y2 += dy / 2;
          x3 = Math.max(x4 - dx, 0);
        }
      } else {
        if ((x1 - boxWidth) < (x4 + boxWidth)) {
          x2 = Math.max(x1 + dx, 0);
          x4 += boxWidth + 2 * arrowWidth;
          x3 = Math.max(x4 + dx, 0);
        } else {
          x1 -= boxWidth;
          x2 = Math.max(x1 - dx / 4, 0);
          x4 += boxWidth + 2 * arrowWidth;
          x3 = Math.max(x4 + dx / 4, 0);
        }
      }

      if (x1 + arrowWidth === x4) {
        x2 = x1 + itemHeight + arrowWidth;
        x3 = x2;
      }

      return 'M' + x1 + ',' + y1 + ' C' + x2 + ',' + y2 + ' ' + x3 + ',' + y3 + ' ' + x4 + ',' + y4;
    }

    var save = function() {
      var defaultLocation = {};
      angular.forEach(object[0], function(node) {
        defaultLocation[node.id] = {
          x: node.offsetLeft,
          y: node.offsetTop
        };
      });
      $localStorage.multiObject = defaultLocation;
      toastr.success('保存成功');
    };
    $scope.save = save;
  }

^ CSS

scss->css

path.link {
  fill: none;
  stroke: $brand-info;
  stroke-width: 1.5px;
}

.object {
  width: 160px;
  position: absolute; /* 注意 */
  // .object-settings {
  //   visibility: hidden;
  //   visibility: visible;
  // }
  // &:hover {
  //   .object-settings {
  //       padding: 5px;
  //       background-color: $gray-lighter;
  //       border: 1.5px solid $btn-primary-img-border;
  //       border-top-width: 0px;
  //       border-bottom-right-radius:0.3em;
  //       border-bottom-left-radius:0.3em;
  //       text-align: center;
  //       visibility: visible;
  //   }
  // }
}

.object-settings {
  padding: 5px;
  background-color: #FFFFFF;
  border: 1.5px solid $btn-primary-img-border;
  border-top-width: 0px;
  border-bottom-right-radius:0.3em;
  border-bottom-left-radius:0.3em;
  text-align: center;
}

.object-attrs {
  margin-top: 40px;
  width: 160px;
  background-color: #FFFFFF;
}
.object-attrs > p {
  text-align: center;
  margin: 0px;
  font-size: 15px;
  font-weight: normal;
  height: 25px;
  overflow: hidden;
  text-overflow: ellipsis;
  color: $brand-primary-dark;
  border: 1.5px solid $btn-primary-img-border;
  border-top-width: 0px;
}

.object-header {
  position: absolute;
  cursor: pointer;
  border-top-left-radius:0.2em;
  border-top-right-radius:0.2em;
  border-bottom-right-radius:0em;
  border-bottom-left-radius:0em;
  background-color: $brand-primary-dark;
  color: #FFF;
  width: 160px;
  height: 40px;
  font-size: 26px;
  text-align: center;
  overflow: hidden;
  text-overflow: ellipsis;
}

^ 效果圖

這裏寫圖片描述

參考

D3 Mouse Events

SVG Marker-end 顯示不出來問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章