How to update the index (i) on bound data in d3? - javascript

On the first enter I bind data to a svg like this:
var dateContainer = svg.selectAll("g.dateContainerClass")
.data(dateData, function(d) {
return d.id
});
var dateContainerEnter = dateContainer.enter()
.append("g")
.attr("class", "dateContainerClass");
At this point the bound data consists of an array with 5 entries. So i is 0 to 4, first to last. Now the array is updated: two new entries are spliced in between 0 and 1.
dateData.splice((i + 1), 0, rawData[(rawIndex + 1)], rawData[(rawIndex + 2)])
After that the new dateData is bound to the selection again, as shown above. What I was expecting to happen is this:
i=0 (old)
i=1 (new)
i=2 (new)
i=3 (old)
i=4 (old)
etc.
What actually happens is this:
i=0 (old)
i=1 (new)
i=2 (new)
i=1 (old)
i=2 (old)
etc.
Am I doing something wrong or is this expected behavior? Any help would be really appreciated!
EDIT:
Added snippet:
Please run snippet full screen and click on Day1. When you inspect the element, the id of class "dateflag" should go up one each, which it does not.
var windowWidth = window.innerWidth;
var storyline = d3.select(".Storyline").append("svg");
function update(dateData, rawData) {
console.log(dateData)
var x = d3.scaleLinear()
.domain([0, (dateData.length - 1)])
.range(["10%", "80%"]);
storyline
.transition()
.duration(500)
.delay(50)
.attr("width", (dateData.length * 20) + "%")
.attr("height", "100%")
var dateFlags = storyline.selectAll("g.dateflag")
.data(dateData, function(d, i) {
return d
});
//ENTER
var dateFlagsEnter = dateFlags.enter()
.append("g")
.attr("class", "dateflag")
.attr("id", function(d, i) {
console.log("ContainerID: " + i)
return i
});
dateFlagsEnter.append("foreignObject")
.attr("class", "timePoints")
.attr("width", "130px")
.attr("height", "100%")
.attr("x", function(d, i) {
return x(i);
})
.attr("y", function(d, i) {
var topBottom;
if ((i % 2) == 1) {
topBottom = "35%";
} else {
topBottom = "40%";
}
return topBottom;
})
.attr("opacity", "0%");
dateFlagsEnter.select(".timePoints").append("xhtml:div")
.attr("class", "tpGroup")
dateFlagsEnter.select(".tpGroup").append("xhtml:div")
.attr("id", "dateLabel")
.append("xhtml:div")
.html(function(d, i) {
return d + " index= " + i;
})
.attr("id", "dateLabelText")
.on("click", function(d, i) {
var clickIndex = i
console.log("clickedID: " + clickIndex)
expand(dateData, clickIndex);
});
//UPDATE
dateFlags.merge(dateFlagsEnter).select(".timePoints")
.transition()
.duration(500)
.delay(50)
.attr("x", function(d, i) {
return x(i);
})
.attr("opacity", "100%");
dateFlags.merge(dateFlagsEnter).select("#dateLabelText")
.html(function(d) {
return d;
});
//EXIT
dateFlags.exit().remove();
};
function expand(dateData, clickIndex) {
var lineIndex = clickIndex
dateData.splice((lineIndex + 1), 0, "Day1.1", "Day1.2")
update(dateData);
};
function getDateData() {
var dateData = ["Day1", "Day2", "Day3", "Day4", "Day5", ]
update(dateData);
};
getDateData();
<div class="Storyline"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

