SVG数据可视化组件基础教程7:自定义柱状图

我是设计师邱兴,一个学习前端的设计师,今天给大家制作一个用SVG实现的自定义柱状图,SVG相较于Echart来说制作简单,但是效果可以非常丰富。

一、目标

通过HTML、CSS和JavaScript创建一个交互式的SVG柱状图,实现以下功能:

  1. 显示多组数据的柱状图。
  2. 支持随机生成数据和重置数据。
  3. 支持拖拽调整柱子的高度。
  4. 鼠标悬停时显示详细信息。

二、所需工具与准备

  1. 工具

    • 一个文本编辑器(如Notepad++、VS Code等)。
    • 浏览器(用于预览效果)。
  2. 基础准备

    • 确保你对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柱状图。这个柱状图具有渐变色柱子、拖拽调整功能、随机数据生成和重置功能,以及鼠标悬停显示详细信息的效果。你可以通过调整代码中的参数来改变柱状图的外观和行为。希望这个教程对你有所帮助!

以上制作的是一个最简单的一个带刻度的仪表盘,我还录制了一个更加美观的带刻度的仪表盘的视频教程,有兴趣的小伙伴可以点击查看。

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax