rakhs rakhs - 5 months ago 105
Javascript Question

Curve d3 line animated using tweendash

Hi I am trying to curve the linestring like given in the fiddle http://jsfiddle.net/nj37gkgq/. Line is an array of links having source and destination in the form of coordinates. This line is connecting two markers in d3.geo maps. How can I achieve this?

Lines should be formed like zig zag from start position till end but here it is curved at some far distance from start point.

<!DOCTYPE html>
<html lang='en'>

<style type="text/css">


.graticule {
fill: none;
stroke: #777;
stroke-width: .5px;
stroke-opacity: .5;
}

.arc {
fill: none;
/*stroke: blue;*/
stroke: #000;
stroke-linejoin: round;
stroke-linecap: round;
/*stroke-opacity: .5;
shape-rendering: crispEdges;*/
}


body{
background-color: white;
}

svg {
background-color: black;
}

.subunit {
fill: grey;
stroke: #777;
stroke-width: 1px;
cursor: pointer;
}


.exterior-boundary {
fill: none;
stroke: black;
stroke-linejoin: round;
stroke-width: 2px;
}


g.california path {
fill: #F7DFAD;
stroke: #777;

}

path {
fill: none;

stroke-linejoin: round;
stroke-linecap: round;
}




</style>

<body>


<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>

<div id="map"></div>

<script type="text/javascript">

var width = 650,
height = 600;

var projection = d3.geo.albersUsa() //works but tried another method above
.scale(2500)
.translate([1000, 360]);

var graticule = d3.geo.graticule();

var path = d3.geo.path()
.projection(projection);

var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height);

//tooltop declaration
var div = d3.select("#map").append("div")
.attr("class", "tooltip")
.style("opacity", 0);


var coordinates = [
[ -122.762, 40.801 ],
[ -117.0978, 34.1178]
];


var url = "https://dl.dropboxusercontent.com/s/08ja6fn03y0e6ky/ca.json?dl=1";

d3.json(url, function(ca) {


svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);

svg.append("path")
.datum(topojson.feature(ca, ca.objects.subunits))
.attr("d", path);

//bind feature data to the map
svg.selectAll(".subunit")
.data(topojson.feature(ca, ca.objects.subunits).features)
.enter().append("path")
.attr("class", function(d) { return "subunit " + d.properties.name; })
.attr("d", path);



//exterior border
svg.append("path")
.datum(topojson.mesh(ca, ca.objects.subunits, function(a, b) { return a === b;}))
.attr("d", path)
.attr("class", "exterior-boundary");

//for stroke of lines
var gradient = svg.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "100%")
.attr("spreadMethod", "pad");

gradient.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color","darkblue")
.attr("stop-opacity", 1);

gradient.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color","#85c1e9")
.attr("stop-opacity", 1);

var lF = d3.svg.line()
.interpolate("basis")
.x(function(d){ return d[0] })
.y(function(d){ return d[1] });

//update
var line = svg.append("path")
.datum(coordinates)
.attr("d", function(c){

var d = {
source: projection(c[0]),
target: projection(c[1])
},

points = [];

points.push(d.source);
points.push([(d.target[0] + d.source[0]) * .4, d.target[1]]);
points.push([(d.target[0] + d.source[0]) * .8, d.source[1]]);
points.push(d.target);
console.log(points);

return lF(points);
})
.attr("stroke-width", "2.5")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.style("stroke", "url(#gradient)");

anim();

function anim() {
line.transition()
.duration(2000)
.attrTween("stroke-dasharray", function() {
var len = this.getTotalLength();
return function(t) {
return (d3.interpolateString("0," + len, len + ",0"))(t)
};
})
.each('end', anim);
}

});


</script>





Answer

EDITS w/ Better Squiggle

For a more all purpose squiggle formula, I think two opposite arcs to the midpoint looks good (c is array of [[x1,y1], [x2,y2]] of long, lat):

function twoArc(c){
    var source = projection(c[0]),
        target = projection(c[1]),
        mid = [(source[0] + target[0])/2, (source[1] + target[1])/2],
        dx1 = mid[0] - source[0],
        dx2 = target[0] - mid[0],
        dy1 = mid[1] - source[1],
        dy2 = target[1] - mid[1],
        dr1 = Math.sqrt(dx1 * dx1 + dy1 * dy1),
        dr2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);

      var rv = "M";
      rv += source[0] + "," + source[1];
      rv += "A" + dr1 + "," + dr1 + " 0 0,1 ";
      rv += mid[0] + "," + mid[1];
      rv += "A" + dr2 + "," + dr2 + " 0 0,0 ";
      rv += target[0] + "," + target[1];

      return rv;
  }

Here's a running example with various "random" coordinates:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  path {
    fill: none;
    stroke: #000;
    stroke-linejoin: round;
    stroke-linecap: round;
  }
</style>

