1. Introduction

Charts and visualizations communicate complex data quickly. For enterprise dashboards and analytics, you often need both custom visualizations (unique shapes, interactions) and standard charts (bar, line, pie) with fast development. Two popular approaches:

In an Angular project you will usually use D3.js natively. If you want Recharts, you must embed React components (wrap as Web Component, microfrontend, or iframe) or pick Angular-native libraries (Ngx-Charts, ngx-charts based on D3).

This article shows:

  1. A complete D3 chart component in Angular (responsive, animated, interactive).

  2. Practical options for using Recharts inside Angular and sample code for wrapping Recharts as a Web Component.

  3. Best practices: performance, accessibility (a11y), testing, responsive layout, and server data handling.

2. Choose the right tool: D3.js vs Recharts vs Angular chart libraries

When to use which:

3. Technical workflow (high-level)

Data source (backend API / static) 
      ↓
Angular Service (fetch + transform)
      ↓
Chart Component (D3 or embedded Recharts)
      ↓
Render to SVG / Canvas / Web Component
      ↓
User interactions → events → component updates

This flow keeps data concerns separate from rendering and makes testing easier.

4. Setup: Angular + D3

First set up an Angular app and add D3.

ng new angular-charts --standalone
cd angular-charts
npm install d3

Create a component for a D3 line chart:

ng generate component charts/line-chart --standalone

5. Implementing a responsive, interactive D3 Line Chart in Angular

Here is a complete example: responsive, animated line with tooltip, axes, brushing (selection), and window-resize handling.

5.1 chart-data.service.ts (fetch or provide data)

// src/app/services/chart-data.service.ts
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';

export interface Datum {
  date: Date;
  value: number;
}

@Injectable({ providedIn: 'root' })
export class ChartDataService {
  // In real app, use HttpClient to fetch from API
  getTimeSeries(): Observable<Datum[]> {
    const now = new Date();
    const data: Datum[] = Array.from({ length: 60 }, (_, i) => ({
      date: new Date(now.getTime() - (59 - i) * 24 * 60 * 60 * 1000),
      value: Math.round(50 + 30 * Math.sin(i / 6) + Math.random() * 20)
    }));
    return of(data);
  }
}

5.2 line-chart.component.ts (D3 integration)

// src/app/charts/line-chart/line-chart.component.ts
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ChartDataService, Datum } from '../../services/chart-data.service';
import * as d3 from 'd3';

@Component({
  selector: 'app-line-chart',
  template: `<div class="chart-container" #container>
               <svg #svg></svg>
             </div>`,
  styleUrls: ['./line-chart.component.scss'],
  standalone: true,
  imports: []
})
export class LineChartComponent implements OnInit, OnDestroy {
  @ViewChild('svg', { static: true }) svgRef!: ElementRef<SVGSVGElement>;
  @ViewChild('container', { static: true }) containerRef!: ElementRef<HTMLDivElement>;

  private svg!: d3.Selection<SVGSVGElement, unknown, null, undefined>;
  private width = 800;
  private height = 400;
  private margin = { top: 20, right: 30, bottom: 40, left: 50 };

  private xScale!: d3.ScaleTime<number, number>;
  private yScale!: d3.ScaleLinear<number, number>;
  private lineGenerator!: d3.Line<Datum>;

  private resizeObserver?: ResizeObserver;
  private tooltip?: d3.Selection<HTMLDivElement, unknown, null, undefined>;

  constructor(private dataService: ChartDataService) {}

  ngOnInit() {
    this.svg = d3.select(this.svgRef.nativeElement);
    this.setupScales();
    this.setupTooltip();
    this.dataService.getTimeSeries().subscribe(data => {
      this.draw(data);
      this.setupResizeObserver(data);
    });
  }

  ngOnDestroy() {
    this.resizeObserver?.disconnect();
    this.tooltip?.remove();
  }

  private setupTooltip() {
    this.tooltip = d3.select(this.containerRef.nativeElement)
      .append('div')
      .attr('class', 'tooltip')
      .style('position', 'absolute')
      .style('pointer-events', 'none')
      .style('opacity', '0')
      .style('background', '#fff')
      .style('padding', '6px 8px')
      .style('border', '1px solid #ddd')
      .style('border-radius', '4px')
      .style('box-shadow', '0 2px 6px rgba(0,0,0,0.1)');
  }

