我是设计师邱兴,一个学习前端的设计师,今天给大家制作一个用SVG实现的自定义柱状图,SVG相较于Echart来说制作简单,但是效果可以非常丰富。
一、目标
通过HTML、CSS和JavaScript创建一个交互式的SVG柱状图,实现以下功能:
- 显示多组数据的柱状图。
- 支持随机生成数据和重置数据。
- 支持拖拽调整柱子的高度。
- 鼠标悬停时显示详细信息。
二、所需工具与准备
-
工具:
- 一个文本编辑器(如Notepad++、VS Code等)。
- 浏览器(用于预览效果)。
-
基础准备:
- 确保你对HTML、CSS和JavaScript有一定的了解。
- 确保你对SVG的基本语法有一定了解。
三、代码分析与操作步骤
1. 创建HTML结构
创建一个HTML文件(如Lesson7.html
)并设置基本的HTML结构:
xml
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SVG 自定义柱状图</title>
<style>
/* 样式部分 */
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 自定义柱状图</h1>
</div>
<div class="controls">
<button onclick="updateRandomData()">随机数据</button>
<button onclick="resetData()">重置数据</button>
<span style="margin-left: 20px; color: #666;">
提示:点击柱子查看详情,拖拽可调整数值
</span>
</div>
<div class="chart-container">
<svg id="barChart" width="100%" height="400" viewBox="0 0 900 400">
<defs>
<!-- 渐变定义 -->
<linearGradient id="barGradient1" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#4CAF50"/>
<stop offset="100%" stop-color="#45a049"/>
</linearGradient>
<linearGradient id="barGradient2" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#2196F3"/>
<stop offset="100%" stop-color="#1976D2"/>
</linearGradient>
<linearGradient id="barGradient3" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#FF9800"/>
<stop offset="100%" stop-color="#F57C00"/>
</linearGradient>
<linearGradient id="barGradient4" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#9C27B0"/>
<stop offset="100%" stop-color="#7B1FA2"/>
</linearGradient>
<linearGradient id="barGradient5" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#E91E63"/>
<stop offset="100%" stop-color="#C2185B"/>
</linearGradient>
<linearGradient id="barGradient6" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#00BCD4"/>
<stop offset="100%" stop-color="#0097A7"/>
</linearGradient>
<!-- 发光效果 -->
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- 坐标轴 -->
<g id="axes"></g>
<!-- 柱子区域 -->
<g id="bars"></g>
<!-- 进度点区域 -->
<g id="progress-dots"></g>
<!-- 标签区域 -->
<g id="labels"></g>
</svg>
</div>
</div>
<script>
// JavaScript部分
</script>
</body>
</html>
2. 添加CSS样式
在<style>
标签中,添加以下CSS样式:
css
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
min-height: 100vh;
}
.container {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
padding: 30px;
margin: 0 auto;
max-width: 1000px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
margin: 0;
font-size: 28px;
}
.controls {
margin-bottom: 30px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: linear-gradient(45deg, #4CAF50, #45a049);
color: white;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
}
input {
padding: 10px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 14px;
width: 100px;
}
input:focus {
outline: none;
border-color: #4CAF50;
}
.chart-container {
border: 2px solid #f0f0f0;
border-radius: 10px;
padding: 20px;
background: #fafafa;
}
.bar {
cursor: pointer;
transition: all 0.3s ease;
}
.bar:hover {
filter: brightness(1.1);
}
.progress-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 0.7; }
50% { opacity: 1; }
100% { opacity: 0.7; }
}
.tooltip {
position: absolute;
background: rgba(0,0,0,0.9);
color: white;
padding: 10px 15px;
border-radius: 8px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
- 设置页面背景为渐变色。
- 使用
flexbox
布局将控制按钮和提示信息居中显示。 - 设置柱状图容器的样式。
- 设置柱子、进度点和工具提示的样式。
3. 编写JavaScript代码
在<script>
标签中,添加以下JavaScript代码来实现柱状图的交互功能:
ini
// 图表数据 - 默认6根柱子
let chartData = [
{ id: 1, name: "产品A", value: 85, maxValue: 100, color: "url(#barGradient1)" },
{ id: 2, name: "产品B", value: 72, maxValue: 100, color: "url(#barGradient2)" },
{ id: 3, name: "产品C", value: 93, maxValue: 100, color: "url(#barGradient3)" },
{ id: 4, name: "产品D", value: 68, maxValue: 100, color: "url(#barGradient4)" },
{ id: 5, name: "产品E", value: 91, maxValue: 100, color: "url(#barGradient5)" },
{ id: 6, name: "产品F", value: 78, maxValue: 100, color: "url(#barGradient6)" }
];
// 图表配置
const config = {
width: 900,
height: 400,
margin: { top: 40, right: 40, bottom: 60, left: 80 },
barWidth: 50,
barSpacing: 30,
maxBarHeight: 250,
colors: [
"url(#barGradient1)",
"url(#barGradient2)",
"url(#barGradient3)",
"url(#barGradient4)",
"url(#barGradient5)",
"url(#barGradient6)"
]
};
let isDragging = false;
let selectedBar = null;
let dragStartY = 0;
let dragStartValue = 0;
// 绘制坐标轴
function drawAxes() {
const axes = document.getElementById('axes');
axes.innerHTML = '';
// Y轴
const yAxis = document.createElementNS("http://www.w3.org/2000/svg", "line");
yAxis.setAttribute("x1", config.margin.left);
yAxis.setAttribute("y1", config.margin.top);
yAxis.setAttribute("x2", config.margin.left);
yAxis.setAttribute("y2", config.height - config.margin.bottom);
yAxis.setAttribute("stroke", "#333");
yAxis.setAttribute("stroke-width", "2");
axes.appendChild(yAxis);
// X轴
const xAxis = document.createElementNS("http://www.w3.org/2000/svg", "line");
xAxis.setAttribute("x1", config.margin.left);
xAxis.setAttribute("y1", config.height - config.margin.bottom);
xAxis.setAttribute("x2", config.width - config.margin.right);
xAxis.setAttribute("y2", config.height - config.margin.bottom);
xAxis.setAttribute("stroke", "#333");
xAxis.setAttribute("stroke-width", "2");
axes.appendChild(xAxis);
// Y轴刻度
for (let i = 0; i <= 10; i++) {
const y = config.height - config.margin.bottom - (i * config.maxBarHeight / 10);
const x = config.margin.left;
// 刻度线
const tick = document.createElementNS("http://www.w3.org/2000/svg", "line");
tick.setAttribute("x1", x - 5);
tick.setAttribute("y1", y);
tick.setAttribute("x2", x);
tick.setAttribute("y2", y);
tick.setAttribute("stroke", "#666");
tick.setAttribute("stroke-width", "1");
axes.appendChild(tick);
// 刻度标签
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
label.setAttribute("x", x - 10);
label.setAttribute("y", y + 4);
label.setAttribute("text-anchor", "end");
label.setAttribute("font-size", "12");
label.setAttribute("fill", "#666");
label.textContent = i * 10;
axes.appendChild(label);
}
}
// 绘制柱子
function drawBars() {
const barsGroup = document.getElementById('bars');
barsGroup.innerHTML = '';
chartData.forEach((item, index) => {
const x = config.margin.left + index * (config.barWidth + config.barSpacing);
const height = (item.value / item.maxValue) * config.maxBarHeight;
const y = config.height - config.margin.bottom - height;
// 柱子背景
const bgRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
bgRect.setAttribute("x", x);
bgRect.setAttribute("y", config.margin.top);
bgRect.setAttribute("width", config.barWidth);
bgRect.setAttribute("height", config.maxBarHeight);
bgRect.setAttribute("fill", "#f0f0f0");
bgRect.setAttribute("rx", "5");
barsGroup.appendChild(bgRect);
// 柱子
const bar = document.createElementNS("http://www.w3.org/2000/svg", "rect");
bar.setAttribute("x", x);
bar.setAttribute("y", y);
bar.setAttribute("width", config.barWidth);
bar.setAttribute("height", height);
bar.setAttribute("fill", item.color);
bar.setAttribute("rx", "5");
bar.setAttribute("class", "bar");
bar.setAttribute("data-id", item.id);
bar.setAttribute("filter", "url(#glow)");
// 添加事件监听
bar.addEventListener('click', (event) => showTooltip(event, item));
bar.addEventListener('mousedown', (e) => startDrag(e, item));
barsGroup.appendChild(bar);
});
}
// 绘制进度点
function drawProgressDots() {
const dotsGroup = document.getElementById('progress-dots');
dotsGroup.innerHTML = '';
chartData.forEach((item, index) => {
const x = config.margin.left + index * (config.barWidth + config.barSpacing) + config.barWidth / 2;
const height = (item.value / item.maxValue) * config.maxBarHeight;
const y = config.height - config.margin.bottom - height - 10;
// 进度点
const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
dot.setAttribute("cx", x);
dot.setAttribute("cy", y);
dot.setAttribute("r", "8");
dot.setAttribute("fill", "#fff");
dot.setAttribute("stroke", "#333");
dot.setAttribute("stroke-width", "2");
dot.setAttribute("class", "progress-dot");
dotsGroup.appendChild(dot);
// 进度值
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", x);
text.setAttribute("y", y - 15);
text.setAttribute("text-anchor", "middle");
text.setAttribute("font-size", "12");
text.setAttribute("fill", "#333");
text.setAttribute("font-weight", "bold");
text.textContent = item.value;
dotsGroup.appendChild(text);
});
}
// 绘制标签
function drawLabels() {
const labelsGroup = document.getElementById('labels');
labelsGroup.innerHTML = '';
chartData.forEach((item, index) => {
const x = config.margin.left + index * (config.barWidth + config.barSpacing) + config.barWidth / 2;
const y = config.height - config.margin.bottom + 30;
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", x);
text.setAttribute("y", y);
text.setAttribute("text-anchor", "middle");
text.setAttribute("font-size", "14");
text.setAttribute("fill", "#333");
text.setAttribute("font-weight", "bold");
text.textContent = item.name;
labelsGroup.appendChild(text);
});
}
// 显示工具提示
function showTooltip(event, item) {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.innerHTML = `
<strong>${item.name}</strong><br>
当前值: ${item.value}<br>
最大值: ${item.maxValue}<br>
完成度: ${Math.round((item.value / item.maxValue) * 100)}%
`;
tooltip.style.left = event.pageX + 10 + 'px';
tooltip.style.top = event.pageY - 10 + 'px';
document.body.appendChild(tooltip);
setTimeout(() => {
document.body.removeChild(tooltip);
}, 2000);
}
// 开始拖拽
function startDrag(event, item) {
isDragging = true;
selectedBar = item;
dragStartY = event.clientY;
dragStartValue = item.value;
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
event.preventDefault();
}
// 拖拽中
function onDrag(event) {
if (!isDragging || !selectedBar) return;
const deltaY = dragStartY - event.clientY;
const deltaValue = Math.round(deltaY / 2);
const newValue = Math.max(0, Math.min(selectedBar.maxValue, dragStartValue + deltaValue));
selectedBar.value = newValue;
redrawWithAnimation();
}
// 停止拖拽
function stopDrag() {
isDragging = false;
selectedBar = null;
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
}
// 随机更新数据
function updateRandomData() {
chartData.forEach((item, index) => {
const newValue = Math.floor(Math.random() * item.maxValue);
item.value = newValue;
});
redrawWithAnimation();
}
// 重置数据
function resetData() {
chartData = [
{ id: 1, name: "产品A", value: 85, maxValue: 100, color: "url(#barGradient1)" },
{ id: 2, name: "产品B", value: 72, maxValue: 100, color: "url(#barGradient2)" },
{ id: 3, name: "产品C", value: 93, maxValue: 100, color: "url(#barGradient3)" },
{ id: 4, name: "产品D", value: 68, maxValue: 100, color: "url(#barGradient4)" },
{ id: 5, name: "产品E", value: 91, maxValue: 100, color: "url(#barGradient5)" },
{ id: 6, name: "产品F", value: 78, maxValue: 100, color: "url(#barGradient6)" }
];
redrawWithAnimation();
}
// 带动画效果的重绘
function redrawWithAnimation() {
drawAxes();
drawBars();
drawProgressDots();
drawLabels();
// 添加从底部涨起的动画效果
const bars = document.querySelectorAll('.bar');
const dots = document.querySelectorAll('.progress-dot');
const valueTexts = document.querySelectorAll('.progress-dots text');
bars.forEach((bar, index) => {
// 初始状态:高度为0
bar.style.height = '0px';
bar.style.y = (config.height - config.margin.bottom) + 'px';
// 初始状态:进度点和数值在底部
if (dots[index]) {
dots[index].style.cy = (config.height - config.margin.bottom - 10) + 'px';
}
if (valueTexts[index]) {
valueTexts[index].style.y = (config.height - config.margin.bottom - 25) + 'px';
valueTexts[index].textContent = '0';
}
setTimeout(() => {
// 获取原始高度和位置
const originalHeight = bar.getAttribute('height');
const originalY = bar.getAttribute('y');
const originalDotY = dots[index] ? dots[index].getAttribute('cy') : 0;
const originalTextY = valueTexts[index] ? valueTexts[index].getAttribute('y') : 0;
const targetValue = chartData[index].value;
// 动画到原始状态
bar.style.transition = 'height 0.6s ease-out, y 0.6s ease-out';
bar.style.height = originalHeight + 'px';
bar.style.y = originalY + 'px';
// 进度点和数值动画
if (dots[index]) {
dots[index].style.transition = 'cy 0.6s ease-out';
dots[index].style.cy = originalDotY + 'px';
}
if (valueTexts[index]) {
valueTexts[index].style.transition = 'y 0.6s ease-out';
valueTexts[index].style.y = originalTextY + 'px';
// 数值累加动画 - 与移动同步
let currentValue = 0;
const increment = targetValue / 36; // 36帧动画 (0.6秒 / 16.67ms)
const countInterval = setInterval(() => {
currentValue += increment;
if (currentValue >= targetValue) {
currentValue = targetValue;
clearInterval(countInterval);
}
valueTexts[index].textContent = Math.round(currentValue);
}, 16.67); // 约60fps (1000ms / 60)
}
// 动画完成后清除内联样式
setTimeout(() => {
bar.style.transition = '';
bar.style.height = '';
bar.style.y = '';
if (dots[index]) {
dots[index].style.transition = '';
dots[index].style.cy = '';
}
if (valueTexts[index]) {
valueTexts[index].style.transition = '';
valueTexts[index].style.y = '';
}
}, 600);
}, index * 150);
});
}
// 重绘图表
function redraw() {
drawAxes();
drawBars();
drawProgressDots();
drawLabels();
}
// 初始化
redraw();
4. 完整代码
将上述代码整合到一个HTML文件中,完整的代码如下:
ini
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SVG 自定义柱状图</title>
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
min-height: 100vh;
}
.container {
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
padding: 30px;
margin: 0 auto;
max-width: 1000px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
margin: 0;
font-size: 28px;
}
.controls {
margin-bottom: 30px;
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
button {
padding: 10px 20px;
border: none;
border-radius: 8px;
background: linear-gradient(45deg, #4CAF50, #45a049);
color: white;
cursor: pointer;
font-size: 14px;
font-weight: bold;
transition: transform 0.2s;
}
button:hover {
transform: translateY(-2px);
}
input {
padding: 10px;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 14px;
width: 100px;
}
input:focus {
outline: none;
border-color: #4CAF50;
}
.chart-container {
border: 2px solid #f0f0f0;
border-radius: 10px;
padding: 20px;
background: #fafafa;
}
.bar {
cursor: pointer;
transition: all 0.3s ease;
}
.bar:hover {
filter: brightness(1.1);
}
.progress-dot {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { opacity: 0.7; }
50% { opacity: 1; }
100% { opacity: 0.7; }
}
.tooltip {
position: absolute;
background: rgba(0,0,0,0.9);
color: white;
padding: 10px 15px;
border-radius: 8px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📊 自定义柱状图</h1>
</div>
<div class="controls">
<button onclick="updateRandomData()">随机数据</button>
<button onclick="resetData()">重置数据</button>
<span style="margin-left: 20px; color: #666;">
提示:点击柱子查看详情,拖拽可调整数值
</span>
</div>
<div class="chart-container">
<svg id="barChart" width="100%" height="400" viewBox="0 0 900 400">
<defs>
<!-- 渐变定义 -->
<linearGradient id="barGradient1" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#4CAF50"/>
<stop offset="100%" stop-color="#45a049"/>
</linearGradient>
<linearGradient id="barGradient2" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#2196F3"/>
<stop offset="100%" stop-color="#1976D2"/>
</linearGradient>
<linearGradient id="barGradient3" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#FF9800"/>
<stop offset="100%" stop-color="#F57C00"/>
</linearGradient>
<linearGradient id="barGradient4" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#9C27B0"/>
<stop offset="100%" stop-color="#7B1FA2"/>
</linearGradient>
<linearGradient id="barGradient5" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#E91E63"/>
<stop offset="100%" stop-color="#C2185B"/>
</linearGradient>
<linearGradient id="barGradient6" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#00BCD4"/>
<stop offset="100%" stop-color="#0097A7"/>
</linearGradient>
<!-- 发光效果 -->
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- 坐标轴 -->
<g id="axes"></g>
<!-- 柱子区域 -->
<g id="bars"></g>
<!-- 进度点区域 -->
<g id="progress-dots"></g>
<!-- 标签区域 -->
<g id="labels"></g>
</svg>
</div>
</div>
<script>
// 图表数据 - 默认6根柱子
let chartData = [
{ id: 1, name: "产品A", value: 85, maxValue: 100, color: "url(#barGradient1)" },
{ id: 2, name: "产品B", value: 72, maxValue: 100, color: "url(#barGradient2)" },
{ id: 3, name: "产品C", value: 93, maxValue: 100, color: "url(#barGradient3)" },
{ id: 4, name: "产品D", value: 68, maxValue: 100, color: "url(#barGradient4)" },
{ id: 5, name: "产品E", value: 91, maxValue: 100, color: "url(#barGradient5)" },
{ id: 6, name: "产品F", value: 78, maxValue: 100, color: "url(#barGradient6)" }
];
// 图表配置
const config = {
width: 900,
height: 400,
margin: { top: 40, right: 40, bottom: 60, left: 80 },
barWidth: 50,
barSpacing: 30,
maxBarHeight: 250,
colors: [
"url(#barGradient1)",
"url(#barGradient2)",
"url(#barGradient3)",
"url(#barGradient4)",
"url(#barGradient5)",
"url(#barGradient6)"
]
};
let isDragging = false;
let selectedBar = null;
let dragStartY = 0;
let dragStartValue = 0;
// 绘制坐标轴
function drawAxes() {
const axes = document.getElementById('axes');
axes.innerHTML = '';
// Y轴
const yAxis = document.createElementNS("http://www.w3.org/2000/svg", "line");
yAxis.setAttribute("x1", config.margin.left);
yAxis.setAttribute("y1", config.margin.top);
yAxis.setAttribute("x2", config.margin.left);
yAxis.setAttribute("y2", config.height - config.margin.bottom);
yAxis.setAttribute("stroke", "#333");
yAxis.setAttribute("stroke-width", "2");
axes.appendChild(yAxis);
// X轴
const xAxis = document.createElementNS("http://www.w3.org/2000/svg", "line");
xAxis.setAttribute("x1", config.margin.left);
xAxis.setAttribute("y1", config.height - config.margin.bottom);
xAxis.setAttribute("x2", config.width - config.margin.right);
xAxis.setAttribute("y2", config.height - config.margin.bottom);
xAxis.setAttribute("stroke", "#333");
xAxis.setAttribute("stroke-width", "2");
axes.appendChild(xAxis);
// Y轴刻度
for (let i = 0; i <= 10; i++) {
const y = config.height - config.margin.bottom - (i * config.maxBarHeight / 10);
const x = config.margin.left;
// 刻度线
const tick = document.createElementNS("http://www.w3.org/2000/svg", "line");
tick.setAttribute("x1", x - 5);
tick.setAttribute("y1", y);
tick.setAttribute("x2", x);
tick.setAttribute("y2", y);
tick.setAttribute("stroke", "#666");
tick.setAttribute("stroke-width", "1");
axes.appendChild(tick);
// 刻度标签
const label = document.createElementNS("http://www.w3.org/2000/svg", "text");
label.setAttribute("x", x - 10);
label.setAttribute("y", y + 4);
label.setAttribute("text-anchor", "end");
label.setAttribute("font-size", "12");
label.setAttribute("fill", "#666");
label.textContent = i * 10;
axes.appendChild(label);
}
}
// 绘制柱子
function drawBars() {
const barsGroup = document.getElementById('bars');
barsGroup.innerHTML = '';
chartData.forEach((item, index) => {
const x = config.margin.left + index * (config.barWidth + config.barSpacing);
const height = (item.value / item.maxValue) * config.maxBarHeight;
const y = config.height - config.margin.bottom - height;
// 柱子背景
const bgRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
bgRect.setAttribute("x", x);
bgRect.setAttribute("y", config.margin.top);
bgRect.setAttribute("width", config.barWidth);
bgRect.setAttribute("height", config.maxBarHeight);
bgRect.setAttribute("fill", "#f0f0f0");
bgRect.setAttribute("rx", "5");
barsGroup.appendChild(bgRect);
// 柱子
const bar = document.createElementNS("http://www.w3.org/2000/svg", "rect");
bar.setAttribute("x", x);
bar.setAttribute("y", y);
bar.setAttribute("width", config.barWidth);
bar.setAttribute("height", height);
bar.setAttribute("fill", item.color);
bar.setAttribute("rx", "5");
bar.setAttribute("class", "bar");
bar.setAttribute("data-id", item.id);
bar.setAttribute("filter", "url(#glow)");
// 添加事件监听
bar.addEventListener('click', (event) => showTooltip(event, item));
bar.addEventListener('mousedown', (e) => startDrag(e, item));
barsGroup.appendChild(bar);
});
}
// 绘制进度点
function drawProgressDots() {
const dotsGroup = document.getElementById('progress-dots');
dotsGroup.innerHTML = '';
chartData.forEach((item, index) => {
const x = config.margin.left + index * (config.barWidth + config.barSpacing) + config.barWidth / 2;
const height = (item.value / item.maxValue) * config.maxBarHeight;
const y = config.height - config.margin.bottom - height - 10;
// 进度点
const dot = document.createElementNS("http://www.w3.org/2000/svg", "circle");
dot.setAttribute("cx", x);
dot.setAttribute("cy", y);
dot.setAttribute("r", "8");
dot.setAttribute("fill", "#fff");
dot.setAttribute("stroke", "#333");
dot.setAttribute("stroke-width", "2");
dot.setAttribute("class", "progress-dot");
dotsGroup.appendChild(dot);
// 进度值
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", x);
text.setAttribute("y", y - 15);
text.setAttribute("text-anchor", "middle");
text.setAttribute("font-size", "12");
text.setAttribute("fill", "#333");
text.setAttribute("font-weight", "bold");
text.textContent = item.value;
dotsGroup.appendChild(text);
});
}
// 绘制标签
function drawLabels() {
const labelsGroup = document.getElementById('labels');
labelsGroup.innerHTML = '';
chartData.forEach((item, index) => {
const x = config.margin.left + index * (config.barWidth + config.barSpacing) + config.barWidth / 2;
const y = config.height - config.margin.bottom + 30;
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("x", x);
text.setAttribute("y", y);
text.setAttribute("text-anchor", "middle");
text.setAttribute("font-size", "14");
text.setAttribute("fill", "#333");
text.setAttribute("font-weight", "bold");
text.textContent = item.name;
labelsGroup.appendChild(text);
});
}
// 显示工具提示
function showTooltip(event, item) {
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.innerHTML = `
<strong>${item.name}</strong><br>
当前值: ${item.value}<br>
最大值: ${item.maxValue}<br>
完成度: ${Math.round((item.value / item.maxValue) * 100)}%
`;
tooltip.style.left = event.pageX + 10 + 'px';
tooltip.style.top = event.pageY - 10 + 'px';
document.body.appendChild(tooltip);
setTimeout(() => {
document.body.removeChild(tooltip);
}, 2000);
}
// 开始拖拽
function startDrag(event, item) {
isDragging = true;
selectedBar = item;
dragStartY = event.clientY;
dragStartValue = item.value;
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
event.preventDefault();
}
// 拖拽中
function onDrag(event) {
if (!isDragging || !selectedBar) return;
const deltaY = dragStartY - event.clientY;
const deltaValue = Math.round(deltaY / 2);
const newValue = Math.max(0, Math.min(selectedBar.maxValue, dragStartValue + deltaValue));
selectedBar.value = newValue;
redrawWithAnimation();
}
// 停止拖拽
function stopDrag() {
isDragging = false;
selectedBar = null;
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
}
// 随机更新数据
function updateRandomData() {
chartData.forEach((item, index) => {
const newValue = Math.floor(Math.random() * item.maxValue);
item.value = newValue;
});
redrawWithAnimation();
}
// 重置数据
function resetData() {
chartData = [
{ id: 1, name: "产品A", value: 85, maxValue: 100, color: "url(#barGradient1)" },
{ id: 2, name: "产品B", value: 72, maxValue: 100, color: "url(#barGradient2)" },
{ id: 3, name: "产品C", value: 93, maxValue: 100, color: "url(#barGradient3)" },
{ id: 4, name: "产品D", value: 68, maxValue: 100, color: "url(#barGradient4)" },
{ id: 5, name: "产品E", value: 91, maxValue: 100, color: "url(#barGradient5)" },
{ id: 6, name: "产品F", value: 78, maxValue: 100, color: "url(#barGradient6)" }
];
redrawWithAnimation();
}
// 带动画效果的重绘
function redrawWithAnimation() {
drawAxes();
drawBars();
drawProgressDots();
drawLabels();
// 添加从底部涨起的动画效果
const bars = document.querySelectorAll('.bar');
const dots = document.querySelectorAll('.progress-dot');
const valueTexts = document.querySelectorAll('.progress-dots text');
bars.forEach((bar, index) => {
// 初始状态:高度为0
bar.style.height = '0px';
bar.style.y = (config.height - config.margin.bottom) + 'px';
// 初始状态:进度点和数值在底部
if (dots[index]) {
dots[index].style.cy = (config.height - config.margin.bottom - 10) + 'px';
}
if (valueTexts[index]) {
valueTexts[index].style.y = (config.height - config.margin.bottom - 25) + 'px';
valueTexts[index].textContent = '0';
}
setTimeout(() => {
// 获取原始高度和位置
const originalHeight = bar.getAttribute('height');
const originalY = bar.getAttribute('y');
const originalDotY = dots[index] ? dots[index].getAttribute('cy') : 0;
const originalTextY = valueTexts[index] ? valueTexts[index].getAttribute('y') : 0;
const targetValue = chartData[index].value;
// 动画到原始状态
bar.style.transition = 'height 0.6s ease-out, y 0.6s ease-out';
bar.style.height = originalHeight + 'px';
bar.style.y = originalY + 'px';
// 进度点和数值动画
if (dots[index]) {
dots[index].style.transition = 'cy 0.6s ease-out';
dots[index].style.cy = originalDotY + 'px';
}
if (valueTexts[index]) {
valueTexts[index].style.transition = 'y 0.6s ease-out';
valueTexts[index].style.y = originalTextY + 'px';
// 数值累加动画 - 与移动同步
let currentValue = 0;
const increment = targetValue / 36; // 36帧动画 (0.6秒 / 16.67ms)
const countInterval = setInterval(() => {
currentValue += increment;
if (currentValue >= targetValue) {
currentValue = targetValue;
clearInterval(countInterval);
}
valueTexts[index].textContent = Math.round(currentValue);
}, 16.67); // 约60fps (1000ms / 60)
}
// 动画完成后清除内联样式
setTimeout(() => {
bar.style.transition = '';
bar.style.height = '';
bar.style.y = '';
if (dots[index]) {
dots[index].style.transition = '';
dots[index].style.cy = '';
}
if (valueTexts[index]) {
valueTexts[index].style.transition = '';
valueTexts[index].style.y = '';
}
}, 600);
}, index * 150);
});
}
// 重绘图表
function redraw() {
drawAxes();
drawBars();
drawProgressDots();
drawLabels();
}
// 初始化
redraw();
</script>
</body>
</html>
四、总结
通过以上步骤,你可以创建一个交互式的SVG柱状图。这个柱状图具有渐变色柱子、拖拽调整功能、随机数据生成和重置功能,以及鼠标悬停显示详细信息的效果。你可以通过调整代码中的参数来改变柱状图的外观和行为。希望这个教程对你有所帮助!
以上制作的是一个最简单的一个带刻度的仪表盘,我还录制了一个更加美观的带刻度的仪表盘的视频教程,有兴趣的小伙伴可以点击查看。
