Having some massive trouble getting my D3 scatter plot visualization running. Didnt know how to reference the data, so its available from a dropbox link here.
There are a few problems.
I am a bit confused about loading my data.
I cannot seem to get the data loaded. I have been successful before, but I am trying to load the data without having to reference a function (i.e., global). However, as you will see - I am getting nothing - [].
Do I need to load it at the bottom of my script and then reference the function within the d3.csv(function(d) {...}, FUNCTION);? Why cant I simple load it to a variable (as I am trying to) at the top of my script. Such that its the first object available?
I also felt like I had a good handle on the Mike Bostock tutorial about .enter(), update(), .exit(). But I know that I have an issue in the comment section of "//ENTER + UPDATE" where I have circle.circle(function(d) {return d;});. I dont understand this.
Overall, I am looking to create a scatter plot with fare as my x-axis, age as my y-axis, then loop through the options of "Female Only", "Male Only", "Children Only" and "All" (starting and ending with All).
I plan to add more to this as I get a better understanding of where I am currently stumbling.
d3.csv("titanic_full.csv", function(d) {
return {
fare: +d.fare,
age: d.age == '' ? NaN : +d.age,
sibsp: +d.sibsp,
pclass: +d.pclass,
sex: d.sex,
name: d.name,
survived: d.survived
};
}, function(error, d) {
//Filter out erroneous age values (263 rows are removed)
var dataset = d.filter(function(d) {
if (d['age'] >= 0) {
return d;
}
});
//*Main Elements Setup*
//Width and height
var w = 650;
var h = 650;
var padding = 20;
//Create scale functions
var xScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) {
return d.fare;
})])
.range([padding, w - padding * 2]); // introduced this to make sure values are not cut off
var yScale = d3.scale.linear()
.domain([0, d3.max(dataset, function(d) {
return d.age;
})])
.range([h - padding, padding]);
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Show Axes
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + (h - padding) + ')')
.call(xAxis);
svg.append('g')
.attr('class', 'y axis')
.attr('transform', 'translate(' + padding + ',0)')
.call(yAxis);
function update(dataset) {
//DATA JOIN
//Join new data with old elements, if any.
var circle = svg.selectAll('circle')
.data(dataset);
//UPDATE
//Update old elements as needed.
circle.attr('class', 'update');
//ENTER
//Create new elements as needed.
circle.enter().append('circle')
.attr('class', 'enter')
.transition()
.duration(1000)
.attr("cx", function(d) {
return xScale(d.fare);
})
.attr("cy", function(d) {
return yScale(d.age);
})
.attr("r", 6)
.attr('fill', function(d) {
if (d.survived === '0') {
return 'green';
} else {
return 'red';
}
})
//ENTER + UPDATE
//Appending to the enter selection expands the update selection to include
//entering elements; so, operations on the update selection after appending to
//the enter selection will apply to both entering and updating nodes.
circle.circle(function(d) {
return d;
});
//EXIT
//Remove old elements as needed.
circle.exit().remove();
};
//The initial display.
update(dataset);
//Work through each selection
var options = ['Female Only', 'Male Only', 'Children Only', 'All']
var option_idx = 0;
console.log('next')
var option_interval = setInterval(function(options) {
if (options == 'Female Only') {
var filteredData = dataset.filter(function(d) {
if (d['sex'] == 'female') {
return d;
}
})
} else if (options == 'Male Only') {
var filteredData = dataset.filter(function(d) {
if (d['sex'] == 'male') {
return d;
}
})
} else if (options == 'Children Only') {
var filteredData = dataset.filter(function(d) {
if (d['age'] <= 13) {
return d;
}
})
} else if (options == 'All') {
var filteredData = dataset.filter(function(d) {
return d;
})
};
console.log('interval')
option_idx++; // increment by one
update(filteredData);
if (option_idx >= options.length) {
clearInterval(option_interval);
};
}, 1500);
});
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 8px;
}
<title>Titanic Visualization - Fare and Age Survival</title>
You should write the whole code inside the d3.csv callback function. Try this way.
d3.csv("titanic_full.csv", function(d) {
return {
fare: +d[fare],
age: d.age == '' ? NaN : +d.age,
sibsp: +d.sibsp,
pclass: +d.pclass
};
}, function(error, dataset) {
//Filter out erroneous age values (263 rows are removed)
var dataset = dataset.filter(function(d) {
if (d['age'] >= 0) {
return d;
}
});
//Remaining code
});
Refer here for more details.
Okay, so I have an answer, but I probably will not explain this as well.
1. I am a bit confused about loading my data.
With the help of #Gilsha, I was able to reconfigure my script to load the data first, with the rest of my script being the 'callback' portion of the d3.csv function. That worked smoothly. I also was able to filter my data to remove the blanks right away. Here is that first part:
d3.csv("titanic_full.csv", function(d) {
return {
fare: +d.fare,
age: d.age == '' ? NaN : +d.age,
sibsp: +d.sibsp,
pclass: +d.pclass,
sex: d.sex,
name: d.name,
survived: d.survived
};
}, function(error, d) {
//Filter out erroneous age values (263 rows are removed)
var dataset = d.filter(function(d) {
if (d['age'] >= 0) {
return d;
}
});
//Rest of script here.
**2. I also felt like I had a good handle on the Mike Bostock tutorial about .enter(), update(), .exit(). Link to Bostock Tutorial I was following **
Couple things that I did wrong here that was holding me up. The main item that I was stuck on was this portion:
//ENTER + UPDATE
//Appending to the enter selection expands the update selection to include
//entering elements; so, operations on the update selection after appending to
//the enter selection will apply to both entering and updating nodes.
circle.circle(function(d) {return d;});
Basically, I was following the tutorial too closely and didnt realize that when he was using "text.text(function(d) {return d;});", he was setting the text attribute (?) to something. This is where I believe I should be setting any changes to my ENTER and UPDATE selections (all the items that I expect to be in the DOM). So, when Mike was doing text.text and I replicated with circle.circle, I should have had circle.text(.....). Or whatever I want there. Anyone care to comment or explain that better??
PS - I had many other errors... throughout, especially in the section of establishing my interval!
Related
I'm stuck on a small problem regarding force simulation in D3.
I have data representing poverty rates for each country, from 1998 to 2008. It's a bubble chart that's split into three clusters, representing poor countries, not-poor countries, and countries with no information.
When the app is initially loaded, it's loaded with the 1998 data. However, I have some buttons at the top, that, when clicked, will change the year, and subsequently the bubbles should rearrange themselves. All I've been able to do, is when the button is clicked, I change a variable year. However, there are functions and variables that use year throughout the code. When year changes, I want to recalculate all the node properties and force parameters that are depending on year
Here's my code. I've included all of it in case you want to try it out. The data file is at the end of this post.
async function init() {
// Set up the canvas
var height = 1000, width = 2000;
var svg = d3.select("#panel1").append("svg")
.attr("height", height)
.attr("width", width)
.attr("class", "bubblePanel");
var canvas = svg.append("g")
.attr("transform", "translate(0,0)");
// Choose what year to look at, based on button clicks.
var year = "X1998"
d3.select("#b1998").on("click", function() {
year = "X1998"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
d3.select("#b1999").on("click", function() {
year = "X1999"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
d3.select("#b2000").on("click", function() {
year = "X2000"
console.log(year)
// NOTIFY SIMULATION OF CHANGE //
})
// Implement the physics of the elements. Three forces act according to the poverty level (poor, not poor, and no info)
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
if (parseFloat(d[year]) >= 10) {
return 1700
} else if (parseFloat(d[year]) === 0) {
return 1000
} else {
return 300
}
}).strength(0.05))
.force("y", d3.forceY(300).strength(0.05))
.force("collide", d3.forceCollide(function(d) {
return radiusScale(d[year])
}));
// Function to pick colour of circles according to region
function pickColor(d) {
if (d === "East Asia & Pacific") {
return "red"
} else if (d === "Europe & Central Asia") {
return "orange"
} else if (d === "Latin America & Caribbean") {
return "yellow"
} else if (d === "Middle East & North Africa") {
return "green"
} else if (d === "North America") {
return "blue"
} else if (d === "South Asia") {
return "indigo"
} else {
return "violet"
}
}
// Set the scales for bubble radius, and text size.
var radiusScale = d3.scaleSqrt().domain([0, 50]).range([20,80]);
var labelScale = d3.scaleSqrt().domain([0,50]).range([10,40]);
// Read the data
await d3.csv("wd3.csv").then(function(data) {
// Assign each data point to a circle that is colored according to region and has radius according to its poverty level
var bubbles = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("fill", function(d) {
return pickColor(d.Region)
})
.attr("r", function(d) {
return radiusScale(d[year])
});
// Assign each ddata point to a text element that shows the counry code of the data point. The text is scaled according to the poverty level
var labels = svg.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", 100)
.attr("y", 100)
.attr("dominant-baseline", "central")
.text(function(d) { return d.XCountryCode })
.style("stroke", "black")
.style("text-anchor", "middle")
.style("font-size", function(d) { return labelScale(d[year]); });
// Code to handle the physics of the bubble and the text
simulation.nodes(data)
.on("tick", ticked)
function ticked() {
bubbles.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
labels.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
}
});
}
When year changes, the data values will change for each country. I want the following parts of my code to be updated.
The x forces on the nodes: Countries can go from poor in one year to not-poor in another year, so their cluster will change
The radius of the circles: The radius represents poverty level. These change from year to year, so the size of the circles will change when a button is clicked
The coordinates of the country labels: These labels are attached to the data as well. So when the x forces on the circles causes the circles to move, the labels should move as well.
I'd greatly appreciate the help.
The data file can be found here. I accidentally named it povertyCSV, but in the code, it's referenced as "wd3.csv"
If I understand the question correctly:
Re-initializing Forces
The functions provided to set parameters of d3 forces such as forceX or forceCollision are executed once per node at initialization of the simulation (when nodes are originally assigned to the layout). This saves a lot of time once the simulation starts: we aren't recalculating force parameters every tick.
However, if you have an existing force layout and want to modify forceX with a new x value or new strength, or forceCollision with a new radius, for example, we can re-initialize the force to perform the recalculation:
// assign a force to the force diagram:
simulation.force("someForce", d3.forceSomeForce().someProperty(function(d) { ... }) )
// re-initialize the force
simulation.force("someForce").initialize(nodes);
This means if we have a force such as:
simulation.force("x",d3.forceX().x(function(d) { return fn(d["year"]); }))
And we update the variable year, all we need to do is:
year = "newValue";
simulation.force("x").initialize(nodes);
Positioning
If the forces are re-initialized (or re-assigned), there is no need to touch the tick function: it'll update the nodes as needed. Labels and circles will continue to be updated correctly.
Also, non-positional things such as color need to be updated in the event handler that also re-initializes the forces. Other than radius, most things should either be updated via the force or via modifying the elements directly, not both.
Radius is a special case:
With d3.forceCollide, radius affects positioning
Radius, however, does not need to be updated every tick.
Therefore, when updating the radius, we need to update the collision force and modify the r attribute of each circle.
If looking for a smooth transition of radius that is reflected graphically and in the collision force, this should be a separate question.
Implementation
I've borrowed from your code to make a fairly generic example. The below code contains the following event listener for some buttons where each button's datum is a year:
buttons.on("click", function(d) {
// d is the year:
year = d;
// reheat the simulation:
simulation
.alpha(0.5)
.alphaTarget(0.3)
.restart();
// (re)initialize the forces
simulation.force("x").initialize(data);
simulation.force("collide").initialize(data);
// update altered visual properties:
bubbles.attr("r", function(d) {
return radiusScale(d[year]);
}).attr("fill", function(d) {
return colorScale(d[year]);
})
})
The following snippet uses arbitrary data and due to its size may not allow for nodes to re-organize perfectly every time. For simplicity, position, color, and radius are all based off the same variable. Ultimately, it should address the key part of the question: When year changes, I want to update everything that uses year to set node and force properties.
var data = [
{year1:2,year2:1,year3:3,label:"a"},
{year1:3,year2:4,year3:5,label:"b"},
{year1:5,year2:9,year3:7,label:"c"},
{year1:8,year2:16,year3:11,label:"d"},
{year1:13,year2:25,year3:13,label:"e"},
{year1:21,year2:36,year3:17,label:"f"},
{year1:34,year2:1,year3:19,label:"g"},
{year1:2,year2:4,year3:23,label:"h"},
{year1:3,year2:9,year3:29,label:"i"},
{year1:5,year2:16,year3:31,label:"j"},
{year1:8,year2:25,year3:37,label:"k"},
{year1:13,year2:36,year3:3,label:"l"},
{year1:21,year2:1,year3:5,label:"m"}
];
// Create some buttons:
var buttons = d3.select("body").selectAll("button")
.data(["year1","year2","year3"])
.enter()
.append("button")
.text(function(d) { return d; })
// Go about setting the force layout:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 300);
var radiusScale = d3.scaleSqrt()
.domain([0, 40])
.range([5,30]);
var colorScale = d3.scaleLinear()
.domain([0,10,37])
.range(["#c7e9b4","#41b6c4","#253494"]);
var year = "year1";
var simulation = d3.forceSimulation()
.force("x", d3.forceX(function(d) {
if (parseFloat(d[year]) >= 15) {
return 100
} else if (parseFloat(d[year]) > 5) {
return 250
} else {
return 400
}
}).strength(0.05))
.force("y", d3.forceY(150).strength(0.05))
.force("collide", d3.forceCollide()
.radius(function(d) {
return radiusScale(d[year])
}));
var bubbles = svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", function(d) {
return radiusScale(d[year])
})
.attr("fill", function(d) {
return colorScale(d[year]);
});
var labels = svg.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d) {
return d.label;
})
.style("text-anchor","middle");
simulation.nodes(data)
.on("tick", ticked)
function ticked() {
bubbles.attr("cx", function(d) {
return d.x;
}).attr("cy", function(d) {
return d.y;
})
labels.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y +5;
})
}
buttons.on("click", function(d) {
// d is the year:
year = d;
simulation
.alpha(0.5)
.alphaTarget(0.3)
.restart();
simulation.force("x").initialize(data);
simulation.force("collide").initialize(data);
bubbles.attr("r", function(d) {
return radiusScale(d[year]);
}).attr("fill", function(d) {
return colorScale(d[year]);
})
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I am trying to create a scatterplot of hundreds of datapoints, each with about 5 different attributes.
The data is loaded from a .csv as an array of objects, each of which looks like this:
{hour: "02",yval: "63",foo: "33", goo:"0", bar:"1"},
I want to display the scatterplot with the following attributes:
Shape for bar:
-circle to represent all points where bar=0, and a triangle-down to represent those where bar=1 (this is a dummy variable).
Color for foo and goo:
All points start as grey. goo is categorical with values [0,1,2] while foo is quantitative with a range from 0-50. foo and goo are mutually exclusive, so only one of them has a value. In other words, for each data point either foo=0 or goo=0.
Points with goo=1 should be orange; points with goo=2 should be red.
foo should be mapped onto a linear color scale from light blue to dark blue, ie d3.scale.linear().domain([0, 50]).range(["#87CEFF", "#0000FF"]);
I can do each of these individually, but defining everything together is creating issues for me.
My code with reproducible data is here: http://jsfiddle.net/qy5ohw0x/3/
Issues
For the symbol, i tried
.append("svg:path")
.attr("d", d3.svg.symbol())
which did not work. I tried a different approach altogether, but this did not map the values correctly:
var series = svg.selectAll("g.series")
.data(dataSet, function(d, i) { return d.bar; })
.enter()
.append("svg:g")
series.selectAll("g.point")
.data(dataSet)
.enter()
.append("svg:path")
.attr("transform", function(d, i) { return "translate(" + d.hour + "," + d.yval + ")"; })
.attr("d", function(d,i, j) { return d3.svg.symbol().type(symbolType[j])(); })
.attr("r", 2);
For the goo colors (grey/orange/red), i mapped the values to the 3 colors manually:
First define var colors = ["grey", "orange", "red"];
Then while drawing the data points chain
.style("fill", function (d) { return colors[d.type]; })
This worked alone, but not with the different symbols.
Finally, can i chain a second color .attr for foo? d3.scale.linear().domain([0, 50]).range(["#87CEFF", "#0000FF"]); would probably work if this is possible.
Again, the jsfiddle is here: http://jsfiddle.net/qy5ohw0x/3/
Thanks!!
Just do all the logic and comparisons in a function(d) for each attribute.
First set up some helpers:
// symbol generators
var symbolTypes = {
"triangleDown": d3.svg.symbol().type("triangle-down"),
"circle": d3.svg.symbol().type("circle")
};
// colors for foo
var fooColors = d3.scale
.linear()
.domain([0, 50])
.range(["#87CEFF", "#0000FF"]);
Then append a path for each symbol:
svg.selectAll("path")
.data(dataSet)
.enter().append("path")
.attr("class", "dot")
// position it, can't use x/y on path, so translate it
.attr("transform", function(d) {
return "translate(" + (x(d.hour) + (Math.random() * 12 - 6)) + "," + y(d.yval) + ")";
})
// assign d from our symbols
.attr("d", function(d,i){
if (d.bar === "0") // circle if bar === 0
return symbolTypes.circle();
else
return symbolTypes.triangleDown();
})
// fill based on goo and foo
.style("fill", function(d,i){
if (d.goo !== "0"){
if (d.goo === "1")
return "red";
else
return "orange";
}else{
return fooColors(d.foo);
}
});
Updated fiddle.
On a side note, I actually think straight d3 is way more intuitive than nvd3 for this situation.
It's much simplier with nvd3.js
function prepareData (data) {
return [{
key: 'Group 1',
values: data.map(function (item) {
item.shape = item.bar == "0" ? 'circle' : 'triangle-down';
item.x = Number(item.hour);
item.y = Number(item.yval);
item.size = 0.1;
item.disabled = Math.random() > 0.4;
return item;
})
}]
}
nv.addGraph(function() {
var chart = nv.models.scatterChart()
.showDistX(false)
.showDistY(true)
.showLegend(false)
//Axis settings
chart.xAxis.tickFormat(d3.format('3.0f'));
chart.yAxis.tickFormat(d3.format('3.0f'));
d3.select('#chart svg')
.datum(prepareData(dataSet))
.call(chart)
// A bit hacky but works
var fooscale = d3.scale.linear().domain([0, 50]).range(["#87CEFF", "#0000FF"]);
function colorer(d) {
if (d.goo == '1')
return 'orange';
else if (d.goo == '2')
return 'red';
else if (d.goo == '0')
return fooscale(d.foo);
return 'gray';
}
d3.selectAll('.nv-point')
.attr({
'stroke': colorer,
'fill': colorer
})
nv.utils.windowResize(chart.update);
return chart;
});
See https://jsfiddle.net/qy5ohw0x/4/
PS Unfortunately Nvd3 lacks docs, so use it's github instead
Using d3, I created a bar graph that displays the text value of each bar on it. I am toggling between two different data sets through a click event on a button. The data sets change successfully on mousedown, i.e. the bar graphs change in size as they should, but I am unable to change the text labels on the bars. My redrawText function does not do anything, and calling my drawText function again just redraws the data on top of the previous label (as one would expect). I am looking for a way to remove the old label and redraw the label reflecting the new data inside my removeText function.
Here is my drawText function, which is called initially to create the label. 'datachoose' is the name of the variable that is selected to graph the proper data set.
function drawText(dataChoose) {
new_svg.selectAll("text.dataChoose")
.data(dataChoose)
.enter().append("text")
.text(function(d) {
return d;
})
/* removed some transform code */
.attr("fill", "white")
.attr("style", "font-size: 12; font-family: Garamond, sans-serif");
}
Here are the relevant parts of my mousedown event handler, which is used to update the data set and redraw the graph:
.on("mousedown", function() {
if (dataChoose == data) {
dataChoose = data2;
}
else {
dataChoose = data;
}
redraw(dataChoose);
redrawText(dataChoose);
});
and here is my redrawText() function
function redrawText(dataChoose) {
var new_text = new_svg.selectAll("text.dataChoose")
.data(dataChoose);
new_text.transition()
.duration(1000)
.text(function(d) {
return d;
})
/* removed transform code */
.attr("fill", "white")
.attr("style", "font-size: 16; font-family: Garamond, sans-serif");
}
Without a full example it's hard to see exactly what you're doing but it looks like if the text label is a property of the data you might not be getting the label field correctly.
Here's a simple example of what I think you're describing as your desired behavior: (LINK): http://tributary.io/inlet/9064381
var svg = d3.select('svg');
var data = [{"tag":"abc","val":123}]
data2 = [{"tag":"ijk","val":321}]
var dataChoose = data;
var myBarGraph = svg.selectAll('rect')
.data(dataChoose)
.enter()
.append('rect')
.attr({
x: 160,
y: 135,
height: 20,
width: function(d) { return d.val; },
fill: 'black'
});
var updateBarGraph = function() {
myBarGraph
.data(dataChoose)
.transition()
.duration(1000)
.attr('width', function(d) { return d.val; })
}
var myText = svg.append('text')
.data(dataChoose)
.attr('x', 129)
.attr('y', 150)
.attr('fill', '#000')
.classed('dataChoose', true)
.text(function(d) { return d.tag })
svg.on("click", function() {
if (dataChoose == data) {
dataChoose = data2;
} else {
dataChoose = data;
}
redrawText();
updateBarGraph();
});
function redrawText() {
myText
.data(dataChoose)
.transition()
.duration(1000)
.style("opacity", 0)
.transition().duration(500)
.style("opacity", 1)
.text(function(d) { return d.tag })
}
EDIT: The other possibility is that your label transition wasn't working because you need to tell d3 how to do the transition for text (see the updated redrawText).
I'm new to D3 and trying to do a moving average of previous and next values on my data in order to smooth it out.
Currently, I have it working using the 2 previous values + the current value. It does work but 1) how would I also use the next values, and 2) what if I wanted to use the 15 previous and 15 next values? (it would be crazy to have 30 individual vars for storing all of them)
I'm used to traditional javascript but lost as to how to traverse the data this way in D3.
Hope someone can enlighten me, thanks.
See the whole code on bl.ocks.org:
http://bl.ocks.org/jcnesci/7439277
Or just the data parsing code here:
d3.json("by_date_mod.json", function(error, data) {
// Setup each row of data by formatting the Date for X, and by converting to a number for Y.
data = data.rows;
data.forEach(function(d) {
d.key = parseDate(String(d.key));
d.value = +d.value;
});
x.domain(d3.extent(data, function(d) { return d.key; }));
y.domain([0, d3.max(data, function(d) { return d.value; })]);
// Setup the moving average calculation.
// Currently is a hacky way of doing it by manually storing and using the previous 3 values for averaging.
// Looking for another way to address previous values so we can make the averaging window much larger (like 15 previous values).
var prevPrevVal = 0;
var prevVal = 0;
var curVal = 0
var movingAverageLine = d3.svg.line()
.x(function(d,i) { return x(d.key); })
.y(function(d,i) {
if (i == 0) {
prevPrevVal = y(d.value);
prevVal = y(d.value);
curVal = y(d.value);
} else if (i == 1) {
prevPrevVal = prevVal;
prevVal = curVal;
curVal = (prevVal + y(d.value)) / 2.0;
} else {
prevPrevVal = prevVal;
prevVal = curVal;
curVal = (prevPrevVal + prevVal + y(d.value)) / 3.0;
}
return curVal;
})
.interpolate("basis");
// Draw the moving average version of the data, as a line.
graph1.append("path")
.attr("class", "average")
.attr("d", movingAverageLine(data));
// Draw the raw data as an area.
graph1.append("path")
.datum(data)
.attr("class", "area")
.attr("d", area);
// Draw the X-axis of the graph.
graph1.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Draw the Y-axis of the graph.
graph1.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Value");
});
You need a function to calculate the moving average:
var movingWindowAvg = function (arr, step) { // Window size = 2 * step + 1
return arr.map(function (_, idx) {
var wnd = arr.slice(idx - step, idx + step + 1);
var result = d3.sum(wnd) / wnd.length;
// Check for isNaN, the javascript way
result = (result == result) ? result : _;
return result;
});
};
var avgData = movingWindowAvg(avg, 7); // 15 step moving window.
Note that this functions fudges the values a bit at the borders of the original array, when a complete window cannot be extracted.
Update: If the result is NaN, convert the result to the present number in the beginning. Checking result == result is the recommended way of testing for NaNs in Javascript.
If you really don't need a variable size window, this cumulative average might be a faster option without the slicing overhead:
function cumAvg(objects, accessor) {
return objects.reduce(
function(avgs, currObj, i) {
if (i == 1) {
return [ accessor(currObj) ];
} else {
var lastAvg = avgs[i - 2]; // reduce idxs are 1-based, arrays are 0
avgs.push( lastAvg + ( (accessor(currObj) - lastAvg) / i) );
return avgs;
}
}
}
With a d3.layout.tree() I am trying to filter a selection to contain only linking lines that link to nodes at a depth less than leafDepth
the following line is how I am trying to do that:
links.enter().filter(function(d){return d.target.depth < leafDepth;})....
without the filter the graph is drawn fine, but with the addition of the filter i get the following error in console:
Uncaught TypeError: Cannot read property 'depth' of undefined
You can see that I am accessing d.target fine in the assignment for linkKey. so I don't understand why d.target is undefined later on when i try to check d.target.depth which I know exists for all elements??
Am I missing something in regards to what happens when I 'pick up' the data using the identity function ?? var link = links.selectAll("path.treeline").data( function(d){return d;} , linkKey);
Once again this all works fine until I add the line .filter(function(d){return d.target.depth < leafDepth;})
Here's the main part of my graph drawing function (excluding a bunch of getter/setters that followed)
Tree = function () {
var width = 1000, //default width
height = 1000, //default height
left = 0,
top = 0,
leafDepth = 5, // depth at which leaf nodes are at
leafClick = function (d){return console.log(d);},
linkKey, // key function for links
pathGenerator = d3.svg.diagonal().projection( function(d) {
return [d.x, height-d.y]; }),
childrenKey = function(d) {
return (!d.values || d.values.length === 0) ? null : d.values; },
linkKey = function(d) { return d.source.key+"_"+d.target.key; }
;
function chart(selection) {
selection.each(function (d) {
// setup layout
var tree = d3.layout.tree()
.size([width,height])
.children( function(d) {
return (!d.values || d.values.length === 0) ? null : d.values;
});
var nodeData = tree.nodes(d);
var linkData = tree.links(nodeData);
// 'this' is the selection
var links = d3.select(this).selectAll("g#links")
.data( [linkData] );
links.enter()
.append("g")
.attr("transform", "translate("+left+", "+top+")")
.attr("id","links");
var linkKey = function(d) { return d.source.key+"_"+d.target.key; }
var link = links.selectAll("path.treeline")
.data( function(d){return d;} , linkKey);
link.enter()
.filter(function(d){return d.target.depth < leafDepth;}) // PROBLEM HERE !!
.append("svg:path")
.attr("class", "treeline")
.attr("d", pathGenerator);
link.transition()
.attr("d", pathGenerator);
link.exit()
.remove();
});
return chart;
};
// a bunch of getter/setters for the vars defined at top
return chart; // this is using a version of the re-usable chart pattern by the way
}
moving the filter to the data assignment works, though it that means they are completely removed them from the join.
var linkLeafFilter = function(d){return d.target.depth < leafDepth;}
var link = links.selectAll("path.treeline")
.data( function(d){return d.filter(linkLeafFilter);} , linkKey);