Reputation: 4127
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
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.
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>
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
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
Reputation: 21163
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