SVG数据可视化组件基础教程10:人体成分可视化

140 阅读4分钟

10.gif

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

一、目标

通过HTML、CSS和JavaScript创建一个交互式的人体成分可视化工具,实现以下功能:

  1. 使用SVG绘制人体轮廓并填充不同成分。
  2. 通过滑块动态调整各成分比例。
  3. 显示各成分的图例和提示信息。

二、所需工具与准备

  1. 工具

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

    • 确保你对HTML、CSS和JavaScript有一定的了解。
    • 确保你对SVG的基本语法有一定了解。

三、代码分析与操作步骤

1. 创建HTML结构

创建一个HTML文件(如Lesson10.html)并设置基本的HTML结构:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>人体成分可视化</title>
  <style>
    /* 样式部分 */
  </style>
</head>
<body>
  <h1>人体成分组成可视化</h1>
  <div class="container">
    <div id="body-visualization-container">
      <svg id="body-visualization" width="250" height="500"></svg>
      <div id="tooltip"></div>
    </div>
    <div class="controls">
      <h2>调整成分比例</h2>
      <div id="sliders"></div>
      <ul id="legend"></ul>
    </div>
  </div>
  <script>
    // JavaScript部分
  </script>
</body>
</html>

2. 添加CSS样式

<style>标签中,添加以下CSS样式:

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  min-height: 100vh;
  background-color: #f4f7f9;
  margin: 0;
}
.container {
  display: flex;
  align-items: flex-start;
  gap: 50px;
  margin-top: 20px;
}
#body-visualization-container {
  position: relative;
  width: 250px;
  height: 500px;
}
#tooltip {
  position: absolute;
  background-color: rgba(0, 0, 0, 0.75);
  color: white;
  padding: 8px 12px;
  border-radius: 4px;
  font-size: 14px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s, transform 0.2s;
  transform: translate(-50%, -120%);
  white-space: nowrap;
}
.controls {
  width: 300px;
  padding: 20px;
  background-color: #fff;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.control-group label {
  display: block;
  margin-bottom: 8px;
  color: #555;
  font-size: 14px;
}
.control-group input[type="range"] {
  width: 100%;
  cursor: pointer;
}
.control-group span {
  float: right;
  font-weight: bold;
  color: #333;
}
#legend {
  margin-top: 20px;
  list-style: none;
  padding: 0;
}
.legend-color {
  width: 18px;
  height: 18px;
  margin-right: 10px;
  border-radius: 4px;
  border: 1px solid rgba(0,0,0,0.1);
}
  • body:设置页面字体、布局和背景颜色。
  • .container:设置容器的布局和间距。
  • #body-visualization-container:设置人体可视化容器的宽度和高度。
  • #tooltip:设置提示框的样式。
  • .controls:设置控制面板的样式。
  • .control-group:设置滑块和标签的样式。
  • #legend:设置图例的样式。

3. 编写JavaScript代码

<script>标签中,添加以下JavaScript代码来实现人体成分可视化和交互功能:

const svg = document.getElementById('body-visualization');
const slidersContainer = document.getElementById('sliders');
const legendContainer = document.getElementById('legend');
const tooltip = document.getElementById('tooltip');
const svgContainer = document.getElementById('body-visualization-container');

const svgWidth = 250;
const svgHeight = 500;

// 默认的人体成分数据
let bodyData = {
  '水': { percentage: 70, color: '#3498db' },
  '蛋白质': { percentage: 15, color: '#e74c3c' },
  '脂肪': { percentage: 10, color: '#f1c40f' },
  '无机盐': { percentage: 5, color: '#95a5a6' }
};

