我正在使用d3创建一个多折线图.我 for each 图例条目添加了一个复选框,对应于一行,其 idea 是,人们可以取消选中一个复选框以删除该行,重新选中一个复选框以将其添加回来.

现在的情况是,每次选中复选框时,我都会重新绘制图表.如果这是drawChart函数第一次运行,我会选中每个复选框. 如果选中了复选框,数据将被过滤,drawChart函数将再次运行,这一次将原始数据与新数据进行比较.

虽然它(排序)工作,传奇色彩正在改变,我觉得这是令人难以置信的笨重.有没有更好的方法?

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 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);

  drawChart(long_data, dataNest, tableKeys, "init")
})

function drawChart(data, dataNest, tableKeys, which) {
  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;
  })]);

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

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

  focus
    .selectAll("path")
    .data(dataNest)
    .enter().append("path")
    .attr('class', 'groups')
    .attr("d", d => {
      d.line = this;
      return line(d[1]);
    })
    .style("stroke", d => color(d[0]))
    .style("stroke-width", 1)
    .style('fill', 'none')
    .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);

  // add a legend
  if (which == "init") {
    var legend = timeseries.append('g')
      .attr('class', 'legend')
      .attr("transform", "translate(" + (timeW - timeMargin.right + 10) + "," + timeMargin.top + ")")

    var legendGroups = legend
      .selectAll(".legendGroup")
      .data(tableKeys, d => d)

    var enterGroups = legendGroups
      .enter()
      .append("g")
      .attr("class", d => "legendGroup " + d.replaceAll(" ", "_"))

    legendGroups
      .exit()
      .remove();

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

    enterGroups
      .append("rect")
      .attr("width", 10)
      .attr("height", 10)
      .attr("fill", d => color(d))
      .attr("x", 30)
      .attr("y", (d, i) => i * 20)
      .attr("class", d => d + ' legend-rect')

    enterGroups
      .append("foreignObject")
      .attr("x", 15)
      .attr("y", (d, i) => i * 20)
      .attr("width", 12)
      .attr("height", 13)
      .attr("id", (d, i) => 'a' + i)
      .append("xhtml:div")
      .html("<input type='checkbox' checked class='check'>")
      .attr('class', 'checkcontainer')
  }
  // legend items for all data, but update checkboxes checked
  else if (which == "checkBoxes") {
    let oldKeys = ['18-25', '26-40', '41-55', '56+', '<18']

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

    var legendGroups = legend
      .selectAll(".legendGroup")
      .data(oldKeys, d => d)

    var enterGroups = legendGroups
      .enter()
      .append("g")
      .attr("class", d => "legendGroup " + d.replaceAll(" ", "_"))

    legendGroups
      .exit()
      .remove();

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

    enterGroups
      .append("rect")
      .attr("width", 10)
      .attr("height", 10)
      .attr("fill", d => color(d))
      .attr("x", 30)
      .attr("y", (d, i) => i * 20)
      .attr("class", d => d + ' legend-rect')

    enterGroups
      .append("foreignObject")
      .attr("x", 15)
      .attr("y", (d, i) => i * 20)
      .attr("width", 12)
      .attr("height", 13)
      .attr("id", (d, i) => 'a' + i)
      .append("xhtml:div")
      .html(function(d) {
        if (tableKeys.indexOf(d) >= 0) {
          return "<input type='checkbox' checked class='check'>"
        } else {
          return "<input type='checkbox' class='check'>"
        }
      })
      .attr('class', 'checkcontainer')
  }

  d3.selectAll('.check').on('click', function(d) {
    let isChecked = this.checked

    let parentEnterGroup = d3.select(this.parentNode.parentNode.parentNode)
    let parentGroupClass = parentEnterGroup._groups[0]
    let parentGroupString = parentGroupClass[0].className.baseVal.split(" ")[1]

    if (isChecked !== true) {
      let newData = data.filter(d => d.bucket !== parentGroupString.replaceAll("_", " "))

      let newDataNest = d3.group(newData, d => d.bucket)
      let newTableKeysa = Array.from(newDataNest.keys())

      drawChart(newData, newDataNest, newTableKeysa, "checkBoxes")
    } else {
      console.log('add it back')
    }
  })

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

.check {
  width: 11px;
  height: 12px;
  filter: grayscale(1);
  margin: 0;
  margin-top: -1px !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<div id="chart"></div>

推荐答案

我个人不喜欢在SVG中使用复选框作为外来对象.虽然您可以保留它们,但我将使用SVG矩形,并在点击时切换它们的填充,使用笔划来保持它们的边界可见.尽管这个答案中的方法也适用于复选框.

我们需要一些方法来跟踪一个框是否被点击,为了方便起见,我只是在复选框的数据中添加了一个属性,d.clicked.当点击一个框时,我们:

  • 在false/true之间切换基准点的值
  • 删除或重新添加其填充的基础上d.点击
  • 使用css隐藏/显示相关的图形线

下面用占位符说明了这一点:

let data = ["one","two","three"];
let colors = ["crimson","steelblue","forestgreen"]

let svg = d3.select("body").append("svg")

let clickedItems = [];

let buttons = svg.selectAll("g")
  .data(data)
  .enter()
  .append("g")
  // use an object as the datum to allow easier modification later:
  .datum(function(d) { return {name: d, clicked: false}})
  .attr("transform", (d,i)=>"translate("+[5,i*20+10]+")");
  
buttons.append("text")
  .attr("y", 10)
  .attr("x", 16)
  .text(d=>d.name)
  
buttons.append("rect")
  .attr("width", 12)
  .attr("height",12)
  .attr("fill", (d,i)=>colors[i])
  .attr("stroke", (d,i)=>colors[i])
  .style("cursor", "pointer")
  .attr("class", "legend-rect")
  .attr("stroke-width", 2)
  .on("click", function(event,d) {
      // Get the index of the node to help select corresponding data:
      // Unused here as I don't have a corresponding graph
      //let i = d3.selectAll(".legend-rect").nodes().indexOf(this);
      
      // record whether a box is clicked or not:
      d.clicked = !d.clicked;
      
      // toggle the rectangle's fill
      d3.select(this).attr("fill", d.clicked ? "transparent" : colors[i]);
      
      // Hide/show the data logic:
      // Unused here as I don't have a corresponding graph
      // d3.selectAll("#identifier"+i).style("display", d.clicked ? "none" : "");
        
      // If we want to track if all boxes are unchecked:
      // Track which boxes are checked:
      d.clicked ? clickedItems.push(d.name) : clickedItems.splice(clickedItems.indexOf(d.name),1);  
       
      // If all are empty:
      if(clickedItems.length == data.length) {
           d3.selectAll(".legend-rect")
              .attr("fill", (d,i) => colors[i])
              .each(d=>d.clicked  = false);
          // clear clicked items array:
          clickedItems = [];
          // Logic for redrawing all data:
            // d3.selectAll(".identifier")
            //  .style("display", "");
      }      
  })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>

除了以上几点,我还添加了一个使用数组(clickedItems)的快速判断,以查看是否所有复选框都未选中,这将导致所有复选框都被重新填充并显示所有数据(再次,只是一个占位符,因为我没有实际的模拟图).另外一个很容易添加的功能是双击删除所有其他行.

我在你的代码中使用过这种方法.我还添加了一些逻辑,以根据显示的数据更新图的比例.有一些优化可以做.

通过使用CSS隐藏/显示数据,数据将始终保持相同的顺序和索引,从而保持相同的 colored颜色 .

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 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);

  drawChart(long_data, dataNest, tableKeys, "init")
})

