d3.js Making Connected Animation Diagram and Editor

Keywords: Javascript less

The effect is shown in the figure above.
This project uses the main d3.jsv4 production, divided into two parts, one is the actual display of the animation map, the other is managers use the mouse to edit the link page. For d3.js how to introduce pictures, how to draw lines and other basic functions, here is no longer introduced, you can find some introductory articles to see. Here we mainly introduce the key issues.

1. Connected animation

The main function of this graph is to request background data through ajax every given time, and dynamically change the value under each picture according to the data returned, and dynamically change the direction of animation flow and whether or not it flows on the line.
Firstly, determine the content of the chart that needs to be configured, such as the storage location of each picture, the color of the connection and animation, the coordinates of the picture and the connection, etc. These data need to be configured in html, preferably as object objects, assigning functions to our own chart class. For example:

var data = {
  element:[{
    image: 'img/work.png',
    pos:[1,1], // Picture location
    linePoint:[], // An array of line coordinates for picture emission
    lineDir:0, // Line segment animation direction
    title: 'work'
  }],
  lineColor:'black', // Connection color
  animateColor: 'red', // Animation color
};
var chart = new Myd3chart('#chart');
chart.lineChart(data);

An array of line coordinates emitted by an image is provided using an external file, which is generated by the editor described later.
When designing our own graph functions, it is better to divide each function into separate functions, so as to facilitate future maintenance and expansion.
The animation line segment adopts the way of css, and the animated line segment can be added with this css:

.animate-line{
  fill: none;
  stroke-width: 1;
  stroke-dasharray: 50 100;
  stroke-dashoffset: 0;
  animation: stroke 6s infinite linear;
}
@keyframes stroke {
  100% {
    stroke-dashoffset: 500; /* If the reverse movement is changed to - 500 */
  }
}

The difficulty of this chart is to dynamically change the flow animation on the line, because the end of line A will be connected to line B. If the animation of line B stops, the animation on line A will still pass through line B, instead of simply stopping the animation on line B. Moreover, if there are more than one access point on the B-segment, the order between the access points should be judged, and only the animation of the access point nearest to the B-start point should be displayed. In addition, it is also necessary to determine whether there is an access line segment on the access line segment. If there is animation on one line segment in the hierarchical relationship, animation will flow out of the access point. (It's a bit ambiguous here)
My method is:
1) Statistics of all access points on each segment, here is the name of the picture, used to determine whether the segment has animation outflow.
2) When receiving the data from the background, judge whether each line segment has animation or not, and if there is animation, directly restore the starting point coordinates of the animated line segment; if there is no animation, judge whether the nearest access point has animation, and if there is animation, change the starting point of the animated line segment to the starting point coordinates of the access point.

// Statistical Access Points
  function findAccessPoint() {
    var accessPoints = [];
    // Record access points on each segment, data for configuration data
    data.eles.forEach(function(d, i){
      if(d.line.length == 0){
        return;
      }
      var acsp = {
        name: d.title.text,
        ap: [], // Access Points, arranged in sequence, with the head close to the starting point
      };
      // On the local segment, every two adjacent points are stored as an element in an array.
      var linePair = [];
      // Starting point of local segment
      var startPos = d.line[0];
      d.line.forEach(function(dd, di){
        if(d.line[di+1]){
          var pair = {
            start: dd,
            end: d.line[di+1]
          };
          linePair.push(pair);
        }        
      });
      // Find access points for each two adjacent points
      linePair.forEach(function(dd, di){
        chartData.eles.forEach(function(ddd, ddi){
          // Exclude yourself and find the access point on your line segment
          if(i != ddi && ddd.line.length > 1){
            // Get the end of this line
            var pos = ddd.line[ddd.line.length - 1];
            // dd.start start start point, dd.end end end point
            // Computation of Y coordinates on the line segment with x coordinates and comparison with the actual y coordinates
            var computeY = dd.start[1] + 
              (pos[0] - dd.start[0])*(dd.end[1] - dd.start[1])/(dd.end[0] - dd.start[0]);
            var dif = Math.abs(computeY - pos[1]);
            // If the error is less than 2, and the end point of the line is between the starting point and the end point of the current line.
            // Consider this point as an access point
            if(dif < 2 && (
              (
                ((pos[0] > dd.start[0]) && (pos[0] < dd.end[0])) ||
                ((pos[0] < dd.start[0]) && (pos[0] > dd.end[0]))
              ) && (
                ((pos[1] > dd.start[1]) && (pos[1] < dd.end[1])) ||
                ((pos[1] < dd.start[1]) && (pos[1] > dd.end[1]))
              )
            )) {
              var dis = Math.pow((pos[0] - startPos[0]),2) + Math.pow((pos[1] - startPos[1]),2);
              var ap = {
                name: ddd.title.text,
                ap: pos,
                distance: dis, // Distance from the starting point
                allNames: [], // All site names passing through this access point
              }
              acsp.ap.push(ap);            
            }
          }
        });
      })
      accessPoints.push(acsp);
    });

    //Sort all access points by distance from the starting point and find the upper site of the access point
    accessPoints.forEach(function(d, i){
      // Sort by distance from small to large
      d.ap.sort(function(a, b){
        return a.distance - b.distance;
      });
      // Find the upper site of each access point
      d.ap.forEach(function(dd, di){
        findPoint(dd.name, dd.allNames);
      });
    });
    // Name is the name of the access point, and arr is the allNames of the access point.
    function findPoint(name, arr){
      accessPoints.forEach(function(d, i){
        // Find the item with the specified name in the array
        if(d.name === name){
          if(d.ap.length>0){
            // Add the name in the ap below this item to the given arr
            d.ap.forEach(function(dd, di){
              arr.push(dd.name);
              // If allNames within that point already have a value, join directly
              if(dd.allNames.length>0){
                dd.allNames.forEach(function(d, i){
                  arr.push(d);
                });
              } else{
                // Recursive Finding Subaccess Points
                findPoint(dd.name, arr);
              }
            });
          } else {
            return;
          }
        }else{
          return;
        }
      });
    }
  }

