Web tech » History » Revision 25
Revision 24 (jun chen, 02/20/2025 02:30 PM) → Revision 25/29 (jun chen, 02/20/2025 02:31 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) | ? | ✔️ | --- ### 实现在X轴下方添加可缩放和拖动的滑块条: 以下是逐步解决方案,实现在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`,支持动态交互和按钮触发操作。 ---