Angular12路由返回后D3.js绘制的环形饼图SVG元素丢失问题
问题核心原因
你遇到的问题主要由以下几个常见的Angular + D3集成错误导致:
- 依赖全局ID选择器查找容器,路由返回时视图未完成渲染、或者ID重复都会导致D3找不到挂载节点
- 每次绘制前没有清空容器残留的旧元素,多次触发绘制会导致DOM异常
- 未处理组件销毁逻辑,定时器、D3过渡动画在组件销毁后仍在执行,会直接清空已渲染的SVG
- 存在CSS变量语法错误,会导致样式异常
完整修复步骤
1. 调整组件逻辑,替换ID选择器为ViewChild、新增生命周期处理
修改后的chart.component.ts代码如下:
import { Component, Input, AfterViewInit, OnChanges, SimpleChanges, ViewChild, ElementRef, OnDestroy } 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, OnDestroy { @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; // 用ViewChild直接获取容器元素,不用全局ID选择 @ViewChild('donutContainer') donutContainer: ElementRef<HTMLDivElement>; private isViewInitialized = false; private drawTimer: number; ngAfterViewInit() { this.isViewInitialized = true; this.init(); } ngOnChanges(changes: SimpleChanges): void { // 仅在视图初始化完成后才执行绘制,避免选不到容器 if (this.isViewInitialized) { this.init(); } } ngOnDestroy(): void { // 清理所有异步逻辑和动画,避免组件销毁后仍操作DOM if (this.drawTimer) clearTimeout(this.drawTimer); if (this.svg) this.svg.interrupt(); } 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); // 先清空容器内旧元素,避免重复叠加 const container = d3.select(this.donutContainer.nativeElement); container.selectAll('*').remove(); this.svg = container.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'); } // 修复CSS变量缺少闭合括号的语法错误 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); // 存储定时器ID,组件销毁时清理 this.drawTimer = window.setTimeout(() => { clearTimeout(this.drawTimer); 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]; } }
2. 修改模板,用模板引用变量替代ID
修改chart.component.html:
<div #donutContainer class="wrap"></div>
内容的提问来源于stack exchange,提问作者derOtterDieb
相关产品推荐
相关产品推荐

