d3目标翻译后的缩放行为

时间:2015-08-09 15:21:51

标签: javascript d3.js svg zoom

有人知道一种方法可以阻止一个元素跳回到上次被拖动的位置吗?请参阅下面的演示。只需点击 Jump ,然后在grey area内拖动。

我尝试通过将zoom translate属性更新为新值来修复它,但在ondrag(d3源代码中为translateTo)期间会覆盖它。

origin中的d3.behavior.zoom是否等效?

实施例



//debug panel/////////////////////////////////////////////////////////////////////////////
var alpha = d3.select("#alpha").text("waiting..."),
  cog = d3.select("#wrapAlpha").insert("i", "#fdg").classed("fa fa-cog fa-spin", true).datum({
    instID: null
  }),
  fdgInst = d3.select("#fdg");
elapsedTime = ElapsedTime("#panel", {
    margin: 0,
    padding: 0
  })
  .message(function(id) {
    return 'fps : ' + d3.format(" >8.3f")(1 / this.aveLap())
  });
elapsedTime.consoleOn = true;

alpha.log = function(e, instID) {
  elapsedTime.mark().timestamp();
  alpha.text(d3.format(" >8.4f")(e.alpha));
  fdgInst.text("fdg instance: " + instID);
};

d3.select("#update").on("click", (function() {
  var dataSet = false;
  return function() {
    fdg(dataSets[(dataSet = !dataSet, +dataSet)])
  }
})());
d3.select("#jump").on("click", (function() {
  return function() {
    var t = d3.transform(fdg.attr("transform")).translate.map(function(d) {
      return d + 200
    })
    fdg.attr("transform", "translate(" + t + ")");
  }
})());
//////////////////////////////////////////////////////////////////////////////////////////
var dataSets = [{
    "nodes": [{
      "name": "node1",
      "r": 10
    }, {
      "name": "node2",
      "r": 10
    }, {
      "name": "node3",
      "r": 30
    }, {
      "name": "node4",
      "r": 15
    }],
    "edges": [{
      "source": 2,
      "target": 0
    }, {
      "source": 2,
      "target": 1
    }, {
      "source": 2,
      "target": 3
    }]
  }, {
    "nodes": [{
      "name": "node1",
      "r": 20
    }, {
      "name": "node2",
      "r": 10
    }, {
      "name": "node3",
      "r": 30
    }, {
      "name": "node4",
      "r": 15
    }, {
      "name": "node5",
      "r": 10
    }, {
      "name": "node6",
      "r": 10
    }],
    "edges": [{
      "source": 2,
      "target": 0
    }, {
      "source": 2,
      "target": 1
    }, {
      "source": 2,
      "target": 3
    }, {
      "source": 2,
      "target": 4
    }, {
      "source": 2,
      "target": 5
    }]
  }],
  svg = SVG({
    width: 600,
    height: 200 - 34,
    margin: {
      top: 25,
      right: 5,
      bottom: 5,
      left: 5
    }
  }, "#viz"),
  fdg = FDG(svg, alpha.log);

fdg(dataSets[0]);

function SVG(size, selector) {
  //delivers an svg background with zoom/drag context in the selector element
  //if height or width is NaN, assume it is a valid length but ignore margin
  var margin = size.margin || {
      top: 0,
      right: 0,
      bottom: 0,
      left: 0
    },
    unitW = isNaN(size.width),
    unitH = isNaN(size.height),
    w = unitW ? size.width : size.width - margin.left - margin.right,
    h = unitH ? size.height : size.height - margin.top - margin.bottom,
    zoomStart = function() {
      return this
    },
    zoomed = function() {
      return this
    },

    zoom = d3.behavior.zoom().scaleExtent([0.4, 4])
    .on("zoom", function(d, i, j) {
      zoomed.call(this, d, i, j);
    })
    .on("zoomstart", function(d, i, j) {
      zoomStart.call(this, d, i, j);
    }),

    svg = d3.select(selector).selectAll("svg").data([
      ["transform root"]
    ]);
  svg.enter().append("svg");
  svg.attr({
    width: size.width,
    height: size.height
  });

  var g = svg.selectAll("#zoom").data(id),
    gEnter = g.enter().append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .call(zoom)
    .attr({
      class: "outline",
      id: "zoom"
    }),
    zoomText = gEnter.append("text")
    .text("transform = translate ( margin.left , margin.top )")
    .style("fill", "#5c5c5c")
    .attr("dy", "-.35em"),
    surface = gEnter.append("rect")
    .attr({
      width: w,
      height: h
    })
    .style({
      "pointer-events": "all",
      fill: "#ccc",
      "stroke-width": 3,
      "stroke": "#fff"
    }),
    surfaceText = gEnter.append("text")
    .text("pointer-events: none")
    .style("fill", "#5c5c5c")
    .attr({
      "dy": "1em",
      "dx": ".2em"
    });

  g.h = h;
  g.w = w;
  g.onZoom = function(cb) {
    zoomed = cb;
  };
  g.onZoomStart = function(cb) {
    zoomStart = cb;
  };
  d3.rebind(g, zoom, "translate")

  return g;
}

