Project

General

Profile

Web tech » History » Revision 26

Revision 25 (jun chen, 02/20/2025 02:31 PM) → Revision 26/29 (jun chen, 03/13/2025 09:12 PM)

# Web tech 

 {{toc}} 


 ## 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> 
     <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("&times;") 
     .on("click", hideModal); 

 const img = modal.append("img") 
     .attr("class", "modal-img"); 

 modal.append("div") 
     .attr("class", "nav-btn prev") 
     .html("&#10094;") 
     .on("click", () => navigate(-1)); 

 modal.append("div") 
     .attr("class", "nav-btn next") 
     .html("&#10095;") 
     .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轴下方添加可缩放和拖动的滑块条: 

 {{collapse(show code...) 

 1. 调整边距和创建滑块容器 
 在原有SVG中增加底部边距,并为滑块条创建新的容器。 

 ```javascript 
 // 修改边距,底部增加空间给滑块条 
 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})`); 
 ``` 

 2. 初始化滑块比例尺和轴 
 使用主图表的原始数据范围定义滑块的比例尺。 

 ```javascript 
 // 主图表的比例尺(可缩放) 
 let x = d3.scaleLinear().range([0, width]); 

 // 滑块的比例尺(固定为完整数据范围) 
 let xSlider = d3.scaleLinear().range([0, width]); 

 // 滑块的X轴 
 const xAxisSlider = d3.axisBottom(xSlider) 
   .tickSize(0) 
   .tickFormat(""); 
 ``` 

 3. 绘制滑块条的背景和轴 
 在滑块区域绘制简化的折线图和轴。 

 ```javascript 
 // 在数据加载后初始化滑块 
 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); 
 }); 
 ``` 

 4. 添加刷子并处理事件 
 使用`d3.brushX`创建交互刷子。 

 ```javascript 
 // 创建刷子 
 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(); 
   } 
 } 
 ``` 

 5. 封装主图表更新函数 
 将主图表的绘制逻辑封装成可重用的函数。 

 ```javascript 
 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); 
 } 
 ``` 

 6. CSS样式调整 
 添加滑块条样式: 

 ```css 
 .slider-line { 
   fill: none; 
   stroke: #666; 
   stroke-width: 1px; 
 } 

 .brush .selection { 
   fill: steelblue; 
   fill-opacity: 0.3; 
   stroke: none; 
 } 

 .brush .handle { 
   fill: steelblue; 
 } 
 ``` 

 7. 完整修改后的代码结构 
 整合上述修改后的核心代码: 

 ```javascript 
 // 设置边距和尺寸 
 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(); 
   } 
 } 
 ``` 
 }} 

 

 ### 交互,圆圈,准心 

 {{collapse(show code...) 

 ``` 
 // 设置图表的尺寸和边距 
 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 切换图表 


 {{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 => 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 内嵌数据显示折线 

 {{collapse(show code...) 

 ```  
 <!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(纯前端实现) 

 {{collapse(show code...) 

 ```html 
 <!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> 
 ``` 
 }} 

 #### 使用步骤: 
 1. 将CSV文件命名为 `data.csv`,格式如下: 
    ```csv 
    x,y 
    1,5 
    2,3 
    3,7 
    4,2 
    5,8 
    ``` 
 2. 将HTML文件和 `data.csv` 放在同一目录下,用浏览器打开HTML文件。 
 3. 效果:支持**缩放、悬停显示数值、拖拽平移**等交互。 

 --- 

 ### Python + Plotly(生成独立HTML文件) 
 #### 特点:适合Python用户,自动化生成图表文件。 

 {{collapse(show code...) 
 ```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") 
 ``` 
 }} 

 #### 使用步骤: 
 1. 安装依赖: 
    ```bash 
    pip install pandas plotly 
    ``` 
 2. 运行代码后,生成 `interactive_chart.html`,用浏览器打开即可看到图表。 

 ### **进阶方案(可选)** 
 1. **动态数据加载**(JavaScript): 

 {{collapse(View details...) 
    ```html 
    <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文件,实时生成图表。 


 2. **添加控件**(Python + Dash): 
 {{collapse(View details...) 
    ```python 
    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`,支持动态交互和按钮触发操作。 

 ---