// 人体轮廓SVG路径
const bodyPath = "M234.9,228c-7.3-1.5-14-18.9-16-26.6-1.5-7.7-5.8-27.6-18.9-44-4.8-6.3,0-10.6-8.7-34.8-7.3-19.8-5.3-37.2-19.8-42.6-12.1-4.4-26.1-2.9-31-21.3-1-3.4,0-4.8,1-8.7,2.9-5.8.5-9.7,2.4-8.7s9.2-12.6,2.4-12.6,3.4-8.2-5.3-17.9c-4.4-4.8-8.2-6.3-14.5-5.8-6.3,0-10.6,1-14.5,5.8-8.7,10.2-3.9,28.5-8.7,34.8-12.6,16.9-1,17.9-5.3,17.9-7.3,0,0,13.5,2.4,12.6,1.5-1,1,0,3.4,2.4,8.7,1.5,3.9,1.9,5.3,1,8.7-5.3,17.9-19.3,16.9-31,21.3-14.5,5.3-12.6,22.2-19.8,42.6-8.7,24.2-3.9,28.5-8.7,34.8-12.6,16.9-16.9,36.3-18.9,44-1.5,7.7-8.7,25.6-16,26.6-5.8,1-17.9,16.9-15,18.9,4.8,3.4,7.7-7.7,9.2-3.9,1,3.9-12.6,20.3-8.2,24.2,4.4,3.9,11.1-17.4,11.6-13.1,0,4.4-7.7,18.9-2.9,20.3,4.8,1,5.8-19.8,7.7-17.9,1.5,1.5-2.9,21.8,1.5,20.8s1.5-20.8,4.8-19.8c3.4,1,1,15.5,3.9,15.5s.5-11.1,2.4-14c1.5-3.4,3.9-17.4,4.8-22.7.5-4.8,0-10.2,8.7-23.2,9.7-13.1,21.8-31,24.7-40.6,1.5-5.3,0-9.7,3.9-15.5,3.4-4.4,5.8-9.7,7.3-12.6,0-1,1.5-1,2.4,0,3.9,4.8,12.6,22.2,1.5,69.6-13.5,59-5.8,88.5-5.3,98.7,0,8.2,3.4,19.3-1,40.6-1,5.3-4.4,13.1-5.3,19.8-3.4,21.8.5,84.1-1.5,100.1,0,2.9-5.3,4.8-8.2,11.6-1.5,3.9,4.8,8.2,7.7,8.7,5.8,0,17.4,3.4,18.9-10.6.5-5.8.5-8.2,0-9.7-1-2.4-1.5-4.8-1.5-7.3,0-16.4,0-42.6,11.6-62.4,9.7-17.4,0-32.9,2.4-38.2,1.9-5.3,6.3-9.7,8.7-32.9,2.4-20.8,12.6-37.7,13.1-69.6h0c1-1.5,1.9-2.9,2.9-2.9s2.4,1,2.9,2.9h0c0,31.9,10.6,48.8,13.1,69.6,2.9,23.2,7.3,27.6,8.7,32.9,1.9,5.3-7.7,20.8,2.4,38.2,11.6,19.8,11.6,46.4,11.6,62.4s-.5,4.8-1.5,7.3c-.5,1-1.9-.5,2.9,0,1,2.9,3.9,8.2,7.3,12.6,4.4,5.8,2.4,10.2,3.9,15.5,2.9,9.7,15,27.6,24.7,40.6,9.7,13.1,8.2,17.9,8.7,23.2.5,4.8,3.4,19.3,4.8,22.7,1.5,3.4-1,18.9,2.4,14s.5-14.5,3.9-15.5c3.4-.5.5,18.9,4.8,19.8,4.4,1,0-18.9,1.5-20.8s3.4,19.3,7.7,17.9c4.8-1-3.4-16-2.9-20.3,0-4.4,7.3,17.4,11.6,13.1,4.4-4.4-9.7-20.8-8.2-24.2,1-3.9,3.9,7.3,9.2,3.9,0-1.9-12.6-17.9-17.9-18.9h-1.9Z";

