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

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

相关推荐
InlaidHarp7 分钟前
Elpis DSL领域模型设计理念
前端
lichenyang4539 分钟前
react-route-dom@6
前端
番茄比较犟11 分钟前
widget的同级移动
前端
每天吃饭的羊15 分钟前
面试题-函数入参为interface类型进行约束
前端
屋外雨大,惊蛰出没39 分钟前
Vue+spring boot前后端分离项目搭建---小白入门
前端·vue.js·spring boot
梦语花43 分钟前
如何在前端项目中优雅地实现异步请求重试机制
前端
彬师傅1 小时前
JSAPITHREE-自定义瓦片服务加载
前端·javascript
番茄比较犟1 小时前
UI更新中Widget比较过程
前端
独立开发者Pony1 小时前
关于我用 Ai 完成了一套系统 99% 代码这件事
前端·javascript·github
番茄比较犟1 小时前
Widget位置移动详细
前端