前置知识: SVG、SVG、SVG

SVG 实战项目

5 min高级

综合运用:仪表盘、环形进度、动画 Logo、数据可视化。

前置知识

建议先阅读以下内容再进入本文:

1. 项目一:环形进度条

结合 path、stroke-dasharray、动画的综合应用。

<svg viewBox="0 0 200 200" width="200" height="200">
  <defs>
    <linearGradient id="progress-grad" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#4f5bd5" />
      <stop offset="100%" stop-color="#00b894" />
    </linearGradient>
    <filter id="progress-glow">
      <feGaussianBlur stdDeviation="3" result="b" />
      <feMerge>
        <feMergeNode in="b" />
        <feMergeNode in="SourceGraphic" />
      </feMerge>
    </filter>
  </defs>

  <!-- 背景圆 -->
  <circle cx="100" cy="100" r="80" fill="none" stroke="#e0e0e0" stroke-width="12" />

  <!-- 进度圆 -->
  <circle
    id="progress-circle"
    cx="100"
    cy="100"
    r="80"
    fill="none"
    stroke="url(#progress-grad)"
    stroke-width="12"
    stroke-linecap="round"
    stroke-dasharray="502"
    stroke-dashoffset="502"
    transform="rotate(-90 100 100)"
    filter="url(#progress-glow)"
  />

  <!-- 中心文本 -->
  <text
    id="progress-text"
    x="100"
    y="100"
    text-anchor="middle"
    dominant-baseline="middle"
    font-size="36"
    font-weight="bold"
    fill="#333"
  >
    0%
  </text>
  <text x="100" y="130" text-anchor="middle" font-size="12" fill="#999">已完成</text>
</svg>

<script>
  const circle = document.getElementById('progress-circle');
  const text = document.getElementById('progress-text');
  const circumference = 2 * Math.PI * 80; // 约 502

  function setProgress(percent) {
    const offset = circumference - (percent / 100) * circumference;
    circle.style.strokeDashoffset = offset;
    text.textContent = percent + '%';
  }

  // 动画到 75%
  let current = 0;
  const target = 75;
  const duration = 2000;
  const start = performance.now();

  function animate(now) {
    const elapsed = now - start;
    const t = Math.min(elapsed / duration, 1);
    const eased = 1 - Math.pow(1 - t, 3); // ease-out cubic
    current = Math.round(eased * target);
    setProgress(current);
    if (t < 1) requestAnimationFrame(animate);
  }
  requestAnimationFrame(animate);
</script>

要点:

  • 圆周长 = 2πr = 502
  • stroke-dasharray 设为周长,stroke-dashoffset 控制进度
  • rotate(-90 100 100) 从 12 点钟方向开始
  • ease-out cubic 缓动让进度增长自然减速

2. 项目二:动态数据图表

完整柱状图,含坐标轴、数据标签、交互悬停。

<svg viewBox="0 0 600 400" class="chart" role="img" aria-labelledby="chart-title chart-desc">
  <title id="chart-title">2024 季度销售额</title>
  <desc id="chart-desc">柱状图展示 Q1 至 Q4 销售额,单位:万元</desc>

  <defs>
    <linearGradient id="bar-grad" x1="0%" y1="0%" x2="0%" y2="100%">
      <stop offset="0%" stop-color="#5b6ee8" />
      <stop offset="100%" stop-color="#4f5bd5" />
    </linearGradient>
    <filter id="bar-shadow">
      <feDropShadow dx="0" dy="2" stdDeviation="2" flood-opacity="0.1" />
    </filter>
  </defs>

  <style>
    .bar {
      transition: opacity 0.2s;
      cursor: pointer;
    }
    .bar:hover {
      opacity: 0.8;
    }
    .label {
      font-family: sans-serif;
      font-size: 12px;
      fill: #666;
    }
    .value {
      font-family: sans-serif;
      font-size: 13px;
      fill: #333;
      font-weight: bold;
    }
    .axis {
      stroke: #ccc;
    }
  </style>

  <!-- Y 轴 -->
  <line class="axis" x1="60" y1="40" x2="60" y2="320" />
  <!-- X 轴 -->
  <line class="axis" x1="60" y1="320" x2="560" y2="320" />

  <!-- 网格线 -->
  <g stroke="#f0f0f0" stroke-dasharray="4 4">
    <line x1="60" y1="80" x2="560" y2="80" />
    <line x1="60" y1="160" x2="560" y2="160" />
    <line x1="60" y1="240" x2="560" y2="240" />
  </g>

  <!-- Y 轴标签 -->
  <g class="label" text-anchor="end">
    <text x="55" y="324">0</text>
    <text x="55" y="244">75</text>
    <text x="55" y="164">150</text>
    <text x="55" y="84">225</text>
  </g>

  <!-- 柱子由 JS 动态生成 -->
  <g id="bars"></g>
</svg>