function createVisualization() {
  svg.innerHTML = '';
  const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
  const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
  clipPath.setAttribute('id', 'body-clip');
  const pathForClip = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  pathForClip.setAttribute('d', bodyPath);
  clipPath.appendChild(pathForClip);
  defs.appendChild(clipPath);
  svg.appendChild(defs);

  const fillGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
  fillGroup.setAttribute('clip-path', 'url(#body-clip)');
  svg.appendChild(fillGroup);

  const outline = document.createElementNS('http://www.w3.org/2000/svg', 'path');
  outline.setAttribute('d', bodyPath);
  outline.setAttribute('fill', 'none');
  outline.setAttribute('stroke', '#333');
  outline.setAttribute('stroke-width', '3');
  svg.appendChild(outline);

  let accumulatedHeight = 0;
  const totalPercentage = Object.values(bodyData).reduce((sum, item) => sum + item.percentage, 0);

  for (const [name, data] of Object.entries(bodyData)) {
    if (data.percentage === 0) continue;

    const height = (data.percentage / totalPercentage) * svgHeight;
    const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
    rect.setAttribute('x', 0);
    rect.setAttribute('y', svgHeight - accumulatedHeight - height);
    rect.setAttribute('width', svgWidth);
    rect.setAttribute('height', height);
    rect.setAttribute('fill', data.color);
    rect.dataset.name = name;
    rect.dataset.percentage = data.percentage;

    fillGroup.appendChild(rect);
    accumulatedHeight += height;

    rect.addEventListener('mousemove', (e) => {
      tooltip.style.opacity = '1';
      const rectBox = rect.getBoundingClientRect();
      const containerBox = svgContainer.getBoundingClientRect();
      tooltip.style.left = `${e.clientX - containerBox.left}px`;
      tooltip.style.top = `${e.clientY - containerBox.top}px`;
      tooltip.innerHTML = `${name}: ${data.percentage.toFixed(1)}%`;
    });

    rect.addEventListener('mouseleave', () => {
      tooltip.style.opacity = '0';
    });
  }
}

function createControls() {
  slidersContainer.innerHTML = '';
  legendContainer.innerHTML = '';

  for (const [name, data] of Object.entries(bodyData)) {
    const group = document.createElement('div');
    group.className = 'control-group';
    const label = document.createElement('label');
    label.setAttribute('for', `${name}-slider`);
    const valueSpan = document.createElement('span');
    valueSpan.id = `${name}-value`;
    valueSpan.textContent = `${data.percentage}%`;
    label.textContent = name;
    label.appendChild(valueSpan);
    const slider = document.createElement('input');
    slider.type = 'range';
    slider.id = `${name}-slider`;
    slider.min = 0;
    slider.max = 100;
    slider.value = data.percentage;
    slider.step = 0.1;
    slider.dataset.name = name;
    slider.addEventListener('input', handleSliderInput);

    group.appendChild(label);
    group.appendChild(slider);
    slidersContainer.appendChild(group);

    const li = document.createElement('li');
    const colorBox = document.createElement('div');
    colorBox.className = 'legend-color';
    colorBox.style.backgroundColor = data.color;
    li.appendChild(colorBox);
    const legendText = document.createElement('span');
    legendText.textContent = name;
    li.appendChild(legendText);
    legendContainer.appendChild(li);
  }
}

function handleSliderInput(e) {
  const currentName = e.target.dataset.name;
  const newValue = parseFloat(e.target.value);
  const oldValue = bodyData[currentName].percentage;
  const diff = newValue - oldValue;

  bodyData[currentName].percentage = newValue;
  const othersToAdjust = Object.keys(bodyData).filter(key => key !== currentName);
  let totalOfOthers = othersToAdjust.reduce((sum, key) => sum + bodyData[key].percentage, 0);

  if (totalOfOthers > 0) {
    for (const key of othersToAdjust) {
      const proportion = bodyData[key].percentage / totalOfOthers;
      bodyData[key].percentage -= diff * proportion;
    }
  }

  normalizeData();
  updateControls();
  createVisualization();
}

function updateControls() {
  for (const name of Object.keys(bodyData)) {
    const slider = document.getElementById(`${name}-slider`);
    const valueSpan = document.getElementById(`${name}-value`);
    if (slider) slider.value = bodyData[name].percentage;
    if (valueSpan) valueSpan.textContent = `${bodyData[name].percentage.toFixed(1)}%`;
  }
}

function normalizeData() {
  for (const key in bodyData) {
    if (bodyData[key].percentage < 0) {
      bodyData[key].percentage = 0;
    }
  }
  const total = Object.values(bodyData).reduce((sum, item) => sum + item.percentage, 0);
  if (total === 0) return;
  for (const key in bodyData) {
    bodyData[key].percentage = (bodyData[key].percentage / total) * 100;
  }
}

normalizeData();
createVisualization();
createControls();
updateControls();

