5

I am trying to use an anonymous function to call two other functions but can't really get it to work. I followed this question on SO: Calling two functions on same click event with d3.js but still can't get it right.

The desired effect is that my barchart changes bars and changes color at the same time. This is what I am trying:

My two functions I want to call:

 function selectDataset(d) {
        let value = this.value;
        if (value == "total") {
            change(datasetTotal);
        } else if (value == "option1") {
            change(datasetOption1);
        } else if (value == "option2") {
            change(datasetOption2);
        }
    }

    function changeColor(d) {
         let value = this.value;
        if (value == "total") {
            d3.selectAll("rect")
                    .transition()
                    .duration(2000)
                    .style("fill", 'blue')
        } else if (value == "option1") {
            d3.selectAll("rect")
                    .transition()
                    .duration(2000)
                    .style("fill", 'red')
        } else if (value == "option2") {
           d3.selectAll("rect")
                    .transition()
                    .duration(2000)
                    .style("fill", 'yellow')
        }

    }

My anonymous function:


    d3.selectAll("input").on("change", function(d) {
         selectDataset.call(this, d);
         changeColor.call(this, d);
        });


and here my change function:

function change(dataset) {

        y.domain(dataset.map(function(d) {
            return d.label;
        }));
        x.domain([0, d3.max(dataset, function(d) {
            return d.value;
        })]);

        svg.select(".y.axis").remove();
        svg.select(".x.axis").remove();

        svg.append("g")
            .attr("class", "x axis")
            .attr("transform", "translate(0," + height + ")")
            .call(xAxis);

        svg.append("g")
            .attr("class", "y axis")
            .call(yAxis)
            .append("text")
            .attr("transform", "rotate(0)")
            .attr("x", 50)
            .attr("dx", ".1em")
            .style("text-anchor", "end")
            .text("Option %");

        var bar = svg.selectAll(".bar")
            .data(dataset, function(d) {
                return d.label;
            });

        var barExit = bar.exit().remove();

        var barEnter = bar.enter()
                        .append("g")
                        .attr("class", "bar");

        var barRects = barEnter.append("rect")
            .attr("x", function(d) {
            return x(0);
        })
        .attr("y", function(d) {
            return y(d.label);
        })
        .attr("width", function(d) {
            return x(d.value);
        })
        .attr("height", y.bandwidth());

        var barTexts = barEnter.append("text")
            .attr("x", function(d) {
                return x(d.value) + 10;
            })
            .attr("y", function(d) {
                return y(d.label) + y.bandwidth() / 2;
            })
            .attr("dy", ".35em")
            .text(function(d) {
                return d.value;
            });

        var barRectUpdate = bar.select("rect")
            .transition()
            .duration(3050)
            .attr("x", function(d) {
            return x(0);
        })
        .attr("y", function(d) {
            return y(d.label);
        })
        .attr("width", function(d) {
            return x(d.value);
        })
        .attr("height", y.bandwidth());

        var barTextsUpdate = bar.select("text")
              .transition()
              .duration(3050)
              .attr("x", function(d) {
              return x(d.value) + 10;
            })
            .attr("y", function(d) {
                return y(d.label) + y.bandwidth() / 2;
            })
            .attr("dy", ".35em")
            .text(function(d) {
                return d.value;
            });
    };