The result of the above function will produce an object, which stores the access point of `mount'on each access line segment. The purpose is to change the animation to facilitate judgment.

// Update Line Animation
    aniLine.each(function(d, i){
        var curLine = d3.select(this);
        // Find the corresponding animation line
        if (dd.name === curLine.attr('tag')) {
          // Processing whether animation is running or not
          if (dd.ani) {
            // This line animation runs
            curLine.style('animation-play-state', 'running');
            curLine.style('display', 'inline');
            // If the animation runs, the original animation path is restored
            curLine.attr('d', function(d){
              return line(chartData.eles[i].line);
            });
          } else {
            // This line animation stops
            // Find the nearest access point to the starting point of the local segment first
            var acp = accessPoints;
            // Find the set of access points of this node from accessPoints
            var ap = [];
            acp.forEach(function(acd, aci){
              if(acd.name === dd.name){
                ap = acd.ap;
              }
            });            
            // Recent animation access point serial number
            var acIndex = -1;
            // Find the nearest animation access point and increment the distance by array number
            for(var j=0;j<ap.length;j++){
              // Copy all subaccess point arrays
              var allNames = ap[j].allNames.concat();
              // Add the Access Point Name
              allNames.push(ap[j].name);
              // Judge whether there is animation in this access point tree, if there is one, you can
              allNames.forEach(function(name,ani){
                data.forEach(function(datad, datai){
                  if(datad.name === name){
                    if(datad.ani){
                      acIndex = j;
                      return;
                    }
                  }
                });
              });
              if(acIndex != -1) {
                break;
              }
            }
            // If there is an animation access point
            if(acIndex != -1){
              curLine.style('animation-play-state', 'running');
              curLine.style('display', 'inline');
              curLine.attr('d', function(d){
                var accp = ap[acIndex].ap;
                var curLine = data.element[i].line.concat();
                // Distance between Access Node and Starting Point
                var disAp = Math.pow((accp[0] - curLine[0][0]),2) +
                Math.pow((accp[1] - curLine[0][1]),2);
                // If there is a node in the current segment that leaves the starting node nearer to the access point
                // Delete this node
                curLine.forEach(function(curld, curli){
                  if(curli > 0){
                    var dis = Math.pow((curld[0] - curLine[0][0]),2) +
                      Math.pow((curld[1] - curLine[0][1]),2);
                    if(dis < disAp){
                      // Delete this point
                      curLine.splice(curli,1);
                    }
                  }
                });
                // Start animation from the access point
                curLine.splice(0,1,accp);
                // debugger;
                return line(curLine);
              });
            }else{
              // This line animation stops
              curLine.style('animation-play-state', 'paused');
              curLine.style('display', 'none');
            }
          }
        }

2. Editor

Because this chart requires a large number of coordinates, it is inefficient to fill in manually, so an editor needs to be developed to modify the chart.
The main use of the editor is to use the mouse to drag the icon, double-click to determine the starting position and start the real-time line drawing state, dynamically draw the line segment with the mouse movement, Click To determine the temporary end point, then click to determine the next end point, right-click to end the dynamic line drawing state. If the mouse clicks on another icon, the end point is the starting coordinate of the icon. The real-time drawing part of the program is inclined, i. e. left or right inclination of 30 degrees.
The editor is simpler than the presentation diagram, and the complex part is dealing with events.

// Drag icons
    var draging = d3.drag()
      .on('drag', function () {
        // When the lengths and widths are the same, iconSize is the size of the icon [width, height]
        var move = iconSize[0] / 2,
          moveSubBg = [25, 53.5], moveTitle = [25, 50];
        var g = d3.select(this),
          eventX = d3.event.x - move,
          eventY = d3.event.y - move;
        // Set icon position
        g.select('.image')
          .attr('x', eventX)
          .attr('y', eventY);
      })
      // Drag End
      .on('end', function () {
        var g = d3.select(this);
        g.select('.subBg')
          .attr('transform', function (d, i) {
          // The processing of subtags automatically conforms to the string length
            var x = parseFloat(d3.select(this).attr('x')) + parseFloat(d3.select(this).attr('width')) / 2,
              // y is not scaled, so you don't have to deal with it.
              y = d3.select(this).attr('y'),
              dsl = (d.title.subTitle.text + '').length;
            var scaleX = dsl * 5.5;
            return 'translate(' + x + ' ' + y + ') scale(' + scaleX + ', 1) translate(' + -x + ' ' + -y + ')';
          });
      });
    // Icon Groups Increase Drag Events
    imageGs.call(draging);

The above drag event is just a call to the basic method.
The real-time line drawing function needs to define the temporary storage object in advance to store the end coordinates of the line segment when the mouse moves.

// When the mouse moves, draw a line in real time to the current position of the mouse, _body Rect is the main area.
    _bodyRect.on('mousemove', function(){
      // If not in real-time line drawing state
      if(!_chartData.drawing){
        return;
      }
      // If there is no endpoint name
      if (!_chartData.linePrePare.name) {
        return;
      }
      /* Real-time line drawing */
      // Judging the inclined direction of line segment, line PrePare is temporary storage of line segment
      var preLines = linePrePare.lines;
      var mousePos = d3.mouse(_bodyRect.node()),
        beforePos = preLines[preLines.length - 1], newy,
        newPos = [];
      if((mousePos[0]>beforePos[0] && mousePos[1]>beforePos[1]) || (mousePos[0]<beforePos[0] && mousePos[1]<beforePos[1])){
        // Tilt to the left Upper left to lower right: y = cy + 0.7*(x-cx)
        newy = beforePos[1] + 0.7 * (mousePos[0] - beforePos[0]);
      } else {
        // Tilt right / left down to right up: y = cy - 0.7*(cx-x)
        newy = beforePos[1] - 0.7 * (mousePos[0] - beforePos[0]);
      }
      newPos = [mousePos[0], newy];
      // Remove old lines
      if(_chartData.tempLine.line){
        _chartData.tempLine.pos = [];
        _chartData.tempLine.line.remove();
      }
      // Draw new lines, tempLine for temporary storage of real-time drawing lines
      _chartData.tempLine.line = _chartData.lineRootG.append('path')
        .attr('class', 'line-path')
        .attr('stroke', chartData.line.color)
        .attr('stroke-width', chartData.line.width)
        .attr('fill', 'none')
        .attr('d', function () {
          var newLine = [
            preLines[preLines.length - 1],
            newPos
          ];
          _chartData.tempLine.pos = newPos;
          return line(newLine);
        });

      // When the mouse moves into the scope of a building Icon
      _chartData.imageGs.on('mouseenter', function(d, i){
        // Remove old lines
        if(_chartData.tempLine.line){
          _chartData.tempLine.pos = [];
          _chartData.tempLine.line.remove();
        }
        // Get the coordinates of icon center points
        var posX = parseFloat(d3.select(this).select('.image').attr('x')) + _chartConf.baseSize[0] / 2;
        var posY = parseFloat(d3.select(this).select('.image').attr('y')) + _chartConf.baseSize[1] / 2;
        // Draw a line with the center point coordinate of the building icon as the end point coordinate
        _chartData.tempLine.line = _chartData.lineRootG.append('path')
          .attr('class', 'line-path')
          .attr('stroke', chartData.line.color)
          .attr('stroke-width', chartData.line.width)
          .attr('fill', 'none')
          .attr('d', function () {
            var newLine = [
              preLines[preLines.length - 1],
              [posX,posY]
            ];
            _chartData.tempLine.pos = [posX,posY];
            return line(newLine);
          });
      });
      // When the mouse moves out of the icon area
      _chartData.imageGs.on('mouseleave', function(d, i){
        // Remove old lines
        if(_chartData.tempLine.line){
          _chartData.tempLine.pos = [];
          _chartData.tempLine.line.remove();
        }
      });
      // Click on the icon and save the line
      _chartData.imageGs.on('click', function (d, i) {
        // Preserve temporary lines
        drawLine();
        // Stop drawing lines in real time
        exitDrawing();
      });
    });
    // Click the right mouse button to stop drawing lines in real time
    _bodyRect.on('contextmenu', function(){
      // Stop drawing lines in real time
      exitDrawing();
      d3.event.preventDefault();
    });
   });
  }

Only part of the code is posted here. If you have any suggestions and questions, please leave a message. Thank you.

Posted by Jim on Thu, 29 Aug 2019 03:07:09 -0700