Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
446 views
in Technique[技术] by (71.8m points)

javascript - D3js Updating Histogram elements not working (General Update Pattern)

I am trying to accomplish something similar to what is here : https://www.opportunityatlas.org/. If you proceed further to this link and click on 'Show Distribution' to see the graph and select 'On Screen' and then move the cursor around the map you will see the size of the rectangles changes and also the update patterns works i.e. if a rectangle was already there it moves horizontally to the new value.

I have tried doing the same but could not achieve the update part. Could you please point me out where I missed out. I have attached a part of my code where there are two data sets data1 and data2 with some id's common but as you can see when you click on update to change the data set all rectangles are in the enter phase and none of them change position for existing ones (none in update phase). It will be helpful if someone can guide me through this one. If there is some other way to implement the same graph that I provided in the link, that would also be helpful if there is some other approach. Thanks in advance !

let initialRender = true;
let selectedData1 = true;
const margin = {
    top: 10,
    right: 30,
    bottom: 30,
    left: 30
  },
  width = 550 - margin.left - margin.right,
  height = 150 - margin.top - margin.bottom;

function printChart(asd, data, dataGradients) {
  const svg = d3.select('#data-viz')
  // const isZoomed = map.getZoom() > zoomThreshold;

  // if (isZoomed !== changedZoom) {
  //   initialRender = true;
  //   changedZoom = isZoomed;
  // }

  // X axis and scale ------------->>>>>>>>>>>>>>>>>>>>
  const xScale = d3.scaleLinear()
    .domain(d3.extent(data.map(d => d.value)))
    .range([0, width])

  const xAxisCall = d3.axisBottom(xScale)
    .tickFormat(d3.format(".2s"))
    .ticks(5)
    .tickSizeOuter(0);

  let xAxis = null
  if (initialRender) {
    d3.select(".axis-x").remove()
    xAxis = svg.append("g")
      .attr("class", "axis-x")
      .attr("transform", "translate(0," + 115 + ")")
    initialRender = false
  } else {
    xAxis = d3.select(".axis-x")
  }

  xAxis.transition()
    .duration(2000)
    .ease(d3.easeSinInOut)
    .call(xAxisCall)
  // X axis and scale <<<<<<<<<<<<<<<<-----------------------------
  const binMin = 5;
  const binMax = 150;
  const tDuration = 3000;

  // Just to calculate max elements in each bin ---------->>>>>>>>>>>>>>>>>>
  let histogram = d3.histogram()
    .value(d => d.value)
    .domain(xScale.domain())
    .thresholds(xScale.ticks(10));

  let bins = histogram(data).filter(d => d.length > 0);
  console.log(bins);
  const max = d3.max(bins.map(bin => bin.length))
  const maxBinSize = max <= 10 ? 10 : max
  // Just to calculate max elements in each bin <<<<<<<<<<<<----------------

  // Decide parameters for histogram ------------>>>>>>>>>>>>>>>>>
  const dotSizeScale = d3.scaleLinear()
    .domain([binMin, binMax])
    .range([10, 4])
  const dotSize = dotSizeScale(maxBinSize);

  const dotSpacingScale = d3.scaleLinear()
    .domain([binMin, binMax])
    .range([12, 6])
  const dotSpacing = dotSpacingScale(maxBinSize);

  const thresholdScale = d3.scaleLinear()
    .domain([binMin, binMax])
    .range([10, 100])
  const threshold = thresholdScale(maxBinSize);

  const yTransformMarginScale = d3.scaleLinear()
    .domain([binMin, binMax])
    .range([100, 100])
  const yTransformMargin = yTransformMarginScale(maxBinSize);

  if (dotSize !== 10) {
    d3.selectAll('.gBin').remove()
    d3.selectAll('rect').remove()
  }

  histogram = d3.histogram()
    .value(d => d.value)
    .domain(xScale.domain())
    .thresholds(xScale.ticks(threshold));

  bins = histogram(data).filter(d => d.length > 0);
  // Decide parameters for histogram <<<<<<<<<<<<<<<<<<<<--------------------------

  // Y axis scale -------------------->>>>>>>>>>>>>>>>>>>>
  var yScale = d3.scaleLinear()
    .range([height, 0]);

  yScale.domain([0, d3.max(bins, (d) => d.length)]);
  svg.append("g")
    .attr("class", "axis-y")
    .call(d3.axisLeft(yScale));
  d3.select(".axis-y")
    .remove()
  // Y axis scale <<<<<<<<<<<<<<<<<<<<<<<-----------------

  const binGroup = svg.selectAll(".gBin")
    .data(bins,
      (d) => {
        console.log('id 1', d.x0)
        return d.x0
      }
    )

  binGroup
    .exit()
    .transition()
    .duration(2000)
    .style("opacity", 0)
    .remove()

  const binGroupEnter = binGroup
    .enter()
    .append("g")
    .merge(binGroup)
    .attr("class", "gBin")
    .attr("x", 1)
    .attr("transform", function(d) {
      return "translate(" + xScale(d.x0) + "," + yTransformMargin + ")";
    })
    .attr("width", 10)

  const elements = binGroupEnter.selectAll("rect")
    .data(d => d.map((p, i) => ({
        id: p.id,
        idx: i,
        value: p.value,
      })),
      function(d) {
        console.log('id 2', d)
        return d.id
      }
    )

  elements.exit()
    .transition()
    .duration(tDuration)
    .style("opacity", 0)
    .remove()

  elements
    .enter()
    .append("rect")
    .merge(elements)
    .attr("y", -(height + margin.top))
    // .on("mouseover", tooltipOn)
    // .on("mouseout", tooltipOff)
    .transition()
    .delay(function(d, i) {
      return 50 * i;
    })
    .duration(tDuration)
    .attr("id", d => d.value)
    .attr("y", (d, i) => -(i * dotSpacing))
    .attr("width", dotSize)
    .attr("height", dotSize)
    // .style("fill", (d) => getBinColor(d.value, dataGradients))
    .style("fill", 'red')
}

