首页 > 解决方案 > 如何在数据更新期间同步散点和折线图?

问题描述

我有一个滑块,可以调整生成数据的函数中的参数。到目前为止,当更改此滑块时,我能够正确调整折线图。但是,散点似乎到处都是,我不知道为什么要这样做。

有没有办法将散点绑定到折线图,以便它们同步更新?

我也不确定退出选择是否正常工作。理想情况下,如果数据大小增加,我希望图表附加,反之亦然。

附件是一个最小的工作示例。

https://jsfiddle.net/jsnewb284/85j6n9ct/

chartGroup
  .selectAll(".line-series")
  .data([data])
  .enter()
    .append("path")
    .attr('class', 'line-series')
    .attr("d", d => drawLine(d))
    .attr("fill", 'none')
    .attr("stroke", "black")
    .attr("stroke-width", 1.5)
    .merge(lines)

chartGroup
  .selectAll(".scatter-points")
  .data(data)
  .enter()
    .append("g")
    .attr('class', 'scatter-points')
    .append("circle")
    .attr("cx", d => xScale(d.x))
    .attr("cy", d => yScale(d.y))
    .attr("r", 5)
    .attr("fill", "black")
    // .merge(scatter)       

lines.attr("d", d => drawLine(d))
scatter.attr("cx", function(d,i){
  let tmp = data[i]
  return xScale(tmp)
}).attr("cy", function(d,i){
  let tmp = data[i]
  return yScale(tmp)
});

lines.exit().remove()
scatter.exit().remove()

标签: d3.js

解决方案


在每次滑块移动时更新 x 轴和 y 轴域,因此您还应该更新轴。这可以通过将轴绘制代码放入函数中并在数据更改时调用它来快速解决。

  1. 数据绑定键功能

你离那里很近。之所以没有按照您的预期行事scatter.enter()scatter.exit()是因为默认情况下,d3.js 使用数组索引作为每个项目的键(即 ID)绑定数据。这就是它识别要添加的新项目、不需要重绘的现有项目以及要删除的旧项目的方式。它适用于许多情况。但在你的情况下它没有,因为你的轴改变了。因此,每个点,即使它们在更改之前已经存在于数据中,也必须以新的 x/y 比例重新绘制。告诉它重绘它们的一种方法是在绑定时将键设置为 null ( .data(data, d => null))。另一种方法是简单地选择所有点,使用.remove().append()不使用enter()/exit()绑定删除并重绘它们。

完整示例:

//let sinWave = Math.sin(x)

let range = function(start, stop, step) {
  step = step || 1;
  let arr = []
  for (let i = start; i < stop; i += step) {
    arr.push(i);
  }
  return arr;
}

let generateSinWave = function(x) {
  let y = []
  x.forEach(function(i) {
    y.push(Math.sin(i))
  });
  return y;
}

const generateData = (n) => {
  x = range(0, n, 1)
  y = generateSinWave(x)

  let labels = ['x', 'y']

  let data = []
  for (let i = 0; i < x.length; i++) {
    data.push({
      x: x[i],
      y: y[i]
    })
  }
  return data;
}
let margin = {
    top: 50,
    right: 30,
    bottom: 30,
    left: 100
  },
  width = 800 - margin.left - margin.right
height = 400 - margin.top - margin.bottom;


let xScale = d3.scaleLinear()
  .range([0, width])

let yScale = d3.scaleLinear()
  .range([height, 0])
  .nice()


let drawLine = d3.line()
  .x(d => xScale(d.x))
  .y(d => yScale(d.y))
//  .curve(d3.curveNatural);