When you set .attr("id" to a function, the function is executed immediately, and only once. That means that when you insert new values into the data array, you also need to re-calculate that attribute. If you move .attr("id" to after the .merge, everything works as expected:
var windowWidth = window.innerWidth;
var storyline = d3.select(".Storyline").append("svg");
function update(dateData, rawData) {
console.log(dateData)
var x = d3.scaleLinear()
.domain([0, (dateData.length - 1)])
.range(["10%", "80%"]);
storyline
.transition()
.duration(500)
.delay(50)
.attr("width", (dateData.length * 20) + "%")
.attr("height", "100%")
var dateFlags = storyline.selectAll("g.dateflag")
.data(dateData, function(d, i) {
return d
});
//ENTER
var dateFlagsEnter = dateFlags.enter()
.append("g")
.attr("class", "dateflag");
dateFlagsEnter.append("foreignObject")
.attr("class", "timePoints")
.attr("width", "130px")
.attr("height", "100%")
.attr("x", function(d, i) {
return x(i);
})
.attr("y", function(d, i) {
var topBottom;
if ((i % 2) == 1) {
topBottom = "35%";
} else {
topBottom = "40%";
}
return topBottom;
})
.attr("opacity", "0%");
dateFlagsEnter.select(".timePoints").append("xhtml:div")
.attr("class", "tpGroup")
dateFlagsEnter.select(".tpGroup").append("xhtml:div")
.attr("id", "dateLabel")
.append("xhtml:div")
.html(function(d, i) {
return d + " index= " + i;
})
.attr("id", "dateLabelText")
.on("click", function(d, i) {
var clickIndex = i
console.log("clickedID: " + clickIndex)
expand(dateData, clickIndex);
});
//UPDATE
dateFlags.merge(dateFlagsEnter)
.attr("id", function(d, i) {
console.log("ContainerID: " + i)
return i
})
.select(".timePoints")
.transition()
.duration(500)
.delay(50)
.attr("x", function(d, i) {
return x(i);
})
.attr("opacity", "100%");
dateFlags.merge(dateFlagsEnter).select("#dateLabelText")
.html(function(d) {
return d;
});
//EXIT
dateFlags.exit().remove();
};
function expand(dateData, clickIndex) {
var lineIndex = clickIndex
dateData.splice((lineIndex + 1), 0, "Day1.1", "Day1.2")
update(dateData);
};
function getDateData() {
var dateData = ["Day1", "Day2", "Day3", "Day4", "Day5", ]
update(dateData);
};
getDateData();
<div class="Storyline"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Related

d3 chart won't centre in UIkit framework

I have designed a chart using d3.js and have embedded it in a div within a UIkit framework, however I can't get it to centre within the div.
I have tried putting the svg in a container and adjusting the CSS by putting the svg in a flexbox, removing the margins, aligning it centre, but none of it works. I have tried this response, this one, this one and this one and none of them work. I'm sure it must be something simple - how can I get it to centre? Code below.
Chart
<div id="age_chart" class="uk-container uk-width-medium-1-2 uk-container-center uk-margin-top">
<script>
// chart based on this example: https://bl.ocks.org/63anp3ca/6bafeb64181d87750dbdba78f8678715
// var svg1 = d3.select("svg1"),
var margin = {top: 20, right: 20, bottom: 100, left: 20},
width1 = d3.select("#age_chart").node().getBoundingClientRect().width,
height1 = 400 - margin.top - margin.bottom;
// g = svg1.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// The scale spacing the groups:
var x0 = d3.scaleBand()
.rangeRound([0, width1])
.paddingInner(0.05);
// The scale for spacing each group's bar:
var x1 = d3.scaleBand()
.padding(0.05);
var y = d3.scaleLinear()
.rangeRound([height1, 0]);
var z = d3.scaleOrdinal()
.range(["#2c7fb8", "#7fcdbb"]);
var svg1 = d3.select('body').append("svg")
.attr("width", width1 + margin.left + margin.right)
.attr("height", height1 + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("data/age_distribution.csv", function(d, i, columns) {
for (var i = 1, n = columns.length; i < n; ++i) d[columns[i]] = +d[columns[i]];
return d;
}).then(function(data) {
console.log(data);
var keys = data.columns.slice(1);
console.log('keys');
console.log(keys);
x0.domain(data.map(function(d) { return d.age; }));
x1.domain(keys).rangeRound([0, x0.bandwidth()]);
y.domain([0, d3.max(data, function(d) { return d3.max(keys, function(key) { return d[key]; }); })]).nice();
svg1.append("g")
.selectAll("g")
.data(data)
.enter().append("g")
.attr("class","bar")
.attr("transform", function(d) { return "translate(" + x0(d.age) + ",0)"; })
.selectAll("rect")
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
.enter().append("rect")
.attr("x", function(d) { return x1(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("width", x1.bandwidth())
.attr("height", function(d) { return height1 - y(d.value); })
.attr("fill", function(d) { return z(d.key); });
svg1.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + height1 + ")")
.call(d3.axisBottom(x0));
svg1.append("g")
.attr("class", "y axis")
.call(d3.axisLeft(y).ticks(null, "s"))
.append("text")
.attr("x", 2)
.attr("y", y(y.ticks().pop()) + 0.5)
.attr("dy", "0.32em")
.attr("fill", "#000")
.attr("font-size", 11)
.attr("text-anchor", "start")
.text("Percentage of population")
.attr("font-family", "Archivo");
var legend1 = svg1.append("g")
.attr("font-family", "inherit")
.attr("font-size", 11)
.attr("text-anchor", "end")
.selectAll("g")
.data(keys.slice().reverse())
.enter().append("g")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend1.append("rect")
.attr("x", width1 * .9)
.attr("width", 15)
.attr("height", 15)
.attr("fill", z)
.attr("stroke", z)
.attr("stroke-width",2)
.on("click",function(d) { update(d) });
legend1.append("text")
.attr("x", width1 * .85)
.attr("y", 9.5)
.attr("dy", "0.32em")
.text(function(d) { return d; });
var filtered = [];
////
//// Update and transition on click:
////
function update(d) {
//
// Update the array to filter the chart by:
//
// add the clicked key if not included:
if (filtered.indexOf(d) == -1) {
filtered.push(d);
// if all bars are un-checked, reset:
if(filtered.length == keys.length) filtered = [];
}
// otherwise remove it:
else {
filtered.splice(filtered.indexOf(d), 1);
}
//
// Update the scales for each group's items:
//
var newKeys = [];
keys.forEach(function(d) {
if (filtered.indexOf(d) == -1 ) {
newKeys.push(d);
}
})
x1.domain(newKeys).rangeRound([0, x0.bandwidth()]);
y.domain([0, d3.max(data, function(d) { return d3.max(keys, function(key) { if (filtered.indexOf(key) == -1) return d[key]; }); })]).nice();
// update the y axis:
svg1.select(".y")
.transition()
.call(d3.axisLeft(y).ticks(null, "s"))
.duration(500);
//
// Filter out the bands that need to be hidden:
//
var bars = svg1.selectAll(".bar").selectAll("rect")
.data(function(d) { return keys.map(function(key) { return {key: key, value: d[key]}; }); })
bars.filter(function(d) {
return filtered.indexOf(d.key) > -1;
})
.transition()
.attr("x", function(d) {
return (+d3.select(this).attr("x")) + (+d3.select(this).attr("width"))/2;
})
.attr("height",0)
.attr("width",0)
.attr("y", function(d) { return height1; })
.duration(500);
//
// Adjust the remaining bars:
//
bars.filter(function(d) {
return filtered.indexOf(d.key) == -1;
})
.transition()
.attr("x", function(d) { return x1(d.key); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height1 - y(d.value); })
.attr("width", x1.bandwidth())
.attr("fill", function(d) { return z(d.key); })
.duration(500);
// update legend:
legend1.selectAll("rect")
.transition()
.attr("fill",function(d) {
if (filtered.length) {
if (filtered.indexOf(d) == -1) {
return z(d);
}
else {
return "white";
}
}
else {
return z(d);
}
})
.duration(100);
}
});
</script>
</div>
The problem is that the SVG isn't in your div.
The position of a script element doesn't decide where the SVG will get appended on the page. If you put it at the bottom of the page, you'll get the same results - you need to do so to wait for the DOM to be fully loaded.
d3.select('body').append("svg") will append the svg directly on the body element. To append it to the element you've created, since it has id age_chart, use:
d3.select('#age_chart').append("svg")

D3 Map + Graph Coordinated Visualization: selecting wrong countries / map changing attributes

Here is my issue: I have a map SVG and a bar Graph SVG. I want to create a coordinated selection. For instance, the user highlights a bar in the graph and the country on the map corresponding to that data is also highlighted and Vice Versa.
Right now I can highlight any country on the map and the proper corresponding bar will also highlight. However, the same does not work for the bars. When I highlight a bar, a random country is highlighted and after that the country names, as displayed in a tooltip, are all jumbled and wrong.
Here is the map -> bar graph highlight:
...
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");
}
}
})
...
Here is the bar graph -> map highlight. This is the function I cannot get to behave properly.
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)
}
}
});
And here is my entire 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
When drawing countries initially you use:
map.selectAll("countries")
.data(b.features)
.enter()
.append("path")
As there are no elements with the tag countries on your page, the initial selection is empty, and .enter().append("path") creates a path for each item in your data array.
But when you do a mouseover on the bars you re-assign the data with a selectAll().data() sequence, but you do it a bit differently:
map.selectAll("path")
.data(b.features)
...
There are paths in your map that aren't countries: the graticule and the outline. Now we've selected all the paths and assigned new data to them. Since the first two items in the selection are the graticule and the outline they now have the data of the first two items in the data array. All the countries will have bound data of a country that is two away from them in the data array. This is why the wrong data will be highlighted when mouseovering the bars and afterwards why the country tooltips are wrong.
It is not clear why you update the data (I don't see it changing), you could append the countries as so:
var countries = map.selectAll("countries")
.data(b.features)
.enter()
.append("path")
... continue as before
or
map.selectAll("countries")
.data(b.features)
.enter()
.append("path")
.attr("class","country")
... continue as before
And then in the mouseover function of the bars use:
countries.each(....
or
map.selectAll(".country").each(...
Either way still lets you update the data with .data() if needed.
I'll note that the each method isn't necessary, but may be preferable in some situations, by the looks of it you could use:
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;
});
})
...
You might try the following to be a bit more consistent in what you create and what you select.
The map:
select by class because you have more path in the svg
add a class to the bar to highlight instead of setting a style
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]) });
};
Drawing of the bars
fix the class, add a space between the bars and country
do not bind new data to the map paths on mouse over and mouse out
select map paths by class not by type
add a class to the map path to highlight instead of setting a style
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]); });

