Dynamic Ring Chart Component with Vue 3 and Canvas
This article explains how to build a reusable Vue 3 component that draws customizable ring charts on a canvas, using TypeScript, props for size, stroke width, and a ratio list to render multiple colored arcs dynamically.
The author received a new requirement to render multiple tables on a page, each containing several ring charts, and was initially inclined to use ECharts, but the boss insisted on a pure CSS solution. After researching CSS ring techniques and finding them unsuitable, the author switched to a Canvas‑based implementation.
Component Overview
A reusable RingChart component is created, supporting custom ring size, stroke width, and a list of ratio‑color pairs ( ratioList ) to define each segment.
Technical Solution
The component is built with Vue 3 and TypeScript. It defines props size , storkWidth , and ratioList , provides default values, and computes the canvas center and radius.
<template>
<canvas ref="canvasDom"></canvas>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, watchEffect } from 'vue';
interface RatioItem { ratio: number; color: string; }
const props = defineProps<{ size?: number; storkWidth?: number; ratioList?: RatioItem[] }>();
const defaultSize = 200;
const defaultStorkWidth = 4;
const defaultRatioList: RatioItem[] = [{ ratio: 1, color: '#C4C9CF4D' }];
const canvasDom = ref<HTMLCanvasElement | null>(null);
let ctx: CanvasRenderingContext2D | null = null;
const size = computed(() => props.size || defaultSize);
const center = computed(() => ({ x: size.value / 2, y: size.value / 2 }));
const radius = computed(() => size.value / 2 - (props.storkWidth || defaultStorkWidth));
const initCanvas = () => {
const dom = canvasDom.value;
if (!dom) return;
ctx = dom.getContext('2d');
if (!ctx) return;
dom.width = size.value;
dom.height = size.value;
drawBackgroundCircle();
drawDataRings();
};
const drawBackgroundCircle = () => {
if (!ctx) return;
drawCircle({ ctx, x: center.value.x, y: center.value.y, radius: radius.value,
lineWidth: props.storkWidth || defaultStorkWidth, color: '#C4C9CF4D',
startAngle: -Math.PI / 2, endAngle: Math.PI * 1.5 });
};
const drawDataRings = () => {
const { ratioList = defaultRatioList } = props;
if (!ctx) return;
let startAngle = -Math.PI / 2;
ratioList.forEach(({ ratio, color }) => {
const endAngle = startAngle + ratio * Math.PI * 2;
drawCircle({ ctx, x: center.value.x, y: center.value.y, radius: radius.value,
lineWidth: props.storkWidth || defaultStorkWidth, color, startAngle, endAngle });
startAngle = endAngle;
});
};
const drawCircle = ({ ctx, x, y, radius, lineWidth, color, startAngle, endAngle }: {
ctx: CanvasRenderingContext2D; x: number; y: number; radius: number;
lineWidth: number; color: string; startAngle: number; endAngle: number;
}) => {
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.lineWidth = lineWidth;
ctx.strokeStyle = color;
ctx.stroke();
ctx.closePath();
};
watchEffect(() => { initCanvas(); });
onMounted(() => { initCanvas(); });
</script>
<style scoped>
canvas { display: block; margin: auto; border-radius: 50%; }
</style>The component initializes the canvas, draws a background ring, and then iterates over ratioList to render each data ring using the arc method, adjusting lineWidth and strokeStyle for visual styling.
Finally, the component can be used elsewhere with a simple tag:
<RingChart :storkWidth="8" :size="60" :ratioList="[
{ ratio: 0.3, color: '#F8766F' },
{ ratio: 0.6, color: '#69CD90' },
{ ratio: 0.1, color: '#FFB800' }
]"></RingChart>Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.