Как перетащить и повернуть орфографическую карту (глобус) с помощью d3.js

#javascript #d3.js #svg #drag

#javascript #d3.js #svg #перетаскивание

Вопрос:

Настройка: Я создаю приложение globe для лучшего визуального представления данных по регионам мира. Он построен d3.js использование topojson для построения геометрии.

В настоящее время я внедряю перетаскивание, как это было достигнуто с помощью ivyywang здесь. (не теряйтесь в математических функциях, если у вас нет статуса «математический ботаник-гуру»)

В настоящее время мой проект находится здесь.

Проблема: я получил орфографическую проекцию глобуса и успешно реализовал функцию перетаскивания… за исключением. Я могу щелкнуть и перетащить глобус только до тех пор, пока мой курсор находится внутри границ страны. Как я могу спроецировать свой SVG так, чтобы весь холст реагировал на мое событие перетаскивания?

Соответствующий код:

сначала я получаю некоторые данные из запроса MySQL и сохраняю их в countryStattistics. И я запускаю ее через следующую функцию, чтобы лучше ее проиндексировать.

 var countryStatistics = (returned from mySQL query)

  //this function build dataById[] setting data keyed to idTopo
function keyIdToData(d){
  countryStatistics.forEach(function(d) {
    dataById[d.idTopo] = d;
  });  
}    

 function visualize(statisticalData, mapType){
  //pass arguments each function call to decide what data to viasually display, and what map type to use

var margin = {top: 100, left: 100, right: 100, bottom:100},
    height = 800 - margin.top - margin.bottom, 
    width = 1200 - margin.left - margin.right;

  //a simple color scale to correlate to data
var colorScale = d3.scaleLinear()
  .domain([0, 100])
  .range(["#646464", "#ffff00"])


 //create svg
var svg = d3.select("#map")
      .append("svg")
      .attr("height", height   margin.top   margin.bottom)
      .attr("width", width   margin.left   margin.right)
      .append("g")
      .attr("transform", "translate("   margin.left   ","   margin.top   ")")
        //here I attmpt to fill the svg with a different color.  but it is unresponsive
      .attr("fill", "blue")
  

Как вы можете видеть в конце этого блока кода, я вызываю .attr («заполнить»… на элементе SVG, но не могу получить цвет для рендеринга. Возможно, это связано с тем, почему мой курсор не отвечает в этом пространстве.

продолжение…

   //set projection type to 2D map or 3d globe dependinding on argument passed see function below
var projection = setMapType(mapType, width, height);

      //a function to call on visualize() to set projection type for map style.
function setMapType(mapType, width, height) {
  if(mapType === "mercator") {
    let projection = d3.geoMercator()
    .translate([ width / 2, height / 2 ])
    .scale(180)
    return projection;
  }else if (mapType === "orthographic"){
    let projection = d3.geoOrthographic()
    .clipAngle(90)
    .scale(240);
    return projection;
  }

  //pass path lines to projections
var path = d3.geoPath()
  .projection(projection);

  //here I create and call the drag function only when globe projection is displayed elected
if(mapType == "orthographic"){
  var drag = d3.drag()
    .on("start", dragstarted)
    .on("drag", dragged);
    svg.call(drag);
}


  //coordinate variables
var gpos0, 
    o0;

function dragstarted(){
  gpos0 = projection.invert(d3.mouse(this));
  o0 = projection.rotate();  
}

function dragged(){
  var gpos1 = projection.invert(d3.mouse(this));
  o0 = projection.rotate();

  var o1 = eulerAngles(gpos0, gpos1, o0);
  projection.rotate(o1);

  svg.selectAll("path").attr("d", path);
}

  //load in the topojson file
d3.queue()
  .defer(d3.json, "world110m.json")
  .await(ready)  
  

Вышеуказанные функции относятся к математическим функциям, необходимым для вычисления орфографического поворота. Вы можете увидеть их в блоке кода ivyywang по первой ссылке вверху.

 function ready (error, data){
    if (error) throw error;
    //output data to see what is happening
  console.log("topojson data: ")
  console.log(data);

    //I suspect there may be an issue with this code. 
  countries = topojson.feature(data, data.objects.countries)
    //bind dataById data into countries topojson variable
  .features.map(function(d) {
    d.properties = dataById[d.id];
    return d
  });

  console.log("countries:")
  console.log(countries)
  

Я подозреваю, что виной всему может быть переменная countries, приведенная чуть выше, в основном потому, что я не совсем понимаю этот код. В этой переменной я связываю данные моей countryStatistics, обработанные keyIdToData() , в виде вложенного объекта «свойства» с моими данными topojson. Я регистрирую ее на консоли, чтобы увидеть данные.

   svg.selectAll(".country")
    .data(countries)
    .enter().append("path")
    .attr("class", "country")
    .attr("d", path)

    //make fill gradient depend on data
    .attr("fill", function(countries){
        //if no data, country is grey
      if(countries.properties == undefined){
        return "rgb(100 100 100)";
      }
        //else pass data to colorScale()
      return colorScale(countries.properties.literacy)
    })
    .on('mouseover', function(d) {
        //on hover set class hovered which simply changes color with a transition time
      d3.select(this).classed("hovered", true)
    })
    .on('mouseout', function(d) {
      d3.select(this).classed("hovered", false)
    })
  }
};
  

наконец-то у нас есть эта маленькая функция, которая

   //this function build dataById[] setting data keyed to idTopo
function keyIdToData(d){
  countryStatistics.forEach(function(d) {
    dataById[d.idTopo] = d;
  });  
}  
  

Возможности: Кажется, что мой рендеринг SVG исключает мою пустую область (не относящуюся к стране). Может быть проблема с моей конструкцией SVG? Или, возможно, я вмешиваюсь в конструкцию SVG, когда изменяю данные и добавляю свой dataById в свой topojson?

Спасибо, что сделали это так далеко, клавиатурный ниндзя. Есть идеи?

Ответ №1:

Проблема

Ваше взаимодействие с мышью происходит только там, где внутри родительского элемента нарисованы пути g , а не в промежутке между ними. Заливка, которую вы применяете к g , переопределяется стилем, который вы применяете к дочерним контурам.


Просматривая то, что у вас есть, ваша переменная svg содержит g :

 //create svg
var svg = d3.select("#map")
  .append("svg")
  ...
  .append("g")  // return a newly created and selected g
  ...
  .attr("fill", "blue") // returns same g
  

Для взаимодействия с мышью g можно взаимодействовать только с теми элементами, которые в ней существуют. Для g , fill атрибут ничего не будет делать напрямую, он применяется только к элементам презентации (и анимации):

В качестве презентации атрибута [заполнить], может быть применено к любому элементу, но он имеет силу только на следующий одиннадцать элементов: <altGlyph> , <circle> , <ellipse> , <path> , <polygon> , <polyline> , <rect> , <text> , <textPath> , <tref> , и <tspan> (МДН)

Использование fill на g вместо этого раскрашивает дочерние элементы, ваши пути. Хотя вы окрашиваете их напрямую, поэтому синий цвет не имеет визуального эффекта:

 var g = d3.select("body")
  .append("svg")
  .append("g")
  .attr("fill","orange");
  
// Inherit fill:
g.append("rect")
  .attr("width",50)
  .attr("height",50)
  
// Override inheritable fill:
g.append("rect")
  .attr("x", 100)
  .attr("width",50)
  .attr("height",50)
  .attr("fill","steelblue");
    
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>  

Решение

Вам нужно создать элемент для взаимодействия при перетаскивании там, где в данный момент нет пути.


Теперь, я не думаю, что вы хотите сделать весь фон svg синим, только ту часть земного шара, которая не является частью страны. Вы можете сделать это с помощью сферы geojson. Технически это не входит в спецификацию geojson, d3 распознает geojson типа shere как охватывающий всю планету (как таковой, он не принимает координат). Перед добавлением стран на глобус добавьте сферу, это дает элемент для взаимодействия с событиями перетаскивания:

 svg.append("path")
  .attr("d", path({type:"Sphere"})
  .attr("fill","blue");
  

Это заполняет океаны (и сушу), поверх которых мы можем добавлять страны. Теперь, поскольку и сфера, и страны являются частью одного и того же g , мы можем реализовать перетаскивание по всей земле так же, как вы делаете сейчас, но теперь нет отверстий, где взаимодействие с мышью не будет работать.

Вот краткая демонстрация с ортографической проекцией и самыми элементарными функциями перетаскивания:

 var svg = d3.select("svg").append("g");

var projection = d3.geoOrthographic()
  .translate([250,250])
  
var path = d3.geoPath().projection(projection);

d3.json("https://unpkg.com/world-atlas@1/world/110m.json").then( function(data) {

  var world = {type:"Sphere"}
  
  svg.append("path")
    .datum(world)
    .attr("d", path)
    .attr("fill","lightblue");
    
  svg.selectAll(null)
    .data(topojson.feature(data,data.objects.land).features)
    .enter()
    .append("path")
    .attr("fill","lightgreen")
    .attr("d",path);
  
  
  svg.call(d3.drag()
    .on("drag", function() {
      var xy = d3.mouse(this);
      projection.rotate(xy)
      svg.selectAll("path")
       .attr("d",path);
    }))
  

 
 
 
 
});  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/topojson-client@3"></script>

<svg width="500" height="500"></svg>  

Комментарии:

1. Это похоже на ответ. Я разбираю и применяю ее. Я расскажу вам, как это происходит.

2. Одно замечание: этот ответ устраняет проблему, но изначально синий отображался только после запуска функции перетаскивания. Я решил эту проблему, поместив var = world creation и svg.appending внутри моей функции ready (){}