function FDG(svg, tickLog) {
  var instID = Date.now();
  force = d3.layout.force()
    .size([svg.w, svg.h])
    .charge(-1000)
    .linkDistance(50)
    .on("end", function() {
      // manage dead instances of force
      // only stop if this instance is the current owner
      if (cog.datum().instID != instID) return true;
      cog.classed("fa-spin", false);
      elapsedTime.stop();
    })
    .on("start", function() {
      // mark as active and brand the insID to establish ownership
      cog.classed("fa-spin", true).datum().instID = instID;
      elapsedTime.start();
    });

  function fdg(data) {
    force
      .nodes(data.nodes)
      .links(data.edges)
      .on("tick", (function(instID) {
        return function(e) {
          if (tickLog) tickLog.call(this, e, instID);
          lines.attr("x1", function(d) {
            return d.source.x;
          }).attr("y1", function(d) {
            return d.source.y;
          }).attr("x2", function(d) {
            return d.target.x;
          }).attr("y2", function(d) {
            return d.target.y;
          });
          node.attr("transform", function(d) {
            return "translate(" + [d.x, d.y] + ")"
          });
        }
      })(instID))
      .start();

    svg.onZoom(zoomed);
    svg.onZoomStart(zoomStart);

    hookDrag(force.drag(), "dragstart.force", function(d) {
      // prevent dragging on the nodes from dragging the canvas
      var e = d3.event.sourceEvent;
      e.stopPropagation();
      d.fixed = e.shiftKey || e.touches && (e.touches.length > 1);
    });
    hookDrag(force.drag(), "dragend.force", function(d) {
      // prevent dragging on the nodes from dragging the canvas
      var e = d3.event.sourceEvent;
      d.fixed = e.shiftKey || d.fixed;
    });

    var content = svg.selectAll("g#fdg").data([data]);
    content.enter().append("g").attr({
      "id": "fdg",
      class: "outline"
    });

    var contentText = content.selectAll(".contentText")
      .data(["transform = translate ( d3.event.translate ) scale ( d3.event.scale )"])
      .enter().append("text").classed("contentText", true)
      .text(id)
      .style("fill", "#5c5c5c")
      .attr({
        "dy": 20,
        "dx": 20
      });

    var lines = content.selectAll(".links")
      .data(linksData),
      linesEnter = lines.enter()
      .insert("line", d3.select("#nodes") ? "#nodes" : null)
      .attr("class", "links")
      .attr({
        stroke: "steelblue",
        "stroke-width": 3
      });
    var nodes = content.selectAll("#nodes")
      .data(nodesData),
      nodesEnter = nodes.enter().append("g")
      .attr("id", "nodes"),
      node = nodes.selectAll(".node")
      .data(id),
      newNode = node.enter().append("g")
      .attr("class", "node")
      .call(force.drag),
      circles = newNode.append("circle")
      .attr({
        class: "content"
      })
      .attr("r", function(d) {
        return d.r
      })
      .style({
        "fill": "red",
        opacity: 0.8
      });

    lines.exit().remove();
    node.exit().remove();

    function nodesData(d) {
      return [d.nodes];
    }

    function linksData(d) {
      return d.edges;
    }

    function hookDrag(target, event, hook) {
      //hook force.drag behaviour
      var stdDragStart = target.on(event);
      target.on(event, function(d) {
        hook.call(this, d);
        stdDragStart.call(this, d);
      });
    }

    function zoomStart() {
      svg.translate(d3.transform(content.attr("transform")).translate)
    }

    function zoomed() {
      var e = d3.event.sourceEvent,
        isWheel = e && ((e.type == "mousewheel") || (e.type == "wheel"));
      force.alpha(0.01);
      return isWheel ? zoomWheel.call(this) : zoomInst.call(this)
    }

    function zoomInst() {
      var t = d3.transform(content.attr("transform"));
      t.translate = d3.event.translate;
      t.scale = d3.event.scale;
      content.attr("transform", t.toString());
    }

    function zoomWheel() {
      var t = d3.transform(content.attr("transform"));
      t.translate = d3.event.translate;
      t.scale = d3.event.scale;
      content.transition().duration(450).attr("transform", t.toString());
    }

    fdg.force = force;
    d3.rebind(fdg, content, "attr")

  };
  return fdg

}

