4
\$\begingroup\$

This is my first D3 code. I am self-taught so I am not sure that the chaining of the transition follows the common best practices. Also, is there a way to avoid duplicating the "group" and the "anim" while keeping the sliding effect I have (cf add 4 time the text "you choose : abc")?

<!DOCTYPE html>
<meta charset="utf-8">
<title>Input test</title>
 <div id="div1"> 

<form onSubmit="return false;">
  Choose a number less than 100.000: <input type="text" name="number" id="number"><br>

  </form>
  <svg width="900" height="300" id="pack1">
  </svg>

</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script>

<script>
// get user number and start anim()
var inputElem = d3.select("#number");
inputElem.on("change", anim);

function anim() {
// remove previous animation
d3.selectAll("g").remove();

var number= d3.select(this).property("value");

if (number<100000){
    // general var
    var color = ["red","blue","orange", "green", "darkkhaki"]   



    //  get choices from user : number saved as array 
    var number_as_array = number.toString().split(''); 


    // add 4 time the text "you choose  : abc" 
    for (var i=0; i<4; i++){
    d3.select("svg").append('g')
        .attr('id', function(){return "group"+i})
        .append("text")
        .attr('x', 10)
        .attr('y', 30)
        .attr("font-family", "sans-serif")
        .attr("font-size", "20px")
        .attr("fill", "black")
        .html(function(){
                var text=["You choose :"]
                for (var index=0; index< number_as_array.length; index++){
                var x_position = (index+1)*30+100
                text.push("<tspan fill=\""+ color[index]+" \"y=\"30\"  x=\"" + x_position +"\" class=\"anim" + i+"\"  >" +number_as_array[index]+"</tspan>")
                        }
                return text.join(' ')
            })
    } 

    // start animation 
    d3.select("svg")
    .transition()
        .delay(1000)
        .duration(5000)
        .on("start", function anim1() { 
            // take abc and move them the line below before the =
            d3.selectAll(".anim1").transition().duration(2000)
                .attr("y", 60)
                .attr("x", function (d,i){
                        return 30+30*i
                })
            // take abc and move them the line below after the =
            d3.selectAll(".anim2").transition().duration(2000)
                .attr("y", 60)
                .attr("x", function (d,i){
                        return 50*number_as_array.length+100*i
                })

            d3.selectAll(".anim3").transition().duration(2000)
                .attr("y", 60)
                .attr("x", function (d,i){
                        return 50*number_as_array.length+100*i
                })

            // add "=" with opacity 0
            d3.select("#group1").append("text")
                .attr("id", "egal")
                .style("opacity", 0.0)
                .append("tspan")
                .attr("id", "egaltspan")
                .attr("y", 60)
                .attr("x", function(){
                return (50*number_as_array.length-30-30*(number_as_array.length+1))/2
                +30+30*number_as_array.length
                })
                .attr("font-family", "sans-serif")
                .attr("font-size", "20px")
                .attr("fill", "black")
                .text("=")
                .transition().duration(1000).on("start", function addtext() {

                    //change opacity of = to 1
                    d3.select("#egal").transition().delay(2000).duration(1000).style("opacity", 1.0)})

                    // change the abc on the right to a*100+b*10+c
                    d3.selectAll(".anim2").transition().delay(3000).duration(2000)
                        .text(function (d,i){
                            if (i <(number_as_array.length-1)){
                                return number_as_array[i] + "*" + 10**(number_as_array.length-i-1) +"    +"
                            } else {
                                return number_as_array[i] + "*" + 10**(number_as_array.length-i-1)
                            }
                        })


                    d3.selectAll(".anim3").transition().delay(3000).duration(2000)
                        .text(function (d,i){
                            if (i <(number_as_array.length-1)){
                                return number_as_array[i] + "*" + 10**(number_as_array.length-i-1) +"    +"
                            } else {
                                return number_as_array[i] + "*" + 10**(number_as_array.length-i-1)
                            }
                        })
        })  // end anim1

    // change the abc on the right to  a*10^2+b*10^1+c10^0
    .transition()
        .delay(0)
        .duration(2000)
        .on("start", function anim2() {
                d3.selectAll(".anim3").transition().delay(0).duration(1000)
                .attr("y", 90)
                .transition().delay(0).duration(1000)
                .text(function (d,i){
                    if (i <(number_as_array.length-1)){
                        return number_as_array[i] + "*10^" +(number_as_array.length-i-1)+ "    +"
                    } else {
                        return number_as_array[i]+"*10^"+(number_as_array.length-i-1)       
                    }
                })
        })

    // add circle
    .transition()
        .delay(0)
        .duration(1000)
        .on("start", function () {

        //add the circle : for each digits in the user's number  
        var g_width = 900/number_as_array.length
        var circle_size = g_width/30
        for (var index=0; index< number_as_array.length; index++){

            // create an array which length = digit
            var data = [];
            var length = number_as_array[index];

            for(var i = 0; i < length; i++) {
                circle= {};
                circle.i = i;
                circle.cx = (circle_size*2*i+3)%g_width;
                circle.cy = (120*(circle_size*2*i+3))%g_width;
                data.push(circle);
            };

            // calculate the position of g representing that digit
            var g_position_x= g_width*index + 50

            //create  g for the digit
            var g = d3.select('svg')
                    .append('g')
                    .attr("transform", "translate(" + g_position_x + "," + 120 + ")")
                    .attr("id", function(d){return number_as_array.length-index-1})

                // display the circle
                g.selectAll('circle')
                    .data(data)
                    .enter()
                    .append('circle')
                    .attr('cx', function(d,i) { return d.cx; })
                    .attr('cy', function(d,i) { return d.cy; })
                    .attr('r', circle_size)
                    .attr('fill', color[index])
                    .attr('name', function(d,i) { return index+"-"+i; });           
            };    


        })  // end add circle   


}} // end if number <10000  + end anim()