  private setupScales() {
    // initial scales; sizes adjusted in draw()
    this.xScale = d3.scaleTime();
    this.yScale = d3.scaleLinear();
    this.lineGenerator = d3.line<Datum>()
      .x(d => this.xScale(d.date))
      .y(d => this.yScale(d.value))
      .curve(d3.curveMonotoneX);
  }

  private draw(data: Datum[]) {
    const containerWidth = this.containerRef.nativeElement.clientWidth || this.width;
    const w = containerWidth - this.margin.left - this.margin.right;
    const h = this.height - this.margin.top - this.margin.bottom;

    this.svg
      .attr('width', containerWidth)
      .attr('height', this.height);

    const g = this.svg.selectAll<SVGGElement, unknown>('.plot')
      .data([null])
      .join('g')
      .attr('class', 'plot')
      .attr('transform', `translate(${this.margin.left},${this.margin.top})`);

    this.xScale.range([0, w]).domain(d3.extent(data, d => d.date) as [Date, Date]);
    this.yScale.range([h, 0]).domain([0, d3.max(data, d => d.value)! * 1.1]);

    // axes
    g.selectAll('.x-axis').data([null]).join('g').attr('class', 'x-axis')
      .attr('transform', `translate(0,${h})`)
      .call(d3.axisBottom(this.xScale).ticks(Math.min(10, data.length)).tickFormat(d3.timeFormat('%b %d') as any));

    g.selectAll('.y-axis').data([null]).join('g').attr('class', 'y-axis')
      .call(d3.axisLeft(this.yScale).ticks(6));

    // line path
    const path = g.selectAll<SVGPathElement, Datum[]>('.line-path')
      .data([data], d => d as any)
      .join('path')
      .attr('class', 'line-path')
      .attr('fill', 'none')
      .attr('stroke', '#0078d4')
      .attr('stroke-width', 2)
      .attr('d', this.lineGenerator as any);

    // add total length animation
    const totalLength = (path.node() as SVGPathElement).getTotalLength();
    path
      .attr('stroke-dasharray', `${totalLength} ${totalLength}`)
      .attr('stroke-dashoffset', totalLength)
      .transition()
      .duration(800)
      .ease(d3.easeCubicOut)
      .attr('stroke-dashoffset', 0);

    // points for tooltip / interaction
    const points = g.selectAll<SVGCircleElement, Datum>('.point')
      .data(data)
      .join('circle')
      .attr('class', 'point')
      .attr('r', 3)
      .attr('cx', d => this.xScale(d.date))
      .attr('cy', d => this.yScale(d.value))
      .attr('fill', '#fff')
      .attr('stroke', '#0078d4')
      .on('mouseover', (event, d) => {
        this.tooltip!.style('opacity', '1')
          .html(`<strong>${d3.timeFormat('%b %d, %Y')(d.date)}</strong><div>Value: ${d.value}</div>`)
          .style('left', `${event.offsetX + 12}px`)
          .style('top', `${event.offsetY - 12}px`);
      })
      .on('mouseout', () => this.tooltip!.style('opacity', '0'));

    // brushing example (range select)
    const brush = d3.brushX()
      .extent([[0, 0], [w, h]])
      .on('end', (event) => {
        if (!event.selection) return;
        const [x0, x1] = event.selection.map(this.xScale.invert);
        console.log('selected range', x0, x1);
      });

    g.selectAll('.brush').data([null]).join('g')
      .attr('class', 'brush')
      .call(brush);
  }

  private setupResizeObserver(data: Datum[]) {
    if (window.ResizeObserver) {
      this.resizeObserver = new ResizeObserver(() => this.draw(data));
      this.resizeObserver.observe(this.containerRef.nativeElement);
    } else {
      window.addEventListener('resize', () => this.draw(data));
    }
  }
}

5.3 line-chart.component.scss

