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:
D3.js — lowest-level, extremely flexible, ideal for custom, unusual charts and precise control of interactions and transitions. Steeper learning curve.
Recharts — React charting library built on D3 primitives. Very productive for common chart types and nice defaults, but React-only.
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:
A complete D3 chart component in Angular (responsive, animated, interactive).
Practical options for using Recharts inside Angular and sample code for wrapping Recharts as a Web Component.
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:
Use D3.js when:
You need custom shapes, custom layouts, nonstandard visual encodings, or fine-grained animation control.
You want full control over DOM, SVG, Canvas rendering, and performance tuning.
Use Recharts (via embedding) when:
You like Recharts’ API and prebuilt chart types and you can accept the overhead of embedding React.
You want fast development of standard charts with polished looks and interactions.
Use Angular-native chart libs (e.g., ngx-charts, ngx-echarts, chart.js via ng2-charts) when:
You need quick chart prototyping inside Angular with less custom work and no cross-framework embedding.
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));
}
}
}

Join the conversation! Your thoughts help the community grow.