Is there a way to get a subset/section of a path in d3.js?

Viewed 351

Assume I create some arbitrary path using the curve part of d3-shape, by providing it a small set of x,y points, like so:

var my_curve = d3.line()
    .x(function(d) { return d.x; })
    .y(function(d) { return d.y; })
    .curve(d3.curveCatmullRom.alpha(0.5));

var curve_dat = [{x:10,y:10},{x:110,y:120},{x:160,y:20},{x:400,y:60},{x:200,y:360},{x:40,y:300}];

var svg_main = d3.select("div#svg_cont")
  .append('svg')
  .attr('width', 600)
  .attr('height', 400);

var path_1 = svg_main.append('path')
              .datum(curve_dat)
              .attr('fill', 'none')
              .attr('stroke', 'red')
              .attr('stroke-width', 3)
              .attr('d', my_curve);

svg_main.selectAll('circle')
              .data(curve_dat)
              .enter()
              .append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'black')
              .attr('stroke-width', 3)
              .attr('cx', d => d.x)
              .attr('cy', d => d.y)
              .attr('r', 5);

which produces:

enter image description here

(where the black circles are the locations of the generating points.)

Now, I want to choose two points along the path, and get the section/subset of the original path from them:

pt1 = path_1.node().getPointAtLength(300);
svg_main.append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'green')
              .attr('stroke-width', 3)
              .attr('cx', pt1.x)
              .attr('cy', pt1.y)
              .attr('r', 5);

pt1 = path_1.node().getPointAtLength(450);
svg_main.append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'green')
              .attr('stroke-width', 3)
              .attr('cx', pt1.x)
              .attr('cy', pt1.y)
              .attr('r', 5);

enter image description here

(where the green circles are the bounds of the section I want to get)

So even though I have the x,y for each of those points, I can't just draw the curve generated by those two, because it'll be different than the original red curve between them.

Is there a smart way to do this? From a bit of searching it seems like there used to be a method called SVGPathElement.createSVGPathSegClosePath(), but it's deprecated and they don't even provide documentation on it.

Is there another way?

1 Answers

Strictly - it's very difficult to get path attribute data to perfectly replicate that curve, as Gerardo notes. Normally I would leave it at that as he is an excellent authority. But, I'm a bit of a contrarian and have had a bit of beer, so let's see what we can do.

Option one is easy - what do we really need the path data for? If it is to simply draw, then we can use a stroke dash array. We use the dash array to hide everything that isn't our path section:

var my_curve = d3.line()
    .x(function(d) { return d.x; })
    .y(function(d) { return d.y; })
    .curve(d3.curveCatmullRom.alpha(0.5));

var curve_dat = [{x:10,y:10},{x:110,y:120},{x:160,y:20},{x:400,y:60},{x:200,y:360},{x:40,y:300}];

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

var path_1 = svg_main.append('path')
              .datum(curve_dat)
              .attr('fill', 'none')
              .attr('stroke', 'red')
              .attr('stroke-width', 3)
              .attr('d', my_curve)
              .attr("stroke-dasharray","0 300 150 1000");

svg_main.selectAll('circle')
              .data(curve_dat)
              .enter()
              .append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'black')
              .attr('stroke-width', 3)
              .attr('cx', d => d.x)
              .attr('cy', d => d.y)
              .attr('r', 5);
              
pt1 = path_1.node().getPointAtLength(300);
svg_main.append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'green')
              .attr('stroke-width', 3)
              .attr('cx', pt1.x)
              .attr('cy', pt1.y)
              .attr('r', 5);

pt1 = path_1.node().getPointAtLength(450);
svg_main.append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'green')
              .attr('stroke-width', 3)
              .attr('cx', pt1.x)
              .attr('cy', pt1.y)
              .attr('r', 5);              
              
           
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

We can animate it too:

var my_curve = d3.line()
    .x(function(d) { return d.x; })
    .y(function(d) { return d.y; })
    .curve(d3.curveCatmullRom.alpha(0.5));

var curve_dat = [{x:10,y:10},{x:110,y:120},{x:160,y:20},{x:400,y:60},{x:200,y:360},{x:40,y:300}];

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

var path_1 = svg_main.append('path')
              .datum(curve_dat)
              .attr('fill', 'none')
              .attr('stroke', 'red')
              .attr('stroke-width', 3)
              .attr('d', my_curve)
              .attr("stroke-dasharray","0 300 0 1000")
              .transition()
              .delay(500)
              .duration(2000)
              .attr("stroke-dasharray","0 300 150 1000");

svg_main.selectAll('circle')
              .data(curve_dat)
              .enter()
              .append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'black')
              .attr('stroke-width', 3)
              .attr('cx', d => d.x)
              .attr('cy', d => d.y)
              .attr('r', 5);
              
pt1 = path_1.node().getPointAtLength(300);
svg_main.append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'green')
              .attr('stroke-width', 3)
              .attr('cx', pt1.x)
              .attr('cy', pt1.y)
              .attr('r', 5);

pt1 = path_1.node().getPointAtLength(450);
svg_main.append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'green')
              .attr('stroke-width', 3)
              .attr('cx', pt1.x)
              .attr('cy', pt1.y)
              .attr('r', 5);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Option two is less clean. If the goal is to have the path d attributes, well that isn't going to be simple, but we could approximate that sub path by sampling points along the subsection to recreate it. If we use a d3 line with a basis curve or something like that we can get pretty close:

var my_curve = d3.line()
    .x(function(d) { return d.x; })
    .y(function(d) { return d.y; })
    .curve(d3.curveCatmullRom.alpha(0.5));

var curve_dat = [{x:10,y:10},{x:110,y:120},{x:160,y:20},{x:400,y:60},{x:200,y:360},{x:40,y:300}];

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

var path_1 = svg_main.append('path')
              .datum(curve_dat)
              .attr("stroke","steelblue")
              .attr('d', my_curve);

svg_main.selectAll('circle')
              .data(curve_dat)
              .enter()
              .append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'black')
              .attr('stroke-width', 3)
              .attr('cx', d => d.x)
              .attr('cy', d => d.y)
              .attr('r', 5);
              
pt1 = path_1.node().getPointAtLength(300);
svg_main.append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'black')
              .attr('stroke-width', 3)
              .attr('cx', pt1.x)
              .attr('cy', pt1.y)
              .attr('r', 5);

pt1 = path_1.node().getPointAtLength(450);
svg_main.append('circle')
              .attr('fill', 'none')
              .attr('stroke', 'green')
              .attr('stroke-width', 3)
              .attr('cx', pt1.x)
              .attr('cy', pt1.y)
              .attr('r', 5);
              

svg_main.append("path")
  .attr("d",getSubPath(path_1))
  .attr("stroke","orange");
  

function getSubPath(path) {
  var path = path.node();
  var start = 300;
  var end = 450;
  var every = 5; // sample a point every 5 pixels on the path.
  var sampledPoints = d3.range((end-start)/every).map(function(d) {
   var p = path.getPointAtLength(start+d*every);
   return [p.x,p.y];
  })
  return d3.line().curve(d3.curveBasis)(sampledPoints); 
}
path {
  fill:none;
  stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

The sampled line is orange, the original line is blue, for most intents and purposes, these lines are indistinguishable (with the orange line being sampled at every 5 pixels along the blue line with a basis curve):

enter image description here

Related