4. 关键参数说明

  • bodyData:存储人体成分数据的对象,包含成分名称、比例和颜色。
  • bodyPath:SVG路径数据,定义了人体轮廓。
  • svgWidthsvgHeight:SVG画布的宽度和高度。
  • createVisualization() :负责创建或更新SVG可视化,包括绘制人体轮廓和填充不同成分。
  • createControls() :创建滑块和图例,用于调整和显示各成分比例。
  • handleSliderInput() :处理滑块输入事件,动态调整各成分比例并保持总和为100%。
  • updateControls() :更新所有滑块和数值显示。
  • normalizeData() :标准化数据,确保各成分比例总和为100%。

5. 完整代码

将上述代码整合到一个HTML文件中,完整的代码如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>人体成分可视化</title>
  <style>
    body {
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      min-height: 100vh;
      background-color: #f4f7f9;
      margin: 0;
    }
    h1 {
      color: #333;
      font-weight: 300;
    }
    .container {
      display: flex;
      align-items: flex-start;
      gap: 50px;
      margin-top: 20px;
    }
    #body-visualization-container {
      position: relative;
      width: 250px;
      height: 500px;
    }
    #tooltip {
      position: absolute;
      background-color: rgba(0, 0, 0, 0.75);
      color: white;
      padding: 8px 12px;
      border-radius: 4px;
      font-size: 14px;
      pointer-events: none;
      opacity: 0;
      transition: opacity 0.2s, transform 0.2s;
      transform: translate(-50%, -120%);
      white-space: nowrap;
    }
    .controls {
      width: 300px;
      padding: 20px;
      background-color: #fff;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.1);
    }
    .controls h2 {
      margin-top: 0;
      font-size: 18px;
      font-weight: 500;
      color: #333;
      border-bottom: 1px solid #eee;
      padding-bottom: 10px;
      margin-bottom: 20px;
    }
    .control-group {
      margin-bottom: 20px;
    }
    .control-group label {
      display: block;
      margin-bottom: 8px;
      color: #555;
      font-size: 14px;
    }
    .control-group input[type="range"] {
      width: 100%;
      cursor: pointer;
    }
    .control-group span {
      float: right;
      font-weight: bold;
      color: #333;
    }
    #legend {
      margin-top: 20px;
      list-style: none;
      padding: 0;
    }
    #legend li {
      display: flex;
      align-items: center;
      margin-bottom: 8px;
      font-size: 14px;
    }
    .legend-color {
      width: 18px;
      height: 18px;
      margin-right: 10px;
      border-radius: 4px;
      border: 1px solid rgba(0,0,0,0.1);
    }
  </style>
