Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Thick Bezier curves in SVG without artifacts

Tags:

css

svg

d3.js

I try to draw thick Bezier lines (for a custom Sankey diagram). I use SVG Paths, with Bezier curves in the form of C x1 y1, x2 y2, x y. I use stroke rather than fill, so that they have constant width (and can represent flows).

It works very well if the lines are thin or if the vertical difference is relatively low. However, if they are very thick, I get some nasty artifacts (looking like horns) - see the bottom right curve from the picture below:

enter image description here

Source: http://jsfiddle.net/stared/83jr5fub/

Is there a way to avoid artifacts, i.e.:

  • ensure there is nothing on the left of x1 or right of x,
  • actual widths on the left and right match stroke-width?
like image 522
Piotr Migdal Avatar asked Dec 14 '15 12:12

Piotr Migdal


People also ask

How do I extend my Bézier curve?

You can extend an open Bézier path. To do so, briefly click one of the path's two end points with the Bézier tool while holding down the SHIFT key. You can then add further Bézier points by clicking. Bézier paths can be connected to other Bézier paths and also to lines, rectangles, polygons and ellipses.

Can Bezier curves be closed?

You can also close a curve while in the process of drawing it by Ctrl + Shift + clicking the start point while the Bezier Curve Tool is active.


2 Answers

I think that he best solution in your case (with the given path), is to make your path closed, and use its fill property.

To do this, you'll have to make a lineTo(0, strokeWidth) at the end of your BezierCurveTo, and then to redraw the bezierCurve in the other way :

var svg = d3.select("#chart");

var data = [
	{t: 5, dy: 10},
	{t: 5, dy: 20},
	{t: 5, dy: 40},
	{t: 20, dy: 10},
	{t: 20, dy: 20},
	{t: 20, dy: 40},
	{t: 50, dy: 10},
	{t: 50, dy: 20},
	{t: 50, dy: 40},
];

var ctrl = 10;
var dx = 40;
var spacing = 100;
var colors = d3.scale.category10();

svg
  .attr("width", 4 * spacing)
  .attr("height", 4 * spacing);

svg.selectAll("path")
  .data(data)
  .enter()
  	.append("path")
    .attr("d", function (d, i) {
      var x1 = spacing + spacing * (i % 3);
      var y1 = spacing + spacing * Math.floor(i / 3);
      return "M" + x1 + "," + y1 +
      "c" + ctrl + "," + 0 +
      " " + (dx - ctrl) + "," + d.dy +
      " " + dx + "," + d.dy +
      // move down for the wanted width
      "l" + (0) + "," + (d.t) +
      // negate all values
      "c" + (ctrl * -1) + "," + 0 +
      " " + ((dx - ctrl) * -1) + "," + (d.dy * -1) +
      " " + (dx * -1) + "," + (d.dy * -1);
  })
  .style("fill", colors(0))
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id="chart"></svg>

And since an animation worth more than 10 thousand words here is one showing what was happening and why it can't be called a browser bug :

@keyframes dash {
  from {
     stroke-dashoffset: -10%;
  }
   to {
     stroke-dashoffset: 90%;
  }
 }
@-webkit-keyframes dash {
  from {
     stroke-dashoffset: -10%;
  }
   to {
     stroke-dashoffset: 90%;
  }
 }
#dashed{
  animation : dash 12s linear infinite;
  }
<svg height="200" width="200" id="chart" viewBox="290 260 100 100">
<path id="dashed" style="fill: none; stroke: rgb(31, 119, 180); stroke-width: 50; stroke-dasharray: 3, 3;" d="M300,300c10,0 30,40 40,40"></path>
<path style="fill: none; stroke: black;" d="M300,300c10,0 30,40 40,40">
</path></svg>
like image 147
Kaiido Avatar answered Oct 06 '22 16:10

Kaiido


Kaiido gave an excellent and complete answer for why the SVG-path with thick stroke-width are displayed with artifacts and how to avoid this. I'll try to provide a bit more info that is specific to D3.js Sankey diagrams, as I was recently facing the same problem as Piotr Migdal.

Original Sankey diagram code

(from Sankey.js in this Sankey example, which is similar to the example Piotr Migdal mentioned)

  // regular forward node
  var x0 = d.source.x + d.source.dx,
      x1 = d.target.x,
      xi = d3.interpolateNumber(x0, x1),
      x2 = xi(curvature),
      x3 = xi(1 - curvature),
      y0 = d.source.y + d.sy + d.dy / 2,
      y1 = d.target.y + d.ty + d.dy / 2;
  return "M" + x0 + "," + y0
       + "C" + x2 + "," + y0
       + " " + x3 + "," + y1
       + " " + x1 + "," + y1;

Modified code

  // regular forward node
  var x0 = d.source.x + d.source.dx,
      x1 = d.target.x,
      xi = d3.interpolateNumber(x0, x1),
      x2 = xi(curvature),
      x3 = xi(1 - curvature),
      y0 = d.source.y + d.sy,
      y1 = d.target.y + d.ty;
  return "M" + x0 + "," + y0
       + "C" + x2 + "," + y0
       + " " + x3 + "," + y1
       + " " + x1 + "," + y1
       // move down for the wanted width
       + "l" + 0  + "," + d.dy
       // draw another path below mirroring the top
       + "C" + x3 + "," + (y1 + d.dy)
       + " " + x2 + "," + (y0 + d.dy)
       + " " + x0 + "," + (y0 + d.dy);

Then you'll also need to change your css:

  • stroke: none
  • set fill color

and remove any D3 code that sets stroke-width of HTML elements.

like image 32
AlexM Avatar answered Oct 06 '22 15:10

AlexM