function id(d) {
  return d;
}

svg {
  outline: 1px solid #282f51;
  pointer-events: all;
  overflow: visible;
}
g.outline {
  outline: 1px solid red;
}
#panel div {
  display: inline-block;
  margin: 0 .25em 3px 0;
}
#panel div div {
  white-space: pre;
  margin: 0 .25em 3px 0;
}
div#inputDiv {
  white-space: normal;
  display: inline-block;
}
.node {
  cursor: default;
}
text {
  font-size: 8px;
}

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css">
<div id="panel">
  <div id="inputDiv">
    <input id="update" type="button" value="update">
    <input id="jump" type="button" value="jump">
  </div>
  <div id="wrapAlpha">alpha:
    <div id="alpha"></div>
  </div>
  <div id="fdg">
  </div>
</div>
<div id="viz"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/40e550b1507cca31b0bb/raw/b83ceb0f8b4a2b6256f079f5887fc5243baedd4f/elapsed%2520time%25201.0.js"></script>
&#13;
&#13;
&#13;

1 个答案:

答案 0 :(得分:0)

排序!

我的基本错误是使用强力移动由缩放行为管理的元素,而不是使用缩放行为来“按照它的方式”。这可以使用zoom.event(selection)或更具体的zoom.translate([x,y]).event(selection)

来完成

Working solution

缩放管理对象公开了这个......

g.jumpTo = function(p, t){
    (t ? g.transition().duration(t) : g)
        .call(zoom.translate(p).event)
};

...提供手动或程序化缩放服务。

//debug panel/////////////////////////////////////////////////////////////////////////////
var alpha = d3.select("#alpha").text("waiting..."),
  cog = d3.select("#wrapAlpha").insert("i", "#fdg").classed("fa fa-cog fa-spin", true).datum({
    instID: null
  }),
  fdgInst = d3.select("#fdg");
elapsedTime = ElapsedTime("#panel", {
    margin: 0,
    padding: 0
  })
  .message(function(id) {
    return 'fps : ' + d3.format(" >8.3f")(1 / this.aveLap())
  });
elapsedTime.consoleOn = true;

alpha.log = function(e, instID) {
  elapsedTime.mark().timestamp();
  alpha.text(d3.format(" >8.4f")(e.alpha));
  fdgInst.text("fdg instance: " + instID);
};

d3.select("#update").on("click", (function() {
  var dataSet = false;
  return function() {
    fdg.data(dataSets[(dataSet = !dataSet, +dataSet)])
  }
})());
d3.select("#jump").on("click", function() {
  var jumpXY = 50;
  fdg.jumpTo(d3.transform(fdg.attr("transform"))
    .translate.map(function(d) {
      return d + jumpXY
    }), 1000);
});
//////////////////////////////////////////////////////////////////////////////////////////
var dataSets = [{
    "nodes": [{
      "name": "node1",
      "r": 10
    }, {
      "name": "node2",
      "r": 10
    }, {
      "name": "node3",
      "r": 30
    }, {
      "name": "node4",
      "r": 15
    }],
    "edges": [{
      "source": 2,
      "target": 0
    }, {
      "source": 2,
      "target": 1
    }, {
      "source": 2,
      "target": 3
    }]
  }, {
    "nodes": [{
      "name": "node1",
      "r": 20
    }, {
      "name": "node2",
      "r": 10
    }, {
      "name": "node3",
      "r": 30
    }, {
      "name": "node4",
      "r": 15
    }, {
      "name": "node5",
      "r": 10
    }, {
      "name": "node6",
      "r": 10
    }],
    "edges": [{
      "source": 2,
      "target": 0
    }, {
      "source": 2,
      "target": 1
    }, {
      "source": 2,
      "target": 3
    }, {
      "source": 2,
      "target": 4
    }, {
      "source": 2,
      "target": 5
    }]
  }],
  svg = SVG({
    width: 600,
    height: 200 - 34,
    margin: {
      top: 25,
      right: 5,
      bottom: 5,
      left: 5
    }
  }, "#viz"),
  fdg = FDG(svg, alpha.log);

