Search code examples
d3.jssvgsvg-filters

Dilate SVG element if overlapping another element


I have a data diagram I created with D3 where I draw a circle for each data point.

When the circles overlap I want to render them slightly bigger so that you can see a difference when several data points pile up at the same position.

I found I can apply an feMorphology filter with dilate but I need to be able to somehow scale the dilation depending on the number of circles at the same position.

Here the SVG demonstration:

<svg width="400" height="120" xmlns="http://www.w3.org/2000/svg">
 <filter id="dilateIfOverlapping">
  <feMorphology operator="dilate" radius="4">
 </filter>
 <g filter="url(#dilateIfOverlapping)">
   <circle cx="60" cy="60" r="30" fill="green" />
   <circle cx="60" cy="60" r="30" fill="green" />
   <circle cx="170" cy="60" r="30" fill="green" />
 </g>
</svg>

This renders two circles of the same size. I want the first one to be bigger as there are two circles on top of each other.

Any idea how to achieve this without programmatically setting a different circle radius?


Solution

  • You can do this kind of thing with feComponentTransfer/feFuncA. But it only handles fully overlapping points vs. partial overlapping.

    The way it works is that it renders the circles first with low opacity. So the more circles rendered at the same point, the higher their opacity. Then the filter chops up the image into different layers with different opacity ranges. Then it applies a different sized blur to the each of the layers and then dials up the opacity on the blurred circle and adds a little fake anti-aliasing with an additional blur.

    The trick here is getting the opacity value of the circles just right, so it lines up with the opacity ranges of the feFuncA tableValues. You'd have to know ahead of time what the maximum number of overlapping points is going to be so you can calibrate this correctly. (This tableValues="0 1 0 0 0" for example, creates 5 opacity ranges 0-20%, 20-40%, 40-60%, 60-80% and 80-100%.)

    <svg width="800px" height="600px">
      <defs>
      <filter id="embiggen" x="-50%" y="-50%" width="200%" height="200%">
        <feComponentTransfer in="SourceGraphic" result="layer1">
          <feFuncA type="discrete" tableValues="0 1 0 0 0" />
          </feComponentTransfer>
          
             <feComponentTransfer in="SourceGraphic" >
          <feFuncA type="discrete" tableValues="0 0 1 0 0"  />
          </feComponentTransfer>
        <feGaussianBlur stdDeviation= "2"/>
        <feComponentTransfer >
          <feFuncA type="discrete" tableValues="0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"/>
          </feComponentTransfer>
          <feGaussianBlur stdDeviation= "0.5" result="layer2"/>
        
        <feComponentTransfer in="SourceGraphic" >
          <feFuncA type="discrete" tableValues="0 0 0 1 0" />
          </feComponentTransfer>
        <feGaussianBlur stdDeviation= "4"/>
        <feComponentTransfer>
        <feFuncA type="discrete" tableValues="0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1"/>
          </feComponentTransfer>
              <feGaussianBlur stdDeviation= "0.5" result="layer3"/>
                      
        <feComponentTransfer in="SourceGraphic">
          <feFuncA type="discrete" tableValues="0 0 0 0 1" />
        </feComponentTransfer>
        <feGaussianBlur stdDeviation= "8"/>
        <feComponentTransfer >
         <feFuncA type="discrete" tableValues="0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1" />                   
        </feComponentTransfer>
         <feGaussianBlur stdDeviation= "0.5" result="layer4"/>
          
        <feMerge>
          <feMergeNode in="layer1"/>      
          <feMergeNode in="layer2"/>
          <feMergeNode in="layer3"/>
          <feMergeNode in="layer4"/>
        </feMerge>
        </defs>
      </filter>
      
      <g filter="url(#embiggen)" shape-rendering="crispEdges">
    
        <circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>  
        <circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="50" cy="50" fill-opacity="0.21" r="10" fill="red"/>  
        
        <circle cx="120" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="120" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="120" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="120" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        
        <circle cx="180" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="180" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        <circle cx="180" cy="50" fill-opacity="0.21" r="10" fill="red"/>
        
        <circle cx="240" cy="50" fill-opacity="0.21" r="10" fill="red"/>
    
      </g>
      
    </svg>