const data1 = [{
  id: 1,
  value: 14
}, {
  id: 13,
  value: 12
}, {
  id: 2,
  value: 50
}, {
  id: 32,
  value: 142
}]
const data2 = [{
  id: 1,
  value: 135
}, {
  id: 7,
  value: 2
}, {
  id: 2,
  value: 50
}, {
  id: 32,
  value: 50
}]
printChart(null, data1, null)

function changeData() {
  selectedData1 ?
    printChart(null, data2, null) :
    printChart(null, data1, null)
  selectedData1 = !selectedData1
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<button onclick="changeData()"> Update data </button>
<svg width="550" height="250" id="data-viz">
      <g transform="translate(30, 100)">
      </g>
    </svg>
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

Your problem appears to be these lines:

if (dotSize !== 10) {
  d3.selectAll('.gBin').remove();
  d3.selectAll('rect').remove();
}

All your elements are removed before any selections are calculated so everything (both your bin g and element rect) become enter.

Another interesting thing is your data key for your bins. Since you are using the x0 your g will also enter/exit depending on how the histogram function calculates the bins.

<html>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
  <button onclick="changeData()">Update data</button>
  <svg width="550" height="250" id="data-viz">
    <g transform="translate(30, 100)"></g>
  </svg>
  <script>
    let initialRender = true;
    let selectedData1 = true;
    const margin = {
        top: 10,
        right: 30,
        bottom: 30,
        left: 30,
      },
      width = 550 - margin.left - margin.right,
      height = 150 - margin.top - margin.bottom;

    function printChart(asd, data, dataGradients) {
    
      console.clear();
    
      const svg = d3.select('#data-viz');
      // const isZoomed = map.getZoom() > zoomThreshold;

      // if (isZoomed !== changedZoom) {
      //   initialRender = true;
      //   changedZoom = isZoomed;
      // }

      // X axis and scale ------------->>>>>>>>>>>>>>>>>>>>
      const xScale = d3
        .scaleLinear()
        .domain(d3.extent(data.map((d) => d.value)))
        .range([0, width]);

      const xAxisCall = d3
        .axisBottom(xScale)
        .tickFormat(d3.format('.2s'))
        .ticks(5)
        .tickSizeOuter(0);

      let xAxis = null;
      if (initialRender) {
        d3.select('.axis-x').remove();
        xAxis = svg
          .append('g')
          .attr('class', 'axis-x')
          .attr('transform', 'translate(0,' + 115 + ')');
        initialRender = false;
      } else {
        xAxis = d3.select('.axis-x');
      }

      xAxis.transition().duration(2000).ease(d3.easeSinInOut).call(xAxisCall);
      // X axis and scale <<<<<<<<<<<<<<<<-----------------------------
      const binMin = 5;
      const binMax = 150;
      const tDuration = 3000;

      // Just to calculate max elements in each bin ---------->>>>>>>>>>>>>>>>>>
      let histogram = d3
        .histogram()
        .value((d) => d.value)
        .domain(xScale.domain())
        .thresholds(xScale.ticks(10));

      let bins = histogram(data).filter((d) => d.length > 0);
      //console.log(bins);
      const max = d3.max(bins.map((bin) => bin.length));
      const maxBinSize = max <= 10 ? 10 : max;
      // Just to calculate max elements in each bin <<<<<<<<<<<<----------------

      // Decide parameters for histogram ------------>>>>>>>>>>>>>>>>>
      const dotSizeScale = d3
        .scaleLinear()
        .domain([binMin, binMax])
        .range([10, 4]);
      const dotSize = dotSizeScale(maxBinSize);

      const dotSpacingScale = d3
        .scaleLinear()
        .domain([binMin, binMax])
        .range([12, 6]);
      const dotSpacing = dotSpacingScale(maxBinSize);

      const thresholdScale = d3
        .scaleLinear()
        .domain([binMin, binMax])
        .range([10, 100]);
      const threshold = thresholdScale(maxBinSize);

      const yTransformMarginScale = d3
        .scaleLinear()
        .domain([binMin, binMax])
        .range([100, 100]);
      const yTransformMargin = yTransformMarginScale(maxBinSize);

      /*
      if (dotSize !== 10) {
        d3.selectAll('.gBin').remove()
        d3.selectAll('rect').remove()
      }
      */

      histogram = d3
        .histogram()
        .value((d) => d.value)
        .domain(xScale.domain())
        .thresholds(xScale.ticks(threshold));

      bins = histogram(data).filter((d) => d.length > 0);
      // Decide parameters for histogram <<<<<<<<<<<<<<<<<<<<--------------------------

      // Y axis scale -------------------->>>>>>>>>>>>>>>>>>>>
      var yScale = d3.scaleLinear().range([height, 0]);

      yScale.domain([0, d3.max(bins, (d) => d.length)]);
      svg.append('g').attr('class', 'axis-y').call(d3.axisLeft(yScale));
      d3.select('.axis-y').remove();
      // Y axis scale <<<<<<<<<<<<<<<<<<<<<<<-----------------

      const binGroup = svg.selectAll('.gBin').data(bins, (d) => {
        //console.log('id 1', d.x0)
        return d.x0;
      });

      binGroup.exit().transition().duration(2000).style('opacity', 0).remove();

      const binGroupEnter = binGroup
        .enter()
        .append('g')
        .merge(binGroup)
        .attr('class', 'gBin')
        .attr('x', 1)
        .attr('transform', function (d) {
          return 'translate(' + xScale(d.x0) + ',' + yTransformMargin + ')';
        })
        .attr('width', 10);

      const elements = binGroupEnter.selectAll('rect').data(
        (d) =>
          d.map((p, i) => ({
            id: p.id,
            idx: i,
            value: p.value,
          })),
        function (d) {
          //console.log('id 2', d)
          return d.id;
        }
      );

      let eex = elements
        .exit()
        .transition()
        .duration(tDuration)
        .style('opacity', 0)
        .remove();

      console.log("rects exiting", eex.nodes().map(e => "rect" + e.getAttribute('id')))

      let een = elements
        .enter()
        .append('rect')
        .attr('id', (d) => d.value);

      console.log("rects entering", een.nodes().map(e => "rect" + e.getAttribute('id')))

      let eem = een
        .merge(elements);

      console.log("rects merge", eem.nodes().map(e => "rect" + e.getAttribute('id')))

      eem
        .attr('y', -(height + margin.top))
        // .on("mouseover", tooltipOn)
        // .on("mouseout", tooltipOff)
        .transition()
        .delay(function (d, i) {
          return 50 * i;
        })
        .duration(tDuration)
        .attr('y', (d, i) => -(i * dotSpacing))
        .attr('width', dotSize)
        .attr('height', dotSize)
        // .style("fill", (d) => getBinColor(d.value, dataGradients))
        .style('fill', 'red');
    }

    const data1 = [
      {
        id: 1,
        value: 14,
      },
      {
        id: 13,
        value: 12,
      },
      {
        id: 2,
        value: 50,
      },
      {
        id: 32,
        value: 142,
      },
    ];
    const data2 = [
      {
        id: 1,
        value: 135,
      },
      {
        id: 7,
        value: 2,
      },
      {
        id: 2,
        value: 50,
      },
      {
        id: 32,
        value: 50,
      },
    ];
    printChart(null, data1, null);

    function changeData() {
      selectedData1
        ? printChart(null, data2, null)
        : printChart(null, data1, null);
      selectedData1 = !selectedData1;
    }
  </script>
</html>

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...