#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 (){}