You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.25 08:15:01