my chart

   var margin = {
            top: (parseInt(d3.select('.area-heat-cool').style('height'), 10) / 20),
            right: (parseInt(d3.select('.area-heat-cool').style('width'), 10) / 20),
            bottom: (parseInt(d3.select('.area-heat-cool').style('height'), 10) / 20),
            left: (parseInt(d3.select('.area-heat-cool').style('width'), 10) / 5)
        },
        width = parseInt(d3.select('.area-heat-cool').style('width'), 10) - margin.left - margin.right,
        height = parseInt(d3.select('.area-heat-cool').style('height'), 10) - margin.top - margin.bottom;

    var div = d3.select(".area-heat-cool").append("div").attr("class", "toolTip");

    var y = d3.scaleBand()
        .rangeRound([height, 0], .2, 0.5)
        .paddingInner(0.1);

    var x = d3.scaleLinear()
        .range([0, width]);

    var xAxis = d3.axisBottom()
        .scale(x);

    var yAxis = d3.axisLeft()
        .scale(y);

    var svg = d3.select(".area-heat-cool").append("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    svg.append("g")
        .attr("class", "x axis")
        .attr("transform", "translate(0," + height + ")")
        .call(xAxis);

    d3.select("input[value=\"total\"]").property("checked", true);
    change(datasetTotal);

dataset example:

data1 = [{label: "example 1", value: 156}
{label: "example 2", value: 189}
{label: "example 3", value: 234}
{label: "example 4", value: 345}
{label: "example 5", value: 346}
{label: "example 6", value: 456}
{label: "example 7", value: 489}
{label: "example 8", value: 567}]; 


data2 = [{label: "example 1", value: 23}
{label: "example 2", value: 211}
{label: "example 3", value: 45}
{label: "example 4", value: 64}
{label: "example 5", value: 95}
{label: "example 6", value: 32}
{label: "example 7", value: 0}
{label: "example 8", value: 234}]; 

And my radio buttons:

<div class="area-heat-cool">
<form>
    <label><input type="radio" name="dataset" id="dataset" value="total" checked> Total</label>
    <label><input type="radio" name="dataset" id="dataset" value="option1"> Option 1</label>
    <label><input type="radio" name="dataset" id="dataset" value="option2"> Option 2</label>
</form>
</div>

When I do this it somehow messes up my data. The color changes but the bars don't move to their right positions anymore. I don't get any errors thrown but the behaviour is not the expected.

Am I using this anonymous function correctly? Any help is very much appreciated. Thanks a lot in advance

Micromegas
  • 1,499
  • 2
  • 20
  • 49
  • Well where is the "change" function? we can only see the "changeColor", but you refer to "change" for the data sets – MKougiouris Aug 19 '19 at 13:18
  • Can you post the rest of the code where the chart is generated, a snippet would be better still. – Vince Aug 19 '19 at 13:24
  • @MKougiouris the change function is called within the ```selectDataset``` method which works correctly when called on its own – Micromegas Aug 19 '19 at 13:27
  • @Micromegas yes, but where is the definition? show us the function so that we can take a look "inside" – MKougiouris Aug 19 '19 at 13:28
  • a yes, apologies, I updated the question – Micromegas Aug 19 '19 at 13:30
  • @VincentChinner this is also most of the code. The rest is only creating the bars. I can also post it though if it helps – Micromegas Aug 19 '19 at 13:32
  • Do you call the change function when you initialise the chart. The reason i asked for a snippet, is that its easier to get to a solution when you see exactly what goes wrong, as "messes up" can mean many things. If you are unable to provide a snippet, you can just add the rest of the code including the markup and a sample of the datasource. – Vince Aug 19 '19 at 14:14
  • What does your input elements look like? I'm asking because the `change` event can trigger differently depending on the element it's attached to: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event – matthias.rocks Aug 19 '19 at 14:40
  • @VincentChinner yes absolutely, sorry! I updated the question with the rest of the code. I will try and add a snippet soon. I was assuming I am just calling my functions incorrectly because separately they work just fine. That's why I tried to not add too much confusing code. – Micromegas Aug 19 '19 at 17:14
  • @matthias.rocks I also added the input elements – Micromegas Aug 19 '19 at 17:14

1 Answers1

3

What happens is that you are applying two transitions to your rect elements when triggering an event. However, when doing that the second transition overwrites the first one.

Then first transition is applied in the change function where you change the size of the rect elements. When the change function returns, you immediately call the changeColor function, which selects all rect elements and applies a different transition that changes the color of the rect elements. Thus the color transition overwrites the size transition.

To fix this, you could apply all transitions to the same elements in one place. In my example I'm going to putting the code from the changeColor function into the change function.

First, change the change function declaration to enable including the radio select value:

function change(dataset, optionSelect) {
  // code not shown
}

Then, update the code in the change that performs the transition of the rect elements:

var barRectUpdate = bar.select("rect")
  .transition()
  .duration(3050)
  .attr("x", function(d) {
    return x(0);
  })
  .attr("y", function(d) {
    return y(d.label);
  })
  .attr("width", function(d) {
    return x(d.value);
  })
  .attr("height", y.bandwidth())
  .style('fill', function () {
    if (optionSelect === "total") {
      return 'blue'
    } else if (optionSelect === "option1") {
      return 'red'
    } else if (optionSelect === "option2") {
      return 'yellow'
    }
  });

Update the selectDataset function to include the radio select value when calling change:

function selectDataset(d) {
  let value = this.value;
  if (value === "total") {
    change(datasetTotal, value);
  } else if (value === "option1") {
    change(datasetOption1, value);
  } else if (value === "option2") {
    change(datasetOption2, value);
  }
}

Remove the call to the changeColor function:

d3.selectAll("input").on("change", function(d) {
  selectDataset.call(this, d);
});

And, finally, remove the changeColor function altogether.

A couple of other things to consider:

  • id attributes should be unique. Right now all your input elements have the same id.
  • Consider changing your string value checks in selectDataset and changeColor to === instead of == to avoid unexpected type conversion when comparing values: Difference between == and === in JavaScript
  • You don't need to store all D3 operations in variables when you are not using those variables afterwards. E.g. bar.select("rect") will work just as well as var barRectUpdate = bar.select("rect").

Hope this helps!

matthias.rocks
  • 625
  • 3
  • 7
  • Oh my... thanks matthias, you really rock! Works exactly like it should.... Well there is a minor thing left, namely that two bars don't change colors. But I guess that could be sth in my css file, I'll check that and comment here. But both transitions work now, thank you so much. I also changed the comparison operators and deleted the variables I don't use. Thank you! – Micromegas Aug 20 '19 at 07:23