<script>
  const data = [
    { label: 'Q1', value: 120 },
    { label: 'Q2', value: 165 },
    { label: 'Q3', value: 210 },
    { label: 'Q4', value: 180 },
  ];

  const svgNS = 'http://www.w3.org/2000/svg';
  const barsGroup = document.getElementById('bars');
  const maxValue = 250;
  const chartHeight = 280;
  const barWidth = 80;
  const gap = 40;
  const startX = 100;

  data.forEach((d, i) => {
    const barHeight = (d.value / maxValue) * chartHeight;
    const x = startX + i * (barWidth + gap);
    const y = 320 - barHeight;

    // 柱子
    const bar = document.createElementNS(svgNS, 'rect');
    bar.setAttribute('class', 'bar');
    bar.setAttribute('x', x);
    bar.setAttribute('y', y);
    bar.setAttribute('width', barWidth);
    bar.setAttribute('height', barHeight);
    bar.setAttribute('fill', 'url(#bar-grad)');
    bar.setAttribute('filter', 'url(#bar-shadow)');
    bar.setAttribute('rx', 4);

    // 初始高度 0,动画展开
    bar.setAttribute('height', 0);
    bar.setAttribute('y', 320);
    bar.style.transition = 'all 0.8s cubic-bezier(0.4, 0, 0.2, 1)';
    bar.style.transitionDelay = i * 0.1 + 's';

    barsGroup.appendChild(bar);

    // 触发动画
    requestAnimationFrame(() => {
      bar.setAttribute('height', barHeight);
      bar.setAttribute('y', y);
    });

    // X 轴标签
    const label = document.createElementNS(svgNS, 'text');
    label.setAttribute('class', 'label');
    label.setAttribute('x', x + barWidth / 2);
    label.setAttribute('y', 340);
    label.setAttribute('text-anchor', 'middle');
    label.textContent = d.label;
    barsGroup.appendChild(label);

    // 数值标签
    const value = document.createElementNS(svgNS, 'text');
    value.setAttribute('class', 'value');
    value.setAttribute('x', x + barWidth / 2);
    value.setAttribute('y', y - 8);
    value.setAttribute('text-anchor', 'middle');
    value.textContent = d.value;
    value.style.opacity = '0';
    value.style.transition = 'opacity 0.4s';
    value.style.transitionDelay = i * 0.1 + 0.4 + 's';
    barsGroup.appendChild(value);

    requestAnimationFrame(() => {
      value.style.opacity = '1';
    });

    // 交互
    bar.addEventListener('click', () => {
      alert(`${d.label}: ${d.value} 万元`);
    });
  });
</script>

结合渐变、路径动画、变换。