Morph areas into bars with D3?

I'm able to generate the following graph using D3 areas:
I want to create the following animation. When the webpage loads, you see the first figure. Then, each of the areas morphs into a bar. Finally, I would like to allow users to toggle between the two figures by clicking "B" or "D".
I was able to add the buttons and the corresponding bars to my figure, but I'm having troubles figuring out how to do the animation. This is the code that I have right now:
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("threshold: ", opts.threshold);
console.log("bars: ", opts.bars);
var margin = {left:50,right:50,top:40,bottom:125};
xMax = d3.max(opts.data, function(d) { return d.x ; });
yMax = d3.max(opts.data, function(d) { return d.y ; });
xMin = d3.min(opts.data, function(d) { return d.x ; });
yMin = d3.min(opts.data, function(d) { return d.y ; });
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height-margin.bottom,0]);
var x = d3.scaleLinear()
.domain([xMin,xMax])
.range([0,width]);
var yAxis = d3.axisLeft(y);
var xAxis = d3.axisBottom(x);
var area = d3.area()
.x(function(d){ return x(d.x) ;})
.y0(height-margin.bottom)
.y1(function(d){ return y(d.y); });
var svg = d3.select(el).append('svg').attr("height","100%").attr("width","100%");
var chartGroup = svg.append("g").attr("transform","translate("+margin.left+","+margin.top+")");
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x< -opts.MME ;})))
.style("fill", opts.colors[0]);
if(opts.MME !==0){
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return (d.x < opts.MME & d.x > -opts.MME) ;})))
.style("fill", opts.colors[1]);
}
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x > opts.MME ;})))
.style("fill", opts.colors[2]);
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+(height-margin.bottom)+")")
.call(xAxis);
var tooltip = d3.tip()
.attr('class', 'd3-tip chart-data-tip')
.offset([30, 0])
.direction('s')
.html(function(d, i) {
return "<strong>" + d + "</strong> <span style='color:" + "white" + "'>"+ "</span>";
});
svg.call(tooltip);
chartGroup.selectAll("path").data(opts.text).on('mouseover', tooltip.show).on('mouseout', tooltip.hide);
// Bars
var yBar = d3.scaleLinear()
.domain([0,1])
.range([height-margin.bottom,0]);
var xBar = d3.scaleBand()
.domain(opts.bars.map(function(d) { return d.x; }))
.rangeRound([0, width]).padding(0.1);
var yAxisBar = d3.axisLeft(yBar);
var xAxisBar = d3.axisBottom(xBar);
var g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.append("g")
.attr("class", "axis axis--x")
.attr("transform","translate(0,"+(height-margin.bottom)+")")
.call(d3.axisBottom(xBar));
g.append("g")
.attr("class", "axis axis--y")
.call(d3.axisLeft(yBar).ticks(10, "%"))
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Probability");
g.selectAll(".bar")
.data(opts.bars)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) { return xBar(d.x); })
.attr("y", function(d) { return yBar(d.y); })
.attr("width", xBar.bandwidth())
.style("fill", function(d) { return d.color; })
.attr("height", function(d) { return height - margin.bottom - yBar(d.y); });
// Add buttons
//container for all buttons
var allButtons= svg.append("g")
.attr("id","allButtons");
//fontawesome button labels
var labels= ["B", "D"];
//colors for different button states
var defaultColor= "#E0E0E0";
var hoverColor= "#808080";
var pressedColor= "#000000";
//groups for each button (which will hold a rect and text)
var buttonGroups= allButtons.selectAll("g.button")
.data(labels)
.enter()
.append("g")
.attr("class","button")
.style("cursor","pointer")
.on("click",function(d,i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode));
d3.select("#numberToggle").text(i+1);
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill",hoverColor);
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill",defaultColor);
}
});
var bWidth= 40; //button width
var bHeight= 25; //button height
var bSpace= 10; //space between buttons
var x0= 20; //x offset
var y0= 10; //y offset
//adding a rect to each toggle button group
//rx and ry give the rect rounded corner
buttonGroups.append("rect")
.attr("class","buttonRect")
.attr("width",bWidth)
.attr("height",bHeight)
.attr("x",function(d,i) {return x0+(bWidth+bSpace)*i;})
.attr("y",y0)
.attr("rx",5) //rx and ry give the buttons rounded corners
.attr("ry",5)
.attr("fill",defaultColor);
//adding text to each toggle button group, centered
//within the toggle button rect
buttonGroups.append("text")
.attr("class","buttonText")
.attr("x",function(d,i) {
return x0 + (bWidth+bSpace)*i + bWidth/2;
})
.attr("y",y0+bHeight/2)
.attr("text-anchor","middle")
.attr("dominant-baseline","central")
.attr("fill","white")
.text(function(d) {return d;});
function updateButtonColors(button, parent) {
parent.selectAll("rect")
.attr("fill",defaultColor);
button.select("rect")
.attr("fill",pressedColor);
}
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
And this is the figure that that code produces:
This does the trick:
HTMLWidgets.widget({
name: 'IMPosterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
//transition
var transDuration = 1000;
var dataDiscrete = opts.bars.map((b, i) => {
b.y = Number(b.y);
b.desc = opts.text[i];
return b;
});
var distParams = {
min: d3.min(opts.data, d => d.x),
max: d3.max(opts.data, d => d.x)
};
distParams.cuts = [-opts.MME, opts.MME, distParams.max];
opts.data = opts.data.sort((a,b) => a.x - b.x);
var dataContinuousGroups = [];
distParams.cuts.forEach((c, i) => {
let data = opts.data.filter(d => {
if (i === 0) {
return d.x < c;
} else if (i === distParams.cuts.length - 1) {
return d.x > distParams.cuts[i - 1];
} else {
return d.x < c && d.x > distParams.cuts[i - 1];
}
});
data.unshift({x:data[0].x, y:0});
data.push({x:data[data.length - 1].x, y:0});
dataContinuousGroups.push({
color: opts.colors[i],
data: data
});
});
var margin = {
top: 50,
right: 20,
bottom: 80,
left: 70
},
dims = {
width: width - margin.left - margin.right,
height: height - margin.top - margin.bottom
};
var xContinuous = d3.scaleLinear()
.domain([distParams.min - 1, distParams.max + 1])
.range([0, dims.width]);
var xDiscrete = d3.scaleBand()
.domain(dataDiscrete.map(function(d) { return d.x; }))
.rangeRound([0, dims.width]).padding(0.1);
var y = d3.scaleLinear()
.domain([0, 1])
.range([dims.height, 0]);
var svg = d3.select(el).append("svg")
.attr("width", dims.width + margin.left + margin.right)
.attr("height", dims.height + margin.top + margin.bottom);
var g = svg
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var xAxis = d3.axisBottom()
.scale(xDiscrete);
var yAxis = d3.axisLeft()
.scale(y)
.ticks(10)
.tickFormat(d3.format(".0%"));
var yLabel = g.append("text")
.attr("class", "y-axis-label")
.attr("transform", "rotate(-90)")
.attr("y", -52)
.attr("x", -160)
.attr("dy", ".71em")
.style("text-anchor", "end")
.style("font-size", 14 + "px")
.text("Probability");
g.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + dims.height + ")")
.call(xAxis);
g.append("g")
.attr("class", "y axis")
.call(yAxis);
var areas = g.selectAll(".area")
.data(dataDiscrete)
.enter().append("path")
.attr("class", "area")
.style("fill", function(d) { return d.color; })
.attr("d", function(d, i) {
let numPts = dataContinuousGroups[i].data.length - 2;
var path = d3.path()
path.moveTo(xDiscrete(d.x), y(0));
for (j=0; j<numPts; j++) {
path.lineTo(xDiscrete(d.x) + j*xDiscrete.bandwidth()/(numPts-1), y(d.y))
}
path.lineTo(xDiscrete(d.x) + xDiscrete.bandwidth(), y(0));
return path.toString();
});
var tooltip = d3.tip()
.attr('class', 'd3-tip chart-data-tip')
.offset([30, 0])
.direction('s')
.html(function(d, i) {
return "<span>" + dataDiscrete[i].desc + "</span>";
});
g.call(tooltip);
areas
.on('mouseover', tooltip.show)
.on('mouseout', tooltip.hide);
var thresholdLine = g.append("line")
.attr("stroke", "black")
.style("stroke-width", "1.5px")
.style("stroke-dasharray", "5,5")
.style("opacity", 1)
.attr("x1", 0)
.attr("y1", y(opts.threshold))
.attr("x2", dims.width)
.attr("y2", y(opts.threshold));
var updateXAxis = function(type, duration) {
if (type === "continuous") {
xAxis.scale(xContinuous);
} else {
xAxis.scale(xDiscrete);
}
d3.select(".x").transition().duration(duration).call(xAxis);
};
var updateYAxis = function(data, duration) {
var extent = d3.extent(data, function(d) {
return d.y;
});
extent[0] = 0;
extent[1] = extent[1] + 0.2*(extent[1] - extent[0]);
y.domain(extent);
d3.select(".y").transition().duration(duration).call(yAxis);
};
var toggle = function(to, duration) {
if (to === "distribution") {
updateYAxis(dataContinuousGroups[0].data.concat(dataContinuousGroups[1].data).concat(dataContinuousGroups[2].data), 0);
updateXAxis("continuous", duration);
areas
.data(dataContinuousGroups)
.transition()
.duration(duration)
.attr("d", function(d) {
var gen = d3.line()
.x(function(p) {
return xContinuous(p.x);
})
.y(function(p) {
return y(p.y);
});
return gen(d.data);
});
thresholdLine
.style("opacity", 0);
g.select(".y.axis")
.style("opacity", 0);
g.select(".y-axis-label")
.style("opacity", 0);
} else {
y.domain([0, 1]);
d3.select(".y").transition().duration(duration).call(yAxis);
updateXAxis("discrete", duration);
areas
.data(dataDiscrete)
.transition()
.duration(duration)
.attr("d", function(d, i) {
let numPts = dataContinuousGroups[i].data.length - 2;
var path = d3.path()
path.moveTo(xDiscrete(d.x), y(0));
for (j=0; j<numPts; j++) {
path.lineTo(xDiscrete(d.x) + j*xDiscrete.bandwidth()/(numPts-1), y(d.y))
}
path.lineTo(xDiscrete(d.x) + xDiscrete.bandwidth(), y(0));
return path.toString();
});
thresholdLine
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1)
.attr("y1", y(opts.threshold))
.attr("y2", y(opts.threshold));
g.select(".y.axis")
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1);
g.select(".y-axis-label")
.transition()
.duration(0)
.delay(duration)
.style("opacity", 1);
}
};
// Add buttons
//container for all buttons
var allButtons = svg.append("g")
.attr("id", "allButtons");
//fontawesome button labels
var labels = ["B", "D"];
//colors for different button states
var defaultColor = "#E0E0E0";
var hoverColor = "#808080";
var pressedColor = "#000000";
//groups for each button (which will hold a rect and text)
var buttonGroups = allButtons.selectAll("g.button")
.data(labels)
.enter()
.append("g")
.attr("class", "button")
.style("cursor", "pointer")
.on("click", function(d, i) {
updateButtonColors(d3.select(this), d3.select(this.parentNode));
d3.select("#numberToggle").text(i + 1);
if (d === "D") {
toggle("distribution", transDuration);
} else {
toggle("discrete", transDuration);
}
})
.on("mouseover", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill", hoverColor);
}
})
.on("mouseout", function() {
if (d3.select(this).select("rect").attr("fill") != pressedColor) {
d3.select(this)
.select("rect")
.attr("fill", defaultColor);
}
});
var bWidth = 40; //button width
var bHeight = 25; //button height
var bSpace = 10; //space between buttons
var x0 = 20; //x offset
var y0 = 10; //y offset
//adding a rect to each toggle button group
//rx and ry give the rect rounded corner
buttonGroups.append("rect")
.attr("class", "buttonRect")
.attr("width", bWidth)
.attr("height", bHeight)
.attr("x", function(d, i) {
return x0 + (bWidth + bSpace) * i;
})
.attr("y", y0)
.attr("rx", 5) //rx and ry give the buttons rounded corners
.attr("ry", 5)
.attr("fill", defaultColor);
//adding text to each toggle button group, centered
//within the toggle button rect
buttonGroups.append("text")
.attr("class", "buttonText")
.attr("x", function(d, i) {
return x0 + (bWidth + bSpace) * i + bWidth / 2;
})
.attr("y", y0 + bHeight / 2)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "central")
.attr("fill", "white")
.text(function(d) {
return d;
});
function updateButtonColors(button, parent) {
parent.selectAll("rect")
.attr("fill", defaultColor);
button.select("rect")
.attr("fill", pressedColor);
}
toggle("distribution", 0);
setTimeout(() => {
toggle("discrete", transDuration);
}, 1000);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});

