前段时间在掘金看到一篇博文,作者提到其公司不允许使用 ECharts,但项目中需要展示简单的圆环图。

于是她用 Canvas 实现了需求,效果也比较不错。评论区也有不少大佬分享了更轻量、更优秀的方案,比如 CSS、SVG、conic-gradient 等,看完让我受益匪浅。
今天就来简单整理一下这些思路,在不依赖 ECharts 的前提下,如何用多种方式实现圆环组件。
纯css实现
使用背景渐变 +内层遮罩是实现圆环最简单的 CSS 方式。
html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>纯 CSS 彩色圆环图</title>
<style>
.circle {
width: 200px;
height: 200px;
border-radius: 50%;
background: conic-gradient(#FF6B6B 0%,
#FF6B6B 30%,
#FFD93D 30%,
#FFD93D 65%,
#6BCB77 65%,
#6BCB77 100%);
mask: radial-gradient(circle at center, transparent 80px, #000 80px);
-webkit-mask: radial-gradient(circle at center, transparent 80px, #000 80px);
}
</style>
</head>
<body>
<div class="circle"></div>
</body>
</html>
效果:

这种方案的核心在于:
- 利用背景渐变(conic-gradient)生成一个圆形多色背景,渐变参数中指定的颜色分段(如:红色、黄色、绿色)按照从 0% 到 100% 顺时针分布到整个圆周上。
- 默认情况下,conic-gradient 生成的是一个实心圆。通过使用径向渐变遮罩(radial-gradient),我们"挖空"了中心区域,只保留外圈,从而形成圆环效果。
如这些css的使用,我们可以借助AI学习,比如使用Trae的Chat模式进行代码解读。

如果你比较专业,直接去MDN的解释也是极佳的。
相关css使用MDN: conic-gradient 、 radial-gradient
CSS函数 conic-gradient()创建一个由渐变组成的图像,渐变的颜色围绕一个中心点旋转(而不是从中心辐射)进行过渡。锥形渐变的例子包括饼图和色轮。
conic-gradient()
函数的结果是 数据类型的对象,此对象是一种特殊的 数据类型。
radial-gradient()
CSS 函数创建一个图像,该图像由从原点辐射的两种或多种颜色之间的渐进过渡组成,其形状可以是圆形或椭圆形。函数的结果是 数据类型的对象,此对象是一种特殊的类型。
在实际项目中,我们可能需要让它变得可配置,这也非常容易,借助Trae的Builder模式可以直接来修改代码:

html
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>纯 CSS 彩色圆环图</title>
<style>
.circle {
width: 200px;
height: 200px;
border-radius: 50%;
background: conic-gradient(#FF6B6B 0%,
#FF6B6B 30%,
#FFD93D 30%,
#FFD93D 65%,
#6BCB77 65%,
#6BCB77 100%);
mask: radial-gradient(circle at center, transparent 80px, #000 80px);
-webkit-mask: radial-gradient(circle at center, transparent 80px, #000 80px);
}
</style>
</head>
<body>
<div class="circle"></div>
<div class="controls">
<label for="size">尺寸:</label>
<input type="number" id="size" min="100" max="500" value="200">
<label for="colors">颜色:</label>
<input type="text" id="colors" value="#FF6B6B 0%,#FF6B6B 30%,#FFD93D 30%,#FFD93D 65%,#6BCB77 65%,#6BCB77 100%">
<button onclick="updateCircle()">更新</button>
</div>
<script>
function updateCircle() {
const size = document.getElementById('size').value;
const colors = document.getElementById('colors').value;
const circle = document.querySelector('.circle');
circle.style.width = size + 'px';
circle.style.height = size + 'px';
circle.style.background = 'conic-gradient(' + colors + ')';
}
</script>
</body>
</html>
生成的代码,我们直接点击接受,看看最终效果:

svg
SVG 拥有天然的图形绘制能力,可控制度高,非常适合实现高质量的圆环图。
xml
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
</head>
<body>
<svg width="120" height="120" viewBox="0 0 120 120">
<circle cx="60" cy="60" r="54" stroke="#e0e0e0" stroke-width="12" fill="none" />
<circle cx="60" cy="60" r="54" stroke="#4caf50" stroke-width="12" fill="none" stroke-dasharray="339.292"
stroke-dashoffset="101.7876" stroke-linecap="round" transform="rotate(-90 60 60)" />
<text x="60" y="65" font-size="20" text-anchor="middle" fill="#333">70%</text>
</svg>
</body>
</html>
stroke-dasharray
和 stroke-dashoffset
用于控制弧长,弧长 = 2πr ≈ 339.292。70% 进度 = 30% 剩余,所以 dashoffset = 339.292 * (1 - 0.7)
效果:

同样的,如果需要做成可配置的,我们也可以将svg的渲染过程封账在一个函数里,通过参数配置最终效果。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="app"></div>
<script>
function createColoredRing({ size, strokeWidth, segments }) {
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
let dashOffset = 0;
const svgNS = "http://www.w3.org/2000/svg";
const svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("width", size);
svg.setAttribute("height", size);
svg.setAttribute("viewBox", `0 0 ${size} ${size}`);
segments.forEach(segment => {
const { color, percentage } = segment;
const segmentLength = (percentage / 100) * circumference;
const gap = circumference - segmentLength;
const circle = document.createElementNS(svgNS, "circle");
circle.setAttribute("cx", size / 2);
circle.setAttribute("cy", size / 2);
circle.setAttribute("r", radius);
circle.setAttribute("fill", "none");
circle.setAttribute("stroke", color);
circle.setAttribute("stroke-width", strokeWidth);
circle.setAttribute("stroke-dasharray", `${segmentLength} ${gap}`);
circle.setAttribute("stroke-dashoffset", -dashOffset);
dashOffset += segmentLength;
svg.appendChild(circle);
});
return svg;
}
const ring = createColoredRing({
size: 160,
strokeWidth: 10,
segments: [
{ color: "red", percentage: 10 },
{ color: "blue", percentage: 30 },
{ color: "orange", percentage: 60 },
]
})
document.getElementById("app").appendChild(ring);
</script>
</body>

配置效果:

canvas
使用 Canvas 绘制圆环,是灵活度最高的一种方式。不仅可以轻松实现颜色渐变、动画过渡等复杂效果,也非常适合封装成独立组件用于多种业务场景。当然,相比其他方式,Canvas 的使用成本和实现难度也更高一些。不过,在这个 AI 助力开发的时代,这些挑战都不再是问题。
Canvas的实现方式非常多,效果也大同小异,这里,我就使用原作者文章中的代码了:
html
<template>
<canvas ref="canvasDom"></canvas>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, watchEffect } from 'vue';
// 定义 props 的类型
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' }];
// canvas DOM 和上下文
const canvasDom = ref<HTMLCanvasElement | null>(null);
let ctx: CanvasRenderingContext2D | null = null;
// 动态计算 canvas 的中心点和半径
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));
// 初始化 canvas
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>
使用
ini
<Ring
:storkWidth="5"
:size="60"
:ratioList="[
{ ratio: 0.3, color: '#FF5733' },
{ ratio: 0.6, color: '#33FF57' },
{ ratio: 0.1, color: '#3357FF' }
]"
></Ring>
最终效果:

总结
总的来说,纯 CSS、SVG、Canvas 三种方式各有优劣:
- 纯 CSS 实现最简单,适合快速展示静态效果,对交互和控制需求不高时非常实用;
- SVG 控制精度高、性能稳定,适合实现带有文本、进度动画、图标组合等场景;
- Canvas 灵活度最高,可以轻松支持动画、渐变、多图层叠加等复杂视觉效果,更适合做成组件或用于复杂图表系统中。
至于具体选择哪种方案,取决于项目的复杂度与开发成本的权衡。当然,在 AI 的辅助下,我们可以更快地探索和实现每一种方式的可能性。如果你有兴趣,不妨动手试试~