如何在d3强制有向图中将链接渲染为弯头连接器

时间:2015-07-01 13:16:38

标签: javascript d3.js

我是D3的新手,这是我迄今为止所做的here

实际代码在这里:

class HomeViewController: UIViewController {
var view1:UIView!
var view2:UIView!
var view3:UIView!
var view4:UIView!
var view5:UIView!
var view6:UIView!
var view7:UIView!
var view8:UIView!



override func viewDidLoad() {

    super.viewDidLoad();

    view1 = self.autoLayoutPanelViewWithColor(UIColor(hex: 0xf6b37f, alpha: 1.0), title:"Kerala", logoImage:UIImage(named: "CD_grid@156px")!, backViewBackgroundColor:UIColor(patternImage: UIImage(named: "kerala")!), backViewTitle:"Area:38,863 Km \n Capital: Thiruvananthapuram")
    view2 = self.autoLayoutPanelViewWithColor(UIColor(hex: 0xf9a451, alpha: 1.0), title:"Goa", logoImage:UIImage(named: "coffee_grid@156px")!, backViewBackgroundColor:UIColor(patternImage: UIImage(named: "goa")!), backViewTitle:"Area:38,863 Km \n Capital: Panaji")
    view3 = self.autoLayoutPanelViewWithColor(UIColor(hex: 0xf78d60, alpha: 1.0), title:"Delhi", logoImage:UIImage(named: "message_grid@156px")!, backViewBackgroundColor:UIColor(patternImage: UIImage(named: "delhi")!), backViewTitle:"Area:38,863 Km \n Capital: Delhi")
    view4 = self.autoLayoutPanelViewWithColor(UIColor(hex: 0xfc7d38, alpha: 1.0), title:"Gujarat", logoImage:UIImage(named: "things_grid@156px")!, backViewBackgroundColor:UIColor(patternImage: UIImage(named: "gujarat")!), backViewTitle:"Area:38,863 Km \n Capital: Gandhinagar")
    view5 = self.autoLayoutPanelViewWithColor(UIColor(hex: 0xeb6100, alpha: 1.0), title:"Maharashtra", logoImage:UIImage(named: "clock_grid@156px")!, backViewBackgroundColor:UIColor(patternImage: UIImage(named: "maharasathra")!), backViewTitle:"Area:38,863 Km \n Capital: Mumbai")
    view6 = self.autoLayoutPanelViewWithColor(UIColor(hex: 0xff5e33, alpha: 1.0), title:"Madhya Pradesh", logoImage:UIImage(named: "settings_grid@156px")!, backViewBackgroundColor:UIColor(patternImage: UIImage(named: "bestplace")!), backViewTitle:"Area:38,863 Km \n Capital: Bhopal")
    view7 = self.autoLayoutPanelViewWithColor(UIColor(hex: 0xeb6100, alpha: 1.0), title:"Maharashtra", logoImage:UIImage(named: "clock_grid@156px")!, backViewBackgroundColor:UIColor(patternImage: UIImage(named: "maharasathra")!), backViewTitle:"Area:38,863 Km \n Capital: Mumbai")
    view8 = self.autoLayoutPanelViewWithColor(UIColor(hex: 0xff5e33, alpha: 1.0), title:"Madhya Pradesh", logoImage:UIImage(named: "settings_grid@156px")!, backViewBackgroundColor:UIColor(patternImage: UIImage(named: "bestplace")!), backViewTitle:"Area:38,863 Km \n Capital: Bhopal")

    self.makeGlobalConstraintsToView(self.view, subviewArray: [view1,view2,view3,view4,view5,view6,view7,view8])

}


func autoLayoutPanelViewWithColor(bgColor:UIColor,title:String,logoImage:UIImage,backViewBackgroundColor:UIColor,backViewTitle:String) -> UIView {

    var view:PanelView           = PanelView(bgColor: bgColor)
    view.backViewTitle           = backViewTitle
    view.backViewBackgroundColor = backViewBackgroundColor
    self.imageViewWithImage(logoImage, title: title, inPanelView: view)
    self.addFlipTapGestureRecognizerToView(view)
    self.view.addSubview(view)

    return view

}

func addFlipTapGestureRecognizerToView(view:UIView) {

    var tap:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "flipView:")
    tap.numberOfTapsRequired = 1

    view.addGestureRecognizer(tap)

}

