Actions
Wiki »
Web tech¶
- Table of contents
- Web tech
How to visualize data:¶
D3 ref: https://observablehq.com/@d3/gallery , https://johan.github.io/d3/ex/
Plotly ref: https://plotly.com/javascript/
交互功能对比¶
功能 | JavaScript/Plotly | Python/Plotly | JavaScript/d3 |
---|---|---|---|
缩放/平移 | ✔️ | ✔️ | ✔️ |
悬停显示数值 | ✔️ | ✔️ | ✔️ |
数据点高亮 | ✔️ | ✔️ | ✔️ |
导出为图片(PNG/JPEG) | ✔️ | ✔️ | ✔️ |
动态更新数据 | ✔️(需额外代码) | ❌ | ✔️ |
旧firefox支持 | ❌(globalthis) | ? | ✔️ |
drc 网页显示¶
<!DOCTYPE html>
<html>
<head>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
/* 新增图片名称样式 */
.modal-content {
position: relative;
text-align: center;
}
.image-name {
color: white;
font-size: 24px;
margin-top: 10px;
text-shadow: 1px 1px 3px rgba(0,0,0,0.5);
}
/* 原有样式保持不变 */
.category { margin: 20px 0; }
.thumbnails {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px;
}
/* 其他样式... */
</style>
</head>
<body>
<div id="gallery"></div>
<script>
// 修改数据结构:每个图片对象包含name和path
const categories = [
{
name: "自然",
photos: [
{ name: "山脉", path: "nature/1.jpg" },
{ name: "湖泊", path: "nature/2.jpg" },
// ...
]
},
// 其他分类...
];
// 创建模态框结构(新增名称显示区域)
const modal = d3.select("body")
.append("div")
.attr("class", "modal");
const modalContent = modal.append("div")
.attr("class", "modal-content");
modalContent.append("img")
.attr("class", "modal-img");
// 新增图片名称元素
modalContent.append("div")
.attr("class", "image-name");
// 其他模态框元素(关闭按钮、导航按钮等)保持不变...
// 修改缩略图绑定方式
categoryDivs.each(function(category) {
d3.select(this)
.append("div")
.attr("class", "thumbnails")
.selectAll(".thumbnail")
.data(category.photos)
.join("img")
.attr("class", "thumbnail")
.attr("src", d => d.path)
.on("click", function(event, d) { // 改为传递完整数据对象
showModal(category.photos, d);
});
});
// 修改显示逻辑
let currentImages = [];
let currentIndex = 0;
function showModal(images, selectedImage) {
currentImages = images;
currentIndex = images.indexOf(selectedImage);
// 同时更新图片和名称
modal.select(".modal-img")
.attr("src", selectedImage.path);
modal.select(".image-name")
.text(selectedImage.name);
modal.style("display", "flex");
}
// 修改导航逻辑
function navigate(direction) {
currentIndex = (currentIndex + direction + currentImages.length) % currentImages.length;
const currentImage = currentImages[currentIndex];
modal.select(".modal-img")
.attr("src", currentImage.path);
modal.select(".image-name")
.text(currentImage.name);
}
// 其他函数保持不变...
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<style>
.category { margin: 20px 0; }
.thumbnails {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 8px;
}
.thumbnail {
width: 120px;
height: 90px;
object-fit: cover;
cursor: pointer;
transition: transform 0.2s;
}
.thumbnail:hover { transform: scale(1.05); }
/* 模态框样式 */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.8);
display: none;
justify-content: center;
align-items: center;
}
.modal-img {
max-width: 80%;
max-height: 80%;
}
.close-btn {
position: absolute;
top: 20px;
right: 20px;
color: white;
font-size: 30px;
cursor: pointer;
}
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
color: white;
font-size: 30px;
cursor: pointer;
}
.prev { left: 20px; }
.next { right: 20px; }
</style>
</head>
<body>
<div id="gallery"></div>
</body>
</html>
// 模拟数据结构
const categories = [
{
name: "自然",
photos: ["nature/1.jpg", "nature/2.jpg", /*...*/]
},
// 其他四个分类...
];
// 创建照片墙
const gallery = d3.select("#gallery");
// 创建分类容器
const categoryDivs = gallery.selectAll(".category")
.data(categories)
.join("div")
.attr("class", "category");
// 添加分类标题
categoryDivs.append("h2")
.text(d => d.name);
// 创建缩略图网格
categoryDivs.each(function(category) {
const container = d3.select(this);
container.append("div")
.attr("class", "thumbnails")
.selectAll(".thumbnail")
.data(category.photos)
.join("img")
.attr("class", "thumbnail")
.attr("src", d => d) // 根据实际路径调整
.on("click", function(event, imgPath) {
showModal(category.photos, imgPath);
});
});
// 创建模态框
const modal = d3.select("body")
.append("div")
.attr("class", "modal");
modal.append("span")
.attr("class", "close-btn")
.html("×")
.on("click", hideModal);
const img = modal.append("img")
.attr("class", "modal-img");
modal.append("div")
.attr("class", "nav-btn prev")
.html("❮")
.on("click", () => navigate(-1));
modal.append("div")
.attr("class", "nav-btn next")
.html("❯")
.on("click", () => navigate(1));
let currentImages = [];
let currentIndex = 0;
// 显示模态框
function showModal(images, selectedImage) {
currentImages = images;
currentIndex = images.indexOf(selectedImage);
img.attr("src", selectedImage);
modal.style("display", "flex");
}
// 隐藏模态框
function hideModal() {
modal.style("display", "none");
}
// 图片导航
function navigate(direction) {
currentIndex = (currentIndex + direction + currentImages.length) % currentImages.length;
img.attr("src", currentImages[currentIndex]);
}
// 点击外部关闭
window.onclick = function(event) {
if (event.target === modal.node()) {
hideModal();
}
}
// 键盘导航
document.addEventListener("keydown", (e) => {
if (modal.style("display") === "flex") {
if (e.key === "ArrowLeft") navigate(-1);
if (e.key === "ArrowRight") navigate(1);
if (e.key === "Escape") hideModal();
}
});
实现在X轴下方添加可缩放和拖动的滑块条:¶
- 调整边距和创建滑块容器
在原有SVG中增加底部边距,并为滑块条创建新的容器。
// 修改边距,底部增加空间给滑块条
const margin = { top: 50, right: 50, bottom: 100, left: 50 };
// 创建主图表容器
const svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// 创建滑块条的容器(位于主图表下方)
const slider = d3.select("svg")
.append("g")
.attr("class", "slider")
.attr("transform", `translate(${margin.left},${height + margin.top + 20})`);
- 初始化滑块比例尺和轴
使用主图表的原始数据范围定义滑块的比例尺。
// 主图表的比例尺(可缩放)
let x = d3.scaleLinear().range([0, width]);
// 滑块的比例尺(固定为完整数据范围)
let xSlider = d3.scaleLinear().range([0, width]);
// 滑块的X轴
const xAxisSlider = d3.axisBottom(xSlider)
.tickSize(0)
.tickFormat("");
- 绘制滑块条的背景和轴
在滑块区域绘制简化的折线图和轴。
// 在数据加载后初始化滑块
d3.csv("data.csv").then(data => {
// ...原有数据处理...
// 初始化滑块比例尺
xSlider.domain(d3.extent(data, d => d.index));
// 绘制滑块背景折线
slider.append("path")
.datum(data)
.attr("class", "slider-line")
.attr("d", d3.line()
.x(d => xSlider(d.index))
.y(0)); // 简化高度
// 添加滑块轴
slider.append("g")
.attr("class", "x-axis-slider")
.call(xAxisSlider);
});
- 添加刷子并处理事件
使用d3.brushX
创建交互刷子。
// 创建刷子
const brush = d3.brushX()
.extent([[0, -10], [width, 10]]) // 设置刷子区域高度
.on("brush", brushing)
.on("end", brushEnded);
// 应用刷子到滑块
slider.append("g")
.attr("class", "brush")
.call(brush);
// 刷子拖动时的处理函数
function brushing(event) {
if (!event.selection) return;
const [x0, x1] = event.selection;
x.domain([xSlider.invert(x0), xSlider.invert(x1)]);
updateMainChart(); // 更新主图表
}
// 刷子结束时的处理(双击重置)
function brushEnded(event) {
if (!event.selection) {
x.domain(xSlider.domain()); // 重置为完整范围
updateMainChart();
}
}
- 封装主图表更新函数
将主图表的绘制逻辑封装成可重用的函数。
function updateMainChart() {
// 更新X轴
svg.select(".x-axis").call(d3.axisBottom(x));
// 更新折线
svg.selectAll(".line")
.attr("d", d3.line()
.x(d => x(d.index))
.y(d => y(d[currentY])));
// 更新点位置
svg.selectAll(".dot")
.attr("cx", d => x(d.index));
// 更新背景区域
drawBackgroundRegions(ranges);
}
- CSS样式调整
添加滑块条样式:
.slider-line {
fill: none;
stroke: #666;
stroke-width: 1px;
}
.brush .selection {
fill: steelblue;
fill-opacity: 0.3;
stroke: none;
}
.brush .handle {
fill: steelblue;
}
- 完整修改后的代码结构
整合上述修改后的核心代码:
// 设置边距和尺寸
const margin = { top: 50, right: 50, bottom: 100, left: 50 };
const width = 960 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;
// 创建SVG容器
const svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom + 50); // 增加底部空间
// 主图表容器
const main = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// 滑块容器
const slider = svg.append("g")
.attr("class", "slider")
.attr("transform", `translate(${margin.left},${height + margin.top + 30})`);
// 初始化比例尺
const x = d3.scaleLinear().range([0, width]);
const xSlider = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
// 加载数据
d3.csv("data.csv").then(data => {
// 数据处理...
data.forEach(d => {
d.index = +d.index;
d.observation1 = +d.observation1;
d.observation2 = +d.observation2;
});
// 设置比例尺
x.domain(d3.extent(data, d => d.index));
xSlider.domain(x.domain());
y.domain([0, d3.max(data, d => Math.max(d.observation1, d.observation2))]);
// 绘制主图表
drawMainChart(data);
// 绘制滑块背景
slider.append("path")
.datum(data)
.attr("class", "slider-line")
.attr("d", d3.line()
.x(d => xSlider(d.index))
.y(0));
// 添加刷子
const brush = d3.brushX()
.extent([[0, -10], [width, 10]])
.on("brush", brushing)
.on("end", brushEnded);
slider.append("g")
.attr("class", "brush")
.call(brush);
});
function drawMainChart(data) {
// 绘制折线、点、轴等...
// 使用x比例尺进行所有定位
}
function updateMainChart() {
// 更新所有依赖x比例尺的元素
main.select(".x-axis").call(d3.axisBottom(x));
main.selectAll(".line").attr("d", ...);
main.selectAll(".dot").attr("cx", ...);
// 更新背景区域...
}
// 刷子事件处理函数
function brushing(event) {
if (!event.selection) return;
const [x0, x1] = event.selection.map(xSlider.invert);
x.domain([x0, x1]);
updateMainChart();
}
function brushEnded(event) {
if (!event.selection) {
x.domain(xSlider.domain());
updateMainChart();
}
}
交互,圆圈,准心¶
// 设置图表的尺寸和边距
const margin = {top: 50, right: 50, bottom: 50, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// 设置SVG的尺寸
const svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// 设置比例尺
const x = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
// 定义折线生成器
const line = d3.line()
.x(d => x(d.index)) // 使用第一列(index)作为X轴
.y(d => y(d.observation)); // 使用观测值作为Y轴
// 初始化Y轴数据为第二列(observation1)
let currentY = "observation1";
// 创建高亮圆圈
const highlightCircle = svg.append("circle")
.attr("class", "highlight-circle")
.attr("r", 7) // 圆圈的半径
.style("fill", "none")
.style("stroke", "red")
.style("stroke-width", 2)
.style("opacity", 0); // 初始不可见
// 创建准心线(水平线和垂直线)
const horizontalLine = svg.append("line")
.attr("class", "crosshair-line")
.style("stroke", "red")
.style("stroke-width", 1)
.style("stroke-dasharray", "3,3") // 虚线样式
.style("opacity", 0); // 初始不可见
const verticalLine = svg.append("line")
.attr("class", "crosshair-line")
.style("stroke", "red")
.style("stroke-width", 1)
.style("stroke-dasharray", "3,3") // 虚线样式
.style("opacity", 0); // 初始不可见
// 解析 label 中的 begin 和 end 标记
function parseLabelRanges(data) {
const ranges = [];
let beginIndex = null;
data.forEach((d, i) => {
if (d.label.startsWith("begin")) {
beginIndex = d.index; // 记录 begin 的 index
} else if (d.label.startsWith("end") && beginIndex !== null) {
ranges.push({ begin: beginIndex, end: d.index }); // 记录 begin 和 end 的范围
beginIndex = null; // 重置 beginIndex
}
});
return ranges;
}
// 绘制绿色背景区域
function drawBackgroundRegions(ranges) {
// 移除旧的背景区域
svg.selectAll(".background-region").remove();
// 绘制新的背景区域
ranges.forEach(range => {
svg.append("rect")
.attr("class", "background-region")
.attr("x", x(range.begin)) // 起始位置
.attr("width", x(range.end) - x(range.begin)) // 宽度
.attr("y", 0) // 从顶部开始
.attr("height", height) // 覆盖整个图表高度
.style("fill", "green")
.style("opacity", 0.2); // 设置透明度
});
}
// 读取CSV文件
d3.csv("data.csv").then(data => {
// 转换数据类型
data.forEach(d => {
d.index = +d.index; // 第一列转换为数值
d.observation1 = +d.observation1; // 第二列转换为数值
d.observation2 = +d.observation2; // 第三列转换为数值
});
// 设置比例尺的域
x.domain(d3.extent(data, d => d.index)); // X轴范围为第一列的最小值和最大值
y.domain([0, d3.max(data, d => Math.max(d.observation1, d.observation2))]; // Y轴范围为0到观测值的最大值
// 解析 label 中的 begin 和 end 标记
const ranges = parseLabelRanges(data);
// 绘制初始折线图(使用observation1)
drawChart(data, ranges);
// 添加按钮点击事件
d3.select("#toggleButton").on("click", function() {
// 切换Y轴数据
currentY = currentY === "observation1" ? "observation2" : "observation1";
// 更新按钮文本
d3.select(this).text(currentY === "observation1" ? "Switch to Observation 2" : "Switch to Observation 1");
// 重新绘制折线图
drawChart(data, ranges);
});
});
// 绘制折线图的函数
function drawChart(data, ranges) {
// 移除旧的折线和点
svg.selectAll(".line").remove();
svg.selectAll(".dot").remove();
// 更新折线生成器的Y值
line.y(d => y(d[currentY]));
// 添加折线
svg.append("path")
.datum(data)
.attr("class", `line ${currentY === "observation1" ? "line1" : "line2"}`)
.attr("d", line);
// 添加点
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("cx", d => x(d.index)) // 使用第一列(index)作为X轴
.attr("cy", d => y(d[currentY])) // 使用当前观测值作为Y轴
.attr("r", 5)
.on("mouseover", function(event, d) {
// 显示高亮圆圈
highlightCircle
.attr("cx", x(d.index))
.attr("cy", y(d[currentY]))
.style("opacity", 1);
// 显示准心线
horizontalLine
.attr("x1", 0)
.attr("x2", width)
.attr("y1", y(d[currentY]))
.attr("y2", y(d[currentY]))
.style("opacity", 1);
verticalLine
.attr("x1", x(d.index))
.attr("x2", x(d.index))
.attr("y1", 0)
.attr("y2", height)
.style("opacity", 1);
// 显示工具提示
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(`
<div><strong>Index:</strong> ${d.index}</div>
<div><strong>Observation:</strong> ${d[currentY]}</div>
<div><strong>Label:</strong> ${d.label}</div>
`)
.style("left", (event.pageX + 5) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
// 隐藏高亮圆圈和准心线
highlightCircle.style("opacity", 0);
horizontalLine.style("opacity", 0);
verticalLine.style("opacity", 0);
// 隐藏工具提示
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
// 绘制绿色背景区域
drawBackgroundRegions(ranges);
// 添加X轴
svg.select(".x-axis").remove(); // 移除旧的X轴
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x));
// 添加Y轴
svg.select(".y-axis").remove(); // 移除旧的Y轴
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y));
// 添加X轴标签
svg.select(".x-axis-label").remove(); // 移除旧的X轴标签
svg.append("text")
.attr("class", "x-axis-label")
.attr("x", width / 2)
.attr("y", height + margin.bottom - 10)
.style("text-anchor", "middle")
.text("时间");
// 添加Y轴标签
svg.select(".y-axis-label").remove(); // 移除旧的Y轴标签
svg.append("text")
.attr("class", "y-axis-label")
.attr("transform", "rotate(-90)")
.attr("x", -height / 2)
.attr("y", -margin.left + 20)
.style("text-anchor", "middle")
.text("观测值");
}
// 添加提示工具
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0)
.style("font-size", "14px") // 放大字体
.style("text-align", "left"); // 左对齐
button 切换图表¶
// 设置图表的尺寸和边距
const margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// 设置SVG的尺寸
const svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// 设置比例尺
const x = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
// 定义折线生成器
const line = d3.line()
.x(d => x(d.index)) // 使用第一列(index)作为X轴
.y(d => y(d.observation)); // 使用观测值作为Y轴
// 初始化Y轴数据为第二列(observation1)
let currentY = "observation1";
// 读取CSV文件
d3.csv("data.csv").then(data => {
// 转换数据类型
data.forEach(d => {
d.index = + parseInt(d.index); // 第一列转换为数值
d.observation1 = + parseFloat(d.observation1); // 第二列转换为数值
d.observation2 = + parseFloat(d.observation2); // 第三列转换为数值
});
// 设置比例尺的域
x.domain(d3.extent(data, d => d.index)); // X轴范围为第一列的最小值和最大值
y.domain([0, d3.max(data, d => Math.max(d.observation1, d.observation2))]); // Y轴范围为0到观测值的最大值
// 绘制初始折线图(使用observation1)
drawChart(data);
// 添加按钮点击事件
d3.select("#toggleButton").on("click", function() {
// 切换Y轴数据
currentY = currentY === "observation1" ? "observation2" : "observation1";
// 更新按钮文本
d3.select(this).text(currentY === "observation1" ? "Switch to Observation 2" : "Switch to Observation 1");
// 重新绘制折线图
drawChart(data);
});
});
// 绘制折线图的函数
function drawChart(data) {
// 移除旧的折线和点
svg.selectAll(".line").remove();
svg.selectAll(".dot").remove();
// 更新折线生成器的Y值
line.y(d => y(d[currentY]));
// 添加折线
svg.append("path")
.datum(data)
.attr("class", `line ${currentY === "observation1" ? "line1" : "line2"}`)
.attr("d", line);
// 添加点
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("cx", d => x(d.index)) // 使用第一列(index)作为X轴
.attr("cy", d => y(d[currentY])) // 使用当前观测值作为Y轴
.attr("r", 5)
.on("mouseover", function(event, d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(`Index: ${d.index}<br>Observation: ${d[currentY]}<br>Label: ${d.label}`)
.style("left", (event.pageX + 5) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
// 添加X轴
svg.select(".x-axis").remove(); // 移除旧的X轴
svg.append("g")
.attr("class", "x-axis")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x));
// 添加Y轴
svg.select(".y-axis").remove(); // 移除旧的Y轴
svg.append("g")
.attr("class", "y-axis")
.call(d3.axisLeft(y));
}
// 添加提示工具
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
js d3 读入 csv 并绘制折线交互¶
{{collapse(show code...)
// 设置图表的尺寸和边距
const margin = {top: 20, right: 30, bottom: 30, left: 40},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// 设置SVG的尺寸
const svg = d3.select("#chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// 设置比例尺
const x = d3.scaleLinear().range([0, width]);
const y = d3.scaleLinear().range([height, 0]);
// 定义折线生成器
const line = d3.line()
.x((d, i) => x(i))
.y(d => y(d.observation));
// 读取CSV文件
d3.csv("data.csv").then(data => {
// 转换数据类型
data.forEach((d, i) => {
d.observation = +d.observation;
d.index = i;
});
// 设置比例尺的域
x.domain([0, data.length - 1]);
y.domain([0, d3.max(data, d => d.observation)]);
// 添加折线
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
// 添加点
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("cx", (d, i) => x(i))
.attr("cy", d => y(d.observation))
.attr("r", 5)
.on("mouseover", function(event, d) {
tooltip.transition()
.duration(200)
.style("opacity", .9);
tooltip.html(`Observation: ${d.observation}<br>Label: ${d.label}`)
.style("left", (event.pageX + 5) + "px")
.style("top", (event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
// 添加X轴
svg.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x));
// 添加Y轴
svg.append("g")
.call(d3.axisLeft(y));
});
// 添加提示工具
const tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>D3.js Line Chart</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
.dot {
fill: steelblue;
stroke: #fff;
}
.tooltip {
position: absolute;
text-align: center;
width: 120px;
height: auto;
padding: 5px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
</style>
</head>
<body>
<div id="chart"></div>
<script src="script.js"></script>
</body>
</html>
js d3 内嵌数据显示折线¶
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分段填充折线图</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
.tooltip {
position: absolute;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid #ccc;
padding: 5px;
font-size: 12px;
pointer-events: none;
opacity: 0;
}
.line {
fill: none;
stroke: black;
stroke-width: 2;
}
</style>
</head>
<body>
<div id="chart"></div>
<div class="tooltip" id="tooltip"></div>
<script>
// 示例数据
const data = [
{ time: "2025-01-01", value: 10, description: "说明1" },
{ time: "2025-01-02", value: 15, description: "说明2" },
{ time: "2025-01-03", value: 20, description: "说明3" },
{ time: "2025-01-04", value: 25, description: "说明4" },
{ time: "2025-01-05", value: 30, description: "说明5" },
{ time: "2025-01-06", value: 35, description: "说明6" },
{ time: "2025-01-07", value: 40, description: "说明7" },
{ time: "2025-01-08", value: 45, description: "说明8" },
{ time: "2025-01-09", value: 50, description: "说明9" },
{ time: "2025-01-10", value: 55, description: "说明10" },
{ time: "2025-01-11", value: 60, description: "说明11" },
{ time: "2025-01-12", value: 65, description: "说明12" },
{ time: "2025-01-13", value: 70, description: "说明13" },
{ time: "2025-01-14", value: 75, description: "说明14" },
{ time: "2025-01-15", value: 80, description: "说明15" },
{ time: "2025-01-16", value: 85, description: "说明16" },
{ time: "2025-01-17", value: 90, description: "说明17" },
{ time: "2025-01-18", value: 95, description: "说明18" },
{ time: "2025-01-19", value: 100, description: "说明19" },
{ time: "2025-01-20", value: 105, description: "说明20" }
];
// 设置图表尺寸
const margin = { top: 20, right: 30, bottom: 30, left: 40 };
const width = 800 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
// 创建 SVG 容器
const svg = d3.select("#chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// 解析时间格式
const parseTime = d3.timeParse("%Y-%m-%d");
// 格式化数据
data.forEach(d => {
d.time = parseTime(d.time);
d.value = +d.value;
});
// 设置比例尺
const x = d3.scaleTime()
.domain(d3.extent(data, d => d.time))
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([height, 0]);
// 添加 X 轴
svg.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x));
// 添加 Y 轴
svg.append("g")
.call(d3.axisLeft(y));
// 创建折线生成器
const line = d3.line()
.x(d => x(d.time))
.y(d => y(d.value));
// 绘制折线
svg.append("path")
.datum(data)
.attr("class", "line")
.attr("d", line);
// 分段填充颜色
const first10 = data.slice(0, 10);
const last10 = data.slice(-10);
const middle = data.slice(10, -10);
// 填充前十个时间段的绿色区域
svg.append("path")
.datum(first10)
.attr("fill", "green")
.attr("opacity", 0.3)
.attr("d", d3.area()
.x(d => x(d.time))
.y0(height)
.y1(d => y(d.value))
);
// 填充中间时间段的蓝色区域
svg.append("path")
.datum(middle)
.attr("fill", "blue")
.attr("opacity", 0.3)
.attr("d", d3.area()
.x(d => x(d.time))
.y0(height)
.y1(d => y(d.value))
);
// 填充后十个时间段的红色区域
svg.append("path")
.datum(last10)
.attr("fill", "red")
.attr("opacity", 0.3)
.attr("d", d3.area()
.x(d => x(d.time))
.y0(height)
.y1(d => y(d.value))
);
// 添加悬停交互
const tooltip = d3.select("#tooltip");
svg.selectAll(".dot")
.data(data)
.enter()
.append("circle")
.attr("class", "dot")
.attr("cx", d => x(d.time))
.attr("cy", d => y(d.value))
.attr("r", 5)
.attr("fill", "steelblue")
.on("mouseover", (event, d) => {
tooltip.style("opacity", 1)
.html(`时间: ${d3.timeFormat("%Y-%m-%d")(d.time)}<br>数值: ${d.value}<br>说明: ${d.description}`)
.style("left", `${event.pageX + 5}px`)
.style("top", `${event.pageY - 20}px`);
})
.on("mouseout", () => {
tooltip.style("opacity", 0);
});
</script>
</body>
</html>
JavaScript + Plotly(纯前端实现)¶
<!DOCTYPE html>
<html>
<head>
<title>交互式图表</title>
<!-- 引入 Plotly.js -->
<script src="https://cdn.plot.ly/plotly-2.24.1.min.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
// 读取CSV文件(假设文件名为 data.csv)
fetch('data.csv')
.then(response => response.text())
.then(csvText => {
// 解析CSV数据
const rows = csvText.split('\n');
const x = [], y = [];
rows.forEach((row, index) => {
if (index === 0) return; // 跳过标题行
const [xVal, yVal] = row.split(',');
x.push(parseFloat(xVal));
y.push(parseFloat(yVal));
});
// 绘制图表
Plotly.newPlot('chart', [{
x: x,
y: y,
type: 'scatter',
mode: 'lines+markers',
marker: { color: 'blue' },
line: { shape: 'spline' }
}], {
title: '交互式数据图表',
xaxis: { title: 'X轴' },
yaxis: { title: 'Y轴' },
hovermode: 'closest'
});
});
</script>
</body>
</html>
使用步骤:¶
- 将CSV文件命名为
data.csv
,格式如下:x,y 1,5 2,3 3,7 4,2 5,8
- 将HTML文件和
data.csv
放在同一目录下,用浏览器打开HTML文件。 - 效果:支持缩放、悬停显示数值、拖拽平移等交互。
Python + Plotly(生成独立HTML文件)¶
特点:适合Python用户,自动化生成图表文件。¶
import pandas as pd
import plotly.express as px
# 1. 读取CSV文件
df = pd.read_csv("data.csv")
# 2. 创建交互式图表
fig = px.line(
df, x='x', y='y',
title='Python生成的交互式图表',
markers=True, # 显示数据点
line_shape='spline' # 平滑曲线
)
# 3. 自定义悬停效果和样式
fig.update_traces(
hoverinfo='x+y', # 悬停显示x和y值
line=dict(width=2, color='royalblue'),
marker=dict(size=8, color='firebrick')
)
# 4. 保存为HTML文件
fig.write_html("interactive_chart.html")
使用步骤:¶
- 安装依赖:
pip install pandas plotly
- 运行代码后,生成
interactive_chart.html
,用浏览器打开即可看到图表。
进阶方案(可选)¶
- 动态数据加载(JavaScript):
View details...View details...
<input type="file" id="csvFile" accept=".csv">
<div id="chart"></div>
<script>
document.getElementById('csvFile').addEventListener('change', function(e) {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = function(e) {
// 解析并绘制图表(代码同方法一)
};
reader.readAsText(file);
});
</script>
- 用户可上传任意CSV文件,实时生成图表。
-
添加控件(Python + Dash):
View details...View details...from dash import Dash, dcc, html import pandas as pd import plotly.express as px app = Dash(__name__) df = pd.read_csv("data.csv") app.layout = html.Div([ dcc.Graph( id='live-chart', figure=px.scatter(df, x='x', y='y', title='Dash动态图表') ), html.Button('更新数据', id='update-button') ]) if __name__ == '__main__': app.run_server(debug=True)
- 运行后访问
http://localhost:8050
,支持动态交互和按钮触发操作。
- 运行后访问
Updated by jun chen 3 months ago · 29 revisions