简体   繁体   中英

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.

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,10c20,0,25,25,180,25" /> </g> <path marker="mark2" markers="10" fill="none" stroke="darkgreen" d="m10,10c40,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>

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" 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>

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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM