You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在D3.js多折线图中默认隐藏两条折线并支持单独显示

修复D3.js折线图默认隐藏分组的单独控制问题

你遇到的问题根源是:初始绘制时未给默认隐藏的折线设置display: none,且图例矩形的初始状态未匹配隐藏逻辑。当点击其中一个分组时,所有折线的路径被重新生成,未被操作的隐藏分组会意外显示。以下是具体修复方案:

关键修改点

  1. 初始绘制时设置折线的display状态:直接生成所有折线路径,用display属性控制初始可见性,确保默认隐藏的折线一开始就不可见。
  2. 图例矩形初始状态匹配隐藏逻辑:默认隐藏的分组对应图例矩形填充设为透明,与交互后的状态保持一致。
  3. 保持分组状态独立控制:每个分组的显示/隐藏仅由自身的clicked属性决定,互不干扰。

修改后的完整代码

JavaScript

let timeW = 960,
  timeH = 450

let timeMargin = {
    top: 20,
    right: 300,
    bottom: 80,
    left: 60
  },
  timeWidth = timeW - timeMargin.left - timeMargin.right,
  timeHeight = timeH - timeMargin.top - timeMargin.bottom;

var x2 = d3.scaleTime().range([0, timeWidth]),
  y2 = d3.scaleLinear().range([timeHeight, 0]);

var xAxis = d3.axisBottom(x2),
  yAxis = d3.axisLeft(y2);

var line = d3.line()
  .x(function(d) {
    return x2(d.date);
  })
  .y(function(d) {
    return y2(d.value);
  });

const seriesColors = ['#ff3300', 'royalblue', 'green', 'turquoise', 'navy']

var color = d3.scaleOrdinal()
  .range(seriesColors);

const parseDate = d3.timeParse("%Y%m%d");

d3.csv("https://raw.githubusercontent.com/sprucegoose1/sample-data/main/age.csv").then(function(data) {

  var long_data = [];
  data.forEach(function(row) {
    row.date = parseDate(row.Date)
    let tableKeys = data.columns.slice(1);
    Object.keys(row).forEach(function(colname) {
      if (colname !== "date" && colname !== "Date") {
        long_data.push({
          "date": row["date"],
          "value": +row[colname],
          "bucket": colname
        });
      }
    });
  });

  data.sort((a, b) => a.date - b.date)

  let dataNest = d3.group(long_data, d => d.bucket)
  let tableKeys = data.columns.slice(1);

  let grouped_data_array = Array.from(dataNest)
  grouped_data_array.forEach(d => {
    d[0] == "<18" || d[0] == "56+" ? d.clicked = true : d.clicked = false
  })

  drawChart(long_data, grouped_data_array, tableKeys)
})

