老板不让用ECharts,还想让我画很多圆环!

7,984 阅读5分钟

需求简介

这几天下来个新需求:要在页面上动态渲染多个表格,每个表格内还要实现若干圆环!

刚拿到这个需求的时候,我第一反应是用echarts实现,简单快捷

然而,老板无情的拒绝了我!他说:

咱这个项目就一个独立页面,你引入个ECharts项目又要大很多!而且一个页面这么多ECharts实例,性能怎么保障?不准用ECharts,用CSS实现!

没办法,我只好百度如何用CSS画圆环。幸运的是,我确实找到了类似的文章:

不幸的事,效果和我的差异很大,代码根本 无法复用!没办法,只能用别的办法实现了。经过一番研究,最终借助Canvas搞出来了,简单的分享一下我的实现思路吧。

圆环组件简介

为了方便复用,我把这个功能封装成了项目可直接复用的组件。并支持自定义圆环大小、圆环宽度和圆环颜色比例配置属性。

<Ring
    :storkWidth="5"
    :size="60"
    :ratioList="[
        { ratio: 0.3, color: '#FF5733' },
        { ratio: 0.6, color: '#33FF57' },
        { ratio: 0.1, color: '#3357FF' }
    ]"
></Ring>

技术方案

实现目标

根据我们的UX需求,我们需要实现一个简单的组件,该组件可以展示一个圆环图表,并根据外部传入的比例数据(如 ratioList)绘制不同颜色的环形区域。

  • 使用 Vue 3 和 TypeScript。
  • 动态绘制环形图,根据传入的数据绘制多个环。
  • 支持自定义环形图的大小和环宽。

创建 Vue 组件框架

首先,我们创建一个名为 RingChart.vue的组件。组件的初始结构非常简单,只包含一个 canvas 元素。

<template>
  <!-- 创建一个 canvas 元素,用于绘制图表 -->
  <canvas ref="canvasDom"></canvas>
</template>

<script lang="ts" setup>
import { ref, onMounted } from 'vue';

// 获取 canvas DOM 元素的引用
const canvasDom = ref<HTMLCanvasElement | null>(null);

// 初始化 canvas 和上下文变量
let ctx: CanvasRenderingContext2D | null = null;
let width: number, height: number;

// 初始化 canvas 尺寸和绘图环境
const initCanvas = () => {
  const dom = canvasDom.value;
  if (!dom) return;
  ctx = dom.getContext('2d');
  if (!ctx) return;

  // 设置 canvas 的宽高
  dom.width = dom.offsetWidth;
  dom.height = dom.offsetHeight;
  width = dom.offsetWidth;
  height = dom.offsetHeight;
};

// 在组件挂载后执行初始化
onMounted(() => {
  initCanvas();
});
</script>

<style scoped>
canvas {
  width: 100%;
  height: 100%;
}
</style>

上述代码中,我们初始化了 canvas 元素,并且设定了 widthheight 属性。

绘制基本的圆环

接下来,我们添加绘制圆环的功能:通过 arc 方法来绘制圆环,设置 lineWidth 来调整环的宽度。

<script lang="ts" setup>
import { ref, onMounted } from 'vue';

// 获取 canvas DOM 元素的引用
const canvasDom = ref<HTMLCanvasElement | null>(null);

// 初始化 canvas 和上下文变量
let ctx: CanvasRenderingContext2D | null = null;
let width: number, height: number;

// 初始化 canvas 尺寸和绘图环境
const initCanvas = () => {
  const dom = canvasDom.value;
  if (!dom) return;
  ctx = dom.getContext('2d');
  if (!ctx) return;

  // 设置 canvas 的宽高
  dom.width = dom.offsetWidth;
  dom.height = dom.offsetHeight;
  width = dom.offsetWidth;
  height = dom.offsetHeight;

  // 调用绘制圆环的方法
  drawCircle({
    ctx,
    x: width / 2,
    y: height / 2,
    radius: 8,
    lineWidth: 4,
    color: '#C4C9CF4D',
    startAngle: -Math.PI / 2,
    endAngle: Math.PI * 1.5,
  });
};

// 绘制一个圆环的方法
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();
};

onMounted(() => {
  initCanvas();
});
</script>
  • drawCircle 函数是绘制圆环的核心。我们通过 arc 方法绘制圆形路径,使用 lineWidth 来调整环的宽度,并用 strokeStyle 给圆环上色。
  • startAngleendAngle 参数决定了圆环的起始和结束角度,通过改变它们可以控制环的覆盖区域。

绘制多个环形区域

现在,我们来实现绘制多个环形区域的功能。我们将通过传入一个 ratioList 数组来动态生成多个环,每个环代表不同的比例区域。

<script lang="ts" setup>
import { ref, computed, onMounted } 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();
};

// 监听画布大小变化
onMounted(() => {
  initCanvas();
});
</script>

上述代码中,我们通过 ratioList 数组传递每个环的比例和颜色,使用 startAngleendAngle 来控制每个环的绘制区域。其中,drawDataRings 函数遍历 ratioList,根据每个数据项的比例绘制环形区域。

现在,我们的组件就实现完毕了,可以在其他地方引入使用了

<RingChart
    :storkWidth="8"
    :size="60"
    :ratioList="[
        { ratio: 0.3, color: '#F8766F' },
        { ratio: 0.6, color: '#69CD90' },
        { ratio: 0.1, color: '#FFB800' }
    ]"
></RRingChart>

组件代码

<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>

使用

<Ring
    :storkWidth="5"
    :size="60"
    :ratioList="[
        { ratio: 0.3, color: '#FF5733' },
        { ratio: 0.6, color: '#33FF57' },
        { ratio: 0.1, color: '#3357FF' }
    ]"
></Ring>

总结

累了,今天不想写总结!