let svg = d3.select('.viz')
  .append('svg')
  .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`)
  .attr("preserveAspectRatio", "xMinYMin meet")
  //.attr('width', `${width + margin.left + margin.right}px`)
  //.attr('height', `${height + margin.top + margin.bottom}px`)
  //.classed("svg-content", true);
  .append('g')
  .attr('class', 'line-chart-container')
  .attr('transform', `translate(${margin.left}, ${margin.top})`);

/* d3.select(".line-chart-container")
          .attr("style", "outline: thin solid black;") 
          .attr("margin-right", "102px") */



const chartGroup = svg.append('g').attr('class', 'line-chart')

// Draw x axis
const xAxisDraw = svg
  .append('g')
  .attr('class', 'x-axis')
  //.style('font', '14px sans-serif')
  .attr('transform', `translate(0, ${height / 2})`);

const yAxisDraw = svg
  .append('g')
  .attr('class', 'y-axis');

function drawAxis() {
  const xAxis = d3.axisBottom(xScale).tickSizeOuter(0);

  xAxisDraw.call(xAxis);

  const yAxis = d3
    .axisLeft(yScale)
    .ticks(10)
  //.tickSizeInner(-width);

  yAxisDraw.call(yAxis);
}

// x axis label
svg.append('text')
  .attr('class', 'axis-label')
  .attr('text-anchor', 'end')
  .text('X axis')
  .attr('x', width)
  .attr('y', height - margin.bottom + 50)

// y axis label
svg.append('text')
  .attr('class', 'axis-label')
  .attr('text-anchor', 'end')
  .attr('transform', 'rotate(-90)')
  .attr('x', margin.top + 50 - (height / 2))
  .attr('y', margin.left - 160)
  .text('Y axis')

// Draw Header
const header = svg
  .append('g')
  .attr('class', 'chart-title')
  .attr('transform', `translate(${width / 2 - 75}, ${margin.top - 75})`)
  .append('text')

header.append('tspan').text('Sine wave')

function buildLine(data) {
  xScale.domain([d3.min(data, d => d.x), d3.max(data, d => d.x)])
  yScale.domain([d3.min(data, d => d.y), d3.max(data, d => d.y)])

  drawAxis();

  data = data.sort(function(a, b) {
    return a - b
  })

  let lines = chartGroup.selectAll(".line-series").data([data]);

  let scatter = chartGroup.selectAll(".scatter-points").data(data, d => null);

  lines.enter()
    .append("path")
    .attr('class', 'line-series')
    .attr("d", d => drawLine(d))
    .attr("fill", 'none')
    .attr("stroke", "black")
    .attr("stroke-width", 1.5)
    .merge(lines)

  scatter.enter()
    .append("g")
    .attr('class', 'scatter-points')
    .append("circle")
    .attr("cx", d => xScale(d.x))
    .attr("cy", d => yScale(d.y))
    .attr("r", 5)
    .attr("fill", "black")
  // .merge(scatter)       

  lines.attr("d", d => drawLine(d))
  scatter.attr("cx", function(d, i) {
    let tmp = data[i]
    return xScale(tmp)
  }).attr("cy", function(d, i) {
    let tmp = data[i]
    return yScale(tmp)
  });
  scatter.exit().remove()
  //console.log(data.length)

}

let xRangeSlider = document.getElementById('slider-x-range');
xRangeSlider.min = 10;
xRangeSlider.max = 100;

let data = generateData(xRangeSlider.value)
buildLine(data)

d3.select('#slider-x-range')
  .on("change", d => {
    // d3.selectAll(".scatter-points").data(data).exit().//remove()
    data = generateData(xRangeSlider.value)
    buildLine(data)
  });
svg {
  display: inline-block;
  position: relative;
  width: 100%;
  padding-bottom: 100%;
  vertical-align: top;
  overflow: hidden;
}

.x-axis,
.y-axis {
  font: 16px sans-serif;
}

.axis-label {
  font: 18px sans-serif;
}

.chart-title {
  font: 24px sans-serif;
}

.x-axis .tick:first-of-type text {
  fill: none;
}

.body {
  display: flex;
}

.chart-group-container {
  margin: 10px 20px 20px 20px;
}

.controls-container {
  display: flex;
  flex-direction: column;
}

.controls-header {
  color: black;
  padding: 0.5rem 2rem;
  text-align: left;
}

.controls-body {
  overflow: auto;
  font-size: 0.8em;
  cursor: default;
}

.slidecontainer {
  text-align: left;
  margin: 10px;
  font-family: sans-serif;
  font-size: 14px;
}

#slider-x-range {
  vertical-align: bottom;
}
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="css/normalize.css">
  <link rel="stylesheet" href="css/skeleton.css">
  <link rel="stylesheet" href="css/skeleton_override.css">

  <title>Document</title>
</head>

<body>
  <div class="chart-group-container">
    <div class="row">
      <div class="controls-container">
        <div class="controls-header">UI Controls</div>
        <div class="controls-body">
          <div class="slider-label">Adjust x axis</div>
          <div class="slidecontainer">
            <span>10</span>
            <input type="range" min="10" max="100" value="1" id="slider-x-range">
            <span>100</span>
          </div>
        </div>
      </div>
    </div>
    <div class="six columns">
      <div class="chart-container">
        <div class="viz">

        </div>
      </div>
    </div>
    <script src="https://d3js.org/d3.v5.min.js"></script>
</body>

</html>


推荐阅读