Search code examples
javascriptcssd3.jssvgstroke

d3.js - why are strokes blurry unless offset by 0.5?


I am trying to build a dashboard using d3 and I have my layout built as a responsive grid. I am using d3 to add an svg element to each item in my grid, and when I add the svg element I also add a rect that is positioned and scaled relative to the size of each grid item. The stroke (stroke-width=1) of the rect is always drawn blurry, and the only way to make it crisp is to add 0.5 to the x and y position of the rect.

Blurry: blurry stroke

Crisp (rect offset by 0.5): crisp stroke, rect offset by 0.5

I understand what anti-aliasing is and how that is fundamentally what is causing the blurry lines. I am trying to understand how/where/why the rects are being placed 0.5 off, or if by default they are placed in-between pixels.

blurry inspected

I am using .clientHeight/.clientWidth to set the viewbox of the svg element, and also to set the size of the rect. Both of those variable return integers. I am using 'fr' units to define the grid, and I tried to switch to absolute pixel values, but it did not help.

Thanks a bunch, here's a snippet and a codepen project:

  let rect = svg.selectAll('rect').data([null]);
  rect.enter().append('rect')
    .merge(rect)
      .attr('x', 10) // if I make these values '10.5'
      .attr('y', 10) // then everything looks crisp...
      .attr('width', props.width - 20)
      .attr('height', props.height - 20)
      .style('fill', 'none')
      .style('stroke', '#FFFFFF')
      .style('stroke-width', '1')

https://codepen.io/markersniffen/project/editor/AKmYea


Solution

  • It's because in SVG (and HTML5's <canvas>) each "pixel" coordinate point exists on a line that runs between pixels.

    So if you have a vertical line that's 1px wide that runs from (0,0) to (0,5) then if the line's stroke was converted to a rectangle its coordinates would be:

    (Illustrations are not to-scale):

       (-0.5, -0.5)  +-------+  (0.5, -0.5)
                     |       |
                     |       |
                     |       |
                     |       |
                     |<-1px->|
                     |       |
                     |       |
                     |       |
                     |       |
       (-0.5,  5.5)  +-------+  (0.5,  5.5)
    
    

    As you noticed, by applying your own 0.5px offset to the point coordinates then the lines snap to the pixel grid again:

             (0, 0)  +-------+  (1, 0)
                     |       |
                     |       |
                     |       |
                     |       |
                     |<-1px->|
                     |       |
                     |       |
                     |       |
                     |       |
             (0, 6)  +-------+  (1, 6)
    
    

    To rectify this (no pun intended), you can continue to use your +0.5px-offset approach, or ensure all strokes for pixel-snapped lines are multiples of 2px wide (but this does mean you can't have a 1px-wide stroked line or path along integer pixel coordinate points).

    You can also change the anti-aliasing settings on each SVG path/shape element, but I don't recommend this because if you have any strokes or points that don't exactly align with the pixel grid (i.e. your SVG file has more than just only horizontal and vertical lines) then the results will look ugly on-screen, especially on low-DPI devices.