.chart-container {
  width: 100%;
  max-width: 900px;
  margin: 0 auto;
  position: relative;
  .tooltip { font-size: 13px; }
  svg { width: 100%; height: auto; display: block; }
}

Notes on the D3 component

6. Accessibility (a11y) for D3 charts

Make charts accessible:

Example: add accessible summary below chart

<div aria-live="polite" class="sr-only" id="chart-summary">
  Showing last 60 days; highest value 95 on Mar 10, 2025.
</div>

Add tabindex="0" to important SVG elements (but be careful — some screen readers handle SVG differently).

7. Using Recharts inside Angular — practical approaches

Recharts is React-based. To use Recharts in Angular you have several options:

Option A — Wrap Recharts as a Web Component (recommended for reuse)

  1. Build a small React app/component using Recharts.

  2. Use react-to-webcomponent to wrap the React component as a standard Custom Element.

  3. Publish the bundle and include its script in Angular; then use <recharts-line> tag.

React side (minimal)

// RechartsLine.js
import React from 'react';
import ReactDOM from 'react-dom';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
import reactToWebComponent from 'react-to-webcomponent';

function RechartsLine({ dataJson }) {
  const data = JSON.parse(dataJson || '[]');
  return (
    <div style={{ width: '100%', height: 300 }}>
      <ResponsiveContainer>
        <LineChart data={data}>
          <XAxis dataKey="date" />
          <YAxis />
          <Tooltip />
          <Line type="monotone" dataKey="value" stroke="#0078d4" />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}

const RechartsElement = reactToWebComponent(RechartsLine, React, ReactDOM);
customElements.define('recharts-line', RechartsElement);

Bundle using webpack/rollup and expose a single script, e.g., recharts-line.js.

Angular usage

Pros: Simple to reuse; looks native in Angular.
Cons: Adds React and Recharts bundle (size), careful about hydration and lifecycle.

Option B — Microfrontend (Module Federation or iframe)

Option C — Convert Recharts look to Angular-native (ngx-charts or custom D3)

8. Example: Wrapping Recharts as Web Component — build steps

  1. Create a small React project that exposes the component using react-to-webcomponent.

  2. Build bundle with rollup or webpack, mark React & ReactDOM as bundled or external per your needs.

  3. Produce a single UMD/ES module script recharts-element.js.

  4. In Angular, load it once (index.html script tag or dynamic import).

  5. Use <recharts-line> with attributes for data or use properties via DOM.

Angular property set example

@ViewChild('recharts', { static: true }) rechartsRef!: ElementRef<HTMLElement>;

ngAfterViewInit() {
  const el = this.rechartsRef.nativeElement;
  (el as any).dataJson = JSON.stringify(this.data);
}

9. Performance considerations

10. Testing charts

Example Jest test for scale

import * as d3 from 'd3';
it('x scale domain covers data dates', () => {
  const dates = [new Date('2025-03-01'), new Date('2025-03-10')];
  const x = d3.scaleTime().domain(d3.extent(dates) as [Date, Date]);
  expect(x.domain()[0]).toEqual(dates[0]);
});

11. Integrating with a backend (ASP.NET Core)

A typical flow: Angular service calls API, receives time-series or aggregated data.

ASP.NET Core controller example

[ApiController]
[Route("api/[controller]")]
public class ChartsController : ControllerBase
{
    [HttpGet("timeseries")]
    public IActionResult TimeSeries()
    {
        var now = DateTime.UtcNow.Date;
        var data = Enumerable.Range(0, 60).Select(i => new {
            date = now.AddDays(-59 + i).ToString("yyyy-MM-dd"),
            value = Math.Round(50 + 30 * Math.Sin(i / 6.0) + new Random().NextDouble() * 10, 2)
        });
        return Ok(data);
    }
}

Angular service uses HttpClient to fetch and convert dates before passing to D3 or Recharts.

12. UX and Interaction patterns

13. Best practices and tips

14. When not to build from scratch

If your requirements are standard (line, bar, pie, area) and you need fast delivery with minimal customization, choose an Angular-native chart library:

Reserve D3 for custom visuals or highly interactive charts.

15. Summary