Related
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]); });
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
}
};
}
});
I am trying to alter the traditional zooming feature on a sunburst chart. Traditionally when you click on a partition, that partition grows to cover 100% of the base layer while all other partitions on the same layer disappear. The children of the selected partition all grow to fill the newly created space.
My current code does just what I stated above. I would like to alter my code to allow for the selected partition to only take up 75% of the base layer. The children elements will grow to cover this new space but the remaining 25% will still contain all other non-selected partitions.
I have tried altering the 't' value that is returned from d3.interpolate() but I have had unpredictable results.
I hope my description is clear.
Does anyone have any thoughts on this?
<script>
var width = 960,
height = 700,
radius = Math.min(width, height) / 2;
var x = d3.scale.linear()
.range([0, 2 * Math.PI]);
var y = d3.scale.linear()
.range([0, radius]);
var color = d3.scale.category20c();
function percent(d) {
var percentage = (d.value / 956129) * 100;
return percentage.toFixed(2);
}
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>" + d.name + "</strong> <span style='color:red'>" + percent(d) + "%</span>";
})
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2 + 10) + ")");
svg.call(tip);
var partition = d3.layout.partition()
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, y(d.y)) })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)) });
d3.json("flare.json", function(error, root) {
var g = svg.selectAll("g")
.data(partition.nodes(root))
.enter().append("g");
var path = g.append("path")
.attr("d", arc)
// .attr("stroke", 'black')
// .style("fill", function(d) { return color((d.children ? d : d.parent).name); })
.style("fill", function(d, i) {
return color(i);
})
.on("click", click)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
var text = g.append("text")
.attr("transform", function(d) { return "rotate(" + computeTextRotation(d) + ")"; })
.attr("x", function(d) { return y(d.y); })
.attr("dx", "6") // margin
.attr("dy", ".35em") // vertical-align
.text(function(d) {
if (percent(d) > 1.35) {
return d.name;
}
})
.attr('font-size', function(d) {
if (d.value < 100000) {
return '10px'
} else {
return '20px';
}
})
.on("click", click)
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
function click(d) {
console.log(d)
// fade out all text elements
text.transition().attr("opacity", 0);
path
.transition()
.duration(750)
.attrTween("d", arcTween(d))
.each("end", function(e, i) {
// check if the animated element's data e lies within the visible angle span given in d
if (e.x >= d.x && e.x < (d.x + d.dx)) {
// get a selection of the associated text element
var arcText = d3.select(this.parentNode).select("text");
// fade in the text element and recalculate positions
arcText.transition().duration(750)
.attr("opacity", 1)
.attr("transform", function() { return "rotate(" + computeTextRotation(e) + ")" })
.attr("x", function(d) { return y(d.y); });
}
});
}
});
d3.select(self.frameElement).style("height", height + "px");
// Interpolate the scales!
function arcTween(d) {
console.log(d.name, x.domain())
console.log(d.name, y.domain())
console.log(d.name, y.range())
var xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, 1]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d, i) {
return i
? function(t) { return arc(d); }
: function(t) {
console.log(t)
x.domain(xd(t));
y.domain(yd(t)).range(yr(t));
return arc(d);
};
};
}
function computeTextRotation(d) {
return (x(d.x + d.dx / 2) - Math.PI / 2) / Math.PI * 180;
}
I found the solution here: https://bl.ocks.org/mbostock/1306365. This example manages the zoom without getting rid of the sibling nodes.
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();
I have developed a force layout to represent relationships between social groups. Now I would like to get the nodes to be distributed in a circle with links joining them. What is the best way to do this?
The complete version of the code (without data) is here http://jsfiddle.net/PatriciaW/zZSJT/
(Why do I have to include code here too? Here is the main portion)
d3.json("/relationships?nocache=" + (new Date()).getTime(),function(error,members){
var links=members.organizations.map(function(members) {
return members.member;
});
var nodes = {};
links.forEach(function(link) {
link.source = nodes[link.xsource] || (nodes[link.xsource] = {source: link.xsource, name: link.xsource, category: link.categorysource, path: link.pathsource, desc: link.descsource, title: link.titlesource});
link.target = nodes[link.xtarget] || (nodes[link.xtarget] = {target: link.xtarget, name: link.xtarget, category: link.categorytarget, path: link.pathtarget, desc: link.desctarget, title: link.titletarget});
});
force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.charge(-120)
.linkDistance(function() {return (Math.random() * 200) + 100;})
.linkStrength(0.5)
.on("tick", tick)
.start();
var link = svg.selectAll(".link")
.data(force.links())
.enter().append("line")
.attr("class", "link");
var node_drag = d3.behavior.drag()
.on("dragstart", dragstart)
.on("drag", dragmove)
.on("dragend", dragend);
var loading = svg.append("text")
.attr("x", width / 2)
.attr("y", height / 2)
.attr("dy", ".35em")
.style("text-anchor", "middle")
.text("Simulating. One moment pleaseā¦");
function dragstart(d, i) {
force.stop() // stops the force auto positioning before you start dragging
}
function dragmove(d, i) {
d.px += d3.event.dx;
d.py += d3.event.dy;
d.x += d3.event.dx;
d.y += d3.event.dy;
tick(); // this is the key to make it work together with updating both px,py,x,y on d !
}
function dragend(d, i) {
d.fixed = true; // of course set the node to fixed so the force doesn't include the node in its auto positioning stuff
tick();
force.resume();
};
var node = svg.selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.on("mouseover", mouseover)
.on("mouseout", mouseout)
.on("click", clickAlert)
.call(node_drag);
node.append("circle")
.attr("r", 8)
.style("fill", function(d) {
return categoryColour [d.category];
})
// add an image marker
node.append("image")
.attr("x",-8)
.attr("y",-8)
.attr("width", 16)
.attr("height", 16)
.attr("xlink:href", function(d) {
return categoryImage [d.category]
})
.on("click", clickAlert)
.style("cursor", "pointer")
node.append("text")
.attr("x", 12)
.attr("dy", ".35em")
.text(function(d) {
return d.name;
});
// Use a timeout to allow the rest of the page to load first.
setTimeout(function() {
// Run the layout a fixed number of times.
// The ideal number of times scales with graph complexity.
force.start();
for (var i = n * n; i > 0; --i) force.tick();
force.stop();
svg.selectAll("line")
.data(links)
.enter().append("line")
.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; });
svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", 4.5);
loading.remove();
}, 10);
function tick() {
link
.attr("x1", function(d) {
return d.source.x + xadj; })
.attr("y1", function(d) {
return d.source.y + yadj; })
.attr("x2", function(d) {
return d.target.x +xadj; })
.attr("y2", function(d) {
return d.target.y +yadj; });
node
.attr("transform", function(d) {
return "translate(" + (d.x + xadj) + "," + (d.y + yadj) + ")";
});
};
function mouseover() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 16);
d3.select(this).select("text")
.attr("font-size","34px")
.style("font-weight", "bold");
};
function mouseout() {
d3.select(this).select("circle").transition()
.duration(750)
.attr("r", 8);
d3.select(this).select("text")
.attr("font-size","12px")
.style("font-weight", "normal");
};
}) // end json
Here's someone else's solution:
This network graph uses the D3 force layout to draw nodes and links, but instead of using d3.force() to find the best node positions, we draw an invisible arc and evenly places nodes along the circumference.
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
<style>
line.node-link, path.node-link {
fill: none;
stroke: black
}
circle.node {
fill: white;
stroke: black
}
circle.node+text {
text-anchor: middle;
}
text {
font-family: sans-serif;
pointer-events: none;
}
</style>
</head>
<body>
<script type="text/javascript">
// number of random nodes (gets crowded at >25 unless you change node diameter)
var num = 20;
// returns random int between 0 and num
function getRandomInt() {return Math.floor(Math.random() * (num));}
// nodes returns a [list] of {id: 1, fixed:true}
var nodes = d3.range(num).map(function(d) { return {id: d}; });
// links returns a [list] of {source: 0, target: 1} (values refer to indicies of nodes)
var links = d3.range(num).map(function(d) { return {source: getRandomInt(), target: getRandomInt()}; });
var width = 500,
height = 500;
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.size([width, height]);
// evenly spaces nodes along arc
var circleCoord = function(node, index, num_nodes){
var circumference = circle.node().getTotalLength();
var pointAtLength = function(l){return circle.node().getPointAtLength(l)};
var sectionLength = (circumference)/num_nodes;
var position = sectionLength*index+sectionLength/2;
return pointAtLength(circumference-position)
}
// fades out lines that aren't connected to node d
var is_connected = function(d, opacity) {
lines.transition().style("stroke-opacity", function(o) {
return o.source === d || o.target === d ? 1 : opacity;
});
}
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// invisible circle for placing nodes
// it's actually two arcs so we can use the getPointAtLength() and getTotalLength() methods
var dim = width-80
var circle = svg.append("path")
.attr("d", "M 40, "+(dim/2+40)+" a "+dim/2+","+dim/2+" 0 1,0 "+dim+",0 a "+dim/2+","+dim/2+" 0 1,0 "+dim*-1+",0")
.style("fill", "#f5f5f5");
force.start();
// set coordinates for container nodes
nodes.forEach(function(n, i) {
var coord = circleCoord(n, i, nodes.length)
n.x = coord.x
n.y = coord.y
});
// use this one for straight line links...
// var lines = svg.selectAll("line.node-link")
// .data(links).enter().append("line")
// .attr("class", "node-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; });
// ...or use this one for curved line links
var lines = svg.selectAll("path.node-link")
.data(links).enter().append("path")
.attr("class", "node-link")
.attr("d", function(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x + "," +
d.source.y + "A" +
dr + "," + dr + " 0 0,1 " +
d.target.x + "," +
d.target.y;
});
var gnodes = svg.selectAll('g.gnode')
.data(nodes).enter().append('g')
.attr("transform", function(d) {
return "translate("+d.x+","+d.y+")"
})
.classed('gnode', true);
var node = gnodes.append("circle")
.attr("r", 25)
.attr("class", "node")
.on("mouseenter", function(d) {
is_connected(d, 0.1)
node.transition().duration(100).attr("r", 25)
d3.select(this).transition().duration(100).attr("r", 30)
})
.on("mouseleave", function(d) {
node.transition().duration(100).attr("r", 25);
is_connected(d, 1);
});
var labels = gnodes.append("text")
.attr("dy", 4)
.text(function(d){return d.id})
</script>
</body>
</html>