I tried to create a dynamic pie-chart with angular and the svg path element.
I succeeded in principle, but it seems that the individual slices do not properly converge in the center. The effect gets worse when one chooses a bigger stroke-width
but even with no stroke-width
it is still noticeable.
Why is that the case and how can I solve it?
import {Component} from '@angular/core';
import {bootstrapApplication} from '@angular/platform-browser';
@Component({
selector: 'app-root',
template: `
<div style="background-color: #dddddd; width: 1000px; height: 1000px;">
<svg viewBox="-5 -5 105 105">
<g transform="translate(50, 50)">
@for( p of paths; track $index ){
<path stroke-width="1" stroke="black" [attr.fill]="colors[$index]" [attr.d]="p"></path>
}
</g>
</svg>
</div>
`,
})
export class Playground {
paths : string[] = [];
colors : string[] = [];
constructor(){
const d = 4; // decimal places
const angles = [0, 1, 2, 2.4, 4, 5, 2*Math.PI];
const n = angles.length;
const r = 25;
for( let i = 0; i < n-1; ++i ){
this.colors.push(this.getRandomColor())
const a0 = angles[i];
const a1 = angles[i+1];
const dA = (a1 - a0)/Math.PI * 180;
const x0 = r * Math.cos(a0);
const y0 = -r * Math.sin(a0);
const x1 = r * Math.cos(a1);
const y1 = -r * Math.sin(a1);
const largeAngle = dA < 180 ? 0 : 1;
const path = `
M 0 0
L ${x0.toFixed(d)} ${y0.toFixed(d)}
A ${r.toFixed(d)} ${r.toFixed(d)} -${dA.toFixed(d)} ${largeAngle} 0 ${x1.toFixed(4)} ${y1.toFixed(4)}
L 0 0 z`;
console.log(path);
this.paths.push(path);
}
}
private getRandomColor() {
var letters = '0123456789ABCDEF';
var color = '#';
for (var i = 0; i < 6; i++) {
color += letters[Math.floor(Math.random() * 16)];
}
return color;
}
}
bootstrapApplication(Playground);
Your calculations don't introduce this issue.
This known issue stems from anti-aliasing when SVG coordinates and the "intrinsic" coordinate space don't perfectly match the ultimate device pixel-grid.
So depending on the scaling – gaps may be visible in an SVG, despite the fact start and end coordinates of its edges are coinciding geometrically/numerically.
We can even illustrate this phenomenon with a far more simple example rendering adjacent rectangles (at least in Chromium/Blink browsers – Firefox has a smarter algorithm for adjacent horizontal or vertical edges):
.resize {
resize: both;
overflow: auto;
outline: 2px dashed #ccc;
}
<h3>Adjacent rectangles – no gap existent</h3>
<div class="resize">
<svg viewBox="0 0 10 10">
<rect y="0" width="10" height="4.1" fill="#333" />
<rect y="4.1" width="10" height="5" fill="#333" />
</svg>
</div>
<h3>Adjacent rectangles – might be floating point coordinates?</h3>
<p>... No not really: All coordinates use integers</p>
<div class="resize">
<svg viewBox="0 0 100 100">
<rect y="0" width="100" height="41" fill="#333" />
<rect y="41" width="100" height="50" fill="#333" />
</svg>
</div>
<h3>Take intrinsic dimensions and integers</h3>
<svg viewBox="0 0 100 100" width="100">
<rect y="0" width="100" height="41" fill="#333" />
<rect y="41" width="100" height="50" fill="#333" />
</svg>
<h3>Take intrinsic dimensions and floats</h3>
<svg viewBox="0 0 100 100" width="100">
<rect y="0" width="100" height="41.1" fill="#333" />
<rect y="41.1" width="100" height="50" fill="#333" />
</svg>
<h3>shape-rendering="crispEdges"</h3>
<svg viewBox="0 0 100 100">
<rect y="0" width="100" height="40.1" fill="#333" shape-rendering="crispEdges" />
<rect y="40.1" width="100" height="50" fill="#333" shape-rendering="crispEdges" />
</svg>
<h3>shape-rendering="geometricPrecision"</h3>
<p>... Slightly disappointing on Chromium</p>
<svg viewBox="0 0 100 100">
<rect y="0" width="100" height="40.1" fill="#333" shape-rendering="geometricPrecision" />
<rect y="40.1" width="100" height="50" fill="#333" shape-rendering="geometricPrecision" />
</svg>
The results will differ slightly across different browsers. However, this is not necessarily an issue related to floating-point coordinates, but rather it is affected by the current scaling passed to the renderer, which may introduce semi-transparent adjacent edge pixels. Ultimately, these produce the "halos" or hairlines.
Diagonals make things more noticeable.
shape-rendering
svg {
display: block;
outline: 1px solid #ccc;
overflow: visible;
}
.stroke path {
stroke-width: 0.1px;
}
.no-stroke path {
stroke: none;
fill: #000;
}
.resize {
resize: both;
overflow: auto;
outline: 2px dashed #ccc;
}
<h3>Gaps due to rendering</h3>
<p>Notice small mageta lines blinking though the segments</p>
<div class="resize">
<svg class="no-stroke" viewBox="-25 -25 50 50">
<!-- magenta background circle -->
<circle cx="0" cy="0" fill="magenta" r="25" />
<path stroke="black" fill="#A95992" d="M 0 0 L 25 0 A 25 25 0 0 0 13.5076 -21.0368 z" />
<path stroke="black" fill="#2B37F6" d="M 0 0 L 13.5076 -21.0368 A 25 25 0 0 0 -10.4037 -22.7324 z" />
<path stroke="black" fill="#94F090" d="M 0 0 L -10.4037 -22.7324 A 25 25 0 0 0 -18.4348 -16.8866 z" />
<path stroke="black" fill="#C7016A" d="M 0 0 L -18.4348 -16.8866 A 25 25 0 0 0 -16.3411 18.9201 z" />
<path stroke="black" fill="#68015C" d="M 0 0 L -16.3411 18.9201 A 25 25 0 0 0 7.0916 23.9731 z" />
<path stroke="black" fill="#AE2EFC" d="M 0 0 L 7.0916 23.9731 A 25 25 0 0 0 25 0 z" />
</svg>
</div>
<h2>Workarounds</h2>
<h3>Add strokes</h3>
<p>Not ideal - we see non-convergent edges in the center</p>
<div class="resize">
<svg class="stroke" viewBox="-25 -25 50 50">
<path stroke="#A95992" fill="#A95992" d="M 0 0 L 25 0 A 25 25 0 0 0 13.5076 -21.0368 z" />
<path stroke="#2B37F6" fill="#2B37F6" d="M 0 0 L 13.5076 -21.0368 A 25 25 0 0 0 -10.4037 -22.7324 z" />
<path stroke="#94F090" fill="#94F090" d="M 0 0 L -10.4037 -22.7324 A 25 25 0 0 0 -18.4348 -16.8866 z" />
<path stroke="#C7016A" fill="#C7016A" d="M 0 0 L -18.4348 -16.8866 A 25 25 0 0 0 -16.3411 18.9201 z" />
<path stroke="#68015C" fill="#68015C" d="M 0 0 L -16.3411 18.9201 A 25 25 0 0 0 7.0916 23.9731 z" />
<path stroke="#AE2EFC" fill="#AE2EFC" d="M 0 0 L 7.0916 23.9731 A 25 25 0 0 0 25 0 z" />
</svg>
</div>
<h3>Add strokes in background</h3>
<p>Fill potential gaps in a more subtle fashion</p>
<div class="resize">
<svg viewBox="-25 -25 50 50">
<g id="bg" class="stroke">
<path stroke="#A95992" fill="#A95992" d="M 0 0 L 25 0 A 25 25 0 0 0 13.5076 -21.0368 z" />
<path stroke="#2B37F6" fill="#2B37F6" d="M 0 0 L 13.5076 -21.0368 A 25 25 0 0 0 -10.4037 -22.7324 z" />
<path stroke="#94F090" fill="#94F090" d="M 0 0 L -10.4037 -22.7324 A 25 25 0 0 0 -18.4348 -16.8866 z" />
<path stroke="#C7016A" fill="#C7016A" d="M 0 0 L -18.4348 -16.8866 A 25 25 0 0 0 -16.3411 18.9201 z" />
<path stroke="#68015C" fill="#68015C" d="M 0 0 L -16.3411 18.9201 A 25 25 0 0 0 7.0916 23.9731 z" />
<path stroke="#AE2EFC" fill="#AE2EFC" d="M 0 0 L 7.0916 23.9731 A 25 25 0 0 0 25 0 z" />
</g>
<!-- foreground -->
<path fill="#A95992" d="M 0 0 L 25 0 A 25 25 0 0 0 13.5076 -21.0368 z" />
<path fill="#2B37F6" d="M 0 0 L 13.5076 -21.0368 A 25 25 0 0 0 -10.4037 -22.7324 z" />
<path fill="#94F090" d="M 0 0 L -10.4037 -22.7324 A 25 25 0 0 0 -18.4348 -16.8866 z" />
<path fill="#C7016A" d="M 0 0 L -18.4348 -16.8866 A 25 25 0 0 0 -16.3411 18.9201 z" />
<path fill="#68015C" d="M 0 0 L -16.3411 18.9201 A 25 25 0 0 0 7.0916 23.9731 z" />
<path fill="#AE2EFC" d="M 0 0 L 7.0916 23.9731 A 25 25 0 0 0 25 0 z" />
</svg>
</div>
<h3>Add strokes as a design concept</h3>
<div class="resize">
<svg viewBox="-25 -25 50 50" stroke="white">
<!-- foreground -->
<path fill="#A95992" d="M 0 0 L 25 0 A 25 25 0 0 0 13.5076 -21.0368 z" />
<path fill="#2B37F6" d="M 0 0 L 13.5076 -21.0368 A 25 25 0 0 0 -10.4037 -22.7324 z" />
<path fill="#94F090" d="M 0 0 L -10.4037 -22.7324 A 25 25 0 0 0 -18.4348 -16.8866 z" />
<path fill="#C7016A" d="M 0 0 L -18.4348 -16.8866 A 25 25 0 0 0 -16.3411 18.9201 z" />
<path fill="#68015C" d="M 0 0 L -16.3411 18.9201 A 25 25 0 0 0 7.0916 23.9731 z" />
<path fill="#AE2EFC" d="M 0 0 L 7.0916 23.9731 A 25 25 0 0 0 25 0 z" />
</svg>
</div>
<h3>Add gaps in the arc calculations </h3>
<p>The gaps are programmatically subtracted from the arc starting and end points</p>
<svg viewBox="0 0 100 100">
<path d="M 51 0.01 A 50 50 0 0 1 84.641 13.945 L 63.418 35.169 A 20 20 0 0 0 51 30.025 z" fill="#bacc33" class="segment segment-12_500 segment-1 " data-percent="12.500"></path>
<path d="M 86.055 15.359 A 50 50 0 0 1 99.99 49 L 69.975 49 A 20 20 0 0 0 64.831 36.582 z" fill="#4fcc33" class="segment segment-12_500 segment-2 " data-percent="12.500"></path>
<path d="M 99.99 51 A 50 50 0 1 1 49 0.01 L 49 30.025 A 20 20 0 1 0 69.975 51 z" fill="#cc3333" class="segment segment-75_000 segment-3 " data-percent="75.000"></path></svg>
All in all it's not you script but the rendering/rasterization.
However, you can simplify your pathData output
const path = `
M 0 0
L ${+x0.toFixed(d)} ${+y0.toFixed(d)}
A ${+r.toFixed(d)} ${+r.toFixed(d)} 0 ${largeAngle} 0
${+x1.toFixed(4)} ${+y1.toFixed(4)}
z`;
Since we're dealing with circular arcs we don't need a xAxisRotation
value – if rx===ry
this parameter is ignored. Also, better convert rounded values to numbers to avoid unnecessary decimals. In your case: We also don't need the explicit lineto closing the path to the starting point – the Z
command already does this.