背景
之前接到一个设计稿,实现以下的效果:
我立马就先入为主了,使用 ECharts 来实现,因为 ECharts 的仪表盘效果和这个很类似,配置一下 API 应该很容易实现,如下所示的 ECharts 仪表盘效果:
于是乎,我就开始翻 ECharts 的配置项手册,示例 Demo,但最终实现并没有想象的那么轻松,实现效果差强人意,最后也没有能够实现,最终放弃了 ECharts,我开始思考如何使用 SVG 来实现。
SVG(Scalable Vector Graphics)是一种用于在网页上绘制图形的 XML 格式。它允许开发者创建复杂的图形和动画,而不需要依赖外部图像文件。因此,使用 SVG 实现这种效果具备可行性。
实现
SVG 实现基础效果
首先,我需要思考如何使用 SVG 来实现这个效果,我思考了一下,使用 SVG 来实现这个效果,设计稿有几个设计点需要注意:
- 有三个圆环
- 圆环内部有数值标识,最大值、最小值、当前值
- 圆环有颜色变化
- 当前值显示
所以我准备借助 Cursor AI 的思路来实现,是否会更容易实现?
先给 AI 投喂下这个设计稿图片,让它帮我生成这些圆环的 SVG 代码,如下所示:
参考了 AI 返回的代码,取一个 SVG 源代码及渲染:
<svg viewBox="0 0 100 50">
<path
class="dial"
d="M 10 50 A 40 40 0 0 1 90 50"
fill="none"
stroke="#DDD"
stroke-width="2"
></path>
<g class="text-container">
<text
class="value-text"
dominant-baseline="central"
fill="#999"
font-family="sans-serif"
font-size="100%"
font-weight="normal"
text-anchor="middle"
x="50"
y="50"
></text>
</g>
<path
d="M 10 50 A 40 40 0 0 1 50 10"
class="value"
fill="none"
stroke-width="8"
style="stroke: rgb(7, 177, 130)"
></path>
<path
d="M 18 50 A 32 32 0 0 1 18.158 46.824"
class="band"
fill="none"
stroke="#d1433f"
stroke-width="2"
></path>
<text
x="20"
y="47"
class="bandLabel"
dominant-baseline="middle"
fill="#999"
font-family="sans-serif"
font-size="50%"
text-anchor="start"
>
-100.0
</text>
<path
d="M 81.842 46.824 A 32 32 0 0 1 82 50"
class="band"
fill="none"
stroke="#d1433f"
stroke-width="2"
></path>
<text
x="78"
y="47"
class="bandLabel"
dominant-baseline="middle"
fill="#999"
font-family="sans-serif"
font-size="50%"
text-anchor="end"
>
-60.0
</text>
</svg>
上述 SVG 代码渲染的图表如下图所示,效果与设计图一致:
由以上代码分析一下 SVG 的构成和实现思路:
分析 SVG 的构成
-
基本元素:
<svg>:SVG 的根元素,定义了画布的大小和视口。<path>:用于绘制复杂的形状,如曲线、直线等。通过d属性定义路径。<text>:用于在 SVG 中添加文本,可以设置字体、大小、颜色等属性。<g>:用于分组其他 SVG 元素,便于管理和操作。
-
属性:
viewBox:定义 SVG 的视口,控制图形的缩放和位置。fill:设置填充颜色。stroke:设置描边颜色。stroke-width:设置描边的宽度。stroke-dasharray和stroke-dashoffset:用于创建虚线效果,常用于表示进度或值。
-
样式和动画:
- 可以使用 CSS 样式来设置 SVG 元素的样式。
- 通过 JavaScript 可以动态修改 SVG 元素的属性,实现动画效果。
分析 SVG 实现图表的思路
-
设计布局:
- 确定图表的整体布局和尺寸,使用
viewBox属性来定义视口。
- 确定图表的整体布局和尺寸,使用
-
绘制基础图形:
- 使用
<path>元素绘制图表的基础结构,如仪表的弧形部分。 - 使用
<text>元素添加刻度和标签。
- 使用
-
动态数据展示:
- 通过 JavaScript 动态计算和更新
<path>的stroke-dasharray和stroke-dashoffset属性,以反映当前值。
- 通过 JavaScript 动态计算和更新
-
交互和动画:
- 添加事件监听器,响应用户交互。
- 使用 CSS 过渡或 JavaScript 动画库来实现平滑的动画效果。
通过以上步骤,可以创建出动态、交互性强的 SVG 图表,适用于数据可视化和用户界面设计。
提取为组件
使用 SVG 实现效果图并不是最终目的,这只是验证能够实现猜想的第一步。由于在实际项目中,图表是根据数据动态渲染的,所以我们要将它拆分为公用组件,根据实际数据动态渲染。
因此,实现这个组件必须要做的是:
-
接收三个必需的 props:
- min: 最小值
- max: 最大值
- value: 当前值
-
实现动态渲染:
- 动态计算并显示圆弧的位置和长度
- 根据值(value)是否在范围内(min~max)切换颜色(红色表示超出范围,绿色表示在范围内)
- 平滑的动画过渡效果
- 自适应的标签位置
再次借助 Cursor AI 来帮助我们
AI 并不能一次问答就能输出较为成熟的代码,需要我们一步步进行调校优化,描述准确尤为重要,加之我们修改一些显而易见的错误,最终能够输出我们想要的效果:
最终经过不断的修改优化,经过测试后完整的组件如下:
<script setup lang="ts">
import { computed } from 'vue'
// 定义组件的 Props 接口
interface Props {
min: number // 最小值
max: number // 最大值
value: number // 当前值
}
// 定义组件名称
defineOptions({
name: 'GaugeSVG'
})
// 获取组件的 props
const props = defineProps<Props>()
// 计算实际显示范围
const effectiveRange = computed(() => {
let effectiveMin = props.min
let effectiveMax = props.max
// 如果当前值小于最小值,调整最小值
if (props.value < props.min) {
effectiveMin = props.value
}
// 如果当前值大于最大值,调整最大值
if (props.value > props.max) {
effectiveMax = props.value
}
return { min: effectiveMin, max: effectiveMax }
})
// 计算圆弧上的坐标的工具函数
const calculatePoint = (angle: number): { x: number; y: number } => {
const radius = 40 // 圆的半径
const centerX = 50 // 圆心 X 坐标
const centerY = 50 // 圆心 Y 坐标
const radians = ((angle - 180) * Math.PI) / 180 // 将角度转换为弧度
return {
x: centerX + radius * Math.cos(radians), // 计算 X 坐标
y: centerY + radius * Math.sin(radians) // 计算 Y 坐标
}
}
// 计算值的圆弧
const getValueArc = computed(() => {
const percentage = Math.min(
Math.max(
(props.value - effectiveRange.value.min) /
(effectiveRange.value.max - effectiveRange.value.min),
0
),
1
)
const angle = 180 * percentage // 计算角度
const point = calculatePoint(angle) // 计算圆弧终点坐标
const largeArcFlag = angle > 180 ? 1 : 0 // 判断是否为大弧
return `M 10 50 A 40 40 0 ${largeArcFlag} 1 ${point.x} ${point.y}` // 返回 SVG 路径
})
// 计算最小值和最大值标记的圆弧
const getMinBandArc = computed(() => 'M 18 50 A 32 32 0 0 1 18.158 46.824')
const getMaxBandArc = computed(() => 'M 81.842 46.824 A 32 32 0 0 1 82 50')
// 计算值的颜色
const getValueColor = computed(
() =>
props.value < props.min || props.value > props.max
? 'rgb(209, 67, 63)' // 红色 - 超出范围
: 'rgb(7, 177, 130)' // 绿色 - 正常范围
)
// 标签位置
const minLabelPos = computed(() => ({
x: 20,
y: 47
}))
const maxLabelPos = computed(() => ({
x: 78,
y: 47
}))
</script>
<template>
<div class="gauge-svg">
<svg viewBox="0 0 100 50">
<!-- 背景圆弧 -->
<path
class="dial"
d="M 10 50 A 40 40 0 0 1 90 50"
fill="none"
stroke="#DDD"
stroke-width="2"
/>
<!-- 值文本 -->
<g class="text-container">
<text
class="value-text"
dominant-baseline="central"
fill="#999"
font-family="sans-serif"
font-size="100%"
font-weight="normal"
text-anchor="middle"
x="50"
y="50"
></text>
</g>
<!-- 值的圆弧 -->
<path
:d="getValueArc"
:style="{ stroke: getValueColor }"
class="value"
fill="none"
stroke-width="8"
/>
<!-- 最小值标记 -->
<path
:d="getMinBandArc"
class="band"
fill="none"
stroke="#d1433f"
stroke-width="2"
/>
<text
:x="minLabelPos.x"
:y="minLabelPos.y"
class="bandLabel"
dominant-baseline="middle"
fill="#999"
font-family="sans-serif"
font-size="70%"
text-anchor="start"
>
{{ min.toFixed(1) }}
<!-- 显示最小值 -->
</text>
<!-- 最大值标记 -->
<path
:d="getMaxBandArc"
class="band"
fill="none"
stroke="#d1433f"
stroke-width="2"
/>
<text
:x="maxLabelPos.x"
:y="maxLabelPos.y"
class="bandLabel"
dominant-baseline="middle"
fill="#999"
font-family="sans-serif"
font-size="70%"
text-anchor="end"
>
{{ max.toFixed(1) }}
<!-- 显示最大值 -->
</text>
</svg>
<p class="gauge-svg-text">{{ value.toFixed(1) }}</p>
</div>
</template>
<style scoped>
.gauge-svg {
text-align: center;
}
.gauge-svg-text {
margin-top: 10px;
font-size: 25px;
color: #999;
}
.value {
transition: all 0.3s ease; /* 添加过渡效果 */
}
</style>
如何使用组件
<template>
<!-- 超出范围 -->
<GaugeSVG :max="-60" :min="-100" :value="20" />
<!-- 在范围内 -->
<GaugeSVG :max="-20" :min="-40" :value="-55" />
<!-- 低于范围 -->
<GaugeSVG :max="-60" :min="-100" :value="-80" />
</template>
分析实现原理
接下来,我们分析一下这个 GaugeSVG 组件的实现原理。这是一个基于 SVG 实现的仪表盘组件,主要用于显示一个数值在指定范围内的可视化表示。
主要设计实现原理如下:
- 组件接口设计
interface Props {
min: number // 最小值
max: number // 最大值
value: number // 当前值
}
组件接收三个基本参数:最小值、最大值和当前值,用于定义仪表盘的显示范围。
- 动态范围计算
const effectiveRange = computed(() => {
let effectiveMin = props.min
let effectiveMax = props.max
// 动态调整显示范围,确保当前值始终在可视范围内
if (props.value < props.min) {
effectiveMin = props.value
}
if (props.value > props.max) {
effectiveMax = props.value
}
return { min: effectiveMin, max: effectiveMax }
})
这个计算属性确保即使当前值超出预设范围,也能在仪表盘上正确显示。
- SVG 坐标计算
const calculatePoint = (angle: number): { x: number; y: number } => {
const radius = 40 // 圆的半径
const centerX = 50 // 圆心 X 坐标
const centerY = 50 // 圆心 Y 坐标
const radians = ((angle - 180) * Math.PI) / 180 // 角度转弧度
return {
x: centerX + radius * Math.cos(radians),
y: centerY + radius * Math.sin(radians)
}
}
这个工具函数用于计算圆弧上的点坐标,使用三角函数将角度转换为具体的 x、y 坐标。
- 值弧计算
const percentage = Math.min(
Math.max(
(props.value - effectiveRange.value.min) /
(effectiveRange.value.max - effectiveRange.value.min),
0
),
1
)
const angle = 180 * percentage
const point = calculatePoint(angle)
const largeArcFlag = angle > 180 ? 1 : 0
以上的这些计算用于生成表示当前值的 SVG 路径的必要参数:
percentage为计算当前值在范围内的百分比- 将百分比转换为角度(0-180 度)
angle - 计算终点坐标
point largeArcFlag用于判断是否为大弧
- 颜色逻辑
const getValueColor = computed(
() =>
props.value < props.min || props.value > props.max
? 'rgb(209, 67, 63)' // 红色 - 超出范围
: 'rgb(7, 177, 130)' // 绿色 - 正常范围
)
根据当前值是否在范围内,动态改变显示颜色:
- 超出范围显示红色
- 在范围内显示绿色
- SVG 结构
组件使用 SVG 元素构建仪表盘:
- 背景圆弧:显示整个范围
- 值弧:显示当前值
- 最小/最大值标记:显示范围边界
- 文本标签:显示具体数值
- 样式处理
.value {
transition: all 0.3s ease; /* 添加过渡效果 */
}
使用 CSS 过渡效果使数值变化时的动画更平滑。
这个组件通过 SVG 实现了精确的仪表盘显示,并且:
- 支持动态范围调整
- 提供颜色变化
- 具有平滑的动画效果
- 显示清晰的数值标记
总结
虽然刚开始我尝试使用 ECharts 实现设计稿中的效果,但经过简单尝试最终实现不理想,因此转向 SVG 的实现。
但由于我之前对 SVG 实现图表并没有研究,因此借助 Cursor AI 来逐步实现一个 SVG 版的动态仪表盘组件,以替代传统的 ECharts 方案,结果是可行的。
最终,将 SVG 实现拆分为一个可复用的 Vue 组件,接收最小值、最大值和当前值作为 props,并动态计算圆弧的位置和长度,根据值是否在范围内切换颜色(红色表示超出范围,绿色表示在范围内),同时添加平滑的动画过渡效果。