function drawChart(data, dataNest, tableKeys) {
  d3.select("#timeseries").remove()

  let timeseries = d3.select("#chart").append('svg')
    .attr('id', 'timeseries')
    .attr("width", timeWidth + timeMargin.left + timeMargin.right)
    .attr("height", timeHeight + timeMargin.top + timeMargin.bottom)

  var graph = timeseries.append('g').attr('transform', 'translate(' + timeMargin.left + ',' + timeMargin.top + ')');

  var focus = timeseries.append("g")
    .attr("class", "focus")
    .attr("transform", "translate(" + timeMargin.left + "," + timeMargin.top + ")");

  x2.domain(d3.extent(data, function(d) {
    return d.date;
  }));
  y2.domain([0, d3.max(data, function(d) {
    return d.value;
  })]);

  focus
    .selectAll("path")
    .data(dataNest)
    .enter().append("path")
    .attr('class', d => 'groups a' + d[0].replaceAll("+", "").replaceAll("<", "").replaceAll(" ", "_"))
    .attr("d", d => line(d[1]))
    .style("stroke", d => color(d[0]))
    .style("stroke-width", 1)
    .style('fill', 'none')
    .style("display", d => d.clicked ? "none" : "block")
    .attr("clip-path", "url(#clip)")

  focus.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + timeHeight + ")")
    .call(xAxis);

  focus.append("g")
    .attr("class", "axis axis--y")
    .call(yAxis);

  var gXAxis = focus.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + timeHeight + ")")
    .call(xAxis);

  var gYAxis = focus.append("g")
    .attr("class", "axis axis--y")
    .call(yAxis);

  let clickedItems = ["<18", "56+"]

  var legend = timeseries.append('g')
    .attr('class', 'legend')
    .attr("transform", "translate(" + (timeW - timeMargin.right + 10) + "," + timeMargin.top + ")")

  var legendGroups = legend
    .selectAll(".legendGroup")
    .data(dataNest, d => d[0])

  var enterGroups = legendGroups
    .enter()
    .append("g")
    .attr("class", d => "legendGroup " + d[0].replaceAll(" ", "_").replace("<", ""))
    .on("click", function(event, d) {
      d.clicked = !d.clicked;

      // 切换图例矩形填充
      d3.select('rect.a' + d[0].replaceAll(" ", "_").replace("+", "").replace("<", "")).attr("fill", d.clicked ? "transparent" : color(d[0]));

      // 切换折线显示
      d3.select("path.a" + d[0].replaceAll(" ", "_").replace("+", "").replace("<", "")).style("display", d.clicked ? "none" : "block");

      // 切换表格行显示(如果有)
      d3.select(".row.a" + d[0].replaceAll(" ", "_").replace("+", "").replace("<", "")).style("display", d.clicked ? "none" : "block");

      let i = d3.selectAll(".legend-rect").nodes().indexOf(this);
      d3.selectAll(".group" + i).style("display", d.clicked ? "none" : "block");

      // 更新clickedItems数组
      d.clicked ? clickedItems.push(d[0]) : clickedItems.splice(clickedItems.indexOf(d[0]), 1);

      // 重新计算坐标轴范围
      const visibleData = data.filter(d => clickedItems.indexOf(d.bucket) === -1);
      x2.domain(d3.extent(visibleData, d => d.date));
      y2.domain([0, d3.max(visibleData, d => d.value)]);

      gYAxis.call(yAxis)
      gXAxis.call(xAxis)

      // 重新绘制所有折线
      focus.selectAll("path.groups")
        .transition()
        .attr("d", d => line(d[1]))
    })

  legendGroups
    .exit()
    .remove();

  enterGroups
    .append("rect")
    .attr("width", 10)
    .attr("height", 10)
    .attr("fill", d => d.clicked ? "transparent" : color(d[0]))
    .attr("stroke", d => color(d[0]))
    .attr("stroke-width", 2)
    .attr('stroke-opacity', 1)
    .attr("x", 30)
    .attr("y", (d, i) => i * 20)
    .attr("class", d => "a" + d[0].replace("+", "").replace("<", "") + ' legend-rect')

  enterGroups
    .append("text")
    .attr('class', 'legend-text')
    .text(d => d[0])
    .attr("x", 45)
    .attr("y", (d, i) => 10 + i * 20)

  enterGroups
    .append("foreignObject")
    .attr("x", 15)
    .attr("y", (d, i) => i * 19)
    .attr("width", 12)
    .attr("height", 14)
    .attr("id", (d, i) => 'a' + i)
    .append("xhtml:tree")
    .html(d => d[0] == "56+" || d[0] == "<18" ? "<input type='checkbox' class='check'>" : "<input type='checkbox' checked class='check'>")
    .attr('class', 'checkcontainer')
}

CSS

#chart {
  height: 450px;
  width: 760px;
}

.check {
  width: 11px;
  height: 12px;
  filter: grayscale(1);
  margin: 0;
  margin-top: -1px !important;
}

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<div id="chart"></div>

效果说明

  • 初始加载时,<18和56+分组的折线默认隐藏,图例矩形为透明,复选框未勾选。
  • 点击任意一个隐藏分组的图例,仅该分组的折线会显示/隐藏,互不影响。
  • 坐标轴范围会自动适配当前可见的数据,保证图表显示始终合理。

内容的提问来源于stack exchange,提问作者sprucegoose

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 08:52:08