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

Настройка: я создаю приложение-глобус, чтобы лучше визуально представлять данные по регионам мира. Он построен 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("fill"... для элемента 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)

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

  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 в свой топожсон?

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


person DMrFrost    schedule 11.03.2019    source источник


Ответы (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 ничего не делает напрямую, он применяется только к элементам презентации (и анимации):

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

Использование 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>

person Andrew Reid    schedule 11.03.2019
comment
Это похоже на ответ. Разбираю и применяю. Будет информировать вас о том, как это происходит. - person DMrFrost; 12.03.2019
comment
Одно примечание: этот ответ устраняет проблему, но изначально синий отображался только после запуска функции перетаскивания. Я решил это, поместив создание var = world и svg.appending внутри моей функции ready(){} - person DMrFrost; 12.03.2019