Master Canvas Donut Chart Animations with JavaScript – A Step‑by‑Step Guide

This article combines a motivational start‑of‑school message with a comprehensive tutorial on building animated donut‑style charts using HTML5 canvas, covering data preparation, ring and ellipse drawing, legend creation, and smooth animation via requestAnimationFrame.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Master Canvas Donut Chart Animations with JavaScript – A Step‑by‑Step Guide

Introduction

The article begins with a brief encouragement for students to set new goals for the semester and then shifts to a technical tutorial on creating animated donut‑style charts with HTML5 canvas.

Data Construction

const donutData = [{
  per: 0.45,
  text: '学习课',
  startColor: '#FFEA33', // yellow
  stopColor: '#d8b616',
  ellipseColor: '#FFD333'
}, {
  per: 0.25,
  text: '复习课',
  startColor: '#7bc31f', // green
  stopColor: '#96ec26',
  ellipseColor: '#8FD43D'
}, {
  per: 0.30,
  text: '拓展课',
  startColor: '#f0870c', // orange
  stopColor: '#ff9413',
  ellipseColor: '#FF8221'
}];

Drawing the Ring

drawRing(startDeg, endDeg, strokeStyle, ellipseColor) {
  const { ctx } = this;
  ctx.save();
  ctx.strokeStyle = strokeStyle;
  ctx.beginPath();
  ctx.lineWidth = R2 - R1;
  ctx.arc(ctx.width / 2, ctx.height / 2, (R1 + R2) / 2, arcDeg(startDeg), arcDeg(endDeg));
  ctx.stroke();
  ctx.restore();
  // this.drawEllipse(startDeg, ellipseColor);
  // this.drawEllipse(endDeg, ellipseColor);
}

Drawing the Ellipse

drawEllipse(rotate, color) {
  const { ctx } = this;
  rotate = deg(rotate);
  const x = 0;
  const y = -(R1 + R2) / 2;
  ctx.save();
  ctx.translate(ctx.width / 2, ctx.height / 2);
  ctx.rotate(rotate);
  ctx.moveTo(x, y);
  ctx.beginPath();
  ctx.fillStyle = color;
  ctx.ellipse(x, y, EllipseR2, EllipseR1, 0, 0, 2 * Math.PI);
  ctx.fill();
  ctx.restore();
}

Legend Implementation

The legend consists of a small dot, a line, an icon with a linear gradient, and a text label. A Legend class encapsulates all drawing logic.

class Legend {
  constructor({ ctx, x, y, textMaxWidth, endX, startColor, stopColor, text }) {
    this.ctx = ctx;
    this.x = x;
    this.y = y;
    this.endX = endX;
    this.textMaxWidth = textMaxWidth;
    this.text = text;
    this.dot = { r: 2.5, opacity: 0.8 };
    this.icon = { w: 12, h: 12, r: 5, startColor, stopColor };
  }
  static MARGIN_BOTTOM = 4;
  static LINE_HEIGHT = 14;
  // draw methods omitted for brevity
}

Animating with requestAnimationFrame

A small helper RafRunner wraps requestAnimationFrame to drive frame‑by‑frame animation.

class RafRunner {
  constructor(requestAnimationFrame = window.requestAnimationFrame.bind(window)) {
    this.requestAnimationFrame = requestAnimationFrame;
    this.timingFunction = x => x;
  }
  handler(fn) { this._handler = fn; }
  start(from, to, duration, timingFunction = x => x) {
    const startTime = performance.now();
    const animate = (now) => {
      const elapsed = now - startTime;
      const progress = Math.min(elapsed / duration, 1);
      const value = from + (to - from) * this.timingFunction(progress);
      this._handler(value, from);
      if (progress < 1) this.requestAnimationFrame(animate);
    };
    this.requestAnimationFrame(animate);
  }
}
draw() {
  const { source } = this;
  if (!source.length) return;
  const raf = new RafRunner();
  let pos = 0;
  raf.handler((recPer) => {
    let part = source[pos];
    const { startPer, per, lgr, ellipseColor } = part;
    if (recPer >= startPer + per) {
      const startDeg = ANGLE_360 * startPer;
      const endDeg = ANGLE_360 * (startPer + per);
      this.drawRing(startDeg, endDeg, lgr, ellipseColor);
      this.drawPartLegend(part);
      pos++;
      part = source[pos];
      if (!part) { this.drawEllipse(0, source[0].ellipseColor); return; }
    }
    const startDeg = ANGLE_360 * part.startPer;
    const endDeg = ANGLE_360 * recPer;
    this.drawRing(startDeg, endDeg, part.lgr, part.ellipseColor);
    this.drawEllipse(0, source[0].ellipseColor);
  });
  raf.start(0, 1, 800, easeInOut);
}

Final Thoughts

The tutorial also discusses handling text overflow, color distribution, legend overlap, and other practical considerations. The complete source code is available at https://github.com/chym123/donut-graph-demo.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

frontendanimationWeb Developmentdonut chart
Tencent IMWeb Frontend Team
Written by

Tencent IMWeb Frontend Team

IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.