fdg.data(dataSets[0]);

function SVG(size, selector) {
  //delivers an svg background with zoom/drag context in the selector element
  //if height or width is NaN, assume it is a valid length but ignore margin
  var margin = size.margin || {
      top: 0,
      right: 0,
      bottom: 0,
      left: 0
    },
    unitW = isNaN(size.width),
    unitH = isNaN(size.height),
    w = unitW ? size.width : size.width - margin.left - margin.right,
    h = unitH ? size.height : size.height - margin.top - margin.bottom,
    zoomStart = function() {
      return this
    },
    zoomed = function() {
      return this
    },
    container,

    zoom = d3.behavior.zoom().scaleExtent([0.4, 4])
    .on("zoom", function(d, i, j) {
      onZoom.call(this, d, i, j);
      zoomed.call(this, d, i, j);
    })
    .on("zoomstart", function(d, i, j) {
      var t = d3.transform(d3.select(this).attr("transform"));
      //zoom.translate(t.translate).scale(t.scale[0]);	// assumes x and y scale is same
      onZoomStart.call(this, d, i, j);
      zoomStart.call(this, d, i, j);
    }),

    svg = d3.select(selector).selectAll("svg").data([
      ["transform root"]
    ]);
  svg.enter().append("svg");
  svg.attr({
    width: size.width,
    height: size.height
  });

  var g = svg.selectAll("#zoom").data(id),
    gEnter = g.enter().append("g")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .call(zoom)
    .attr({
      class: "outline",
      id: "zoom"
    }),
    zoomText = gEnter.append("text")
    .text("g#zoom: transform = translate ( margin.left , margin.top ); .call(zoom)")
    .style("fill", "#5c5c5c")
    .attr("dy", "-.35em"),
    surface = gEnter.append("rect")
    .attr({
      width: w,
      height: h
    })
    .style({
      "pointer-events": "all",
      fill: "#ccc",
      "stroke-width": 3,
      "stroke": "#fff"
    }),
    surfaceText = gEnter.append("text")
    .text("event capture surface: style='pointer-events: none'")
    .style("fill", "#5c5c5c")
    .attr({
      "dy": "1em",
      "dx": ".2em"
    });

  function onZoomStart() {
    // zoom translate and scale are initially [0,0] and 1
    // this needs to be aligned with the container to stop
    // jump back to zero before first jump transition
    var t = d3.transform(container.attr("transform"));
    zoom.translate(t.translate);
    zoom.scale(t.scale[0]);
  }

  function onZoom() {
    var e = d3.event.sourceEvent,
      isWheel = e && ((e.type == "mousewheel") || (e.type == "wheel")),
      t = d3.transform(container.attr("transform"));
    t.translate = d3.event.translate;
    t.scale = [d3.event.scale, d3.event.scale];
    return isWheel ? zoomWheel.call(this, t, container) : zoomInst.call(this, t, container)
  }

  function zoomInst(t, target) {
    target.attr("transform", t.toString());
  }

  function zoomWheel(t, target) {
    target.transition().duration(450).attr("transform", t.toString());
  }

  g.h = h;
  g.w = w;
  g.zoom = zoom;
  g.container = function(selection) {
    var d3_data, d3_datum;
    if (selection) {
      container = g.selectAll(selection);
      // temporarily subclass container
      d3_data = container.data;
      d3_datum = container.datum;
      // need a reference to the update selection
      // so force data methods back to here
      container.data = function() {
        delete container.data; // remove the sub-classing
        return container = d3_data.apply(container, arguments)
      }
      container.datum = function() {
        delete container.datum; // remove the sub-classing
        return container = d3_datum.apply(container, arguments)
      }
    }
    return container;
  }
  g.onZoom = function(cb) {
    zoomed = cb;
  };
  g.onZoomStart = function(cb) {
    zoomStart = cb;
  };
  g.jumpTo = function(p, t) {
    (t ? g.transition().duration(t) : g)
    .call(zoom.translate(p).event)
  };
  d3.rebind(g, zoom, "translate");
  d3.rebind(g, zoom, "scale");

  return g;
}

