There are 2 set of arrays, the initial dataset is data and the newer dataset is data2. After adding data to the plot, panning and zooming works. I've created a Chart class for handling d3.js plots.
However after updating the plot with the array data2 using chart.update(chart.data2), only the points from data pans and zooms, the points from data2 remains stationary. Is there an error in the code for chart.update()?
jsfiddle: http://jsfiddle.net/kzQ6K/
chart.update()
/**
* Update Chart with new Data
*/
this.update = function(newData) {
uniqueNewData = _.difference(newData, this.data);
this.data = _.union(this.data, uniqueNewData);
// Update axes
var yMin = d3.min(this.data.map( function(d) {return d.score;} ));
var yMax = d3.max(this.data.map( function(d) {return d.score;} ));
var xMin = d3.min(this.data.map( function(d) {return d.timestamp;} ));
var xMax = d3.max(this.data.map( function(d) {return d.timestamp;} ));
y.domain([yMin, yMax]);
x.domain([xMin, xMax]);
// Draw rects
var svg = d3.select(this.div + ' svg');
var rects = svg.selectAll('rect.data')
.data(this.data, function(d) { return d.timestamp || d3.select(this).attr('timestamp'); });
rects
.enter()
.append('rect')
.classed('data', true);
rects
.attr('x', function(d) { return x(d.timestamp); })
.attr('y', function(d) { return y(d.score); })
.attr('timestamp', function(d) { return d.timestamp; })
.attr('width', 4)
.attr('height', 10)
.attr('fill', 'red')
.attr('transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')');
svg.select('.x.axis').call(xAxis);
svg.select('.y.axis').call(yAxis);
}
The problem is that the zoomed function is not defined as a class member, and uses the variables data, x, y, etc. from its closure. These variables are not updated when the zoomed function is called.
Though this way of encapsulating the Charts may work out, the most standard way of doing it is by creating d3-esque functions.
Related
I just started learning javascript and d3.js by taking a couple of lynda.com courses. My objective is to create a function that takes an array of numbers and a cutoff and produces a plot like this one:
I was able to write javascript code that generates this:
Alas, I'm having troubles figuring out a way to tell d3.js that the area to the left of -opts.threshold should be read, the area in between -opts.threshold and opts.threshold blue, and the rest green.
This is my javascript code:
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
var margin = {left:50,right:50,top:40,bottom:0};
var xMax = opts.x.reduce(function(a, b) {
return Math.max(a, b);
});
var yMax = opts.y.reduce(function(a, b) {
return Math.max(a, b);
});
var xMin = opts.x.reduce(function(a, b) {
return Math.min(a, b);
});
var yMin = opts.y.reduce(function(a, b) {
return Math.min(a, b);
});
var y = d3.scaleLinear()
.domain([0,yMax])
.range([height,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,i){ return x(opts.x[i]) ;})
.y0(height)
.y1(function(d){ return y(d); });
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.y));
chartGroup.append("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
In case this is helpful, I saved all my code on a public github repo.
There are two proposed solutions in this answer, using gradients or using multiple areas. I will propose an alternate solution: Use the area as a clip path for three rectangles that together cover the entire plot area.
Make rectangles by creating a data array that holds the left and right edges of each rectangle. Rectangle height and y attributes can be set to svg height and zero respectively when appending rectangles, and therefore do not need to be included in the array.
The first rectangle will have a left edge at xScale.range()[0], the last rectangle will have an right edge of xScale.range()[1]. Intermediate coordinates can be placed with xScale(1), xScale(-1) etc.
Such an array might look like (using your proposed configuration and x scale name):
var rects = [
[x.range()[0],x(-1)],
[x(-1),x(1)],
[x(1),x.range()[1]]
]
Then place them:
.enter()
.append("rect")
.attr("x", function(d) { return d[0]; })
.attr("width", function(d) { return d[1] - d[0]; })
.attr("y", 0)
.attr("height",height)
Don't forget to set a clip-path attribute for the rectangles:
.attr("clip-path","url(#areaID)"), and to set fill to three different colors.
Now all you have to do is set your area's fill and stroke to none, and append your area to a clip path with the specified id:
svg.append("clipPath)
.attr("id","area")
.append("path")
.attr( // area attributes
...
Here's the concept in action (albeit using v3, which shouldn't affect the rectangles or text paths.
Thanks to #andrew-reid suggestion, I was able to implement the solution that uses multiple areas.
HTMLWidgets.widget({
name: 'IMposterior',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(opts) {
console.log("MME: ", opts.MME);
console.log("threshold: ", opts.threshold);
console.log("prob: ", opts.prob);
console.log("colors: ", opts.colors);
console.log("data: ", opts.data);
var margin = {left:50,right:50,top:40,bottom:0};
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,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)
.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]);
chartGroup.append("path")
.attr("d", area(opts.data.filter(function(d){ return d.x > opts.MME ;})))
.style("fill", opts.colors[2]);
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("g")
.attr("class","axis x")
.attr("transform","translate(0,"+height+")")
.call(xAxis);
},
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
};
}
});
I am working on a d3.js Dashboard (so only one dashboard and many layouts). I wanted to display a scatterplot animated, with the dots moving regarding some filter I am applying.
But in order, I have to display something first which is not happening.
Here are my program, which is a scatterplot.js file in an MVC format.
function scatterplot(DOMElement){
var scatObj = {}; // main object
scatObj.setTooltipText = function(f){
tooltipText = f;
//updateInteractions();
return scatObj;
}
scatObj.setMouseclickCallback = function(f){
mouseclickCallback = f;
//updateInteractions();
return scatObj;
}
// function loading data and rendering it in scatter chart
// parameters:
// - dataset in following format:
// [{"key": year, "value": money}, {...}, ...]
// returns:
// - scatObj for chaining
scatObj.loadAndRender = function(data){
dataset = data;
render();
return scatObj;
}
// ---- PRIVATE VARIABLES
// sizing vars
var dataset = []; // Initialize empty array
var numDataPoints = dataset.length;
for(var i=0; i<numDataPoints; i++) {
var newNumber1 = d.value.x; // New random integer
var newNumber2 = d.value.y; // New random integer
dataset.push([newNumber1, newNumber2]); // Add new number to array
}
// Setup settings for graphic
var canvas_width = 500;
var canvas_height = 300;
var padding = 30; // for chart edges
// Create scale functions
var xScale = d3.scaleLinear() // xScale is width of graphic
.domain([0, d3.max(dataset, function(d) {
return d[0]; // input domain
})])
.range([padding, canvas_width - padding * 2]); // output range
var yScale = d3.scaleLinear() // yScale is height of graphic
.domain([0, d3.max(dataset, function(d) {
return d[1]; // input domain
})])
.range([canvas_height - padding, padding]); // remember y starts on top going down so we flip
// scales
// Define X axis
var xAxis = d3.axisBottom()
.scale(xScale)
// Define Y axis
var yAxis = d3.axisLeft()
.scale(yScale)
var svg = d3.select("DOMElement") // This is where we put our vis
.append("svg")
.attr("width", canvas_width)
.attr("height", canvas_height);
var tooltip = d3.select(DOMElement).append("div")
.classed("tooltip", true);
// interaction settings
var tooltipText = function(d, i){return "tooltip over element "+i;}
var mouseoverCallback = function(d, i){ };
var mouseoutCallback = function(d, i){ };
var mouseclickCallback = function(d, i){ console.log(d,i)};
var keySelected = null;
// ---- PRIVATE FUNCTIONS
function render(){
GUP_scat();
}
function GUP_scat(){
// GUP
// Create Circles
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle") // Add circle svg
.attr("cx", function(d) {
return xScale(d[0]); // Circle's X
})
.attr("cy", function(d) { // Circle's Y
return yScale(d[1]);
})
.attr("r", 2); // radius
// Add to X axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (canvas_height - padding) +")")
.call(xAxis);
// Add to Y axis
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + padding +",0)")
.call(yAxis);
// On click, update with new data
d3.select(DOMElement)
.on("click", function() {
keySelected = (keySelected == d.key) ? null : d.key;
var numValues = dataset.length; // Get original dataset's length
dataset = []; // Initialize empty array
for(var i=0; i<numValues; i++) {
var newNumber1 = d.value.x; // Random int for x
var newNumber2 = d.value.y; // Random int for y
dataset.push([newNumber1, newNumber2]); // Add new numbers to array
}
// Update scale domains
xScale.domain([0, d3.max(dataset, function(d) {
return d[0]; })]);
yScale.domain([0, d3.max(dataset, function(d) {
return d[1]; })]);
// Update circles
svg.selectAll("circle")
.data(dataset) // Update with new data
.transition() // Transition from old to new
.duration(1000) // Length of animation
.each("start", function() { // Start animation
d3.select(this) // 'this' means the current element
.attr("fill", "red") // Change color
.attr("r", 5); // Change size
})
.delay(function(d, i) {
return i / dataset.length * 500; // Dynamic delay (i.e. each item delays a little longer)
})
//.ease("linear") // Transition easing - default 'variable' (i.e. has acceleration), also: 'circle', 'elastic', 'bounce', 'linear'
.attr("cx", function(d) {
return xScale(d[0]); // Circle's X
})
.attr("cy", function(d) {
return yScale(d[1]); // Circle's Y
})
.each("end", function() { // End animation
d3.select(this) // 'this' means the current element
.transition()
.duration(500)
.attr("fill", "black") // Change color
.attr("r", 2); // Change radius
});
// Update X Axis
svg.select(".x.axis")
.transition()
.duration(1000)
.call(xAxis);
// Update Y Axis
svg.select(".y.axis")
.transition()
.duration(100)
.call(yAxis);
mouseclickCallback(d, i);
});}
return scatObj; // returning the main object
}
I am calling it like this in my main.js file :
dash.append('div').attr('id', 'scat1').classed("scatterplot", true)
.append("h3").text("Per Scateub");
var scat1 = scatterplot("div#scat1")
.setTooltipText(function(d, i){
return "<b>"+d.data.key +" : "+d3.format(",.2f")(d.value)+"</b> /10 ";
})
.loadAndRender(dataManager.getFteOverall());
scat1.setMouseclickCallback(function(d, i){
dataManager.setNameFilter(d.key);
redraw();
})
and here is my nest :
dmObj.getFteOverall= function(){
var nestedData = d3.nest()
.key(function(d){ return d.naAsses;})
.rollup(function (v) { return {
x: d3.sum(v,function(d){
return (d.four*4,d.three*3,d.two*2,d.one)/100 }),
y: d3.sum(v, function(d){
return (d.fte)
}
)
};
})
.sortKeys(d3.ascending)
.entries(filteredData());
return nestedData;
}
I know it can seem a bit a stupid question, but I have been struggling with it, and I have already been stuck for a while.
I hope I am clear and you will be able to help me (just in case I am working with d3 V4).
Thanks to advance guys.
I've created a stacked chart animation/update app. However there appears to be NaN values being passed into the y and height variables. I am unsure as to what is wrong. If you toggle the data the charts eventually fill up.
jsFiddle
but the problem may occur first in setting the yaxis
svg.select("g.y")
.transition()
.duration(500)
.call(methods.yAxis);
It looks like something goes wrong in the bar rect enter/exit code.
//_morph bars
var bar = stacks.selectAll("rect")
.data(function(d) {
return d.blocks;
});
// Enter
bar.enter()
.append("rect")
.attr("class", "bar")
.attr("y", function(d) { return methods.y(d.y1); })
.attr("width", methods.x.rangeBand())
.style("fill", function(d) { return methods.color(d.name); });
// Update
bar
.attr("y", methods.height)
.attr("height", initialHeight)
.attr("width", methods.x.rangeBand())
.transition()
.duration(500)
.attr("x", function(d) { return methods.x(d.Label); })
.attr("width", methods.x.rangeBand())
.attr("y", function(d) { return methods.y(d.y1); })
.attr("height", function(d) { return methods.y(d.y0) - methods.y(d.y1); })
// Exit
bar.exit()
.transition()
.duration(250)
.attr("y", function(d) { return methods.y(d.y1); })
.attr("height", function(d) { methods.y(d.y0) - methods.y(d.y1); })
.remove();
//__morph bars
I've managed to narrow down the problem to the setDBlock function.
It appears if another chart has the same set of data, it takes on additional object parameters inside the dblock obj.
http://jsfiddle.net/XnngU/44/
I'm not sure at this stage as to how to clean it up. But I have isolated this via a legend and a function.
setDBlocks: function(incomingdata){
var data = incomingdata.slice(0);
methods.color.domain(d3.keys(data[0]).filter(function(key) { return key !== "Label"; }));
data.forEach(function(d) {
console.log("D", d);
var y0 = 0;
if(d["blocks"] == undefined){
d.blocks = methods.color.domain().map(function(name) {
var val = d[name];
if(isNaN(val)){
val = 0;
}
return {name: name, values: val, y0: y0, y1: y0 += +val};
});
}
d.total = d.blocks[d.blocks.length - 1].y1;
});
}
I've fixed the anomaly by deleting data in the update function. I'm not sure why though the data is not unique - it looks like if the data is the same - as the last chart - it gets modified accordingly and used again for its next chart. Is there a better way of cleaning this up - I've tried to keep objects unique and clean by cloning/splicing but maybe that is contributing towards the problem.
delete d.blocks;
delete d.total;
http://jsfiddle.net/XnngU/53/
update: function(data){
methods.el = this;
var selector = methods.el["selector"];
data.forEach(function(d) {
delete d.blocks;
delete d.total;
});
methods.animateBars(selector, data);
}
I've an incredibly basic syntax question. I've been learning d3, SVG, and Javascript mostly by editing someone else's code, which is challenging.
The goal is to update a y axis after updating the data and the scale, which is based on the data. I want the axis--ticks and labels and all--to transition with the domain of the data. The axis isn't getting updated. The problem might be related to scope, or I'm referencing the wrong SVG element. (There are actually several plots getting updated simultaneously, but I'm just focusing on the axis of one of them here.)
function makeYaxis (chart, scale, nticks, label, width, height, xmf, visName)
{
var yAxis = d3.svg.axis()
.scale(scale)
.orient("left")
.ticks(nticks);
chart.append("svg:g")
.attr("class","y axis")
.append("g")
.attr("transform","translate(60,1)") // fix magic #
.call(yAxis);
var xMove = xmf.yylMarginFactor * width - 1;
var yMove = (((1 - xmf.xxbMarginFactor) * height +
xmf.xxtMarginFactor * height) / 2);
chart.append("svg:text")
.attr("class", visName + "xLabel")
.attr("x", 0)
.attr("y", 0)
.attr("dy", "-2.8em")
.text(label)
.attr("transform", "rotate(-90) translate(-" + yMove + "," + xMove + ")");
}
function makeYscale (plotMargin, thedata, xmf)
{
var dataMin = d3.min(thedata[0]);
var dataMax = d3.max(thedata[0]);
var yyMargin = d3.max([plotMargin * (dataMax - dataMin),0.05]);
var yy = d3.scale.linear()
.domain([dataMin - yyMargin, yyMargin + dataMax])
.range([(1 - xmf.xxbMarginFactor) * height, xmf.xxtMarginFactor * height]);
return yy;
}
// Set up this visualization
var chart = d3.select("#vis1")
.append("svg:svg")
.attr("class", "vis1chart")
.attr("width", width)
.attr("height", height);
var yy = makeYscale(plotMargin, thetas, xmf);
makeYaxis(chart, yy, nYTicks, "parameter", width, height, xmf, "vis1");
var points = chart.selectAll("circle.vis1points")
.data(thetas)
.enter().append("svg:circle")
.attr("class", "vis1points")
.attr("cx", function (d, i) { return xx(i); })
.attr("cy", function (d, i) { return yy(d); })
.attr("r", 3);
points.transition()
.attr("cx", function (d, i) { return xx(i); })
.attr("cy", yy)
.duration(dur);
vis1move();
function vis1move ()
{
function movePoints ()
{
var tmp = chart.selectAll(".vis1points").data(thetas[0], function (d, i) { return i; } );
var opa1 = points.style("stroke-opacity");
var opa2 = points.style("fill-opacity");
tmp
.enter().insert("svg:circle")
.attr("class", "vis1points")
.attr("cx", function (d, i) { return xx(i); })
.attr("cy", yy)
.attr("r", 3)
.style("stroke-opacity", opa1)
.style("fill-opacity", opa2);
tmp.transition()
.duration(10)
.attr("cx", function (d, i) { return xx(i); })
tmp.exit()
.transition().duration(10).attr("r",0).remove();
};
// (Code for updating theta, the data, goes here)
// Update axes for this vis
yy = makeYscale(plotMargin, thetas, xmf);
var transition = chart.transition().duration(10);
var yAxis = d3.svg.axis()
.scale(yy)
.orient("left")
.ticks(4);
transition.select("y axis").call(yAxis);
movePoints(); // Previously had been before axis update (fixed)
}
Sorry I can't get this self-contained.
Is transition.select.("y axis").call(yAxis) correct? Is there anything glaringly off here?
Edit: To keep things simple, I now have
// Update axes for this vis
yy = makeYscale(plotMargin, thetas, xmf);
var yAxis = d3.svg.axis().scale(yy);
chart.select("y axis").transition().duration(10).call(yAxis);
I'm wondering if something in the way I created the axis in the first place (in makeYaxis()) prevents me from selecting it properly. The yy function is returning the right values under the hood, and the data points are getting plotted and rescaled properly. It's just that the axis is "stuck."
Following meetamit's suggestions and the example here, I have stumbled on what appears to be a solution.
First, in function makeYaxis(), I removed the line .append("g"). I also updated the class name to yaxis. The function now reads
function makeYaxis (chart, scale, nticks, label, width, height, xmf, visName)
{
var yAxis = d3.svg.axis()
.scale(scale)
.orient("left")
.ticks(nticks);
chart.append("svg:g")
.attr("class","yaxis") // note new class name
// .append("g")
.attr("transform","translate(60,1)")
.call(yAxis);
// everything else as before
}
I then added an extra period in my call to select(".yaxis"):
chart.select(".yaxis").transition().duration(10).call(yAxis);
I would be very grateful if anyone could explain to me exactly why this solution appears to work.
I am trying to draw a heatmap with d3 using data from a csv: this is what I have so far
Given a csv file:
row,col,score
0,0,0.5
0,1,0.7
1,0,0.2
1,1,0.4
I have an svg and code as follows:
<svg id="heatmap-canvas" style="height:200px"></svg>
<script>
d3.csv("sgadata.csv", function(data) {
data.forEach(function(d) {
d.score = +d.score;
d.row = +d.row;
d.col = +d.col;
});
//height of each row in the heatmap
//width of each column in the heatmap
var h = gridSize;
var w = gridSize;
var rectPadding = 60;
$('#heatmap-canvas').empty();
var mySVG = d3.select("#heatmap-canvas")
.style('top',0)
.style('left',0);
var colorScale = d3.scale.linear()
.domain([-1, 0, 1])
.range([colorLow, colorMed, colorHigh]);
rowNest = d3.nest()
.key(function(d) { return d.row; })
.key(function(d) { return d.col; });
dataByRows = rowNest.entries(data);
mySVG.forEach(function(){
var heatmapRow = mySVG.selectAll(".heatmap")
.data(dataByRows, function(d) { return d.key; })
.enter().append("g");
//For each row, generate rects based on columns - this is where I get stuck
heatmapRow.forEach(function(){
var heatmapRects = heatmapRow
.selectAll(".rect")
.data(function(d) {return d.score;})
.enter().append("svg:rect")
.attr('width',w)
.attr('height',h)
.attr('x', function(d) {return (d.row * w) + rectPadding;})
.attr('y', function(d) {return (d.col * h) + rectPadding;})
.style('fill',function(d) {
if(d.score == NaN){return colorNA;}
return colorScale(d.score);
})
})
</script>
My problem is with the nesting. My nesting is based on 2 keys, row first (used to generate the rows) then for each row, there are multiple nested keys for the columns and each of these contain my score.
I am not sure how to proceed i.e. loop over columns and add rectangles with the colors
Any help would be appreciated
While you could used a subselect (see d3.js building a grid of rectangles) to work with nested data in d3 it's not really needed in this case. I put together an example using your data at http://jsfiddle.net/QWLkR/2/. This is the key part:
var heatMap = svg.selectAll(".heatmap")
.data(data, function(d) { return d.col + ':' + d.row; })
.enter().append("svg:rect")
.attr("x", function(d) { return d.row * w; })
.attr("y", function(d) { return d.col * h; })
.attr("width", function(d) { return w; })
.attr("height", function(d) { return h; })
.style("fill", function(d) { return colorScale(d.score); });
Basically you can just use the row and col to calculate the correct position of the squares in your heatmap.