<body>
  <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
  <script src="//d3js.org/topojson.v1.min.js"></script>
  <script>
    var width = 600,
      height = 350;

    var coordinates = [
      [-118, 34], 
      [-74, 40],
      [-86.75, 33.57],
      [-92.38, 35.22],
      [-84.87, 34.53],
      [-83.80, 41.60],
      [-96.07, 33.07],
      [-112.02, 41.18],
      [-111.0, 41.33]
    ];

    var projection = d3.geo.albersUsa()
      .scale(700)
      .translate([width / 2, height / 2]);

    var path = d3.geo.path()
      .projection(projection);

    var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height);

    d3.json("https://rawgit.com/jgoodall/us-maps/master/topojson/state.json", function(error, us) {
      if (error) return console.error(error);

      svg.append("path")
        .datum(topojson.mesh(us))
        .attr("d", path);

      var line = svg.append("path")
        .datum(twoRand())
        .attr("d", twoArc)
        .style("stroke", "steelblue")
        .style("stroke-width", 3)
        .style("fill", "none");

      anim();
      
      function twoArc(c){
        var source = projection(c[0]),
            target = projection(c[1]),
            mid = [(source[0] + target[0])/2, (source[1] + target[1])/2],
            dx1 = mid[0] - source[0],
            dx2 = target[0] - mid[0],
            dy1 = mid[1] - source[1],
            dy2 = target[1] - mid[1],
            dr1 = Math.sqrt(dx1 * dx1 + dy1 * dy1),
            dr2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
          
          var rv = "M";
          rv += source[0] + "," + source[1];
          rv += "A" + dr1 + "," + dr1 + " 0 0,1 ";
          rv += mid[0] + "," + mid[1];
          rv += "A" + dr2 + "," + dr2 + " 0 0,0 ";
          rv += target[0] + "," + target[1];
          
          return rv;
      }
      
      function twoRand(){
        var i1 = Math.floor(Math.random() * coordinates.length),
            i2 = Math.floor(Math.random() * coordinates.length);
        return [coordinates[i1], coordinates[i2]];
      }

      function anim() {
        
        line.datum(twoRand())
          .attr("d", twoArc);
        
        line.transition()
          .duration(2000)
          .attrTween("stroke-dasharray", function() {
            var len = this.getTotalLength();
            return function(t) {
              return (d3.interpolateString("0," + len, len + ",0"))(t)
            };
          })
          .each('end', anim);
      }
    });
  </script>

EDITS with First Squiggle Attempt

Here's an example with a "squiggly" line. I generate it by inserting jittered points into an array and using a d3 line-fit interpolation:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  path {
    fill: none;
    stroke: #000;
    stroke-linejoin: round;
    stroke-linecap: round;
  }
</style>

<body>
  <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
  <script src="//d3js.org/topojson.v1.min.js"></script>
  <script>
    var width = 600,
      height = 350;

    var coordinates = [
      [-118, 34], //start point
      [-74, 40] //end point
    ];

    var projection = d3.geo.albersUsa()
      .scale(700)
      .translate([width / 2, height / 2]);

    var path = d3.geo.path()
      .projection(projection);

    var lF = d3.svg.line()
      .interpolate("basis")
      .x(function(d){ return d[0] })
      .y(function(d){ return d[1] });

    var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height);

    d3.json("https://rawgit.com/jgoodall/us-maps/master/topojson/state.json", function(error, us) {
      if (error) return console.error(error);

      svg.append("path")
        .datum(topojson.mesh(us))
        .attr("d", path);

      var line = svg.append("path")
        .datum(coordinates)
        .attr("d", function(c) {
          var d = {
            source: projection(c[0]),
            target: projection(c[1])
          },
          points = [];

          points.push(d.source);
          points.push([(d.target[0] - d.source[0]) * 0.4, d.target[1]]);
          points.push([(d.target[0] - d.source[0]) * 0.8, d.source[1]]);
          points.push(d.target);
          
          return lF(points);
        })
        .style("stroke", "steelblue")
        .style("stroke-width", 3)
        .style("fill", "none");

      anim();

      function anim() {
        line.transition()
          .duration(2000)
          .attrTween("stroke-dasharray", function() {
            var len = this.getTotalLength();
            return function(t) {
              return (d3.interpolateString("0," + len, len + ",0"))(t)
            };
          })
          .each('end', anim);
      }
    });
  </script>


With Single Arc

Coded this up before I saw your comment, but you seem to be stuck on not the dash tween but how to compute a path. I see know you want a curved path, but here's an example with a simple arc on a map (from LA to NY):

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  path {
    fill: none;
    stroke: #000;
    stroke-linejoin: round;
    stroke-linecap: round;
  }
</style>

<body>
  <script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
  <script src="//d3js.org/topojson.v1.min.js"></script>
  <script>
    var width = 600,
      height = 350;

    var coordinates = [
      [-118, 34], //start point
      [-74, 40] //end point
    ];

    var projection = d3.geo.albersUsa()
      .scale(700)
      .translate([width / 2, height / 2]);

    var path = d3.geo.path()
      .projection(projection);

    var svg = d3.select("body").append("svg")
      .attr("width", width)
      .attr("height", height);

    d3.json("https://rawgit.com/jgoodall/us-maps/master/topojson/state.json", function(error, us) {
      if (error) return console.error(error);

      svg.append("path")
        .datum(topojson.mesh(us))
        .attr("d", path);

      var line = svg.append("path")
        .datum(coordinates)
        .attr("d", function(c) {
          var d = {
            source: projection(c[0]),
            target: projection(c[1])
          };
          var dx = d.target[0] - d.source[0],
            dy = d.target[1] - d.source[1],
            dr = Math.sqrt(dx * dx + dy * dy);
          return "M" + d.source[0] + "," + d.source[1] + "A" + dr + "," + dr +
            " 0 0,1 " + d.target[0] + "," + d.target[1];
        })
        .style("stroke", "steelblue")
        .style("stroke-width", 3)
        .style("fill", "none");

      anim();

      function anim() {
        line.transition()
          .duration(2000)
          .attrTween("stroke-dasharray", function() {
            var len = this.getTotalLength();
            return function(t) {
              return (d3.interpolateString("0," + len, len + ",0"))(t)
            };
          })
          .each('end', anim);
      }
    });
  </script>

Give me a few minutes and all see about a "snaked" line.