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.