function drawChart(data, dataNest, tableKeys, which) {

  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;
  })]);

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

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

  focus
    .selectAll("path")
    .data(dataNest)
    .enter().append("path")
    .attr('class', function(d,i) { return 'groups group'+i; })
    .attr("d", d => {
      d.line = this;
      return line(d[1]);
    })
    .style("stroke", d => color(d[0]))
    .style("stroke-width", 1)
    .style('fill', 'none')
    .attr("clip-path", "url(#clip)")

  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);

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

  var legendGroups = legend
      .selectAll(".legendGroup")
      .data(tableKeys, d => d)
      .enter()
       .datum((d)=> d={clicked: false, name: d} )
      .append("g")
      .attr("transform", (d,i)=>"translate("+[0,i*20]+")");
      
    legendGroups.append("text")
      .attr('class', 'legend-text')
      .text(d => d.name)
      .attr("x", 45)
      .attr("y", 10);
 

    legendGroups.append("rect")
      .attr("width", 10)
      .attr("height", 10)
      .attr("fill", d => color(d))
      .attr("stroke", d=>color(d))
      .attr("stroke-width", 2)
      .style("cursor","pointer")
      .attr("x", 30)
      .attr("class", d => ' legend-rect')
      .on("click", function(event,d) {
            // record whether it is clicked or not:
            d.clicked = !d.clicked;
            // toggle the rectangle's fill
            d3.select(this).attr("fill", d.clicked ? "transparent" : d=>color(d.name));
            // hide/show the associated line
            let i = d3.selectAll(".legend-rect").nodes().indexOf(this);
            d3.selectAll(".group"+i).style("display", d.clicked ? "none" : "");
            
            // Logic for all empty:
            // keep track of unchecked boxes (also used in rescaling below):
            d.clicked ? clickedItems.push(d.name) : clickedItems.splice(clickedItems.indexOf(d.name),1);           
            
            // If all are empty:
            if(clickedItems.length == tableKeys.length) {
              d3.selectAll(".legend-rect")
                .attr("fill", d => color(d.name))
                .each(d=>d.clicked  = false);
              d3.selectAll(".groups")
                .style("display", "");
              clickedItems = [];
            }            
          
            // For re-scaling:
            // filter scale values       
            x2.domain(d3.extent(data.filter(d=>clickedItems.indexOf(d.bucket) == -1), function(d) {
              return d.date;
            }));
            y2.domain([0, d3.max(data.filter(d=>clickedItems.indexOf(d.bucket) == -1), function(dd) {
                return dd.value;
            })]);
           
            gYAxis.call(yAxis)
            gXAxis.call(xAxis)
            
            focus.selectAll("path.groups")
              .transition() // May want to use clip area for the line here.
              .attr("d", d => {
                return line(d[1]);
             })
            
            
            
      })
      
     

  
 
  

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

.check {
  width: 11px;
  height: 12px;
  filter: grayscale(1);
  margin: 0;
  margin-top: -1px !important;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"></script>
<div id="chart"></div>

Javascript相关问答推荐

JavaScript文本区域阻止KeyDown/KeyUp事件本身上的Alt GR +键组合

materialized - activeIndex返回-1

通过使用100%间隔时间来代表我们还剩多少时间来倒计时

传递一个大对象以在Express布局中呈现

colored颜色 检测JS,平均图像 colored颜色 检测JS

如何从URL获取令牌?

无法从NextJS组件传递函数作为参数'

在open shadow—root中匹配时,使用jQuery删除一个封闭的div类

如何强制Sphinx中的自定义js/css文件始终加载更改而不缓存?

更改预请求脚本中重用的JSON主体变量- Postman

禁用.js文件扩展名并从目录导入隐式根index.js时,找不到NodeJS导入模块

AddEventListner,按键事件不工作

如何根据输入数量正确显示alert ?

用另一个带有类名的div包装元素

重新呈现-react -筛选数据过多

Chart.js Hover线条在鼠标离开时不会消失

固定动态、self 调整的优先级队列

如何创建一个for循环,用于计算仪器刻度长度并将其放入一个HTML表中?

MUI-TABLE:MUI表组件中交替行的不同 colored颜色 不起作用

:host::ng-Deep不将样式应用于material 复选框