D3 карта + скоординированная визуализация графика: выбор неправильных стран / изменение карты
Вот моя проблема: у меня есть карта SVG и гистограмма SVG. Я хочу создать скоординированный выбор. Например, пользователь выделяет столбец на графике, а страна на карте, соответствующая этим данным, также выделяется и наоборот.
Прямо сейчас я могу выделить любую страну на карте, и соответствующая соответствующая полоса также будет выделена. Однако то же самое не работает для баров. Когда я выделяю полосу, подсвечивается случайная страна, и после этого названия стран, отображаемые во всплывающей подсказке, смешиваются и вводятся неверно.
Вот карта -> основной момент гистограммы:
...
map.selectAll("countries")
.data(b.features)
.enter()
.append("path")
.attr("d", path)
//.style("stroke", "black")
.on("mouseover", function(d) {
activeDistrict = d.properties.ADMIN,
chart.selectAll("rect")
.each(function(d) {
if(d){
if (d.Country == activeDistrict){
console.log("confirmed" + d.Country)
d3.select(this).style("stroke", "blue").style("stroke-width", "3");
}
}
})
...
Вот гистограмма -> подсветка карты. Это функция, которую я не могу вести себя правильно.
var bars = chart.selectAll(".bars")
.data(data)
.enter()
.append("rect")
.on("mouseover", function(d) {
activeDistrict = d.Country,
//console.log(activeDistrict),
map.selectAll("path")
.data(b.features)
.each(function(d) {
if (d){
//console.log("activeDistrict = " + activeDistrict)
if (d.properties.ADMIN == activeDistrict){
d3.select(this).style("stroke", "blue").style("stroke-width", "3");
console.log(d.properties.ADMIN + "=" + activeDistrict)
}
}
});
А вот и весь мой JS:
<script>
window.onload = setMap();
function setMap(){
d3.csv("/data/blah.csv").then(function(data) {
//console.log(data);
d3.json("/data/blah.topojson").then(function(data2) {
//console.log(data2);
//Code with data here
var width = window.innerWidth * 0.5, // 960
height = 460;
var activeDistrict;
//chart vars
var chartWidth = window.innerWidth * 0.425,
chartHeight = 473,
leftPadding = 25,
rightPadding = 2,
topBottomPadding = 5,
chartInnerWidth = chartWidth - leftPadding - rightPadding,
chartInnerHeight = chartHeight - topBottomPadding * 2,
translate = "translate(" + leftPadding + "," + topBottomPadding + ")";
var yScale = d3.scaleLinear()
.range([0, chartHeight])
.domain([0, 2000]);
//create new svg container for the map
var map = d3.select("body")
.append("svg")
.attr("class", "map")
.attr("width", width)
.attr("height", height);
//create new svg container for the chart
var chart = d3.select("body")
.append("svg")
.attr("width", chartWidth)
.attr("height", chartHeight)
.attr("class", "chart");
//create Albers equal area conic projection centered on France
var projection = d3.geoNaturalEarth1()
.center([0, 0])
.rotate([-2, 0, 0])
//.parallels([43, 62])
.scale(175)
.translate([width / 2, height / 2]);
var path = d3.geoPath()
.projection(projection);
//translate TopoJSON
d3.selectAll(".boundary")
.style("stroke-width", 1 / 1);
var b = topojson.feature(data2, data2.objects.ne_10m_admin_0_countries);
//console.log(b)
//console.log(b.features[1].properties.ADMIN) //country name
var graticule = d3.geoGraticule();
var attrArray = ["blah blah blah"];
function joinData(b, data){
//loop through csv to assign each set of csv attribute values to geojson region
for (var i=0; i<data.length; i++){
var csvRegion = data[i]; //the current region
var csvKey = data[i].Country; //the CSV primary key
//console.log(data[i].Country)
//loop through geojson regions to find correct region
for (var a=0; a<b.features.length; a++){
var geojsonProps = b.features[a].properties; //gj props
var geojsonKey = geojsonProps.ADMIN; //the geojson primary key
//where primary keys match, transfer csv data to geojson properties object
if (geojsonKey == csvKey){
//assign all attributes and values
attrArray.forEach(function(attr){
var val = parseFloat(csvRegion[attr]); //get csv attribute value
geojsonProps[attr] = val; //assign attribute and value to geojson properties
});
};
};
};
return b;
};
joinData(b,data);
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
//Dynamically Call the current food variable to change the map
var currentFood = "Beef2";
var valArray = [];
data.forEach(function(element) {
valArray.push(parseInt(element[currentFood]));
});
var currentMax = Math.max.apply(null, valArray.filter(function(n) { return !isNaN(n); }));
console.log("Current Max Value is " + currentMax + " for " + currentFood)
var color = d3.scaleQuantile()
.domain(d3.range(0, (currentMax + 10)))
.range(d3.schemeReds[7]);
function drawMap(currentMax){
d3.selectAll("path").remove();
// Going to need to do this dynamically
// Set to ckmeans
var color = d3.scaleQuantile()
.domain(d3.range(0, currentMax))
.range(d3.schemeReds[7]);
//console.log(b[1].Beef1)
map.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
map.append("path")
.datum(graticule.outline)
.attr("class", "graticule outline")
.attr("d", path);
console.log(map.selectAll("path").size())
map.selectAll("countries")
.data(b.features)
.enter()
.append("path")
.attr("d", path)
//.style("stroke", "black")
.on("mouseover", function(d) {
activeDistrict = d.properties.ADMIN,
chart.selectAll("rect")
.each(function(d) {
if(d){
//console.log("activeDistrict = " + activeDistrict)
if (d.Country == activeDistrict){
console.log("confirmed" + d.Country)
d3.select(this).style("stroke", "blue").style("stroke-width", "3");
}
}
})
tooltip.transition() //(this.parentNode.appendChild(this))
.duration(200)
.style("opacity", .9)
.style("stroke-opacity", 1.0);
tooltip.html(d.properties.ADMIN + "<br/>" + d.properties[currentFood] + "(kg/CO2/Person/Year)")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
activeDistrict = d.properties.ADMIN,
chart.selectAll("rect")
.each(function(d) {
if (d){
//console.log("activeDistrict = " + activeDistrict)
if (d.Country == activeDistrict){
d3.select(this).style("stroke", "none").style("stroke-width", "0");
}
}
})
tooltip.transition()
.duration(500)
.style("opacity", 0)
.style("stroke-opacity", 0);
})
.style("fill", function(d) { return color(d.properties[currentFood]) });
};
drawMap(currentMax);
console.log("sum", d3.sum(valArray))
//console.log(map.selectAll("path")._groups[0][200].__data__.properties.ADMIN)
function setChart(data, data2, currentMax, valArray){
d3.selectAll("rect").remove();
d3.selectAll("text").remove();
var color = d3.scaleQuantile()
.domain(d3.range(0, (currentMax + 10)))
.range(d3.schemeReds[7]);
var chartBackground = chart.append("rect2")
.attr("class", "chartBackground")
.attr("width", chartInnerWidth)
.attr("height", chartInnerHeight)
.attr("transform", translate);
var yScale = d3.scaleLinear()
.range([0, chartHeight])
.domain([0, (currentMax+10)]);
var chartTitle = chart.append("text")
.attr("x", 20)
.attr("y", 40)
.attr("class", "chartTitle")
.text(currentFood.slice(0, -1));
var chartSub = chart.append("text")
.attr("x", 20)
.attr("y", 90)
.attr("class", "chartSub")
.text((d3.sum(valArray)*76) + " Billion World Total");
// Place Axis at some point
var bars = chart.selectAll(".bars")
.data(data)
.enter()
.append("rect")
.on("mouseover", function(d) {
activeDistrict = d.Country,
//console.log(activeDistrict),
map.selectAll("path")
.data(b.features)
.each(function(d) {
if (d){
//console.log("activeDistrict = " + activeDistrict)
if (d.properties.ADMIN == activeDistrict){
d3.select(this).style("stroke", "blue").style("stroke-width", "3");
console.log(d.properties.ADMIN + "=" + activeDistrict)
}
}
});
tooltip.transition() //(this.parentNode.appendChild(this))
.duration(200)
.style("opacity", .9)
.style("stroke-opacity", 1.0);
tooltip.html(d.Country + "<br/>" + d[currentFood] + "(kg/CO2/Person/Year)")
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
map.selectAll("path")
.data(b.features)
.each(function(d) {
if (d){
//console.log("activeDistrict = " + activeDistrict)
if (d.properties.ADMIN == activeDistrict){
d3.select(this).style("stroke", "none").style("stroke-width", "0");
console.log(d.properties.ADMIN + "=" + activeDistrict)
}
}
});
tooltip.transition()
.duration(500)
.style("opacity", 0)
.style("stroke-opacity", 0);
})
.sort(function(a, b){
return a[currentFood]-b[currentFood]
})
.transition() //add animation
.delay(function(d, i){
return i * 5
})
.duration(1)
.attr("class", function(d){
return "bars" + d.Country;
})
.attr("width", chartWidth / data.length - 1)
.attr("x", function(d, i){
return i * (chartWidth / data.length);
})
.attr("height", function(d){
return yScale(parseFloat(d[currentFood]));
})
.attr("y", function(d){
return chartHeight - yScale(parseFloat(d[currentFood]));
})
.style("fill", function(d){ return color(d[currentFood]); });
};
setChart(data, data2, currentMax, valArray);
function createDropdown(data){
//add select element
var dropdown = d3.select("body")
.append("select")
.attr("class", "dropdown")
.on("change", function(){
changeAttribute(this.value, data)
});
//add initial option
var titleOption = dropdown.append("option")
.attr("class", "titleOption")
.attr("disabled", "true")
.text("Select Attribute");
//add attribute name options
var attrOptions = dropdown.selectAll("attrOptions")
.data(attrArray)
.enter()
.append("option")
.attr("value", function(d){ return d })
.text(function(d){ return d });
};
createDropdown(data);
function changeAttribute(attribute, data){
//change the expressed attribute
currentFood = attribute;
var valArray = [];
data.forEach(function(element) {
valArray.push(parseInt(element[currentFood]));
});
var currentMax = Math.max.apply(null, valArray.filter(function(n) { return !isNaN(n); }));
console.log("Current Max Value is " + currentMax + " for " + currentFood)
// Set a dynamic color range
var color = d3.scaleQuantile()
.domain(d3.range(0, currentMax))
.range(d3.schemeReds[7]);
//recolor enumeration units
drawMap(currentMax);
//reset chart bars
setChart(data, data2, currentMax, valArray);
};
}); //csv
}); //json
}; // end of setmap
2 ответа
При рисовании стран изначально вы используете:
map.selectAll("countries")
.data(b.features)
.enter()
.append("path")
Так как нет элементов с тегом countries
на вашей странице, первоначальный выбор пуст, и .enter().append("path")
создает путь для каждого элемента в вашем массиве данных.
Но когда вы наводите курсор мыши на панели, вы переназначаете данные с помощью selectAll().data()
последовательность, но вы делаете это немного по-другому:
map.selectAll("path")
.data(b.features)
...
На вашей карте есть пути, которые не являются странами: сетка и план. Теперь мы выбрали все пути и присвоили им новые данные. Поскольку первые два элемента в выделении - это сетка и контур, теперь у них есть данные первых двух элементов в массиве данных. Все страны будут иметь привязанные данные о стране, которая находится на расстоянии двух от них в массиве данных. Вот почему неправильные данные будут выделены при наведении курсора на указатель мыши, а затем, почему всплывающие подсказки по стране неверны.
Непонятно, почему вы обновляете данные (я не вижу их изменения), вы можете добавить страны следующим образом:
var countries = map.selectAll("countries")
.data(b.features)
.enter()
.append("path")
... continue as before
или же
map.selectAll("countries")
.data(b.features)
.enter()
.append("path")
.attr("class","country")
... continue as before
А затем в функции наведения мыши из панелей используйте:
countries.each(....
или же
map.selectAll(".country").each(...
В любом случае вы можете обновить данные с помощью .data()
если нужно.
Отмечу, что each
Метод не обязателен, но может быть предпочтительным в некоторых ситуациях, по внешнему виду вы можете использовать:
var bars = chart.selectAll(".bars")
.data(data)
.enter()
.append("rect")
.on("mouseover", function(d) {
activeDistrict = d.Country,
map.selectAll(".country")
.data(b.features)
.style("stroke", function(d) {
if (d.properties.ADMIN == activeDistrict) return "blue"; else return color(d.properties[currentFood])
})
.style("stroke-width", function(d) {
if (d.properties.ADMIN == activeDistrict) return "3" else return 0;
});
})
...
Вы можете попробовать следующее, чтобы быть более последовательным в том, что вы создаете и что вы выбираете.
Карта:
- выберите по классу, потому что у вас есть больше
path
в SVG добавить класс в строку, чтобы выделить вместо установки стиля
map.selectAll(".countries") .data(b.features) .enter() .append("path") .attr("class", "countries") .attr("d", path) //.style("stroke", "black") .on("mouseover", function(d) { activeDistrict = d.properties.ADMIN, chart.selectAll(".bars") .classed("highlight", function(d) { return d && d.Country === activeDistrict; }); tooltip.transition() //(this.parentNode.appendChild(this)) .duration(200) .style("opacity", .9) .style("stroke-opacity", 1.0); tooltip.html(d.properties.ADMIN + "<br/>" + d.properties[currentFood] + "(kg/CO2/Person/Year)") .style("left", (d3.event.pageX) + "px") .style("top", (d3.event.pageY - 28) + "px"); }) .on("mouseout", function(d) { activeDistrict = d.properties.ADMIN, chart.selectAll(".bars") .classed("highlight", false); tooltip.transition() .duration(500) .style("opacity", 0) .style("stroke-opacity", 0); }) .style("fill", function(d) { return color(d.properties[currentFood]) }); };
Рисование баров
- исправить класс, добавить пробел между
bars
а такжеcountry
- не привязывать новые данные к путям карты при наведении мыши и
- выбрать пути к карте по классу, а не по типу
добавить класс к пути карты, чтобы выделить вместо установки стиля
var bars = chart.selectAll(".bars") .data(data) .enter() .append("rect") .on("mouseover", function(d) { activeDistrict = d.Country; //console.log(activeDistrict), map.selectAll(".countries") .classed("highlight", function(d) { return d && d.properties.ADMIN === activeDistrict; }); tooltip.transition() //(this.parentNode.appendChild(this)) .duration(200) .style("opacity", .9) .style("stroke-opacity", 1.0); tooltip.html(d.Country + "<br/>" + d[currentFood] + "(kg/CO2/Person/Year)") .style("left", (d3.event.pageX) + "px") .style("top", (d3.event.pageY - 28) + "px"); }) .on("mouseout", function(d) { map.selectAll(".countries") .classed("highlight", false); tooltip.transition() .duration(500) .style("opacity", 0) .style("stroke-opacity", 0); }) .sort(function(a, b){ return a[currentFood]-b[currentFood] }) .transition() //add animation .delay(function(d, i){ return i * 5 }) .duration(1) .attr("class", function(d){ return "bars " + d.Country; }) .attr("width", chartWidth / data.length - 1) .attr("x", function(d, i){ return i * (chartWidth / data.length); }) .attr("height", function(d){ return yScale(parseFloat(d[currentFood])); }) .attr("y", function(d){ return chartHeight - yScale(parseFloat(d[currentFood])); }) .style("fill", function(d){ return color(d[currentFood]); });