</script>

\$\endgroup\$
4
\$\begingroup\$

That's a pretty good code for a first attempt with D3. There are some considerations, though.

First of all, if you have an if condition testing the size of the input, it's a good idea writing the else. For instance:

else {
    d3.select("svg").append('g')
        .append("text")
        .attr("y", 50)
        .text("Number out of the range, please choose a new one.")
}

Now let's move to the big issues. The first one is this:

d3.selectAll("g").remove();

Any seasoned D3 developer will frown upon this. It's normally not a good practice removing elements to repainting them: you should update them instead. However, as this involves a really big refactor in your code, I'll leave this just as an advice.

The second issue that shows that your code is not an idiomatic D3 is the use of for loops to append elements. Like this:

for (var i = 0; i < 4; i++) {
    d3.select("svg").append('g')
    //etc...

Do not use for loops to append element. Use a data-binding approach instead, which is the idiomatic D3. So, the above code can be:

d3.select("svg").selectAll(null)
    .data(d3.range(4))
    .enter()
    .append('g')
    //etc...

Better than that, name your selections:

var groups = d3.select("svg").selectAll(null)
    .data(d3.range(4))
    .enter()
    .append('g')
    //etc...

For instance, instead of doing...

d3.select("svg")

... all the time, you can do:

var svg = d3.select("svg")

And simply use svg whenever you need. That's a good practice in a D3 code, so you can latter on refer to your selections without relying on classes or IDs.

Also, remove the "You choose" from the selection in the example above: you don't want to show it 4 times.

Finally, you're using the start listener of a transition selection to control other transitions. This is also not an idiomatic D3 approach, and things can quickly become complicated to control and understand. Instead of that, use a simple d3.timeout, or even a vanilla JS setTimeout:

d3.timeout(function(){
    //do your transitions here
}, 5000);
//   ^----- time in milliseconds

Here is your code with those changes:

<!DOCTYPE html>
<meta charset="utf-8">
<title>Input test</title>
<div id="div1">

  <form onSubmit="return false;">
    Choose a number less than 100.000: <input type="text" name="number" id="number"><br>

  </form>
  <svg width="900" height="300" id="pack1">
  </svg>

</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script>

<script>
  // get user number and start anim()
  var inputElem = d3.select("#number");
  inputElem.on("change", anim);

  function anim() {
    // remove previous animation
    d3.selectAll("g").remove();

    var number = d3.select(this).property("value");

    if (number < 100000) {
      // general var
      var color = ["red", "blue", "orange", "green", "darkkhaki"]



      //  get choices from user : number saved as array 
      var number_as_array = number.toString().split('');

      d3.select("svg").append("g")
        .append("text")
        .text("You choose :")
        .attr('x', 10)
        .attr('y', 30)
        .attr("font-family", "sans-serif")
        .attr("font-size", "20px")
        .attr("fill", "black");

      // add 4 time the text "you choose  : abc" 
      var groups = d3.select("svg").selectAll(null)
        .data(d3.range(4))
        .enter()
        .append('g')
        .attr('id', function(d) {
          return "group" + d
        })
        .append("text")
        .attr('x', 10)
        .attr('y', 30)
        .attr("font-family", "sans-serif")
        .attr("font-size", "20px")
        .attr("fill", "black")
        .html(function(d) {
          var text = [""]
          for (var index = 0; index < number_as_array.length; index++) {
            var x_position = (index + 1) * 30 + 100
            text.push("<tspan fill=\"" + color[index] + " \"y=\"30\"  x=\"" + x_position + "\" class=\"anim" + d + "\"  >" + number_as_array[index] + "</tspan>")
          }
          return text.join(' ')
        })

      // start animation 
      d3.timeout(function() {
        // take abc and move them the line below before the =
        d3.selectAll(".anim1").transition().duration(2000)
          .attr("y", 60)
          .attr("x", function(d, i) {
            return 30 + 30 * i
          })
        // take abc and move them the line below after the =
        d3.selectAll(".anim2").transition().duration(2000)
          .attr("y", 60)
          .attr("x", function(d, i) {
            return 50 * number_as_array.length + 100 * i
          })

        d3.selectAll(".anim3").transition().duration(2000)
          .attr("y", 60)
          .attr("x", function(d, i) {
            return 50 * number_as_array.length + 100 * i
          })

        // add "=" with opacity 0
        d3.select("#group1").append("text")
          .attr("id", "egal")
          .style("opacity", 0.0)
          .append("tspan")
          .attr("id", "egaltspan")
          .attr("y", 60)
          .attr("x", function() {
            return (50 * number_as_array.length - 30 - 30 * (number_as_array.length + 1)) / 2 +
              30 + 30 * number_as_array.length
          })
          .attr("font-family", "sans-serif")
          .attr("font-size", "20px")
          .attr("fill", "black")
          .text("=")
          .transition().duration(1000).on("start", function addtext() {

            //change opacity of = to 1
            d3.select("#egal").transition().delay(2000).duration(1000).style("opacity", 1.0)
          })

        // change the abc on the right to a*100+b*10+c
        d3.selectAll(".anim2").transition().delay(3000).duration(2000)
          .text(function(d, i) {
            if (i < (number_as_array.length - 1)) {
              return number_as_array[i] + "*" + 10 ** (number_as_array.length - i - 1) + "    +"
            } else {
              return number_as_array[i] + "*" + 10 ** (number_as_array.length - i - 1)
            }
          })


        d3.selectAll(".anim3").transition().delay(3000).duration(2000)
          .text(function(d, i) {
            if (i < (number_as_array.length - 1)) {
              return number_as_array[i] + "*" + 10 ** (number_as_array.length - i - 1) + "    +"
            } else {
              return number_as_array[i] + "*" + 10 ** (number_as_array.length - i - 1)
            }
          })
      }, 1000) // end anim1

      // change the abc on the right to  a*10^2+b*10^1+c10^0
      d3.timeout(function() {
        d3.selectAll(".anim3").transition().delay(0).duration(1000)
          .attr("y", 90)
          .transition().delay(0).duration(1000)
          .text(function(d, i) {
            if (i < (number_as_array.length - 1)) {
              return number_as_array[i] + "*10^" + (number_as_array.length - i - 1) + "    +"
            } else {
              return number_as_array[i] + "*10^" + (number_as_array.length - i - 1)
            }
          })
      }, 4000)

      // add circle
      d3.timeout(function() {

        //add the circle : for each digits in the user's number  
        var g_width = 900 / number_as_array.length
        var circle_size = g_width / 30
        for (var index = 0; index < number_as_array.length; index++) {

          // create an array which length = digit
          var data = [];
          var length = number_as_array[index];

          for (var i = 0; i < length; i++) {
            circle = {};
            circle.i = i;
            circle.cx = (circle_size * 2 * i + 3) % g_width;
            circle.cy = (120 * (circle_size * 2 * i + 3)) % g_width;
            data.push(circle);
          };

          // calculate the position of g representing that digit
          var g_position_x = g_width * index + 50

          //create  g for the digit
          var g = d3.select('svg')
            .append('g')
            .attr("transform", "translate(" + g_position_x + "," + 120 + ")")
            .attr("id", function(d) {
              return number_as_array.length - index - 1
            })

          // display the circle
          g.selectAll('circle')
            .data(data)
            .enter()
            .append('circle')
            .attr('cx', function(d, i) {
              return d.cx;
            })
            .attr('cy', function(d, i) {
              return d.cy;
            })
            .attr('r', circle_size)
            .attr('fill', color[index])
            .attr('name', function(d, i) {
              return index + "-" + i;
            });
        };


      }, 6000) // end add circle   


    } else {
      d3.select("svg").append('g')
        .append("text")
        .attr("y", 50)
        .text("Number out of the range, please choose a new one.")
    }
  }

</script>

PS: It's worth mentioning that the math for your circles' positions has some problems.

|improve this answer|||||
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for?Browse other questions tagged or ask your own question.