<svg viewBox="0 0 400 120" width="400" height="120">
  <defs>
    <linearGradient id="logo-grad" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#4f5bd5">
        <animate
          attributeName="stop-color"
          values="#4f5bd5;#00b894;#4f5bd5"
          dur="6s"
          repeatCount="indefinite"
        />
      </stop>
      <stop offset="100%" stop-color="#00b894">
        <animate
          attributeName="stop-color"
          values="#00b894;#4f5bd5;#00b894"
          dur="6s"
          repeatCount="indefinite"
        />
      </stop>
    </linearGradient>
    <filter id="logo-glow">
      <feGaussianBlur stdDeviation="2" result="b" />
      <feMerge>
        <feMergeNode in="b" />
        <feMergeNode in="SourceGraphic" />
      </feMerge>
    </filter>
  </defs>

  <style>
    .logo-text {
      font-family: 'Inter', sans-serif;
      font-size: 48px;
      font-weight: bold;
      fill: url(#logo-grad);
      filter: url(#logo-glow);
    }
    .logo-letter {
      transform-origin: center;
      transform-box: fill-box;
    }
    .logo-letter:nth-child(1) {
      animation: bounce 2s ease-in-out infinite;
    }
    .logo-letter:nth-child(2) {
      animation: bounce 2s ease-in-out infinite 0.1s;
    }
    .logo-letter:nth-child(3) {
      animation: bounce 2s ease-in-out infinite 0.2s;
    }
    .logo-letter:nth-child(4) {
      animation: bounce 2s ease-in-out infinite 0.3s;
    }
    .logo-letter:nth-child(5) {
      animation: bounce 2s ease-in-out infinite 0.4s;
    }
    .logo-letter:nth-child(6) {
      animation: bounce 2s ease-in-out infinite 0.5s;
    }
    @keyframes bounce {
      0%,
      100% {
        transform: translateY(0);
      }
      50% {
        transform: translateY(-8px);
      }
    }
  </style>

  <text x="200" y="75" text-anchor="middle" class="logo-text">
    <tspan class="logo-letter">F</tspan>
    <tspan class="logo-letter">A</tspan>
    <tspan class="logo-letter">N</tspan>
    <tspan class="logo-letter">D</tspan>
    <tspan class="logo-letter">E</tspan>
    <tspan class="logo-letter">X</tspan>
  </text>

  <!-- 装饰下划线 -->
  <path
    d="M 100 95 Q 200 105 300 95"
    fill="none"
    stroke="url(#logo-grad)"
    stroke-width="2"
    stroke-linecap="round"
  >
    <animate
      attributeName="d"
      values="M 100 95 Q 200 105 300 95; M 100 95 Q 200 85 300 95; M 100 95 Q 200 105 300 95"
      dur="3s"
      repeatCount="indefinite"
    />
  </path>
</svg>

4. 项目四:交互式地图

简化版中国地图节点示意。

<svg viewBox="0 0 800 600" class="map" role="img" aria-labelledby="map-title">
  <title id="map-title">节点分布图</title>

  <defs>
    <radialGradient id="node-grad">
      <stop offset="0%" stop-color="#4f5bd5" />
      <stop offset="100%" stop-color="#3a47b8" />
    </radialGradient>
    <filter id="node-glow">
      <feGaussianBlur stdDeviation="4" result="b" />
      <feMerge>
        <feMergeNode in="b" />
        <feMergeNode in="SourceGraphic" />
      </feMerge>
    </filter>
    <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
      <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#f0f0f0" stroke-width="1" />
    </pattern>
  </defs>

  <style>
    .node {
      cursor: pointer;
      transition: transform 0.2s;
      transform-origin: center;
      transform-box: fill-box;
    }
    .node:hover {
      transform: scale(1.3);
    }
    .label {
      font-family: sans-serif;
      font-size: 12px;
      fill: #333;
      pointer-events: none;
    }
    .connection {
      stroke: #4f5bd5;
      stroke-width: 1.5;
      opacity: 0.4;
    }
    .pulse-ring {
      fill: none;
      stroke: #4f5bd5;
      stroke-width: 2;
    }
  </style>

  <!-- 背景网格 -->
  <rect width="800" height="600" fill="url(#grid)" />

  <!-- 连接线 -->
  <g class="connections">
    <line class="connection" x1="200" y1="200" x2="400" y2="300" />
    <line class="connection" x1="400" y1="300" x2="600" y2="200" />
    <line class="connection" x1="400" y1="300" x2="500" y2="450" />
    <line class="connection" x1="200" y1="200" x2="300" y2="450" />
  </g>

  <!-- 节点 -->
  <g class="nodes">
    <g class="node-group" transform="translate(200, 200)">
      <circle class="pulse-ring" r="15">
        <animate attributeName="r" values="15;25;15" dur="2s" repeatCount="indefinite" />
        <animate attributeName="opacity" values="1;0;1" dur="2s" repeatCount="indefinite" />
      </circle>
      <circle class="node" r="8" fill="url(#node-grad)" filter="url(#node-glow)" />
      <text class="label" y="-15" text-anchor="middle">北京</text>
    </g>

    <g class="node-group" transform="translate(400, 300)">
      <circle class="pulse-ring" r="15">
        <animate
          attributeName="r"
          values="15;25;15"
          dur="2s"
          begin="0.5s"
          repeatCount="indefinite"
        />
        <animate
          attributeName="opacity"
          values="1;0;1"
          dur="2s"
          begin="0.5s"
          repeatCount="indefinite"
        />
      </circle>
      <circle class="node" r="8" fill="url(#node-grad)" filter="url(#node-glow)" />
      <text class="label" y="-15" text-anchor="middle">武汉</text>
    </g>

    <g class="node-group" transform="translate(600, 200)">
      <circle class="pulse-ring" r="15">
        <animate attributeName="r" values="15;25;15" dur="2s" begin="1s" repeatCount="indefinite" />
        <animate
          attributeName="opacity"
          values="1;0;1"
          dur="2s"
          begin="1s"
          repeatCount="indefinite"
        />
      </circle>
      <circle class="node" r="8" fill="url(#node-grad)" filter="url(#node-glow)" />
      <text class="label" y="-15" text-anchor="middle">上海</text>
    </g>

    <g class="node-group" transform="translate(300, 450)">
      <circle class="pulse-ring" r="15">
        <animate
          attributeName="r"
          values="15;25;15"
          dur="2s"
          begin="1.5s"
          repeatCount="indefinite"
        />
        <animate
          attributeName="opacity"
          values="1;0;1"
          dur="2s"
          begin="1.5s"
          repeatCount="indefinite"
        />
      </circle>
      <circle class="node" r="8" fill="url(#node-grad)" filter="url(#node-glow)" />
      <text class="label" y="-15" text-anchor="middle">成都</text>
    </g>

    <g class="node-group" transform="translate(500, 450)">
      <circle class="pulse-ring" r="15">
        <animate attributeName="r" values="15;25;15" dur="2s" begin="2s" repeatCount="indefinite" />
        <animate
          attributeName="opacity"
          values="1;0;1"
          dur="2s"
          begin="2s"
          repeatCount="indefinite"
        />
      </circle>
      <circle class="node" r="8" fill="url(#node-grad)" filter="url(#node-glow)" />
      <text class="label" y="-15" text-anchor="middle">广州</text>
    </g>
  </g>

  <script>
    document.querySelectorAll('.node').forEach((node) => {
      node.addEventListener('click', (e) => {
        const group = e.target.closest('.node-group');
        const label = group.querySelector('.label').textContent;
        alert(`选中:${label}`);
      });
    });
  </script>
</svg>

5. 项目五:加载动画集

常用加载动画合集。

<svg viewBox="0 0 300 100" width="300" height="100">
  <style>
    .spinner-1 {
      animation: spin 1.5s linear infinite;
      transform-origin: center;
      transform-box: fill-box;
    }
    .spinner-2 circle:nth-child(1) {
      animation: fade 1.2s ease-in-out infinite;
    }
    .spinner-2 circle:nth-child(2) {
      animation: fade 1.2s ease-in-out infinite 0.2s;
    }
    .spinner-2 circle:nth-child(3) {
      animation: fade 1.2s ease-in-out infinite 0.4s;
    }
    .spinner-3 {
      animation: bounce 1s ease-in-out infinite;
      transform-origin: center;
      transform-box: fill-box;
    }
    @keyframes spin {
      to {
        transform: rotate(360deg);
      }
    }
    @keyframes fade {
      0%,
      100% {
        opacity: 0.3;
      }
      50% {
        opacity: 1;
      }
    }
    @keyframes bounce {
      0%,
      100% {
        transform: scaleY(1);
      }
      50% {
        transform: scaleY(0.5);
      }
    }
  </style>

  <!-- 旋转环 -->
  <g transform="translate(50, 50)">
    <g class="spinner-1">
      <circle
        r="20"
        fill="none"
        stroke="#4f5bd5"
        stroke-width="3"
        stroke-dasharray="30 95"
        stroke-linecap="round"
      />
    </g>
  </g>

  <!-- 三点跳动 -->
  <g class="spinner-2" transform="translate(150, 50)">
    <circle cx="-15" r="6" fill="#4f5bd5" />
    <circle cx="0" r="6" fill="#4f5bd5" />
    <circle cx="15" r="6" fill="#4f5bd5" />
  </g>

  <!-- 脉冲条 -->
  <g transform="translate(250, 50)">
    <rect x="-15" y="-20" width="6" height="40" rx="3" fill="#4f5bd5" class="spinner-3" />
    <rect
      x="-3"
      y="-20"
      width="6"
      height="40"
      rx="3"
      fill="#4f5bd5"
      class="spinner-3"
      style="animation-delay: 0.2s"
    />
    <rect
      x="9"
      y="-20"
      width="6"
      height="40"
      rx="3"
      fill="#4f5bd5"
      class="spinner-3"
      style="animation-delay: 0.4s"
    />
  </g>
</svg>

6. 项目六:折线图

<svg viewBox="0 0 600 300" class="line-chart" role="img" aria-labelledby="line-title">
  <title id="line-title">月度趋势图</title>

  <defs>
    <linearGradient id="area-grad" x1="0%" y1="0%" x2="0%" y2="100%">
      <stop offset="0%" stop-color="#4f5bd5" stop-opacity="0.3" />
      <stop offset="100%" stop-color="#4f5bd5" stop-opacity="0" />
    </linearGradient>
  </defs>

  <style>
    .grid-line {
      stroke: #f0f0f0;
    }
    .axis-line {
      stroke: #ccc;
    }
    .label {
      font-family: sans-serif;
      font-size: 11px;
      fill: #666;
    }
    .data-point {
      fill: #fff;
      stroke: #4f5bd5;
      stroke-width: 2;
      cursor: pointer;
      transition: r 0.2s;
    }
    .data-point:hover {
      r: 6;
    }
  </style>

  <!-- 网格 -->
  <g class="grid-line">
    <line x1="60" y1="50" x2="560" y2="50" />
    <line x1="60" y1="120" x2="560" y2="120" />
    <line x1="60" y1="190" x2="560" y2="190" />
  </g>

  <!-- 坐标轴 -->
  <line class="axis-line" x1="60" y1="260" x2="560" y2="260" />
  <line class="axis-line" x1="60" y1="20" x2="60" y2="260" />

  <!-- Y 轴标签 -->
  <g class="label" text-anchor="end">
    <text x="55" y="264">0</text>
    <text x="55" y="194">30</text>
    <text x="55" y="124">60</text>
    <text x="55" y="54">90</text>
  </g>

  <!-- 数据 -->
  <!-- 月份 -->
  <g class="label" text-anchor="middle">
    <text x="100" y="280">1月</text>
    <text x="180" y="280">2月</text>
    <text x="260" y="280">3月</text>
    <text x="340" y="280">4月</text>
    <text x="420" y="280">5月</text>
    <text x="500" y="280">6月</text>
  </g>

  <!-- 折线 -->
  <path
    d="M 100 200 L 180 160 L 260 180 L 340 100 L 420 80 L 500 60"
    fill="none"
    stroke="#4f5bd5"
    stroke-width="2.5"
    stroke-linecap="round"
    stroke-linejoin="round"
    stroke-dasharray="800"
    stroke-dashoffset="800"
    id="line-path"
  >
    <animate attributeName="stroke-dashoffset" from="800" to="0" dur="2s" fill="freeze" />
  </path>

  <!-- 面积 -->
  <path
    d="M 100 200 L 180 160 L 260 180 L 340 100 L 420 80 L 500 60 L 500 260 L 100 260 Z"
    fill="url(#area-grad)"
    opacity="0"
  >
    <animate attributeName="opacity" from="0" to="1" dur="1s" begin="1.5s" fill="freeze" />
  </path>

  <!-- 数据点 -->
  <g id="data-points">
    <circle class="data-point" cx="100" cy="200" r="4" />
    <circle class="data-point" cx="180" cy="160" r="4" />
    <circle class="data-point" cx="260" cy="180" r="4" />
    <circle class="data-point" cx="340" cy="100" r="4" />
    <circle class="data-point" cx="420" cy="80" r="4" />
    <circle class="data-point" cx="500" cy="60" r="4" />
  </g>
</svg>

<script>
  const values = [25, 35, 30, 65, 75, 85];
  const points = document.querySelectorAll('.data-point');
  points.forEach((p, i) => {
    p.setAttribute('data-value', values[i] + '万');
    p.style.opacity = '0';
    p.style.transition = 'opacity 0.3s';
    setTimeout(
      () => {
        p.style.opacity = '1';
      },
      2000 + i * 100
    );
    p.addEventListener('click', () => {
      alert(`数值:${values[i]}万`);
    });
  });
</script>

7. 项目七:饼图

<svg viewBox="0 0 400 400" width="400" height="400">
  <defs>
    <filter id="pie-shadow">
      <feDropShadow dx="0" dy="4" stdDeviation="6" flood-opacity="0.15" />
    </filter>
  </defs>

  <style>
    .slice {
      cursor: pointer;
      transition: transform 0.2s;
      transform-origin: center;
      transform-box: fill-box;
    }
    .slice:hover {
      transform: scale(1.05);
    }
    .label {
      font-family: sans-serif;
      font-size: 14px;
      fill: #fff;
      font-weight: bold;
    }
  </style>

  <g id="pie" transform="translate(200, 200)" filter="url(#pie-shadow)">
    <!-- 由 JS 生成 -->
  </g>
</svg>

<script>
  const data = [
    { label: '产品 A', value: 35, color: '#4f5bd5' },
    { label: '产品 B', value: 25, color: '#00b894' },
    { label: '产品 C', value: 20, color: '#f9a825' },
    { label: '产品 D', value: 15, color: '#d63031' },
    { label: '其他', value: 5, color: '#8854d0' },
  ];

  const svgNS = 'http://www.w3.org/2000/svg';
  const pie = document.getElementById('pie');
  const radius = 120;
  const total = data.reduce((sum, d) => sum + d.value, 0);

  let cumAngle = -90; // 从 12 点钟方向开始

  data.forEach((d, i) => {
    const angle = (d.value / total) * 360;
    const startAngle = cumAngle;
    const endAngle = cumAngle + angle;

    const startRad = (startAngle * Math.PI) / 180;
    const endRad = (endAngle * Math.PI) / 180;

    const x1 = radius * Math.cos(startRad);
    const y1 = radius * Math.sin(startRad);
    const x2 = radius * Math.cos(endRad);
    const y2 = radius * Math.sin(endRad);

    const largeArc = angle > 180 ? 1 : 0;

    const path = document.createElementNS(svgNS, 'path');
    path.setAttribute('class', 'slice');
    path.setAttribute(
      'd',
      `M 0 0 L ${x1} ${y1} A ${radius} ${radius} 0 ${largeArc} 1 ${x2} ${y2} Z`
    );
    path.setAttribute('fill', d.color);
    path.setAttribute('data-label', d.label);
    path.setAttribute('data-value', d.value);

    path.addEventListener('click', () => {
      alert(`${d.label}: ${d.value}%`);
    });

    pie.appendChild(path);

    // 标签
    const midAngle = (startAngle + endAngle) / 2;
    const midRad = (midAngle * Math.PI) / 180;
    const labelX = radius * 0.6 * Math.cos(midRad);
    const labelY = radius * 0.6 * Math.sin(midRad);

    const label = document.createElementNS(svgNS, 'text');
    label.setAttribute('class', 'label');
    label.setAttribute('x', labelX);
    label.setAttribute('y', labelY);
    label.setAttribute('text-anchor', 'middle');
    label.setAttribute('dominant-baseline', 'middle');
    label.textContent = d.value + '%';
    pie.appendChild(label);

    cumAngle = endAngle;
  });
</script>

8. 总结

知识点应用项目
坐标系与 viewBox所有项目
path 与弧线饼图、环形进度
渐变与图案Logo、图表装饰
变换节点定位、悬停缩放
滤镜阴影、发光
动画进度条、加载动画、路径绘制
CSS 样式化主题色、悬停状态
JavaScript数据驱动、交互响应
可访问性title/desc、aria-label
性能节点复用、批量插入

9. 扩展资源

10. 附录:语法速查

环形进度条语法

stroke-dasharray 控制进度 <circle r="<半径>" stroke-dasharray="<周长>" stroke-dashoffset="<偏移>" transform="rotate(-90 <cx> <cy>)">

<svg viewBox="0 0 200 200" width="200" height="200">
  <defs>
    <linearGradient id="progress-grad" x1="0%" y1="0%" x2="100%" y2="100%">
      <stop offset="0%" stop-color="#4f5bd5" />
      <stop offset="100%" stop-color="#00b894" />
    </linearGradient>
  </defs>

  <!-- 背景圆 -->
  <circle cx="100" cy="100" r="80" fill="none" stroke="#e0e0e0" stroke-width="12" />

  <!-- 进度圆:rotate(-90) 让起点在 12 点钟方向 -->
  <circle
    id="progress-circle"
    cx="100"
    cy="100"
    r="80"
    fill="none"
    stroke="url(#progress-grad)"
    stroke-width="12"
    stroke-linecap="round"
    stroke-dasharray="502"
    stroke-dashoffset="502"
    transform="rotate(-90 100 100)"
  />

  <text x="100" y="100" text-anchor="middle" dominant-baseline="middle" font-size="36" font-weight="bold" fill="#333">0%</text>
</svg>

<script>
  const circle = document.getElementById('progress-circle');
  const circumference = 2 * Math.PI * 80; // 约 502

  function setProgress(percent) {
    const offset = circumference - (percent / 100) * circumference;
    circle.style.strokeDashoffset = offset;
  }
</script>

进度条相关属性表

属性说明示例
stroke-dasharray虚线长度(设为周长)502
stroke-dashoffset偏移量(控制进度)0~502
stroke-linecap线段端点样式round
transform旋转起点到 12 点rotate(-90 cx cy)

渐变描边语法

url() 引用渐变 <element stroke="url(#<gradient-id>)">

<defs>
  <linearGradient id="bar-grad" x1="0%" y1="0%" x2="0%" y2="100%">
    <stop offset="0%" stop-color="#5b6ee8" />
    <stop offset="100%" stop-color="#4f5bd5" />
  </linearGradient>
</defs>

<rect x="10" y="20" width="80" height="200" fill="url(#bar-grad)" rx="4" />

阴影滤镜语法

feDropShadow 投影 <filter id="<id>"><feDropShadow dx="..." dy="..." stdDeviation="..." flood-opacity="..." /></filter>

<defs>
  <filter id="bar-shadow">
    <feDropShadow dx="0" dy="2" stdDeviation="2" flood-opacity="0.1" />
  </filter>
</defs>

<rect x="10" y="20" width="80" height="200" fill="#4f5bd5" filter="url(#bar-shadow)" />

图表轴线语法

坐标轴绘制 <line x1="..." y1="..." x2="..." y2="..." stroke="..." />

<!-- Y 轴 -->
<line class="axis" x1="60" y1="40" x2="60" y2="320" stroke="#ccc" />
<!-- X 轴 -->
<line class="axis" x1="60" y1="320" x2="560" y2="320" stroke="#ccc" />

<!-- 网格线 -->
<g stroke="#f0f0f0" stroke-dasharray="4 4">
  <line x1="60" y1="80" x2="560" y2="80" />
  <line x1="60" y1="160" x2="560" y2="160" />
  <line x1="60" y1="240" x2="560" y2="240" />
</g>

<!-- Y 轴标签 -->
<g class="label" text-anchor="end" font-size="12" fill="#666">
  <text x="55" y="324">0</text>
  <text x="55" y="244">75</text>
  <text x="55" y="164">150</text>
</g>

JS 动态创建 SVG 元素

createElementNS 创建 document.createElementNS("<svg-ns>", "<tag>")

const svgNS = 'http://www.w3.org/2000/svg';
const bar = document.createElementNS(svgNS, 'rect');
bar.setAttribute('x', 100);
bar.setAttribute('y', 50);
bar.setAttribute('width', 80);
bar.setAttribute('height', 200);
bar.setAttribute('fill', 'url(#bar-grad)');
bar.setAttribute('rx', 4);
svg.appendChild(bar);

常用元素创建 API

API用途
createElementNS(svgNS, 'rect')创建矩形
createElementNS(svgNS, 'circle')创建圆形
createElementNS(svgNS, 'line')创建线段
createElementNS(svgNS, 'text')创建文本
createElementNS(svgNS, 'path')创建路径
createElementNS(svgNS, 'g')创建分组
setAttribute(name, value)设置属性
getAttribute(name)读取属性
appendChild(el)追加子元素

requestAnimationFrame 动画

JS 动画循环 requestAnimationFrame(<callback>)

const duration = 2000;
const target = 75;
const start = performance.now();
let current = 0;

function animate(now) {
  const elapsed = now - start;
  const t = Math.min(elapsed / duration, 1);
  // ease-out cubic 缓动
  const eased = 1 - Math.pow(1 - t, 3);
  current = Math.round(eased * target);
  setProgress(current);
  if (t < 1) requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

CSS transition 动画

transition 延迟入场 <selector> { transition: all <dur> <easing>; transition-delay: <delay>; }

.bar {
  transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
  transition-delay: 0.1s;
}
.value {
  opacity: 0;
  transition: opacity 0.4s;
  transition-delay: 0.5s;
}
// 触发动画
requestAnimationFrame(() => {
  bar.setAttribute('height', barHeight);
  value.style.opacity = '1';
});

path 动画语法

animate 动画 d 属性 <path d="..."><animate attributeName="d" values="..." dur="..." repeatCount="indefinite" /></path>

<path d="M 100 95 Q 200 105 300 95" fill="none" stroke="url(#logo-grad)" stroke-width="2" stroke-linecap="round">
  <animate
    attributeName="d"
    values="M 100 95 Q 200 105 300 95; M 100 95 Q 200 85 300 95; M 100 95 Q 200 105 300 95"
    dur="3s"
    repeatCount="indefinite"
  />
</path>

stop-color 动画

渐变色 stop 颜色变化 <stop offset="..." stop-color="..."><animate attributeName="stop-color" values="..." dur="..." repeatCount="indefinite" /></stop>

<defs>
  <linearGradient id="logo-grad" x1="0%" y1="0%" x2="100%" y2="100%">
    <stop offset="0%" stop-color="#4f5bd5">
      <animate
        attributeName="stop-color"
        values="#4f5bd5;#00b894;#4f5bd5"
        dur="6s"
        repeatCount="indefinite"
      />
    </stop>
    <stop offset="100%" stop-color="#00b894">
      <animate
        attributeName="stop-color"
        values="#00b894;#4f5bd5;#00b894"
        dur="6s"
        repeatCount="indefinite"
      />
    </stop>
  </linearGradient>
</defs>

tspan 文本拆分动画

tspan 逐字符动画 <text><tspan class="letter">F</tspan><tspan class="letter">A</tspan>...</text>

<style>
  .logo-text {
    font-family: 'Inter', sans-serif;
    font-size: 48px;
    font-weight: bold;
    fill: url(#logo-grad);
  }
  .logo-letter {
    transform-origin: center;
    transform-box: fill-box;
  }
  .logo-letter:nth-child(1) { animation: bounce 2s ease-in-out infinite; }
  .logo-letter:nth-child(2) { animation: bounce 2s ease-in-out infinite 0.1s; }
  .logo-letter:nth-child(3) { animation: bounce 2s ease-in-out infinite 0.2s; }
  @keyframes bounce {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-8px); }
  }
</style>

<text x="200" y="75" text-anchor="middle" class="logo-text">
  <tspan class="logo-letter">F</tspan>
  <tspan class="logo-letter">A</tspan>
  <tspan class="logo-letter">N</tspan>
  <tspan class="logo-letter">D</tspan>
  <tspan class="logo-letter">E</tspan>
  <tspan class="logo-letter">X</tspan>
</text>

tspan 属性表

属性说明示例
x绝对 x 坐标100
y绝对 y 坐标75
dx相对 x 偏移5
dy相对 y 偏移0
rotate旋转角度0
textLength文本长度200

radialGradient 节点语法

径向渐变节点 <radialGradient id="..."><stop offset="..." stop-color="..." /></radialGradient>

<defs>
  <radialGradient id="node-grad">
    <stop offset="0%" stop-color="#4f5bd5" />
    <stop offset="100%" stop-color="#3a47b8" />
  </radialGradient>
</defs>

<circle cx="200" cy="200" r="8" fill="url(#node-grad)" />

pattern 网格背景

pattern 平铺图案 <pattern id="..." width="..." height="..." patternUnits="userSpaceOnUse">...</pattern>

<defs>
  <pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
    <path d="M 40 0 L 0 0 0 40" fill="none" stroke="#f0f0f0" stroke-width="1" />
  </pattern>
</defs>

<rect width="800" height="600" fill="url(#grid)" />

pattern 属性表

属性说明默认值
width单元格宽度-
height单元格高度-
patternUnits单位坐标系objectBoundingBox
patternContentUnits内容单位userSpaceOnUse
patternTransform变换none
xx 偏移0
yy 偏移0

脉冲动画语法

节点脉冲环 <circle class="pulse-ring" r="..."><animate attributeName="r" values="..." dur="..." repeatCount="indefinite" /></circle>

<g class="node-group" transform="translate(200, 200)">
  <!-- 脉冲环 -->
  <circle class="pulse-ring" r="15" fill="none" stroke="#4f5bd5" stroke-width="2">
    <animate attributeName="r" values="15;25;15" dur="2s" repeatCount="indefinite" />
    <animate attributeName="opacity" values="1;0;1" dur="2s" repeatCount="indefinite" />
  </circle>
  <!-- 节点圆 -->
  <circle r="8" fill="url(#node-grad)" filter="url(#node-glow)" />
  <!-- 标签 -->
  <text y="-15" text-anchor="middle" font-size="12" fill="#333">北京</text>
</g>

begin 延迟动画

begin 延迟启动 <animate attributeName="..." values="..." dur="..." begin="<delay>" repeatCount="indefinite" />

<!-- 第二个节点延迟 0.5s 启动 -->
<animate attributeName="r" values="15;25;15" dur="2s" begin="0.5s" repeatCount="indefinite" />

<!-- 第三个节点延迟 1s 启动 -->
<animate attributeName="r" values="15;25;15" dur="2s" begin="1s" repeatCount="indefinite" />

begin 事件触发表

触发方式语法说明
时间2s2 秒后启动
点击click元素点击时启动
鼠标进入mouseover鼠标进入时启动
元素结束elemId.end指定动画结束时启动
元素开始elemId.begin指定动画开始时启动
事件+时间click+2s点击后延迟 2s 启动

连接线语法

节点间连线 <line x1="..." y1="..." x2="..." y2="..." stroke="..." stroke-width="..." opacity="..." />

<g class="connections">
  <line x1="200" y1="200" x2="400" y2="300" stroke="#4f5bd5" stroke-width="1.5" opacity="0.4" />
  <line x1="400" y1="300" x2="600" y2="200" stroke="#4f5bd5" stroke-width="1.5" opacity="0.4" />
</g>

hover 交互语法

CSS hover 缩放 <selector>:hover { transform: scale(<factor>); }

.node {
  cursor: pointer;
  transition: transform 0.2s;
  transform-origin: center;
  transform-box: fill-box;
}
.node:hover {
  transform: scale(1.3);
}
<circle class="node" r="8" fill="url(#node-grad)" />

折线图语法

polyline 折线 <polyline points="<x1>,<y1> <x2>,<y2> ..." fill="none" stroke="..." stroke-width="..." />

<svg viewBox="0 0 600 400">
  <polyline
    points="50,300 100,250 150,280 200,200 250,220 300,150 350,180 400,120"
    fill="none"
    stroke="#4f5bd5"
    stroke-width="3"
    stroke-linecap="round"
    stroke-linejoin="round"
  />
</svg>

path 折线 <path d="M <x1> <y1> L <x2> <y2> L <x3> <y3> ..." />

<path
  d="M 50 300 L 100 250 L 150 280 L 200 200 L 250 220 L 300 150"
  fill="none"
  stroke="#00b894"
  stroke-width="2"
/>

polyline / path 折线属性表

属性说明示例
points点坐标列表(polyline)50,300 100,250
d路径数据(path)M 50 300 L 100 250
fill填充(折线用 none)none
stroke描边颜色#4f5bd5
stroke-width描边宽度3

区域填充语法

path 填充区域 <path d="M <start> L <points> L <end> Z" fill="url(#<grad>)" opacity="..." />

<defs>
  <linearGradient id="area-grad" x1="0%" y1="0%" x2="0%" y2="100%">
    <stop offset="0%" stop-color="#4f5bd5" stop-opacity="0.4" />
    <stop offset="100%" stop-color="#4f5bd5" stop-opacity="0" />
  </linearGradient>
</defs>

<!-- 折线下方填充区域 -->
<path
  d="M 50 300 L 100 250 L 150 280 L 200 200 L 250 220 L 300 150 L 300 350 L 50 350 Z"
  fill="url(#area-grad)"
/>

数据点语法

数据点圆 <circle cx="<x>" cy="<y>" r="<radius>" fill="..." />

<g class="points">
  <circle cx="50" cy="300" r="4" fill="#4f5bd5" />
  <circle cx="100" cy="250" r="4" fill="#4f5bd5" />
  <circle cx="150" cy="280" r="4" fill="#4f5bd5" />
  <circle cx="200" cy="200" r="4" fill="#4f5bd5" />
</g>

饼图 path 语法

A 弧线绘制扇形 <path d="M <cx> <cy> L <x1> <y1> A <r> <r> 0 <large-arc> 1 <x2> <y2> Z" fill="..." />

<svg viewBox="0 0 200 200">
  <!-- 扇形 1:0° → 90°(large-arc=0) -->
  <path
    d="M 100 100 L 100 0 A 100 100 0 0 1 200 100 Z"
    fill="#4f5bd5"
  />
  <!-- 扇形 2:90° → 180°(large-arc=0) -->
  <path
    d="M 100 100 L 200 100 A 100 100 0 0 1 100 200 Z"
    fill="#00b894"
  />
  <!-- 扇形 3:180° → 270° -->
  <path
    d="M 100 100 L 100 200 A 100 100 0 0 1 0 100 Z"
    fill="#f9a825"
  />
  <!-- 扇形 4:270° → 360° -->
  <path
    d="M 100 100 L 0 100 A 100 100 0 0 1 100 0 Z"
    fill="#d63031"
  />
</svg>

A 弧线命令参数表

参数说明示例
rxx 轴半径100
ryy 轴半径100
x-axis-rotationx 轴旋转角度0
large-arc-flag大弧标志(0 小弧 / 1 大弧)0
sweep-flag扫描方向(0 逆时针 / 1 顺时针)1
x终点 x200
y终点 y100

饼图圆环语法

stroke-dasharray 绘制圆环段 <circle r="<r>" stroke="..." stroke-dasharray="<segment> <remaining>" stroke-dashoffset="<offset>" />

<svg viewBox="0 0 200 200">
  <!-- 圆环半径 80,周长 ≈ 502 -->
  <!-- 第一段 25%:dasharray=125.5 376.5,offset=0 -->
  <circle cx="100" cy="100" r="80" fill="none"
    stroke="#4f5bd5" stroke-width="30"
    stroke-dasharray="125.5 376.5"
    stroke-dashoffset="0"
    transform="rotate(-90 100 100)" />
  <!-- 第二段 25%:offset=-125.5 -->
  <circle cx="100" cy="100" r="80" fill="none"
    stroke="#00b894" stroke-width="30"
    stroke-dasharray="125.5 376.5"
    stroke-dashoffset="-125.5"
    transform="rotate(-90 100 100)" />
  <!-- 第三段 25%:offset=-251 -->
  <circle cx="100" cy="100" r="80" fill="none"
    stroke="#f9a825" stroke-width="30"
    stroke-dasharray="125.5 376.5"
    stroke-dashoffset="-251"
    transform="rotate(-90 100 100)" />
  <!-- 第四段 25%:offset=-376.5 -->
  <circle cx="100" cy="100" r="80" fill="none"
    stroke="#d63031" stroke-width="30"
    stroke-dasharray="125.5 376.5"
    stroke-dashoffset="-376.5"
    transform="rotate(-90 100 100)" />
</svg>

折线图入场动画

stroke-dasharray 绘制动画 <polyline stroke-dasharray="<length>" stroke-dashoffset="<length>"><animate attributeName="stroke-dashoffset" from="..." to="0" dur="..." fill="freeze" /></polyline>

<polyline
  points="50,300 100,250 150,280 200,200 250,220 300,150"
  fill="none"
  stroke="#4f5bd5"
  stroke-width="3"
  stroke-dasharray="600"
  stroke-dashoffset="600"
>
  <animate
    attributeName="stroke-dashoffset"
    from="600"
    to="0"
    dur="2s"
    fill="freeze"
  />
</polyline>

柱状图入场动画

rect 高度动画 <rect><animate attributeName="height" from="0" to="..." dur="..." fill="freeze" /><animate attributeName="y" from="..." to="..." dur="..." fill="freeze" /></rect>

<rect x="100" y="320" width="80" height="0" fill="url(#bar-grad)" rx="4">
  <animate attributeName="height" from="0" to="200" dur="0.8s" fill="freeze" />
  <animate attributeName="y" from="320" to="120" dur="0.8s" fill="freeze" />
</rect>

transform 平移语法

transform translate 平移 <g transform="translate(<x>, <y>)">...</g>

<g transform="translate(200, 200)">
  <circle r="8" fill="url(#node-grad)" />
  <text y="-15" text-anchor="middle">节点</text>
</g>

<g transform="translate(400, 300)">
  <circle r="8" fill="url(#node-grad)" />
  <text y="-15" text-anchor="middle">节点 2</text>
</g>

rotate 旋转语法

transform rotate 旋转 <element transform="rotate(<angle> [<cx> <cy>])">

<!-- 旋转进度条起点 -->
<circle
  cx="100" cy="100" r="80"
  transform="rotate(-90 100 100)"
  stroke-dasharray="502"
  stroke-dashoffset="502"
/>

scale 缩放语法

transform scale 缩放 <element transform="scale(<sx> [<sy>])">

<g transform="scale(1.5)">
  <circle cx="100" cy="100" r="50" fill="#4f5bd5" />
</g>

综合滤镜链语法

feGaussianBlur + feMerge 发光 <filter id="..."><feGaussianBlur stdDeviation="..." result="b" /><feMerge><feMergeNode in="b" /><feMergeNode in="SourceGraphic" /></feMerge></filter>

<defs>
  <filter id="glow">
    <feGaussianBlur stdDeviation="3" result="b" />
    <feMerge>
      <feMergeNode in="b" />
      <feMergeNode in="SourceGraphic" />
    </feMerge>
  </filter>
</defs>

<circle cx="100" cy="100" r="50" fill="#4f5bd5" filter="url(#glow)" />

事件处理语法

addEventListener 绑定 element.addEventListener('<event>', <callback>)

const bar = document.querySelector('.bar');
bar.addEventListener('click', (e) => {
  console.log('点击柱子');
});

bar.addEventListener('mouseenter', (e) => {
  bar.style.opacity = '0.8';
});

bar.addEventListener('mouseleave', (e) => {
  bar.style.opacity = '1';
});

常用事件表

事件触发时机
click点击
mouseenter鼠标进入
mouseleave鼠标离开
mouseover鼠标移过
mouseout鼠标移出
keydown键盘按下
focus获得焦点
blur失去焦点

DocumentFragment 批量插入

批量插入优化 const fragment = document.createDocumentFragment(); ... svg.appendChild(fragment);

const svgNS = 'http://www.w3.org/2000/svg';
const fragment = document.createDocumentFragment();

for (let i = 0; i < 100; i++) {
  const circle = document.createElementNS(svgNS, 'circle');
  circle.setAttribute('cx', Math.random() * 400);
  circle.setAttribute('cy', Math.random() * 300);
  circle.setAttribute('r', 3);
  circle.setAttribute('fill', '#4f5bd5');
  fragment.appendChild(circle);
}

svg.appendChild(fragment); // 一次性插入,减少重排

数据驱动渲染

数据 → SVG data.forEach(d => { const el = createElementNS(...); el.setAttribute(...); svg.appendChild(el); })

const data = [
  { label: 'Q1', value: 120 },
  { label: 'Q2', value: 165 },
  { label: 'Q3', value: 210 },
  { label: 'Q4', value: 180 },
];

const svgNS = 'http://www.w3.org/2000/svg';
const maxValue = 250;
const chartHeight = 280;

data.forEach((d, i) => {
  const barHeight = (d.value / maxValue) * chartHeight;
  const bar = document.createElementNS(svgNS, 'rect');
  bar.setAttribute('x', 100 + i * 120);
  bar.setAttribute('y', 320 - barHeight);
  bar.setAttribute('width', 80);
  bar.setAttribute('height', barHeight);
  bar.setAttribute('fill', 'url(#bar-grad)');
  svg.appendChild(bar);
});

图例语法

图例项 <g class="legend"><rect /><text>...</text></g>

<g class="legend" transform="translate(450, 50)">
  <rect x="0" y="0" width="12" height="12" fill="#4f5bd5" />
  <text x="20" y="10" font-size="12" fill="#333">销售额</text>

  <rect x="0" y="20" width="12" height="12" fill="#00b894" />
  <text x="20" y="30" font-size="12" fill="#333">利润</text>

  <rect x="0" y="40" width="12" height="12" fill="#f9a825" />
  <text x="20" y="50" font-size="12" fill="#333">成本</text>
</g>

标题与描述语法

图表 title 与 desc <svg><title>...</title><desc>...</desc>...</svg>

<svg viewBox="0 0 600 400" role="img" aria-labelledby="chart-title chart-desc">
  <title id="chart-title">2024 季度销售额</title>
  <desc id="chart-desc">柱状图展示 Q1 至 Q4 销售额,单位:万元</desc>
  <!-- 图表内容 -->
</svg>

path 命令综合参考

M/L/H/V 直线 <path d="M <x> <y> L <x> <y> H <x> V <y>" />

<path d="M 10 10 L 50 10 H 90 V 50 L 50 50 Z" fill="none" stroke="#333" />

C/S/Q/T/Bézier 曲线 <path d="M <x> <y> C <cx1> <cy1> <cx2> <cy2> <x> <y> S <cx2> <cy2> <x> <y> Q <cx> <cy> <x> <y> T <x> <y>" />

<!-- 三次贝塞尔 -->
<path d="M 10 50 C 30 10, 70 10, 90 50" fill="none" stroke="#333" />
<!-- 二次贝塞尔 -->
<path d="M 10 50 Q 50 10, 90 50" fill="none" stroke="#333" />

A 弧线 <path d="M <x1> <y1> A <rx> <ry> <rotation> <large-arc> <sweep> <x2> <y2>" />

<path d="M 10 50 A 40 40 0 0 1 90 50" fill="none" stroke="#333" />

path 命令总表

命令含义参数
M移动到(绝对)x y
m移动到(相对)x y
L直线到(绝对)x y
l直线到(相对)x y
H水平线到x
V垂直线到y
C三次贝塞尔cx1 cy1 cx2 cy2 x y
S平滑三次贝塞尔cx2 cy2 x y
Q二次贝塞尔cx cy x y
T平滑二次贝塞尔x y
A弧线rx ry rot large-arc sweep x y
Z / z闭合路径(无参数)