Project

General

Profile

Actions

Web tech » History » Revision 22

« Previous | Revision 22/29 (diff) | Next »
jun chen, 02/19/2025 11:47 AM


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) ? ✔️

以下是逐步解决方案,实现在X轴下方添加可缩放和拖动的滑块条:

1. 调整边距和创建滑块容器

在原有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})`);

2. 初始化滑块比例尺和轴

使用主图表的原始数据范围定义滑块的比例尺。

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

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

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

3. 绘制滑块条的背景和轴

在滑块区域绘制简化的折线图和轴。

// 在数据加载后初始化滑块
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创建交互刷子。

// 创建刷子
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. 封装主图表更新函数

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

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样式调整

添加滑块条样式:

.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();
  }
}

最终效果

  1. X轴下方显示带有数据轮廓的滑块条
  2. 拖动滑块两端可缩放显示范围
  3. 拖动中间区域可平移显示范围
  4. 双击滑块区域重置为完整范围
  5. 主图表所有元素实时响应范围变化

通过这种方式,用户可以通过滑块条直观地控制主图表的显示范围,实现数据的交互式探索。所有图形元素都会根据当前选择的X轴范围动态更新,保持可视化的一致性。


// 设置图表的尺寸和边距
const margin = { top: 50, right: 50, bottom: 100, 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"); // 左对齐

交互,圆圈,准心

show code...

button 切换图表

show code...

js d3 内嵌数据显示折线

show code...

JavaScript + Plotly(纯前端实现)

show code...

使用步骤:

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

Python + Plotly(生成独立HTML文件)

特点:适合Python用户,自动化生成图表文件。

show code...

使用步骤:

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

进阶方案(可选)

  1. 动态数据加载(JavaScript):

View details...

  • 用户可上传任意CSV文件,实时生成图表。
  1. 添加控件(Python + Dash):
    View details...

    • 运行后访问 http://localhost:8050,支持动态交互和按钮触发操作。

Updated by jun chen 4 months ago · 29 revisions