func makeGlobalConstraintsToView(view:UIView,subviewArray:Array<AnyObject>) {

    var dic:NSDictionary = [
        "view1":subviewArray[0],
        "view2":subviewArray[1],
        "view3":subviewArray[2],
        "view4":subviewArray[3],
        "view5":subviewArray[4],
        "view6":subviewArray[5],
        "view7":subviewArray[6],
        "view8":subviewArray[7]
    ];
    self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view5][view6(==view5)]|",
        options: NSLayoutFormatOptions.AlignAllBaseline,
        metrics: nil,
        views: dic as [NSObject : AnyObject]))
    self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view7][view8(==view7)]|",
        options: NSLayoutFormatOptions.AlignAllBaseline,
        metrics: nil,
        views: dic as [NSObject : AnyObject]))
    self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view3][view4(==view3)]|",
        options: NSLayoutFormatOptions.AlignAllBaseline,
        metrics: nil,
        views: dic as [NSObject : AnyObject]))
    self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[view1][view2(==view1)]|",
        options: NSLayoutFormatOptions.AlignAllBaseline,
        metrics: nil,
        views: dic as [NSObject : AnyObject]))
    self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-22-[view1][view3(==view1)][view5(==view1)][view7(==view1)]|",
        options: nil,
        metrics: nil,
        views: dic as [NSObject : AnyObject]))

    self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-22-[view2][view4(==view2)][view6(==view2)][view8(==view2)]|",
        options: nil,
        metrics: nil,
        views: dic as [NSObject : AnyObject]))

}

func flipView(sender:UITapGestureRecognizer) {

    var flipDuration = 0.8
    UIView.transitionWithView(sender.view!,
        duration: flipDuration,
        options: UIViewAnimationOptions.TransitionFlipFromLeft,
        animations: {() -> Void in

        },
        completion: {(finished:Bool) -> Void in

            sleep(1)
            let storyboard = UIStoryboard(name: "Main", bundle: nil)
            let secondViewController = storyboard.instantiateViewControllerWithIdentifier("DetailedViewController") as! UIViewController
            self.presentViewController(secondViewController, animated: true, completion: nil)
        }
    )
    dispatch_after(dispatch_time_t(flipDuration / 2) * 1000, dispatch_get_main_queue(), { () -> Void in
        (sender.view as! PanelView).changeView()

    })

}

func imageViewWithImage(image:UIImage,title:String,inPanelView:UIView) {

    var panelLogo       = UIImageView(image: image)
    var label           = UILabel(frame: CGRectMake(0, 70, 130, 40))
    label.textColor     = UIColor.whiteColor();
    label.text          = title
    label.textAlignment = NSTextAlignment.Center
    label.lineBreakMode = .ByWordWrapping

    panelLogo.setTranslatesAutoresizingMaskIntoConstraints(false)
    panelLogo.addSubview(label)
    inPanelView.addSubview(panelLogo)
    inPanelView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-20-[panelLogo]-20-|",
        options: NSLayoutFormatOptions.AlignAllBaseline,
        metrics: nil,
        views: ["panelLogo":panelLogo]))
    inPanelView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-25-[panelLogo]-25-|",
        options: NSLayoutFormatOptions.AlignAllBaseline,
        metrics: nil,
        views: ["panelLogo":panelLogo]))

}
除了正在渲染的链接之外,

这可以正常工作。例如,此链接:

enter image description here

但我希望该链接为:

enter image description here

我怎样才能在d3中实现这个目标?

2 个答案:

答案 0 :(得分:6)

惯用法是使用path元素而不是line,并使用d3.svg.line()创建链接。这样箭头也可以工作,并且它完全且容易动画。

备注

