atlas_scoffed
atlas_scoffed

Reputation: 4127

D3js customise line style using shapes along an SVG path

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 class="line" 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.

Upvotes: 0

Views: 341

Answers (3)

herrstrietzel
herrstrietzel

Reputation: 17155

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.

Update: As @Danny '365CSI' Engelman pointed, offset-distance and offset-path are currently (2022) not supported in Safari:
Caniuse offset-distance and offset-path.

Example 1: no animations

let svg = document.querySelector('svg');
// define pattern symbol element
let patternElMarkup =
`<symbol id="patternEl" class="patternEl">
    <path class="patternPath"  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 class="offsetPath" 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" class="patternEl">
    <path class="patternPath"  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 class="offsetPath" 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>

Upvotes: 0

smpa01
smpa01

Reputation: 4282

Here is a d3 solution. Please read through my comments in the code.

//a container to create a partition
const partition = [];

//define desired partition here
for (let i = 0; i < 10; i++) {
    partition.push(i / 10)
};

//generate d3 built in symbol;
const star =
    d3.symbol().type(d3.symbolStar).size(50);

//make it data-bound
d3.select('svg')
    .append('g')
    .attr('class', 'starContainer')
    .selectAll('path')
    .data(partition)
    .join('path')
    .attr('d', star)
    .attr("transform", (d) => {
        const path = d3.select('.line').node();
        const length = path.getTotalLength();
        const point = path.getPointAtLength(length * d);
        const x = point.x;
        const y = point.y;
        return `translate(${x},${y})`;
    })

//validating the above with svg-text
d3.select('svg')
    .append('g')
    .attr('class', 'textContainer')
    .selectAll('text')
    .data(partition)
    .join('text')
    .attr('x', (d) => {
        const path = d3.select('.line').node();
        const length = path.getTotalLength();
        const point = path.getPointAtLength(length * d);
        return point.x
    })
    .attr('y', (d) => {
        const path = d3.select('.line').node();
        const length = path.getTotalLength();
        const point = path.getPointAtLength(length * d);
        return point.y
    })
    .text((d) => {
        return d3.format(',.1%')(d)
    })
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="https://d3js.org/d3.v7.min.js"></script>
</head>
<body>

<svg width="1280" height="720" viewBox="0 0 1280 720">
   <path class="line" d="M530,116.2995087503838L454.28571428571433,122.98894688363525L227.14285714285717,102.0018421860608L151.42857142857142,65.41142155357693L75.71428571428571,50.420632483880865L0,0" stroke="red" fill="none"></path>
</svg> 
 
</body>
<script type="text/javascript">


</script>
</html>

Upvotes: 0

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>

Upvotes: 1

Related Questions