Question Details

No question body available.

Tags

html angular svg pie-chart

Answers (3)

Accepted Answer Available
Accepted Answer
July 24, 2025 Score: 1 Rep: 19,678 Quality: High Completeness: 100%

TL;DR: it's the renderer not your coordinates

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;

}

Adjacent rectangles – no gap existent

Adjacent rectangles – might be floating point coordinates?

... No not really: All coordinates use integers

Take intrinsic dimensions and integers

Take intrinsic dimensions and floats

shape-rendering="crispEdges"

shape-rendering="geometricPrecision"

... Slightly disappointing on Chromium

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.

Workarounds

  1. cheat! we can close these non existing gaps by adding strokes of the same fill color around the pie-chart segments
    1.2 Duplicate the pie segments as a background with strokes to fill the gaps
  2. Tweak rendering behavior via shape-rendering
  3. Add significant visible gaps as a part of your design concept: Also, be aware that some users may have visual impairments, such as colour vision deficiencies or other forms of reduced vision, which can make distinguishing between pie-chart segments difficult.
    In terms of both accessibility and ease of use, this is probably the best workaround.

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;

}

Gaps due to rendering

Notice small mageta lines blinking though the segments

Workarounds

Add strokes

Not ideal - we see non-convergent edges in the center

Add strokes in background

Fill potential gaps in a more subtle fashion

Add strokes as a design concept

Add gaps in the arc calculations

The gaps are programmatically subtracted from the arc starting and end points

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.

Related posts

July 23, 2025 Score: 1 Rep: 22,339 Quality: Medium Completeness: 100%

You have 96.8% of the code for a native JavaScript Web Component!

  • its faster without the Angular mumbo-jumbo
  • will run in every Framework (or NO Framework)
  • and as Stack Snippet here in Stack Overflow

When doing Math, you want a bigger viewBox, to cancel out rounding errors.

Note: I did a 1 KB Web Component without Math.PI many moons ago.

~~n; // no decimals for faster calculations const angles = [0, 1, 2, 2.4, 4, 5, 2 Math.PI]; const radius = ~~this.getAttribute("radius") || 666; // larger is better accuracy const strokewidth = radius/20; let paths = ""; for (let i = 0; i < angles.length - 1; ++i) { let fill = '#'; for (let c = 0; c < 6; c++) fill += '0123456789ABCDEF'[Math.floor(Math.random() 16)]; const a0 = angles[i], a1 = angles[i + 1]; const dA = fixed((a1 - a0) / Math.PI 180); const x0 = fixed( radius Math.cos(a0)); const y0 = fixed(-radius Math.sin(a0)); const x1 = fixed( radius Math.cos(a1)); const y1 = fixed(-radius Math.sin(a1)); const largeAngle = dA < 180 ? 0 : 1; const stroke = stroke-width="${strokewidth}" stroke="black"; paths += ``; } let x = radius + strokewidth; let viewBox = -${x} -${x} ${x 2} ${x * 2}; let style=svg{height:160px;background:red}"; this.innerHTML = ${style}${paths} } })

(second pie) clearly shows when SVG causes rounding errors

July 23, 2025 Score: 1 Rep: 59,121 Quality: Medium Completeness: 80%

You are using "path" instead of "arc".

In this old SO you have a directive "SvgArc" -see the two links in the answer to understand arcs-

in this stackblitz, with the ideas of the before post a "pieChart" component using this directive.

I hope can serve you as inspiration

The code of pieChart that use the directive:

@Component({
  selector: 'pieChart',
  standalone: true,
  imports: [SvgArc, CommonModule],
  template: `

@for(dat of data;track dat) {

}

@for(dat of data;track dat) {
{{dat.label}} : {{dat.value}}
}

`, styles: [ ` .wrapper{ position:relative; } .tooltip {

display: block; position: absolute; box-sizing: border-box; font-size: 0.9em; background-color: white; border-style: solid; border-color: gray; border-width: 1px; border-radius: 2px; padding:2px 4px; opacity:0; transition: opacity .75s; } svg{ filter: drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4)); } `, ], }) export class PieChartComponent { data: any[] = []; colors: string[] = [ '#FFDEAD', '#FFA07A', '#E9967A', '#FFA500', '#FF8C00', '#FF7F50', '#FF6347', '#DEB887', '#D2B48C', '#F4A460', '#DAA520', '#CD853F', '#B8860B', '#D2691E', '#A0522D', ]; @ViewChildren('tooltip') tooltip!: QueryList; @ViewChild('center') center!: ElementRef; @ViewChild('wrapper') wrapper!: ElementRef; @Input('data') set data(value: any[]) { const total = value.reduce((a: number, b: any) => a + b.value, 0); const data = value.map((x: any, index: number) => (360 * x.value) / total); data.forEach((x: number, index: number) => { if (index) data[index] += data[index - 1]; }); this.data = data.map((x: any, index: number) => ({ from: index ? data[index - 1] : 0, to: x, label: value[index].label, value: value[index].value, color:value[index].color||this.colors[index%15] })); this.data[this.data.length - 1].to = 360; } constructor(private renderer: Renderer2) {} mouseEnter(data: any, index: number): void { const ang = (Math.PI * (data.to + data.from)) / 360; const rect = this.center.nativeElement.getBoundingClientRect(); const offset = this.wrapper.nativeElement.getBoundingClientRect(); const center = { x: rect.x-offset.x+ 0.5 * rect.width, y: rect.y-offset.y + 0.5 * rect.height, }; let x = ${center.x + 0.5 * rect.width * Math.cos(ang)}px; let y = ${center.y + 0.5 * rect.height * Math.sin(ang)}px;

const tooltip = this.tooltip.find((
, i: number) => i == index) || null; this.renderer.setStyle(tooltip?.nativeElement, 'left', x); this.renderer.setStyle(tooltip?.nativeElement, 'top', y); this.renderer.setStyle(tooltip?.nativeElement, 'opacity', 1); }

public mouseLeave(index: number): void { const tooltip = this.tooltip.find((_, i: number) => i == index) || null; this.renderer.setStyle(tooltip?.nativeElement, 'opacity', 0); } }