使用 d3.js 在数据图中绘制箭头

Draw arrow inside a datamaps using d3.js

我是 d3.js 的新手,我正在尝试创建自定义地图 svg。

根据一些参考,我做了一个自定义地图,如下所示。

上面输出的代码片段在这里。

https://jsfiddle.net/9kbp4h6j/

"use strict"

var svg = d3.select("body").append("svg").append("g").attr("transform", "translate(100,50)")

svg.append("svg:defs")
  .append("svg:marker")
  .attr("id", "arrow")
  .attr("refX", 2)
  .attr("refY", 6)
  .attr("markerWidth", 13)
  .attr("markerHeight", 13)
  .attr("orient", "auto")
  .append("svg:path")
  .attr("d", "M2,2 L2,11 L10,6 L2,2");


var line = d3.svg.line()
  .x(function (point) {
    return point.lx;
  })
  .y(function (point) {
    return point.ly;
  });

function lineData(d) {
  // i'm assuming here that supplied datum 
  // is a link between 'source' and 'target'
  var points = [{
      lx: d.source.x,
      ly: d.source.y
    },
    {
      lx: d.target.x,
      ly: d.target.y
    }
  ];
  return line(points);
}

var path = svg.append("path")
  .data([{
    source: {
      x: 0,
      y: 0
    },
    target: {
      x: 80,
      y: 80
    }
  }])
  .attr("class", "line")
  //.style("marker-end", "url(#arrow)")
  .attr("d", lineData);
//var arrow = svg.append("svg:path")
//.attr("d", "M2,2 L2,11 L10,6 L2,2");


console.log(d3.svg.symbol())

var arrow = svg.append("svg:path")
  .attr("d", d3.svg.symbol().type("triangle-down")(10, 1));


arrow.transition()
  .duration(2000)
  .ease("linear")
  .attrTween("transform", translateAlong(path.node()))
//.each("end", transition);


// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
  var l = path.getTotalLength();
  var ps = path.getPointAtLength(0);
  var pe = path.getPointAtLength(l);
  var angl = Math.atan2(pe.y - ps.y, pe.x - ps.x) * (180 / Math.PI) - 90;
  var rot_tran = "rotate(" + angl + ")";
  return function (d, i, a) {
    console.log(d);

    return function (t) {
      var p = path.getPointAtLength(t * l);
      return "translate(" + p.x + "," + p.y + ") " + rot_tran;
    };
  };
}

var totalLength = path.node().getTotalLength();

path
  .attr("stroke-dasharray", totalLength + " " + totalLength)
  .attr("stroke-dashoffset", totalLength)
  .transition()
  .duration(2000)
  .ease("linear")
  .attr("stroke-dashoffset", 0);


var bubble_map = new Datamap({
  element: document.getElementById('canada'),
  scope: 'canada',
  geographyConfig: {
    popupOnHover: true,
    highlightOnHover: true,
    borderColor: '#444',
    borderWidth: 0.5,
    dataUrl: 'https://rawgit.com/Anujarya300/bubble_maps/master/data/geography-data/canada.topo.json'
    //dataJson: topoJsonData
  },
  fills: {
    'MAJOR': '#306596',
    'MEDIUM': '#0fa0fa',
    'MINOR': '#bada55',
    defaultFill: '#dddddd'
  },
  data: {
    'JH': {
      fillKey: 'MINOR'
    },
    'MH': {
      fillKey: 'MINOR'
    }
  },
  setProjection: function (element) {
    var projection = d3.geo.mercator()
      .center([-106.3468, 68.1304]) // always in [East Latitude, North Longitude]
      .scale(250)
      .translate([element.offsetWidth / 2, element.offsetHeight / 2]);

    var path = d3.geo.path().projection(projection);
    return {
      path: path,
      projection: projection
    };
  }
});

