Home > OS >  D3js customise line style using shapes along an SVG path
D3js customise line style using shapes along an SVG path

Time:07-29

A standard line in an SVG graphic allows altering basic properties such as stroke width, color, linecap, and dasharray to created dashed, or dotted lines.

Is it possible to add more complex features to lines?

For example, is it possible to replicate a shape along a pre-existing line? Similar to a dotted line, but with stars, or crosses?

Use case might be a printed black & white line chart, where color coding the lines is not easily legible.

A simple path drawn with D3 might use a function like this:

const drawLine = d3.line()
        .y(d => y(d.y))
        .x(d => x(d.x))

With output

<path  d="M530,116.2995087503838L454.28571428571433,122.98894688363525L227.14285714285717,102.0018421860608L151.42857142857142,65.41142155357693L75.71428571428571,50.420632483880865L0,0"></path>

Is it possible to evenly space shapes along this path? The 'points' being unrelated to anything in the data.

Edit: Some clever CSS tricks to create custom line patterns is also a valid solution.

CodePudding user response:

Is it possible to evenly space shapes along this path?
The 'points' being unrelated to anything in the data.

NOT using <marker>

A native Web Component that will proces
<path marker="mark1" markers="5" d="...path..." />

and add <animateMotion> for each marker will do the job.

Set dur=0.0001 to display 'instant' (you can't set it to 0)

<svg-path-markers>
  <svg viewBox="0 0 200 70" style="background:pink">
    <defs>
      <g id="mark1">
        <circle cx="0" cy="0" r="5"/>
        <rect x="-2" y="-2" width="4" height="4" fill="gold" />
      </g>
      <use id="mark2" href="#mark1" y="10" fill="green" transform="scale(.5)"/>
    </defs>
    <g fill="blue">
      <path marker="mark1" markers="5" fill="none" stroke="teal"
            d="m10,6c20,0,25,25,180,25" />
    </g>
    <path marker="mark2" markers="10" fill="none" 
          stroke="red" d="m10,15c40,0,45,35,180,35" />
  </svg>
</svg-path-markers>
<script>
  customElements.define("svg-path-markers", class extends HTMLElement {
    connectedCallback() {
      setTimeout(() => this.querySelectorAll("[marker]")
                           .forEach(p=>this.markPath(p)));
    }
    markPath(path,steps = ~~path.getAttribute("markers") ){
      let id = path.id || (path.id = this.localName   Math.random()*1e18); // a unique id
      const marker = dist => `<use href="#${path.getAttribute("marker")}">
                               <animateMotion dur="1s" keyPoints="0;${dist}" 
                                 keyTimes="0;1" fill="freeze" calcMode="linear">
                               <mpath href="#${id}"/></animateMotion></use>`;
      path.insertAdjacentHTML("afterend", Array(steps)
                                           .fill(0)
                                           .map((_,i) => marker(i*(1/(steps-1))))
                                           .join(""));
    }
  })
</script>

CodePudding user response:

You could also mimic a custom stroke style using css offset-path.

Similar to svg's <mpath> you can define a path to align elements with.

The main difference: we can distribute multiple elements along the path using offset-distance – so we don't need to mimic offset by stopping/delaying animations.

Example 1: no animations

let svg = document.querySelector('svg');
// define pattern symbol element
let patternElMarkup =
`<symbol id="patternEl" >
    <path   d="M10 16.92l-6.18 3.08l0.88-7.14l-4.7-5.22l6.72-1.34l3.28-6.3l3.28 6.3l6.72 1.34l-4.7 5.22l0.88 7.14"></path>
</symbol>`;
svg.insertAdjacentHTML('afterbegin', patternElMarkup);

let patternEl = document.querySelector('.patternEl');
let offsetPath = document.querySelector('.offsetPath');
let offsetPathD = offsetPath.getAttribute('d');
let pathLength = offsetPath.getTotalLength();

// insert offset Path css
let style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
style.textContent = `  
.patternEl{
    offset-path: path('${offsetPathD}');
}`;
svg.insertBefore(style, svg.children[0] );

let patternCount = 6;
let startOffset = 0;
let endOffset = 0;
let steps = 100/pathLength * (pathLength) / (patternCount-1   startOffset   endOffset);
let offSetRotate = 0;
let offsetPattern = 0;

for (let i = startOffset; i < patternCount 1; i  ) {
    offsetPattern = steps*i;
    if(offsetPattern<=100){
        //add use instances of pattern
        let use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
        use.setAttribute('href', '#patternEl');
        use.setAttribute('x', '-10');
        use.setAttribute('y', '-10');
        use.setAttribute('fill', 'gold');
        use.classList.add('patternEl');
        use.setAttribute('style', `offset-distance: ${offsetPattern}%; offset-rotate: ${offSetRotate}deg` );
        svg.appendChild(use);
    }
}
    <svg viewBox="0 0 530 122.989" overflow="visible">
        <path  d="M530 116.3l-75.714 6.689l-227.143-20.987l-75.714-36.59l-75.715-14.991l-75.714-50.421" fill="none" stroke="#ccc" stroke-width="1"/>
    </svg>

Example 2: animated; using start and end offsets

let svg = document.querySelector('svg');
// add pattern symbol element
let patternElMarkup =
`<symbol id="patternEl" >
    <path   d="M10 16.92l-6.18 3.08l0.88-7.14l-4.7-5.22l6.72-1.34l3.28-6.3l3.28 6.3l6.72 1.34l-4.7 5.22l0.88 7.14"></path>
</symbol>`;
svg.insertAdjacentHTML('afterbegin', patternElMarkup);

let patternEl = document.querySelector('.patternEl');
let offsetPath = document.querySelector('.offsetPath');
let offsetPathD = offsetPath.getAttribute('d');
let pathLength = offsetPath.getTotalLength();

// add offset Path css
let style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
style.textContent = `  
.patternEl{
    offset-path: path('${offsetPathD}');
    animation: animateOffset 1s linear reverse;
    opacity:1;
}
@keyframes animateOffset{
to {
    offset-distance: 100%;
    opacity:0;
}
}`;
svg.insertBefore(style, svg.children[0] );

let patternCount = 6;
let startOffset = 1;
let endOffset = 1;
let steps = 100/pathLength * (pathLength) / (patternCount-1   startOffset   endOffset);
let offSetRotate = 0;
let offsetPattern = 0;


for (let i = startOffset; i < patternCount 1; i  ) {
    offsetPattern = steps*i;
    if(offsetPattern<=100){
        //add use instances of pattern
        let use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
        use.setAttribute('href', '#patternEl');
        use.setAttribute('x', '-10');
        use.setAttribute('y', '-10');
        use.setAttribute('fill', 'gold');
        use.classList.add('patternEl');
        use.setAttribute('style', `offset-distance: ${offsetPattern}%; offset-rotate: ${offSetRotate}deg` );
        svg.appendChild(use);
    }
}
<svg viewBox="0 0 530 122.989" overflow="visible">
        <path  d="M530 116.3l-75.714 6.689l-227.143-20.987l-75.714-36.59l-75.715-14.991l-75.714-50.421" fill="none" stroke="#ccc" stroke-width="1"/>
    </svg>

  • Related