I use D3 pie chart to create a donut, with a little animation to fill it in my angular (12) app. When I first load a page, the chart I made works fine (it's the page that is loaded after login). Then if I go to another page, and come back by any way (previous page from browser or routing) the pie chart firstly appears and then completly disappears, the svg element isn't in the DOM anymore.
For example, on first load I have this svg in my DOM :
<svg width="80" height="80">
<g transform="translate(40, 40)" id="g-7621300556">
<linearGradient x1="0" x2="1" y1="1" y2="0.5" id="gradient-url-7621300556">
<stop offset="0%" stop-color="var(--secondary)" stop-opacity="0.1"></stop>
<stop offset="100%" stop-color="var(--secondary)" stop-opacity="1"></stop>
</linearGradient>
<path fill="url(#gradient-url-7621300556)" d="M-5.970153145843346e-15,32.5A32.5,32.5,0,0,1,-30.90933677959249,-10.043052317185793L-26.15405419811672,-8.497967345311057A27.5,27.5,0,0,0,-5.051668046482832e-15,27.5Z" style="transform: rotate(180deg)" opacity="0.3" stroke=""></path>
<path fill="transparent" d="M-30.90933677959249,-10.043052317185793A32.5,32.5,0,1,1,1.990051048614449e-15,32.5L1.6838893488276107e-15,27.5A27.5,27.5,0,1,0,-26.15405419811672,-8.497967345311057Z" style="transform: rotate(180deg)" opacity="1" stroke="var(--background)"></path>
<text text-anchor="middle" dy=".3em" id="text-7621300556" fill="var(--secondary)">30 %</text>
</g>
</svg>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
And then just nothing, no svg at all. I made this gif to illustrate :
Here's what I've done :
chart.component.ts :
import { Component, Input, AfterViewInit, OnChanges, SimpleChanges } from '@angular/core';
import * as d3 from 'd3';
export class ChartDataSet {
filledPartPercent: number;
emptyPartPercent: number;
label?: string;
}
@Component({
selector: 'chart',
templateUrl: './chart.component.html',
styleUrls: ['./chart.component.scss'],
})
export class ChartComponent implements AfterViewInit, OnChanges {
@Input() donutId: string;
@Input() dataSet: ChartDataSet;
@Input() width: number;
@Input() margin: number;
@Input() duration = 1500;
@Input() thickness = 2.5;
@Input() anglesRange = Math.PI;
@Input() cssVarColor = 'var(--secondary)';
@Input() gradient = false;
height: number;
radius: number;
svg: any;
randomize: number;
constructor() {
}
ngAfterViewInit() {
this.init();
}
ngOnChanges(changes: SimpleChanges): void {
this.init();
}
private init(): void {
this.randomize = Math.floor(Math.random() * (10000 - 10) 10);
this.height = this.width;
this.radius = (this.width / 2) - this.margin;
this.donutDraw();
}
private donutDraw() {
const percent = this.dataSet.filledPartPercent;
const color = this.cssVarColor;
const id = this.donutId;
const random = this.randomize;
let gradientElement = null;
const data = {
lower: this.calcPercent(0),
upper: this.calcPercent(percent)
};
const arc = d3.arc().innerRadius(this.radius - this.thickness).outerRadius(this.radius this.thickness);
const pie = d3.pie().value(d => d).sort(null).startAngle(this.anglesRange * -1).endAngle(this.anglesRange);
this.svg = d3.select('#donut-' id).append('svg')
.attr('width', this.width)
.attr('height', this.height)
.append('g')
.attr('transform', 'translate(' this.width / 2 ', ' this.height / 2 ')')
.attr('id', 'g-' id random);
if (this.gradient) {
gradientElement = this.svg.append('linearGradient').attr('x1', '0').attr('x2', '1').attr('y1', '1').attr('y2', '0.5')
.attr('id', 'gradient-url-' id random);
gradientElement.append('stop').attr('offset', '0%').attr('stop-color', color).attr('stop-opacity', '0.1');
gradientElement.append('stop').attr('offset', '100%').attr('stop-color', color).attr('stop-opacity', '1');
}
let path = this.svg.selectAll('path')
.data(pie(data.lower))
.enter().append('path')
.attr('fill', 'var(--background')
.attr('d', arc)
.each(function(d) { this._current = d; });
const text = this.svg.append('text').attr('text-anchor', 'middle').attr('dy', '.3em')
.attr('id', 'text-' id random);
const timeout = setTimeout(() => {
clearTimeout(timeout);
path = path.data(pie(data.upper));
path.transition()
.duration(this.duration)
.attrTween('d', function(a, index) {
const self = this;
const i = d3.interpolate(this._current, a);
const i2 = d3.interpolateNumber(0, percent);
this._current = i(0);
return t => {
d3.select(self).attr('fill', index !== 0 ? 'transparent' : gradientElement != null ? 'url(#gradient-url-' id random ')' : color )
.attr('style', 'transform: rotate(180deg)')
.attr('opacity', index !== 0 ? 1 : i2(t) / 100)
.attr('stroke', index !== 0 ? 'var(--background)' : '');
text.text(Math.round(i2(t)) ' %').attr('fill', color);
return arc(i(t));
};
});
});
}
private calcPercent(percent) {
return [percent, 100 - percent];
}
}
chart.component.html
<div [id]="'donut-' donutId" class="wrap"></div>
chart.component.scss
.wrap {
width: 100%;
height: 100%;
display: block;
}
I tried several things based on other SO pages like this one, but nothing worked so far. My chart has a random generated id, so does the gradient, I don't know what to do next. I also tried to reproduce it on Stackblitz, but there it works fine ...
I have no idea what's causing this, if you need any more code, just let me know.
CodePudding user response:
As suggested in comments, the problem wasn't D3.js or SVG in general, but related to this issue.
In my case, I was missing a trackBy function on a ngFor, which was leading to reload components several times.
Correcting it fixed my issue, but didn't totally explain why it was disappearing and not just reloading. I found out that I was missing few things to do it properly, in one word : NgOnDestroy.
I added this to my code, so that it won't disappear if reloaded :
export class ChartComponent implements AfterViewInit, OnChanges, OnDestroy {
@ViewChild('donutRef', { static: true}) donutRef: ElementRef; // <--- top div of this component
[......]
svg: any; // <--- this.svg = d3.select('#donut-' id).append('svg') when initialising the chart
[......]
public ngOnDestroy() {
this.svg.remove();
this.donutRef.nativeElement.remove();
}
[......]
}
I hope this can help if anyone encouters this kind of issue.