let bubbles = [{
    centered: "MB",
    fillKey: "MAJOR",
    radius: 8,
    state: "Manitoba"
  },
  {
    centered: "AB",
    fillKey: "MAJOR",
    radius: 8,
    state: "Alberta"
  },
  {
    centered: "NT",
    fillKey: "MAJOR",
    radius: 8,
    state: "Northwest Territories"
  },
  {
    centered: "NU",
    fillKey: "MEDIUM",
    radius: 8,
    state: "Nunavut"
  },
  {
    centered: "BC   ",
    fillKey: "MEDIUM",
    radius: 8,
    state: "British Columbia"
  },
  {
    centered: "QC",
    fillKey: "MINOR",
    radius: 8,
    state: "Québec"
  },
  {
    centered: "NB",
    fillKey: "MINOR",
    radius: 8,
    state: "New Brunswick"
  }

]
// // ISO ID code for city or <state></state>
setTimeout(() => { // only start drawing bubbles on the map when map has rendered completely.
  bubble_map.bubbles(bubbles, {
    popupTemplate: function (geo, data) {
      return `<div class="hoverinfo">city: ${data.state}, Slums: ${data.radius}%</div>`;
    }
  });
}, 1000);
.line {
  stroke: blue;
  stroke-width: 1.5px;
  fill: white;
}

circle {
  fill: red;
}
#marker {
 stroke: black;
 fill: black;
}
<!DOCTYPE html>
<html>
  <meta charset="utf-8">
  <body>
    <script src="http://d3js.org/d3.v3.min.js"></script>
    <script src="http://d3js.org/topojson.v1.min.js"></script>
    <script src="https://rawgit.com/Anujarya300/bubble_maps/master/data/geography-data/datamaps.none.js"></script>
    <div id="canada" style="height: 600px; width: 900px;"></div>
  </body>
</html>

我有一个附加到 body 的标记,但我需要的实际输出是,

  1. The arrow must be starting from the bubble shown in the image
  2. It should end on some random directions so that a popup template box can be added to describe the actual location.

所以最后我需要的实际输出应该看起来像这样。

感谢任何帮助。

可以通过在数据中包含 2 个字段来单独自定义行:arrowDirectionAnglearrowLineLength

"use strict"

var line = d3.svg.line()
    .x(function (point) {
        return point.lx;
    })
    .y(function (point) {
        return point.ly;
    });

function lineData(d) {
    // i'm assuming here that supplied datum 
    // is a link between 'source' and 'target'
    var points = [{
        lx: d.source.x,
        ly: d.source.y
    },
    {
        lx: d.target.x,
        ly: d.target.y
    }
    ];
    return line(points);
}

// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
    var l = path.getTotalLength();
    var ps = path.getPointAtLength(0);
    var pe = path.getPointAtLength(l);
    var angl = Math.atan2(pe.y - ps.y, pe.x - ps.x) * (180 / Math.PI) - 90;
    var rot_tran = "rotate(" + angl + ")";
    return function (d, i, a) {
        //console.log(d);

        return function (t) {
            var p = path.getPointAtLength(t * l);
            return "translate(" + p.x + "," + p.y + ") " + rot_tran;
        };
    };
}

var bubble_map = new Datamap({
    element: document.getElementById('canada'),
    scope: 'canada',
    geographyConfig: {
        popupOnHover: true,
        highlightOnHover: true,
        borderColor: '#444',
        borderWidth: 0.5,
        dataUrl: 'https://rawgit.com/Anujarya300/bubble_maps/master/data/geography-data/canada.topo.json'
        //dataJson: topoJsonData
    },
    fills: {
        'MAJOR': '#306596',
        'MEDIUM': '#0fa0fa',
        'MINOR': '#bada55',
        defaultFill: '#dddddd'
    },
    data: {
        'JH': {
            fillKey: 'MINOR'
        },
        'MH': {
            fillKey: 'MINOR'
        }
    },
    setProjection: function (element) {
        var projection = d3.geo.mercator()
            .center([-106.3468, 68.1304]) // always in [East Latitude, North Longitude]
            .scale(250)
            .translate([element.offsetWidth / 2, element.offsetHeight / 2]);

        var path = d3.geo.path().projection(projection);
        return {
            path: path,
            projection: projection
        };
    }
});