Add panning to D3 force-directed graph; Keep getting "Cannot read property 'translate' of null" error

I am trying to add panning only (don't want zoom for now), to my D3 force-directed graph visualization. No matter what I try, I keep getting an error - "Cannot read property 'translate' of null". I feel as though I am setting things up like in all the examples I find, but something isn't quite right.
Here's my latest attempt. As far as I know, the relevant code should be at the top where I define svg, but I included the rest in case something is affecting it of which I'm not aware.
var isiOS = false;
var agent = navigator.userAgent.toLowerCase();
if(agent.indexOf('iphone') >= 0 || agent.indexOf('ipad') >= 0){
isiOS = true;
};
var h = 1000;
var w = 1000;
var width = 1500,
var height = 1500;
var default_node_color = '#FF9900';
var shining_node_color = '#fc5f05';
var text_color = 'white';
var link_color = '#666699';
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.behavior.zoom().on("zoom", function() {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
}))
.append('g');
var g = svg.append('g');
var force = d3.layout.force()
.gravity(0.05)
.distance(100)
.charge(-100)
.size([w, h]);
d3.xhr("{{ url_for('visualization_data', interactions_filter = interactions_filter)}}")
.header("Content-Type", "application/json")
.post(
JSON.stringify({
"fileindicator": "{{fileindicator}}"
}), function(error, rawData) {
if (error) throw error;
var json = JSON.parse(rawData.response);
force
.nodes(json.nodes)
.links(json.links)
.start();
var link_scale = d3.scale.linear()
.domain([0, d3.max(json.links, function(d) {
return (d.value);
})])
.range([1, 13]);
var link = g.selectAll(".link")
.data(json.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) {
return link_scale(d.value);
})
.style('stroke', link_color)
.attr("fill-opacity", .75);
var node = g.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag)
.style("fill", default_node_color);
var radius_scale = d3.scale.sqrt()
.domain([0, d3.max(json.nodes, function(d) {
return (d.n_interactions);
})])
.range([2, 17]);
if (isiOS) {
// single click centers node
var centerEvent = "click";
} else {
// double click centers node
var centerEvent = "dblclick";
}
var all_links = d3.selectAll('.link');
var all_nodes = d3.selectAll('.node');
//populate select box
var select_box = $('.chzn-select');
select_box.empty();
select_box.trigger('chosen:updated');
var character_to_node_id = []
for (var i = 0; i < all_nodes[0].length; i++) {
var option = '<option value="' + all_nodes[0][i].__data__.index + '">' + all_nodes[0][i].__data__.name + '</option>';
character_to_node_id.push(option);
select_box.append("<option value='' selected='selected'></option>");
select_box.append(character_to_node_id);
var onClick = window.onClick = function(d, i) {
// console.log(this)
// console.log('d, i, this:', d, i, this);
var index_of_clicked_node = d.index;
// Moves clicked node to center
var dcx = (w / 2 - d.x * zoom.scale());
var dcy = (h / 2 - d.y * zoom.scale());
zoom.translate([dcx, dcy]);
g.transition()
.duration(2000)
.attr("transform", "translate(" + dcx + "," + dcy + ")scale(" + zoom.scale() + ")");
//select all links NOT related to the clicked node and fade
var fade_links = d3.selectAll('.link').filter(function(d) {
return index_of_clicked_node !== d.source.index && index_of_clicked_node !== d.target.index;
});
// console.log(fade_links);
fade_links.style('opacity', .2);
var active_links = d3.selectAll('.link').filter(function(d) {
return index_of_clicked_node === d.source.index || index_of_clicked_node === d.target.index;
});
// active_links.style('stroke', 'red')
active_links.style('opacity', .85);
var active_link_id_list = [];
active_links[0].forEach(function(link) {
active_link_id_list.push(link.__data__.target.index)
active_link_id_list.push(link.__data__.source.index)
});
console.log('number of active links: ', active_link_id_list.length);
//select all nodes not connected to the clicked node and fade
var fade_nodes = d3.selectAll('.node').filter(function(d) {
return !active_link_id_list.includes(d.index);
});
fade_nodes.style('opacity', .2);
var active_nodes = d3.selectAll('.node').filter(function(d) {
return active_link_id_list.includes(d.index);
});
console.log('number of active nodes: ', active_nodes);
d3.selectAll('circle').filter(function(d, i) {
if (d.name === name) {
// call click event on the circle
window.onClick(d, i);
}
});
active_nodes.style('opacity', .85)
.transition()
.duration(1000)
.style('fill', shining_node_color)
.transition()
.duration(1000)
.style('fill', default_node_color);
};
node.append('circle')
.attr("class", "circle")
.attr("r", function(d, i) {
return radius_scale(d.n_interactions);
})
.style('opacity', .85)
.style('stroke', 'black')
.style('stroke-width', 10)
.style('stroke-opacity', 0.0)
.call(force.drag)
.on(centerEvent, onClick);
var text_scale = d3.scale.linear()
.domain([0, d3.max(json.nodes, function(d) {
return (d.n_interactions);
})])
.range([6, 16]);
node.append("text")
.attr("dx", 12)
.attr("dy", ".35em")
.attr('fill', 'black')
.text(function(d) {
return d.name
})
.style("font-size", function(d) {
return text_scale(d.n_interactions) + "px";
})
.style('font-family', "futura-pt")
.style('fill', text_color);
force.on("tick", function() {
link.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
});
node.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
$('#loading-text').hide();

Setting color of marker-end arrow in D3.js

The code below displays marker-ends on arrows/paths/lines as intended, but the color of the marker-end does not vary by line (i.e., it is always the same orange color, not the color of its respective line). I think the code is defaulting to the color assigned to the first field of my data(?). Any advice would be appreciated.
<script src="http://www.protobi.com/javascripts/d3.v3.min.js"></script>
<script src="http://labratrevenge.com/d3-tip/javascripts/d3.tip.v0.6.3.js"></script>
<script src="http://www.protobi.com/examples/pca/pca.js"></script>
<script type="text/javascript">
var margin = {top: 20, right: 20, bottom: 20, left: 20};
var width = 1500 - margin.left - margin.right;
var height = 1500 - margin.top - margin.bottom;
var angle = Math.PI * 0;
var color = d3.scale.category10();
var x = d3.scale.linear().range([width, 0]); // switch to match how R biplot shows it
var y = d3.scale.linear().range([height, 0]);
x.domain([-3.5,3.5]).nice()
y.domain([-3.5,3.5]).nice()
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
var svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("/brand.csv", function(error, data) {
var matrix = data.map(function(d){
return d3.values(d).slice(1,d.length).map(parseFloat);
});
var pca = new PCA();
matrix = pca.scale(matrix,true,true);
pc = pca.pca(matrix,2)
var A = pc[0]; // this is the U matrix from SVD
var B = pc[1]; // this is the dV matrix from SVD
var brand_names = Object.keys(data[0]); // first row of data file ["ATTRIBUTE", "BRAND A", "BRAND B", "BRAND C", ...]
brand_names.shift(); // drop the first column label, e.g. "ATTRIBUTE"
data.map(function(d,i){
label: d.ATTRIBUTE,
d.pc1 = A[i][0];
d.pc2 = A[i][1];
});
var label_offset = {
"Early/First line": 20,
"Unfamiliar":10,
"Convenient": -5
}
var brands = brand_names
.map(function(key, i) {
return {
brand: key,
pc1: B[i][0]*4,
pc2: B[i][1]*4
}
});
function rotate(x,y, dtheta) {
var r = Math.sqrt(x*x + y*y);
var theta = Math.atan(y/x);
if (x<0) theta += Math.PI;
return {
x: r * Math.cos(theta + dtheta),
y: r * Math.sin(theta + dtheta)
}
}
data.map(function(d) {
var xy = rotate(d.pc1, d.pc2, angle);
d.pc1 = xy.x;
d.pc2 = xy.y;
});
brands.map(function(d) {
var xy = rotate(d.pc1, d.pc2, angle);
d.pc1 = xy.x;
d.pc2 = xy.y;
});
var showAxis = false; // normally we don't want to see the axis in PCA, it's meaningless
if (showAxis) {
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("PC1");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("class", "label")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("PC2");
}
var legend = svg.selectAll(".legend")
.data(color.domain())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
svg.selectAll(".dot")
.data(data)
.enter().append("circle")
.attr("class", "dot")
.attr("r", 10.5)
.attr("cx", function(d) { return x(d.pc1); })
.attr("cy", function(d) { return y(d.pc2); })
.style("fill", function(d) { return color(d['species']); })
.on('mouseover', onMouseOverAttribute)
.on('mouseleave', onMouseLeave);
svg.selectAll("text.brand")
.data(brands)
.enter().append("text")
.attr("class", "label-brand")
.attr("x", function(d) { return x(d.pc1) + 10; })
.attr("y", function(d) { return y(d.pc2) + 0; })
.text(function(d) { return d['brand']})
svg.selectAll("marker.brand")
.data(brands)
.enter().append("svg:marker")
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 6)
.attr('markerWidth', 10)
.attr('markerHeight', 10)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.style("fill", function(d) { return color(d['brand']); });
svg.selectAll(".line")
.data(brands)
.enter().append("line")
.attr("class", "square")
.attr('x1', function(d) { return x(-d.pc1);})
.attr('y1', function(d) { return y(-d.pc2); })
.attr("x2", function(d) { return x(d.pc1); })
.attr("y2", function(d) { return y(d.pc2); })
.style("stroke", function(d) { return color(d['brand']); })
.style('marker-end', "url(#end-arrow)")
.on('mouseover', onMouseOverBrand)
.on('mouseleave', onMouseLeave);
svg.selectAll("text.attr")
.data(data)
.enter().append("text")
.attr("class", "label-attr")
.attr("x", function(d,i ) { return x(d.pc1)+4 ; })
.attr("y", function(d ,i) { return y(d.pc2) + (label_offset[d.ATTRIBUTE]||0); })
.text(function(d,i) { return d.ATTRIBUTE})
var pctFmt = d3.format('0%')
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([10, 20])
.direction('e')
.html(function(values,title) {
var str =''
str += '<h3>' + (title.length==1 ? 'Brand ' : '' )+ title + '</h3>'
str += "<table>";
for (var i=0; i<values.length; i++) {
if (values[i].key != 'ATTRIBUTE' && values[i].key != 'pc1' && values[i].key != 'pc2') {
str += "<tr>";
str += "<td>" + values[i].key + "</td>";
str += "<td class=pct>" + pctFmt(values[i].value) + "</td>";
str + "</tr>";
}
}
str += "</table>";
return str;
});
svg.call(tip);
function getSpPoint(A,B,C){
var x1=A.x, y1=A.y, x2=B.x, y2=B.y, x3=C.x, y3=C.y;
var px = x2-x1, py = y2-y1, dAB = px*px + py*py;
var u = ((x3 - x1) * px + (y3 - y1) * py) / dAB;
var x = x1 + u * px, y = y1 + u * py;
return {x:x, y:y}; //this is D
}
// draw line from the attribute a perpendicular to each brand b
function onMouseOverAttribute(a,j) {
brands.forEach(function(b, idx) {
var A = { x: 0, y:0 };
var B = { x: b.pc1, y: b.pc2 };
var C = { x: a.pc1, y: a.pc2 };
b.D = getSpPoint(A,B,C);
});
svg.selectAll('.tracer')
.data(brands)
.enter()
.append('line')
.attr('class', 'tracer')
.attr('x1', function(b,i) { return x(a.pc1); return x1; })
.attr('y1', function(b,i) { return y(a.pc2); return y1; })
.attr('x2', function(b,i) { return x(b.D.x); return x2; })
.attr('y2', function(b,i) { return y(b.D.y); return y2; })
.style("stroke", function(d) { return "#aaa"});
delete a.D;
var tipText = d3.entries(a);
tip.show(tipText, a.ATTRIBUTE);
};
// draw line from the brand axis a perpendicular to each attribute b
function onMouseOverBrand(b,j) {
data.forEach(function(a, idx) {
var A = { x: 0, y:0 };
var B = { x: b.pc1, y: b.pc2 };
var C = { x: a.pc1, y: a.pc2 };
a.D = getSpPoint(A,B,C);
});
svg.selectAll('.tracer')
.data(data)
.enter()
.append('line')
.attr('class', 'tracer')
.attr('x1', function(a,i) { return x(a.D.x); })
.attr('y1', function(a,i) { return y(a.D.y); })
.attr('x2', function(a,i) { return x(a.pc1); })
.attr('y2', function(a,i) { return y(a.pc2); })
.style("stroke", function(d) { return "#aaa"});
var tipText = data.map(function(d) {
return {key: d.ATTRIBUTE, value: d[b['brand']] }
})
tip.show(tipText, b.brand);
};
function onMouseLeave(b,j) {
svg.selectAll('.tracer').remove()
tip.hide();
}
});
While you are creating an svg:marker for each line, you give them all the same id. When they are then used on your line elements, since they all have the same id you are only using one of them.
Simple fix, give them unique ids:
svg.selectAll("marker.brand")
.data(brands)
.enter().append("svg:marker")
.attr('id', function(d,i){
return 'end-arrow' + i; //<-- append index postion
})
...
svg.selectAll(".line")
.data(brands)
.enter().append("line")
.attr("class", "square")
.attr('x1', function(d) {
return x(-d.pc1);
})
.attr('y1', function(d) {
return y(-d.pc2);
})
.attr("x2", function(d) {
return x(d.pc1);
})
.attr("y2", function(d) {
return y(d.pc2);
})
.style("stroke", function(d) {
return color(d['brand']);
})
.style('marker-end', function(d,i){
return "url(#end-arrow"+i+")"; //<--use the one with the right id
})
....
Example here.

Categories

Resources