我是设计师邱兴,一个学习前端的设计师,今天给大家制作一个用SVG实现的金字塔可视化组件,SVG相较于Echart来说制作简单,但是效果可以非常丰富。
SVG金字塔可视化组件制作教程
一、目标
通过HTML、CSS和JavaScript创建一个交互式的SVG金字塔可视化组件,实现以下功能:
- 使用SVG绘制金字塔结构。
- 通过滑块动态调整各层比例。
- 显示各层的图例和提示信息。
二、所需工具与准备
- 工具:
- 一个文本编辑器(如Notepad++、VS Code等)。
- 浏览器(用于预览效果)。
- 基础准备:
- 确保你对HTML、CSS和JavaScript有一定的了解。
- 确保你对SVG的基本语法有一定了解。
三、代码分析与操作步骤
1. 创建HTML结构
创建一个HTML文件(如Lesson11.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 class="pyramid-svg-container">
<svg id="pyramid-svg" viewBox="0 0 320 400"></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;
background: #f4f7f9;
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
h1 {
color: #333;
font-weight: 300;
}
.container {
display: flex;
gap: 40px;
margin-top: 20px;
}
.pyramid-svg-container {
position: relative;
width: 320px;
height: 400px;
}
#pyramid-svg {
width: 320px;
height: 400px;
display: block;
}
#tooltip {
position: absolute;
background: rgba(0,0,0,0.8);
color: #fff;
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;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
padding: 20px;
}
.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-color {
width: 18px;
height: 18px;
margin-right: 10px;
border-radius: 4px;
border: 1px solid rgba(0,0,0,0.1);
}
body:设置页面字体、背景颜色和布局。.container:设置容器的布局和间距。.pyramid-svg-container:设置SVG容器的宽度和高度。#tooltip:设置提示框的样式。.controls:设置控制面板的样式。.control-group:设置滑块和标签的样式。#legend:设置图例的样式。
3. 编写JavaScript代码
在<script>标签中,添加以下JavaScript代码来实现金字塔可视化和交互功能:
let pyramidData = [
{ name: '第1层', value: 40, color: '#f1c40f' },
{ name: '第2层', value: 30, color: '#e67e22' },
{ name: '第3层', value: 20, color: '#3498db' },
{ name: '第4层', value: 10, color: '#9b59b6' }
];
const svg = document.getElementById('pyramid-svg');
const slidersContainer = document.getElementById('sliders');
const legendContainer = document.getElementById('legend');
const tooltip = document.getElementById('tooltip');
const svgContainer = document.querySelector('.pyramid-svg-container');
const svgWidth = 320;
const svgHeight = 400;
const pyramidBaseWidth = 260;
const pyramidTopWidth = 40;
const pyramidHeight = 340;
const pyramidX = (svgWidth - pyramidBaseWidth) / 2;
const pyramidY = 40;
function drawPyramid() {
svg.innerHTML = '';
let total = pyramidData.reduce((sum, d) => sum + d.value, 0);
let y = pyramidY;
let prevWidth = pyramidTopWidth;
let prevX = (svgWidth - pyramidTopWidth) / 2;
let topLeft = null;
let topRight = null;
let bottomLeft = null;
let bottomRight = null;
for (let i = 0; i < pyramidData.length; i++) {
let d = pyramidData[i];
let h = d.value / total * pyramidHeight;
let nextWidth = pyramidTopWidth + (pyramidBaseWidth - pyramidTopWidth) * ((y + h - pyramidY) / pyramidHeight);
let nextX = (svgWidth - nextWidth) / 2;
let path = `M${prevX},${y} L${prevX + prevWidth},${y} L${nextX + nextWidth},${y + h} L${nextX},${y + h} Z`;
let layer = document.createElementNS('http://www.w3.org/2000/svg', 'path');
layer.setAttribute('d', path);
layer.setAttribute('fill', d.color);
layer.setAttribute('stroke', '#fff');
layer.setAttribute('stroke-width', 2);
layer.dataset.index = i;
svg.appendChild(layer);
layer.addEventListener('mousemove', (e) => {
tooltip.style.opacity = '1';
const containerBox = svgContainer.getBoundingClientRect();
tooltip.style.left = `${e.clientX - containerBox.left}px`;
tooltip.style.top = `${e.clientY - containerBox.top}px`;
tooltip.innerHTML = `${d.name}: ${d.value.toFixed(1)}%`;
});
layer.addEventListener('mouseleave', () => {
tooltip.style.opacity = '0';
});
if (i === 0) {
topLeft = [prevX, y];
topRight = [prevX + prevWidth, y];
}
if (i === pyramidData.length - 1) {
bottomLeft = [nextX, y + h];
bottomRight = [nextX + nextWidth, y + h];
}
y += h;
prevWidth = nextWidth;
prevX = nextX;
}
if (topLeft && topRight && bottomLeft && bottomRight) {
let outline = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
outline.setAttribute('points', `
${topLeft[0]},${topLeft[1]}
${topRight[0]},${topRight[1]}
${bottomRight[0]},${bottomRight[1]}
${bottomLeft[0]},${bottomLeft[1]}
`);
outline.setAttribute('fill', 'none');
outline.setAttribute('stroke', '#333');
outline.setAttribute('stroke-width', 3);
svg.appendChild(outline);
}
}
function createControls() {
slidersContainer.innerHTML = '';
legendContainer.innerHTML = '';
for (let i = 0; i < pyramidData.length; i++) {
let d = pyramidData[i];
let group = document.createElement('div');
group.className = 'control-group';
let label = document.createElement('label');
label.setAttribute('for', `layer${i}-slider`);
let valueSpan = document.createElement('span');
valueSpan.id = `layer${i}-value`;
valueSpan.textContent = `${d.value}%`;
label.textContent = d.name;
label.appendChild(valueSpan);
let slider = document.createElement('input');
slider.type = 'range';
slider.id = `layer${i}-slider`;
slider.min = 0;
slider.max = 100;
slider.value = d.value;
slider.step = 0.1;
slider.dataset.index = i;
slider.addEventListener('input', handleSliderInput);
group.appendChild(label);
group.appendChild(slider);
slidersContainer.appendChild(group);
let li = document.createElement('li');
let colorBox = document.createElement('div');
colorBox.className = 'legend-color';
colorBox.style.backgroundColor = d.color;
li.appendChild(colorBox);
let legendText = document.createElement('span');
legendText.textContent = d.name;
li.appendChild(legendText);
legendContainer.appendChild(li);
}
}
function handleSliderInput(e) {
let idx = parseInt(e.target.dataset.index);
let newValue = parseFloat(e.target.value);
let oldValue = pyramidData[idx].value;
let diff = newValue - oldValue;
pyramidData[idx].value = newValue;
let others = pyramidData.map((d, i) => i).filter(i => i !== idx);
let totalOthers = others.reduce((sum, i) => sum + pyramidData[i].value, 0);
if (totalOthers > 0) {
for (let i of others) {
let prop = pyramidData[i].value / totalOthers;
pyramidData[i].value -= diff * prop;
}
}
normalizeData();
updateControls();
drawPyramid();
}
function updateControls() {
for (let i = 0; i < pyramidData.length; i++) {
let slider = document.getElementById(`layer${i}-slider`);
let valueSpan = document.getElementById(`layer${i}-value`);
if (slider) slider.value = pyramidData[i].value;
if (valueSpan) valueSpan.textContent = `${pyramidData[i].value.toFixed(1)}%`;
}
}
function normalizeData() {
for (let d of pyramidData) {
if (d.value < 0) d.value = 0;
}
let total = pyramidData.reduce((sum, d) => sum + d.value, 0);
if (total === 0) return;
for (let d of pyramidData) {
d.value = d.value / total * 100;
}
}
normalizeData();
drawPyramid();
createControls();
updateControls();
4. 关键参数说明
pyramidData:存储金字塔各层的数据,包括名称、比例和颜色。svgWidth和svgHeight:SVG画布的宽度和高度。pyramidBaseWidth和pyramidTopWidth:金字塔底部和顶部的宽度。pyramidHeight:金字塔的高度。pyramidX和pyramidY:金字塔的起始X和Y坐标。drawPyramid():绘制金字塔的函数,包括各层和轮廓线。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;
background: #f4f7f9;
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
h1 {
color: #333;
font-weight: 300;
}
.container {
display: flex;
gap: 40px;
margin-top: 20px;
}
.pyramid-svg-container {
position: relative;
width: 320px;
height: 400px;
}
#pyramid-svg {
width: 320px;
height: 400px;
display: block;
}
#tooltip {
position: absolute;
background: rgba(0,0,0,0.8);
color: #fff;
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;
background: #fff;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
padding: 20px;
}
.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-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 class="pyramid-svg-container">
<svg id="pyramid-svg" viewBox="0 0 320 400"></svg>
<div id="tooltip"></div>
</div>
<div class="controls">
<h2>调整各层比例</h2>
<div id="sliders"></div>
<ul id="legend"></ul>
</div>
</div>
<script>
let pyramidData = [
{ name: '第1层', value: 40, color: '#f1c40f' },
{ name: '第2层', value: 30, color: '#e67e22' },
{ name: '第3层', value: 20, color: '#3498db' },
{ name: '第4层', value: 10, color: '#9b59b6' }
];
const svg = document.getElementById('pyramid-svg');
const slidersContainer = document.getElementById('sliders');
const legendContainer = document.getElementById('legend');
const tooltip = document.getElementById('tooltip');
const svgContainer = document.querySelector('.pyramid-svg-container');
const svgWidth = 320;
const svgHeight = 400;
const pyramidBaseWidth = 260;
const pyramidTopWidth = 40;
const pyramidHeight = 340;
const pyramidX = (svgWidth - pyramidBaseWidth) / 2;
const pyramidY = 40;
function drawPyramid() {
svg.innerHTML = '';
let total = pyramidData.reduce((sum, d) => sum + d.value, 0);
let y = pyramidY;
let prevWidth = pyramidTopWidth;
let prevX = (svgWidth - pyramidTopWidth) / 2;
let topLeft = null;
let topRight = null;
let bottomLeft = null;
let bottomRight = null;
for (let i = 0; i < pyramidData.length; i++) {
let d = pyramidData[i];
let h = d.value / total * pyramidHeight;
let nextWidth = pyramidTopWidth + (pyramidBaseWidth - pyramidTopWidth) * ((y + h - pyramidY) / pyramidHeight);
let nextX = (svgWidth - nextWidth) / 2;
let path = `M${prevX},${y} L${prevX + prevWidth},${y} L${nextX + nextWidth},${y + h} L${nextX},${y + h} Z`;
let layer = document.createElementNS('http://www.w3.org/2000/svg', 'path');
layer.setAttribute('d', path);
layer.setAttribute('fill', d.color);
layer.setAttribute('stroke', '#fff');
layer.setAttribute('stroke-width', 2);
layer.dataset.index = i;
svg.appendChild(layer);
layer.addEventListener('mousemove', (e) => {
tooltip.style.opacity = '1';
const containerBox = svgContainer.getBoundingClientRect();
tooltip.style.left = `${e.clientX - containerBox.left}px`;
tooltip.style.top = `${e.clientY - containerBox.top}px`;
tooltip.innerHTML = `${d.name}: ${d.value.toFixed(1)}%`;
});
layer.addEventListener('mouseleave', () => {
tooltip.style.opacity = '0';
});
if (i === 0) {
topLeft = [prevX, y];
topRight = [prevX + prevWidth, y];
}
if (i === pyramidData.length - 1) {
bottomLeft = [nextX, y + h];
bottomRight = [nextX + nextWidth, y + h];
}
y += h;
prevWidth = nextWidth;
prevX = nextX;
}
if (topLeft && topRight && bottomLeft && bottomRight) {
let outline = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
outline.setAttribute('points', `
${topLeft[0]},${topLeft[1]}
${topRight[0]},${topRight[1]}
${bottomRight[0]},${bottomRight[1]}
${bottomLeft[0]},${bottomLeft[1]}
`);
outline.setAttribute('fill', 'none');
outline.setAttribute('stroke', '#333');
outline.setAttribute('stroke-width', 3);
svg.appendChild(outline);
}
}
function createControls() {
slidersContainer.innerHTML = '';
legendContainer.innerHTML = '';
for (let i = 0; i < pyramidData.length; i++) {
let d = pyramidData[i];
let group = document.createElement('div');
group.className = 'control-group';
let label = document.createElement('label');
label.setAttribute('for', `layer${i}-slider`);
let valueSpan = document.createElement('span');
valueSpan.id = `layer${i}-value`;
valueSpan.textContent = `${d.value}%`;
label.textContent = d.name;
label.appendChild(valueSpan);
let slider = document.createElement('input');
slider.type = 'range';
slider.id = `layer${i}-slider`;
slider.min = 0;
slider.max = 100;
slider.value = d.value;
slider.step = 0.1;
slider.dataset.index = i;
slider.addEventListener('input', handleSliderInput);
group.appendChild(label);
group.appendChild(slider);
slidersContainer.appendChild(group);
let li = document.createElement('li');
let colorBox = document.createElement('div');
colorBox.className = 'legend-color';
colorBox.style.backgroundColor = d.color;
li.appendChild(colorBox);
let legendText = document.createElement('span');
legendText.textContent = d.name;
li.appendChild(legendText);
legendContainer.appendChild(li);
}
}
function handleSliderInput(e) {
let idx = parseInt(e.target.dataset.index);
let newValue = parseFloat(e.target.value);
let oldValue = pyramidData[idx].value;
let diff = newValue - oldValue;
pyramidData[idx].value = newValue;
let others = pyramidData.map((d, i) => i).filter(i => i !== idx);
let totalOthers = others.reduce((sum, i) => sum + pyramidData[i].value, 0);
if (totalOthers > 0) {
for (let i of others) {
let prop = pyramidData[i].value / totalOthers;
pyramidData[i].value -= diff * prop;
}
}
normalizeData();
updateControls();
drawPyramid();
}
function updateControls() {
for (let i = 0; i < pyramidData.length; i++) {
let slider = document.getElementById(`layer${i}-slider`);
let valueSpan = document.getElementById(`layer${i}-value`);
if (slider) slider.value = pyramidData[i].value;
if (valueSpan) valueSpan.textContent = `${pyramidData[i].value.toFixed(1)}%`;
}
}
function normalizeData() {
for (let d of pyramidData) {
if (d.value < 0) d.value = 0;
}
let total = pyramidData.reduce((sum, d) => sum + d.value, 0);
if (total === 0) return;
for (let d of pyramidData) {
d.value = d.value / total * 100;
}
}
normalizeData();
drawPyramid();
createControls();
updateControls();
</script>
</body>
</html>
四、总结
通过以上步骤,你可以创建一个交互式的SVG金字塔可视化组件。这个组件使用SVG绘制金字塔结构,通过滑块动态调整各层比例,并显示各层的图例和提示信息。你可以通过调整代码中的参数来改变金字塔的外观和行为。希望这个教程对你有所帮助!
以上制作的是一个最简单的一个带刻度的仪表盘,我还录制了一个更加美观的带刻度的仪表盘的视频教程,有兴趣的小伙伴可以点击查看。