let bubbles = [{
    centered: "MB",
    fillKey: "MAJOR",
    radius: 8,
    state: "Manitoba",
    arrowDirectionAngle: 90,
    arrowLineLength: 120
},
{
    centered: "AB",
    fillKey: "MAJOR",
    radius: 8,
    state: "Alberta",
    arrowDirectionAngle: 90,
    arrowLineLength: 100
},
{
    centered: "NT",
    fillKey: "MAJOR",
    radius: 8,
    state: "Northwest Territories",
    arrowDirectionAngle: 180,
    arrowLineLength: 130
},
{
    centered: "NU",
    fillKey: "MEDIUM",
    radius: 8,
    state: "Nunavut",
    arrowDirectionAngle: -25,
    arrowLineLength: 80
},
{
    centered: "BC   ",
    fillKey: "MEDIUM",
    radius: 8,
    state: "British Columbia",
    arrowDirectionAngle: 125,
    arrowLineLength: 65
},
{
    centered: "QC",
    fillKey: "MINOR",
    radius: 8,
    state: "Québec",
    arrowDirectionAngle: -25,
    arrowLineLength: 70
},
{
    centered: "NB",
    fillKey: "MINOR",
    radius: 8,
    state: "New Brunswick",
    arrowDirectionAngle: 65,
    arrowLineLength: 50
}

]


function renderArrows(targetElementId) {
    let svgRoot = d3.select("#" + targetElementId).select("svg");

    svgRoot.append("svg:defs")
        .append("svg:marker")
        .attr("id", "arrow")
        .attr("refX", 2)
        .attr("refY", 6)
        .attr("markerWidth", 13)
        .attr("markerHeight", 13)
        .attr("orient", "auto")
        .append("svg:path")
        .attr("d", "M2,2 L2,11 L10,6 L2,2");

    let linesGroup = svgRoot.append("g");

    linesGroup.attr("class", "lines");

    let bubbleElements = svgRoot.selectAll(".datamaps-bubble")[0];

    bubbleElements.forEach(function (bubbleElement) {
        let xPosition = bubbleElement.cx.baseVal.value;
        let yPosition = bubbleElement.cy.baseVal.value;
        let datum = d3.select(bubbleElement).datum();

        let degree = datum.arrowDirectionAngle;
        let radius = datum.arrowLineLength;
        let theta = degree * Math.PI / 180;

        let path = linesGroup.append("path")
            .data([{
                source: {
                    x: xPosition,
                    y: yPosition
                },
                target: {
                    x: xPosition + radius * Math.cos(theta),
                    y: yPosition + radius * Math.sin(theta)
                }
            }])
            .style("stroke", "blue")
            .style("stroke-width", "1.5px")
            .style("fill", "white")
            //.style("marker-end", "url(#arrow)")
            .attr("d", lineData);


        let arrow = svgRoot.append("svg:path")
            .attr("d", d3.svg.symbol().type("triangle-down")(10, 1));


        arrow.transition()
            .duration(2000)
            .ease("linear")
            .attrTween("transform", translateAlong(path.node()))

        var totalLength = path.node().getTotalLength();

        path
            .attr("stroke-dasharray", totalLength + " " + totalLength)
            .attr("stroke-dashoffset", totalLength)
            .transition()
            .duration(2000)
            .ease("linear")
            .attr("stroke-dashoffset", 0);

    });
}

// // ISO ID code for city or <state></state>
setTimeout(() => { // only start drawing bubbles on the map when map has rendered completely.
    bubble_map.bubbles(bubbles, {
        popupTemplate: function (geo, data) {
            return `<div class="hoverinfo">city: ${data.state}, Slums: ${data.radius}%</div>`;
        }
    });

    renderArrows("canada");

}, 1000);
.line {
    stroke: blue;
    stroke-width: 1.5px;
    fill: white;
}

circle {
    fill: red;
}

#marker {
    stroke: black;
    fill: black;
}
<!DOCTYPE html>
<html>
  <meta charset="utf-8">
  <body>
    <script src="http://d3js.org/d3.v3.min.js"></script>
    <script src="http://d3js.org/topojson.v1.min.js"></script>
    <script src="https://rawgit.com/Anujarya300/bubble_maps/master/data/geography-data/datamaps.none.js"></script>
    <div id="canada" style="height: 600px; width: 900px;"></div>
  </body>