</head>
<body>
  <h1>人体成分组成可视化</h1>
  <div class="container">
    <div id="body-visualization-container">
      <svg id="body-visualization" width="250" height="500"></svg>
      <div id="tooltip"></div>
    </div>
    <div class="controls">
      <h2>调整成分比例</h2>
      <div id="sliders"></div>
      <ul id="legend"></ul>
    </div>
  </div>
  <script>
    const svg = document.getElementById('body-visualization');
    const slidersContainer = document.getElementById('sliders');
    const legendContainer = document.getElementById('legend');
    const tooltip = document.getElementById('tooltip');
    const svgContainer = document.getElementById('body-visualization-container');

    const svgWidth = 250;
    const svgHeight = 500;

    let bodyData = {
      '水': { percentage: 70, color: '#3498db' },
      '蛋白质': { percentage: 15, color: '#e74c3c' },
      '脂肪': { percentage: 10, color: '#f1c40f' },
      '无机盐': { percentage: 5, color: '#95a5a6' }
    };

    const bodyPath = "M234.9,228c-7.3-1.5-14-18.9-16-26.6-1.5-7.7-5.8-27.6-18.9-44-4.8-6.3,0-10.6-8.7-34.8-7.3-19.8-5.3-37.2-19.8-42.6-12.1-4.4-26.1-2.9-31-21.3-1-3.4,0-4.8,1-8.7,2.9-5.8.5-9.7,2.4-8.7s9.2-12.6,2.4-12.6,3.4-8.2-5.3-17.9c-4.4-4.8-8.2-6.3-14.5-5.8-6.3,0-10.6,1-14.5,5.8-8.7,10.2-3.9,28.5-8.7,34.8-12.6,16.9-1,17.9-5.3,17.9-7.3,0,0,13.5,2.4,12.6,1.5-1,1,0,3.4,2.4,8.7,1.5,3.9,1.9,5.3,1,8.7-5.3,17.9-19.3,16.9-31,21.3-14.5,5.3-12.6,22.2-19.8,42.6-8.7,24.2-3.9,28.5-8.7,34.8-12.6,16.9-16.9,36.3-18.9,44-1.5,7.7-8.7,25.6-16,26.6-5.8,1-17.9,16.9-15,18.9,4.8,3.4,7.7-7.7,9.2-3.9,1,3.9-12.6,20.3-8.2,24.2,4.4,3.9,11.1-17.4,11.6-13.1,0,4.4-7.7,18.9-2.9,20.3,4.8,1,5.8-19.8,7.7-17.9,1.5,1.5-2.9,21.8,1.5,20.8s1.5-20.8,4.8-19.8c3.4,1,1,15.5,3.9,15.5s.5-11.1,2.4-14c1.5-3.4,3.9-17.4,4.8-22.7.5-4.8,0-10.2,8.7-23.2,9.7-13.1,21.8-31,24.7-40.6,1.5-5.3,0-9.7,3.9-15.5,3.4-4.4,5.8-9.7,7.3-12.6,0-1,1.5-1,2.4,0,3.9,4.8,12.6,22.2,1.5,69.6-13.5,59-5.8,88.5-5.3,98.7,0,8.2,3.4,19.3-1,40.6-1,5.3-4.4,13.1-5.3,19.8-3.4,21.8.5,84.1-1.5,100.1,0,2.9-5.3,4.8-8.2,11.6-1.5,3.9,4.8,8.2,7.7,8.7,5.8,0,17.4,3.4,18.9-10.6.5-5.8.5-8.2,0-9.7-1-2.4-1.5-4.8-1.5-7.3,0-16.4,0-42.6,11.6-62.4,9.7-17.4,0-32.9,2.4-38.2,1.9-5.3,6.3-9.7,8.7-32.9,2.4-20.8,12.6-37.7,13.1-69.6h0c1-1.5,1.9-2.9,2.9-2.9s2.4,1,2.9,2.9h0c0,31.9,10.6,48.8,13.1,69.6,2.9,23.2,7.3,27.6,8.7,32.9,1.9,5.3-7.7,20.8,2.4,38.2,11.6,19.8,11.6,46.4,11.6,62.4s-.5,4.8-1.5,7.3c-.5,1-1.9-.5,2.9,0,1,2.9,3.9,8.2,7.3,12.6,4.4,5.8,2.4,10.2,3.9,15.5,2.9,9.7,15,27.6,24.7,40.6,9.7,13.1,8.2,17.9,8.7,23.2.5,4.8,3.4,19.3,4.8,22.7,1.5,3.4-1,18.9,2.4,14s.5-14.5,3.9-15.5c3.4-.5.5,18.9,4.8,19.8,4.4,1,0-18.9,1.5-20.8s3.4,19.3,7.7,17.9c4.8-1-3.4-16-2.9-20.3,0-4.4,7.3,17.4,11.6,13.1,4.4-4.4-9.7-20.8-8.2-24.2,1-3.9,3.9,7.3,9.2,3.9,0-1.9-12.6-17.9-17.9-18.9h-1.9Z";

    function createVisualization() {
      svg.innerHTML = '';
      const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
      const clipPath = document.createElementNS('http://www.w3.org/2000/svg', 'clipPath');
      clipPath.setAttribute('id', 'body-clip');
      const pathForClip = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      pathForClip.setAttribute('d', bodyPath);
      clipPath.appendChild(pathForClip);
      defs.appendChild(clipPath);
      svg.appendChild(defs);

      const fillGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
      fillGroup.setAttribute('clip-path', 'url(#body-clip)');
      svg.appendChild(fillGroup);

      const outline = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      outline.setAttribute('d', bodyPath);
      outline.setAttribute('fill', 'none');
      outline.setAttribute('stroke', '#333');
      outline.setAttribute('stroke-width', '3');
      svg.appendChild(outline);
      
      let accumulatedHeight = 0;
      const totalPercentage = Object.values(bodyData).reduce((sum, item) => sum + item.percentage, 0);

      for (const [name, data] of Object.entries(bodyData)) {
        if(data.percentage === 0) continue;

        const height = (data.percentage / totalPercentage) * svgHeight;
        
        const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
        rect.setAttribute('x', 0);
        rect.setAttribute('y', svgHeight - accumulatedHeight - height);
        rect.setAttribute('width', svgWidth);
        rect.setAttribute('height', height);
        rect.setAttribute('fill', data.color);
        rect.dataset.name = name;
        rect.dataset.percentage = data.percentage;

        fillGroup.appendChild(rect);
        
        accumulatedHeight += height;

        rect.addEventListener('mousemove', (e) => {
            tooltip.style.opacity = '1';
            const rectBox = rect.getBoundingClientRect();
            const containerBox = svgContainer.getBoundingClientRect();
            tooltip.style.left = `${e.clientX - containerBox.left}px`;
            tooltip.style.top = `${e.clientY - containerBox.top}px`;
            tooltip.innerHTML = `${name}: ${parseFloat(data.percentage).toFixed(1)}%`;
        });

        rect.addEventListener('mouseleave', () => {
            tooltip.style.opacity = '0';
        });
      }
    }

    function createControls() {
      slidersContainer.innerHTML = '';
      legendContainer.innerHTML = '';

      for (const [name, data] of Object.entries(bodyData)) {
        const group = document.createElement('div');
        group.className = 'control-group';
        
        const label = document.createElement('label');
        label.setAttribute('for', `${name}-slider`);
        const valueSpan = document.createElement('span');
        valueSpan.id = `${name}-value`;
        valueSpan.textContent = `${data.percentage}%`;
        label.textContent = name;
        label.appendChild(valueSpan);

        const slider = document.createElement('input');
        slider.type = 'range';
        slider.id = `${name}-slider`;
        slider.min = 0;
        slider.max = 100;
        slider.value = data.percentage;
        slider.step = 0.1;
        slider.dataset.name = name;

        slider.addEventListener('input', handleSliderInput);

        group.appendChild(label);
        group.appendChild(slider);
        slidersContainer.appendChild(group);

        const li = document.createElement('li');
        const colorBox = document.createElement('div');
        colorBox.className = 'legend-color';
        colorBox.style.backgroundColor = data.color;
        li.appendChild(colorBox);
        const legendText = document.createElement('span');
        legendText.textContent = name;
        li.appendChild(legendText);
        legendContainer.appendChild(li);
      }
    }

    function handleSliderInput(e) {
      const currentName = e.target.dataset.name;
      const newValue = parseFloat(e.target.value);
      const oldValue = bodyData[currentName].percentage;
      const diff = newValue - oldValue;

      bodyData[currentName].percentage = newValue;
      
      const othersToAdjust = Object.keys(bodyData).filter(key => key !== currentName);
      let totalOfOthers = othersToAdjust.reduce((sum, key) => sum + bodyData[key].percentage, 0);

      if (totalOfOthers > 0) {
           for (const key of othersToAdjust) {
            const proportion = bodyData[key].percentage / totalOfOthers;
            bodyData[key].percentage -= diff * proportion;
           }
      }

      normalizeData();
      updateControls();
      createVisualization();
    }

    function updateControls() {
      for (const name of Object.keys(bodyData)) {
        const slider = document.getElementById(`${name}-slider`);
        const valueSpan = document.getElementById(`${name}-value`);
        if(slider) slider.value = bodyData[name].percentage;
        if(valueSpan) valueSpan.textContent = `${bodyData[name].percentage.toFixed(1)}%`;
      }
    }
    
    function normalizeData() {
      for(const key in bodyData) {
        if (bodyData[key].percentage < 0) {
          bodyData[key].percentage = 0;
        }
      }

      const total = Object.values(bodyData).reduce((sum, item) => sum + item.percentage, 0);
      if (total === 0) return;

      for (const key in bodyData) {
        bodyData[key].percentage = (bodyData[key].percentage / total) * 100;
      }
    }

    normalizeData();
    createVisualization();
    createControls();
    updateControls();
  </script>
</body>
</html>

四、总结

通过以上步骤,你可以创建一个交互式的人体成分可视化工具。这个工具使用SVG绘制人体轮廓并填充不同成分,通过滑块动态调整各成分比例,并显示各成分的图例和提示信息。你可以通过调整代码中的参数来改变人体图标的外观和行为。希望这个教程对你有所帮助!

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

封面1.png