在使用这个(非常有趣!)示例时,我发现了一些系统性问题......

  1. Bug in IE
    显然,MS无法解决此问题,但渲染元素时会出现问题。 work-arround的目的是在其父级上插入路径,这就是强制ontick事件处理程序中此行的用途...
    link.each(function() {this.parentNode.insertBefore(this, this); });
  2. 过滤器剪辑问题
    在示例中,我们有像d="M28,46L28,23L77,23"这样的路径元素指令来渲染两条正交线。这适用于过滤器,并且投影按预期呈现,但是,当拖动节点使得其中一条线的长度短于标记的相应尺寸时,出现问题:路径元素,包括标记,开始被过滤器修剪 我不明白究竟发生了什么,但似乎过滤器的边界框(路径边界框的百分比)会折叠到零高度,这会以某种方式剪切引用路径元素。一旦路径边界框变为零,问题就会消失(至少在Chrome和Opera中会消失...)。
  3. enter image description here

    作为我尝试管理上述问题的一部分,我尝试将路径元素中的所有数字限制为整数,这是通过使用此代码向节点数据添加量化器getter来实现的。

     force.nodes().forEach(function(d) {
        d.q = {};
        Object.keys(d).forEach(function (p) {
          if (!isNaN(d[p])) Object.defineProperty(d.q, p, {
            get: function () {
              return Math.round(d[p])
            }
          });
        })
      });
    

    这会在每个节点数据上创建一个q对象,并为任何返回数值的成员提供一个getter - 我不需要考虑哪些成员,所以我只需要点击它们 - 那就是允许我这样做,例如......

      node.attr("transform", function (d) {
        return "translate(" + d.q.x + "," + d.q.y + ")";
      })  
    

    因此,d.q.xd.q.yd.xd.y的四舍五入版本。 我打算在linkPath函数中使用它来使路径d中的所有数字都成为整数,但我意识到使用自定义x和{更好地实现了这一点此处y对象中的{1}}访问者...

    d3.svg.line()

    var connector = d3.svg.line().interpolate("linear") .x(function(d){return Math.round(d[0])}) .y(function(d){return Math.round(d[1])}); function linkPath(d){ var h1 = d.source.height, w1 = d.source.width, x1 = d.source.x + w1/2, y1 = d.source.y + h1/2, h2 = d.target.height, w2 = d.target.width, x2 = d.target.x - markerW - 4, y2 = d.target.y + h2/2; return connector([[x1, y1], [x1, y2], [x2, y2]]); } 返回的函数接受d3.svg.line().interpolate("linear")形式的点数组,并使用提供的标准插值器为路径[[p1x, p1y], [p2x, p2y], ... ]属性构造字符串值(It& #39;尝试其他标准d3插值器功能(例如基础)也很有趣。通过添加自定义访问器,可以确保提供的所有坐标都舍入到最接近的整数值 在强制tick回调中调用的函数d只是根据链接数据构造一个包含三个点的数组,并将该数组传递给linkPath函数并返回一个可用作connector元素的d属性。调用签名确保为每个元素传递一个绑定数据的副本...

    path

    因此,绑定到每个链接的数据用于创建三个点,这三个点被插值并呈现为路径。

    工作代码

    需要管理一些问题以确保连接器和箭头正常工作,但这些并不是真正相关的,所以我没有修复代码的混乱...

    &#13;
    &#13;
    link.attr("d", linkPath);  
    
    &#13;
      var width = 600,
        height = 148,
        constant = 10,
        color = "#BCD8CD"
    
      var scale = .75, w = 70*scale, h = 50*scale,
        nodes = [
        {label: '1st stage', x:   constant, y: 20*scale , width:w,height:h , color :color , stage: true },
        {label: '2nd stage', x: constant + 150*scale , y: 20*scale ,width:w,height:h ,color :color, stage: true },
        {label: '3rd stage', x: constant + 279*scale, y: 20*scale ,width:w,height:h, color :color, stage: false },
        {label: '4th stage', x: constant + 460*scale, y: 20*scale ,width:w,height:h, color :color, stage: false },
        {label: '5th stage', x: constant + 660*scale, y: 20*scale ,width:w,height:h ,color :color, stage: false },
        {label: '6th stage', x: constant + 350*scale, y: 100*scale ,width:w,height:h, color :color, stage: true }
        ].map(function(d, i){return (d.fixed = (i != 5), d)});
    
      var links = [
        { source: 0, target: 1 },
        { source: 1, target: 2},
        { source: 2, target: 3},
        { source: 3, target: 4},
        { source: 1, target: 5}
      ];
    
      var svg = d3.select('body').append('svg')
        .attr('width', width)
        .attr('height', height);
    
      var markerW = 4, markerH = 3,
        marker = svg.append('marker')
        .attr('id',"triangle")
        .attr('viewBox',"0 0 10 10")
        .attr('refX',"0")
        .attr('refY',5)
        .attr('markerUnits','strokeWidth')
        .attr('markerWidth',markerW)
        .attr('markerHeight',markerH)
        .attr('orient','auto')
    
      var path = marker.append('path')
        .attr('d',"M 0 0 L 10 5 L 0 10 z")
    
      var force = d3.layout.force()
        .size([width, height])
        .nodes(nodes)
        .links(links)
        .linkDistance(width/4)
        .on("tick", function(e){
          //hack to force IE to do it's job!
          link.each(function() {this.parentNode.insertBefore(this, this); });
    
          link.attr("d", linkPath);
          node.attr("transform", function (d) {
            return "translate(" + d.q.x + "," + d.q.y + ")";
          })
        });
        force.nodes().forEach(function(d) {
          d.q = {};
          Object.keys(d).forEach(function (p) {
            if (!isNaN(d[p])) Object.defineProperty(d.q, p, {
              get: function () {
                return Math.round(d[p])
              }
            });
          })
        });
    
      var connector = d3.svg.line().interpolate("linear")
        .x(function(d){return Math.round(d[0])})
        .y(function(d){return Math.round(d[1])});
      function linkPath(d){
        return connector([[d.source.x + d.source.width/2, d.source.y + d.source.height/2],
          [d.source.x + d.source.width/2, d.target.y + d.target.height/2],
          [d.target.x  - markerW - 4, d.target.y + d.target.height/2]]);
      }
    
      var link = svg.selectAll('.link')
        .data(links)
        .enter().append('path')
        .attr("stroke-width", "2")
        .attr('marker-end','url(#triangle)')
        .attr('stroke','black')
        .attr("fill", "none");
    
      var defs = svg.append("defs");
    
      // create filter with id #drop-shadow
      // height=130% so that the shadow is not clipped
      var filter = defs.append("filter")
        .attr("id", "drop-shadow")
        .attr({"height": "200%", "width": "200%", x: "-50%", y: "-50%"});
    
      // SourceAlpha refers to opacity of graphic that this filter will be applied to
      // convolve that with a Gaussian with standard deviation 3 and store result
      // in blur
      filter.append("feGaussianBlur")
        .attr("in", "SourceAlpha")
        .attr("stdDeviation", 3)
        .attr("result", "blur");
    
      // translate output of Gaussian blur to the right and downwards with 2px
      // store result in offsetBlur
      var feOffset = filter.append("feOffset")
        .attr("in", "blur")
        .attr("dx", 2)
        .attr("dy", 2)
        .attr("result", "offsetBlur");
    
      // overlay original SourceGraphic over translated blurred opacity by using
      // feMerge filter. Order of specifying inputs is important!
      var feMerge = filter.append("feMerge");
    
      feMerge.append("feMergeNode")
        .attr("in", "offsetBlur")
      feMerge.append("feMergeNode")
        .attr("in", "SourceGraphic");
    
      var node = svg.selectAll('.node')
        .data(nodes)
        .enter().append('g')
        .attr('class', 'node')
        .attr("transform", function(d){
          return "translate("+ d.q.x+","+ d.q.y+")";
        })
      .call(force.drag)
    
      node.append("rect").attr("class", "nodeRect")
        .attr("rx", 6)
        .attr("ry", 6)
        .attr('width', function(d) { return d.width; })
        .attr('height', function(d) { return d.height; })
        .style("fill", function(d) { return d.color; })
        .transition()
        .duration(1000) // this is 1s
        .delay(1000)
        .style("fill",function(d){if(d.stage) return "#FF9966"})
        .style("filter",function(d){if(d.stage) return "url(#drop-shadow)"})
    
    
      node.append("text").style("text-anchor", "middle")
        .style("pointer-events", "none")
        .style("font-weight", 900)
        .attr("fill", "white")
        .style("stroke-width", "0.3px")
        .style("font-size", 16*scale + "px")
        .attr("y", function (d){return d.height/2+6*scale;})
        .attr("x", function (d){return d.width/2;})
        .text(function (d) {return d.label;})
    
      force.start();
    
        link.attr("d", linkPath)
       .transition()
        .duration(1000) // this is 1s
        .delay(1000)
        .style("filter",function(d){if(d.source.stage) return "url(#drop-shadow)"});
    
      d3.select("svg").append("text").attr({"y": height - 20, fill: "black"}).text("drag me!")
    &#13;
    svg { overflow: visible;}
    .node {
      fill: #ccc;
      stroke: #fff;
      stroke-width: 2px;
    }
    .link {
      stroke: #777;
      stroke-width: 2px;
    }
    g.hover {
      background-color: rgba(0, 0, 0, .5);
    }
    &#13;
    &#13;
    &#13;

答案 1 :(得分:1)

我通过创建一个空标签找到了解决方法:

Working demo

var nodes = [
    {label: '1st stage', x:   constant, y: 215 , width:70,height:50 , color :color , stage: true },
    {label: '2nd stage', x: constant + 150 , y: 215 ,width:70,height:50 ,color :color, stage: true },
    {label: '3rd stage', x: constant + 279, y: 215 ,width:70,height:50, color :color, stage: false },
    {label: '4th stage', x: constant + 460, y: 215 ,width:70,height:50, color :color, stage: false },
    {label: '5th stage', x: constant + 660, y: 215 ,width:70,height:50 ,color :color, stage: false },
    {label: '', x: constant + 185, y: 370 ,width:0,height:0 ,color :color, stage: true },
    {label: '6th stage', x: constant + 350, y: 350 ,width:70,height:50, color :color, stage: true }
];

var links = [
    { source: 0, target: 1 },
    { source: 1, target: 2},
    { source: 2, target: 3},
    { source: 3, target: 4},
    { source: 1, target: 5},
    { source: 5, target: 6}
];