</html>

使用路径线和箭头分别添加线条,使用鼠标事件您可以根据悬停气泡的中心来操纵路径的位置,对于工具提示您也可以使用 d3.select() 来获取div 工具提示并更改其 innerHTML 属性以显示消息。

这是一个可行的解决方案:

var svg = d3.select("body").append("svg").append("g").attr("transform", "translate(100,50)")

      svg.append("svg:defs")
        .append("svg:marker")
        .attr("id", "arrow") 
        .attr("refX", 2)
        .attr("refY", 6)
        .attr("markerWidth", 13)
        .attr("markerHeight", 13)
        .attr("orient", "auto")
        .append("svg:path")
        .attr("d", "M2,2 L2,11 L10,6 L2,2");


      var line = d3.svg.line()
                  .x( function(point) { return point.lx; })
                  .y( function(point) { return point.ly; });

      function lineData(d){
        // i'm assuming here that supplied datum 
        // is a link between 'source' and 'target'
        var points = [
            {lx: d.source.x, ly: d.source.y},
            {lx: d.target.x, ly: d.target.y}
        ];
        return line(points);
      }


  // Returns an attrTween for translating along the specified path element.
  function translateAlong(path) {
    var l = path.getTotalLength();
    var ps = path.getPointAtLength(0);
    var pe = path.getPointAtLength(l);
    var angl = Math.atan2(pe.y - ps.y, pe.x - ps.x) * (180 / Math.PI) - 90;
    var rot_tran = "rotate(" + angl + ")";
    return function(d, i, a) {
      return function(t) {
        var p = path.getPointAtLength(t * l);
        if(t < 0.111) {
          return '';
        }
        return "translate(" + p.x + "," + p.y + ") " + rot_tran;
      };
    };
  }




    var bubble_map = new Datamap({
        element: document.getElementById('canada'),
        scope: 'canada',
        geographyConfig: {
            popupOnHover: true,
            highlightOnHover: true,
            borderColor: '#444',
            borderWidth: 0.5,
            dataUrl: 'https://rawgit.com/Anujarya300/bubble_maps/master/data/geography-data/canada.topo.json'
            //dataJson: topoJsonData
        },
        fills: {
            'MAJOR': '#306596',
            'MEDIUM': '#0fa0fa',
            'MINOR': '#bada55',
            defaultFill: '#dddddd'
        },
        data: {
            'JH': { fillKey: 'MINOR' },
            'MH': { fillKey: 'MINOR' }
        },
        setProjection: function (element) {
              var projection = d3.geo.mercator()
            .center([-106.3468, 68.1304]) // always in [East Latitude, North Longitude]
            .scale(250)
            .translate([element.offsetWidth / 2, element.offsetHeight / 2]);

            var path = d3.geo.path().projection(projection);
            return { path: path, projection: projection };
        }
    });

    let bubbles = [
        {
            centered: "MB",
            fillKey: "MAJOR",
            radius: 8,
            state: "Manitoba"
        },
        {
            centered: "AB",
            fillKey: "MAJOR",
            radius: 8,
            state: "Alberta"
        },
        {
            centered: "NT",
            fillKey: "MAJOR",
            radius: 8,
            state: "Northwest Territories"
        },
        {
            centered: "NU",
            fillKey: "MEDIUM",
            radius: 8,
            state: "Nunavut"
        },
        {
            centered: "BC   ",
            fillKey: "MEDIUM",
            radius: 8,
            state: "British Columbia"
        },
        {
            centered: "QC",
            fillKey: "MINOR",
            radius: 8,
            state: "Québec"
        },
        {
            centered: "NB",
            fillKey: "MINOR",
            radius: 8,
            state: "New Brunswick"
        }

    ];
    // ISO ID code for city or <state></state>
    setTimeout(() => { // only start drawing bubbles on the map when map has rendered completely.
        bubble_map.bubbles(bubbles, {
            popupTemplate: function (geo, data) {
                return ``;
                // return `<div class="hoverinfo">city: ${data.state}, Slums: ${data.radius}%</div>`;
            }
        });
        const line_data = [{source: {x:0, y:0}, target: {x:100, y:100}}];
        var path = d3.select('.datamap').append("path")
          .data(line_data)
          .attr("class", "line")
                .attr("d", lineData);

        var arrow = d3.select('.datamap').append("svg:path")
          .attr('d', null)
          .attr('class', 'tri');
    }, 1000);

    setTimeout(() => { // only start drawing bubbles on the map when map has rendered completely.
        const svg = d3.select('.datamap');
        var circles = d3.selectAll('circle');
        
        var state_data = d3.selectAll('circle').data();


        circles.on("mouseover", function(d, i) {
          if(!document.querySelectorAll(".active").length) {
            let x = circles[0][i].cx.baseVal.value;
            let y = circles[0][i].cy.baseVal.value+8;
            
            state_info = state_data[i];

            const line_data = [{source: {x, y}, target: {x , y : y + 100}}];
            if(i === 2) {
              line_data[0].source.x = line_data[0].source.x - 8;
              line_data[0].source.y = line_data[0].source.y - 8;
              line_data[0].target.y = line_data[0].source.y;
              line_data[0].target.x = line_data[0].source.x - 150;
            }
            if(i === 3) {
              line_data[0].source.x = line_data[0].source.x + 8;
              line_data[0].source.y = line_data[0].source.y - 8;
              line_data[0].target.y = line_data[0].source.y;
              line_data[0].target.x = line_data[0].source.x + 100;
            }

            if(i === 4) {
              line_data[0].source.x = line_data[0].source.x - 8;
              line_data[0].source.y = line_data[0].source.y - 8;
              line_data[0].target.y = line_data[0].source.y + 50;
              line_data[0].target.x = line_data[0].source.x / 2;
            }

            if(i === 5) {
              line_data[0].source.x = line_data[0].source.x + 8;
              line_data[0].source.y = line_data[0].source.y - 8;
              line_data[0].target.y = line_data[0].source.y;
              line_data[0].target.x = line_data[0].source.x  + 100;
            }

            var path = d3.select('path.line');
            path.data(line_data)
              .attr("class", "line")
              .attr("d", lineData);

            var arrow = d3.select('.tri');
            
            arrow.attr("d", d3.svg.symbol().type("triangle-down")(10,1));
            arrow.interrupt();
            arrow.transition()
              .duration(2000)
              .ease("linear")
              .attr("class", "tri")
              .attrTween("transform", translateAlong(path.node()))
              .each("end", () => {
                d3.select('.datamaps-hoverover')
                .style('display','block')
                .style('left', (line_data[0].target.x + 10) + "px")
                .style('top', (line_data[0].target.y + 10) +"px")
                .html('<div class="hoverinfo"><strong> this is from the custom tooltip: city: ' + state_info.state + ', Slum: ' + state_info.radius + '%</strong></div>');
              });

              var totalLength = path.node().getTotalLength();
              path.interrupt();
              path
                .attr("stroke-dasharray", totalLength + " " + totalLength)
                .attr("stroke-dashoffset", totalLength)
                .transition()
                  .duration(2000)        
                  .ease("linear")
                  .attr("stroke-dashoffset", 0);
            
            }
        });

        circles.on("mouseout", function(d, i) {
          d3.select('path.line').attr('d', null);
          d3.select('path.tri').attr('d', null);
          d3.select('.datamaps-hoverover')
            .style('display','none')
            .html('');
        })

      }, 1500);
.line {
  stroke: blue !important;
  fill: blue
}

.tri {
  stroke: blue !important;
  fill: blue
}

circle {
  fill: red;
}

#marker {
  stroke: black;
  fill: black;
}
<!DOCTYPE html>
<meta charset="utf-8">
<html>
  <head>
    <script src="https://d3js.org/d3.v3.min.js"></script>
    <script src="https://d3js.org/topojson.v1.min.js"></script>
    <script src="https://rawgit.com/Anujarya300/bubble_maps/master/data/geography-data/datamaps.none.js"></script>
  </head>
  <body>
    <div id="canada" style="height: 600px; width: 900px;"></div>
  </body>
</html>