bwrr
bwrr

Reputation: 621

How to update data in d3 chart with an onchange slider event?

Attached is my minimum working example in JS Fiddle. I am able to only show the initial data, but when I change the slider, the data does not update. Where am I going wrong in my code here? Apologies for the poor structure - I am still a beginner in D3.

https://jsfiddle.net/pv02z8em/1/

   chartGroup
    .selectAll('.line-series')
    .data(data, d=> d.x)
    .join(
      enter => {
        enter.append('path')
        .attr('class', d => `line-series x_${d.x}`)
        .attr("d", drawLine(data))
        .style('stroke', 'dodgerblue')
        .style('stroke-width', 2)
        .style('fill', 'none')
        .style('opacity', 1)
      },
      update => { update.transition().duration(500) },
      exit => { exit.remove() }
    )
}

Upvotes: 1

Views: 941

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

You have several issues in that code:

  1. You are not selecting the slider by its ID. It should be d3.select('#slider-x-range');

  2. In the listener, you're not calling buildLine(data);

  3. Remove everything inside buildLine that isn't related to the path itself, otherwise you'll create different SVGs every time the user moves the slider;

  4. Your join structure is currently appending several paths, one above the other. This is certainly not what you want. It could be just:

    let path = chartGroup.selectAll('.line-series')
        .data([data]);
    
    path = path.enter()
        .append('path')
        .attr('class', "line-series")
        .attr("d", d => drawLine(d))
        .style('stroke', 'dodgerblue')
        .style('stroke-width', 2)
        .style('fill', 'none')
        .style('opacity', 1)
        .merge(path)
    
    path.transition().duration(500).attr("d", d => drawLine(d))
    

Here is your code with these and other minor changes:

<!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">

    <style>
      svg {
        display: inline-block;
        position: relative;
        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;
      }

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

  <body>
    <div class="chart-group-container">
      <div class="row">
        <div class="six columns">
          <div class="chart-container">
            <div class="viz">

            </div>
          </div>
        </div>
        <div class="six columns"></div>
        <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>


      <script src="https://d3js.org/d3.v5.min.js"></script>
      <script>
        //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;
        }

      </script>
      <script>
        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.curveBasis);


        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 xAxis = d3.axisBottom(xScale).tickSizeOuter(0);

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

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

        const yAxisDraw = svg
          .append('g')
          .attr('class', 'y-axis')
          .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)])

          let path = chartGroup
            .selectAll('.line-series')
            .data([data]);

          path = path.enter().append('path')
            .attr('class', "line-series")
            .attr("d", d => drawLine(d))
            .style('stroke', 'dodgerblue')
            .style('stroke-width', 2)
            .style('fill', 'none')
            .style('opacity', 1)
            .merge(path)

          path.transition().duration(500).attr("d", d => drawLine(d))
        }

        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 => {
            data = generateData(xRangeSlider.value)
            buildLine(data)
          });

      </script>
  </body>

</html>

As you'll find out, the transition is not what you probably expect: that's an unrelated issue, the interpolation of the d property string (you can find more info here).

Upvotes: 1

Related Questions