function FDG(svg, tickLog) {
  var instID = Date.now(),
    force = d3.layout.force()
    .size([svg.w, svg.h])
    .charge(-1000)
    .linkDistance(50)
    .on("end", function() {
      // manage dead instances of force
      // only stop if this instance is the current owner
      if (cog.datum().instID != instID) return true;
      cog.classed("fa-spin", false);
      elapsedTime.stop();
    })
    .on("start", function() {
      // mark as active and brand the insID to establish ownership
      cog.classed("fa-spin", true).datum().instID = instID;
      elapsedTime.start();
    })
  fdg = {};

  function data(data) {
    force
      .nodes(data.nodes)
      .links(data.edges)
      .on("tick", (function(instID) {
        return function(e) {
          if (tickLog) tickLog.call(this, e, instID);
          lines.attr("x1", function(d) {
            return d.source.x;
          }).attr("y1", function(d) {
            return d.source.y;
          }).attr("x2", function(d) {
            return d.target.x;
          }).attr("y2", function(d) {
            return d.target.y;
          });
          node.attr("transform", function(d) {
            return "translate(" + [d.x, d.y] + ")"
          });
        }
      })(instID))
      .start();

    svg.onZoom(zoomed);

    hookDrag(force.drag(), "dragstart.force", function(d) {
      // prevent dragging on the nodes from dragging the canvas
      var e = d3.event.sourceEvent;
      e.stopPropagation();
      d.fixed = e.shiftKey || e.touches && (e.touches.length > 1);
    });
    hookDrag(force.drag(), "dragend.force", function(d) {
      // prevent dragging on the nodes from dragging the canvas
      var e = d3.event.sourceEvent;
      d.fixed = e.shiftKey || d.fixed;
    });

    var content = svg.container("g#fdg").data([data]);
    content.enter().append("g").attr({
      "id": "fdg",
      class: "outline"
    });

    var contentText = content.selectAll(".contentText")
      .data(["transform = translate ( d3.event.translate ) scale ( d3.event.scale )"])
      .enter().append("text").classed("contentText", true)
      .text(id)
      .style("fill", "#5c5c5c")
      .attr({
        "dy": 20,
        "dx": 20
      });

    var lines = content.selectAll(".links")
      .data(linksData),
      linesEnter = lines.enter()
      .insert("line", d3.select("#nodes") ? "#nodes" : null)
      .attr("class", "links")
      .attr({
        stroke: "steelblue",
        "stroke-width": 3
      });
    var nodes = content.selectAll("#nodes")
      .data(nodesData),
      nodesEnter = nodes.enter().append("g")
      .attr("id", "nodes"),
      node = nodes.selectAll(".node")
      .data(id),
      newNode = node.enter().append("g")
      .attr("class", "node")
      .call(force.drag),
      circles = newNode.append("circle")
      .attr({
        class: "content"
      })
      .attr("r", function(d) {
        return d.r
      })
      .style({
        "fill": "red",
        opacity: 0.8
      });

    lines.exit().remove();
    node.exit().remove();

    function nodesData(d) {
      return [d.nodes];
    }

    function linksData(d) {
      return d.edges;
    }

    function hookDrag(target, event, hook) {
      //hook force.drag behaviour
      var stdDragStart = target.on(event);
      target.on(event, function(d) {
        hook.call(this, d);
        stdDragStart.call(this, d);
      });
    }

    function zoomed() {
      force.alpha(0.01);
    }

    fdg.force = force;
    fdg.jumpTo = svg.jumpTo;
    // zoom context services
    //  content is the target for zoom movements in zoomed
    d3.rebind(fdg, content, "attr");
    //  access the current transform state in zoom listener coordinates
    d3.rebind(fdg, svg.zoom, "translate")

  };
  fdg.data = data;
  return fdg

}

function id(d) {
  return d;
}
svg {
  outline: 1px solid #282f51;
  pointer-events: all;
  overflow: visible;
}
g.outline {
  outline: 1px solid red;
}
#panel div {
  display: inline-block;
  margin: 0 .25em 3px 0;
}
#panel div div {
  white-space: pre;
  margin: 0 .25em 3px 0;
}
div#inputDiv {
  white-space: normal;
  display: inline-block;
}
.node {
  cursor: default;
}
text {
  font-size: 8px;
}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.3.0/css/font-awesome.min.css">
<div id="panel">
  <div id="inputDiv">
    <input id="update" type="button" value="update">
    <input id="jump" type="button" value="jump">
  </div>
  <div id="wrapAlpha">alpha:
    <div id="alpha"></div>
  </div>
  <div id="fdg">
  </div>
</div>
<div id="viz"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/40e550b1507cca31b0bb/raw/b83ceb0f8b4a2b6256f079f5887fc5243baedd4f/elapsed%2520time%25201.0.js"></script>