D3.js 移动端柱状图缩放加载更多

89 阅读3分钟

D3.js 移动端柱状图实现文档

功能特性

✅ 动态数据加载
✅ 平滑缩放控制
✅ 点击高亮交互
✅ 自适应Tooltip
✅ 增加双轴线
✅ 移动端触摸优化

核心实现

1. 基础配置

// 设备检测
const isMobile = false // 手动设置移动端模式
const transitionDuration = isMobile ? 100 : 300; // 动画时长配置

// 初始数据集
const initialData = [
  { name: "A", value: 30 },
  { name: "B", value: 80 },
  // ...其他数据
];

2.增加双轴线

创建双轴线

      // 创建 x 轴比例尺 - 用于映射类别数据到水平位置
      // scaleBand() 用于处理离散数据,如柱状图的类别
      const xOriginal = d3
        .scaleBand()
        .domain(currentData.map((d) => d.name)) // 设置域为数据名称数组
        .range([margin.left, width - margin.right | 0]) // 设置值域为图表宽度范围
        .padding(0.1); // 设置柱子之间的间距

      // 创建左侧 y 轴比例尺 - 用于映射柱状图数值到垂直位置
      // scaleLinear() 用于处理连续数值数据
      const yLeftOriginal = d3
        .scaleLinear()
        .domain([0, d3.max(currentData, (d) => d.barValue) * 1.1]) // 设置域为0到最大值的1.1倍
        .nice() // 优化刻度值为较为规整的数字
        .range([height - margin.bottom | 0, margin.top | 0]); // 设置值域为图表高度范围
        
      // 创建右侧 y 轴比例尺 - 用于映射折线图数值到垂直位置
      const yRightOriginal = d3
        .scaleLinear()
        .domain([0, d3.max(currentData, (d) => d.lineValue) * 1.1]) // 设置域为0到最大值的1.1倍
        .nice()
        .range([height - margin.bottom | 0, margin.top | 0]); // 设置值域为图表高度范围

      // 创建比例尺副本用于后续缩放操作
      let x = xOriginal.copy();
      let yLeft = yLeftOriginal.copy();
      let yRight = yRightOriginal.copy();

3. 柱状图高亮逻辑

.on("click", function(event, d) {
  // 清除所有高亮
  g.selectAll(".bar").style("fill", "steelblue");
  // 设置当前高亮
  d3.select(this).style("fill", "#ff7f0e");
  
  // 边界检测逻辑
  const rightOverflow = (rect.left + tooltipRect.width) > window.innerWidth;
  const leftOverflow = (rect.left - tooltipRect.width) < 0;
  
  // 位置自适应算法
  leftPos = rightOverflow && !leftOverflow ? 
    rect.left - tooltipRect.width : 
    (leftOverflow && rightOverflow ? 
      (window.innerWidth - tooltipRect.width)/2 : 
      rect.left);
});

4. 动态数据加载

function fetchMoreData(scale) {
  return new Promise((resolve) => {
    setTimeout(() => {
      if (scale < scaleRatio) {
        // 缩小时追加数据
        resolve([...currentData, ...generateRandomData(5)]);
      } else if (scale > scaleRatio) {
        // 放大时裁剪数据
        resolve(currentData.slice(0, Math.max(3, currentData.length -4)));
      }
    }, 200);
  });
}

5. 缩放控制优化

因为手机端缩放时候缩放

// 获取的缩比例
const k = event.transform.k;

这个 K 值是动态,一直缩小会导致放大的时候如果用固定值无法判断,所以需要每次记录最后的 lastScale,来判断这次的 k 和上次 k 的大小,从而判断用户是放大还是缩小。还有一个问题就是 手机端屏幕 k 值会因为你的缩放速度之类的呈现不规则变换,就是上次缩放大了,第二次如果是放大但是 k 值没错超过上次放大。会导致无法用 k 值的大小来判断是不是缩放。从而 增加了下面的办法,来限制每次都只增加 0.1,从而避免这个问题



const zoom = d3.zoom()
  .on("zoom", (event) => {
    // 缩放平滑处理
    if (Math.abs(currentScale - lastScale) > 0.1) {
      currentScale = currentScale > lastScale 
        ? lastScale + 0.1 
        : lastScale - 0.1;
    }
    
    // 清除可视化状态
    d3.selectAll(".bar").style("fill", "steelblue");
    d3.select(".tooltip").style("opacity", 0);
  });

性能优化

🔧 位运算加速布局计算

.attr("y", height - margin.bottom | 0) // 快速取整

🔧 防抖控制数据请求频率

const debouncedCheckFetch = debounce(checkAndFetchData, 100);

🔧 差异更新DOM元素

const bars = g.selectAll(".bar").data(data, d => d.name);

样式配置

/* 高亮样式 */
.bar-highlighted {
  fill: #ff7f0e;
  stroke: #ff6b0e;
  transition: all 0.3s ease;
}

/* 加载提示 */
.loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

完整的代码