I am trying to get some dart code working with D3, but I am having really strange problems when I call d3.layout.force().start(). In my code below, I have commented out everything below force.size(). When I run this code and look at the console output in browser, I get the result of the single js.context.console.log() And it prints out my object just as written when assigning word_map. The problem is that as soon as I uncomment force.start() (only that one line btw) the object word_map starts to change and the console.log prints out an object whos x and y values are NAN.
Why does force.start() change the x and y values of my object to NAN and how is this change being shown in my console.log statement when the console.log occurs long before the force.start() is being called. It is like force.start() is being called first and the console.log is being called second.
import 'dart:html';
import 'package:js/js.dart' as js;
import 'dart:async';
import 'dart:js';
List<Map<String, String >> word_map;
void main() {
draw_visualization();
}
void draw_visualization(){
word_map =new List<Map<String,String>>();
word_map= [{'word':'hello','x':30.0,'y':20.0, 'fixed':false}];
JsObject word_list_js = new JsObject.jsify(word_map);
js.context.console.log(word_list_js);
print ("Made it here first");
var force = d3.layout.force()
.nodes(word_list_js)
.links(js.array([]))
.size([600,400]);
/* .start();
var chart = d3.select("body")
.append("svg")
.attr('width', 600)
.attr('height', 400);
var node = chart.selectAll('.node')
.data(word_list_js).enter().append('circle')
.attr('class','node')
.attr('r', 5)
.attr('cx', new js.FunctionProxy((d,i,e){return d.x;}))
.attr('cy',new js.FunctionProxy((d,i,e){return d.y;}))
.style('fill', 'blue')
force.on('tick', new js.FunctionProxy((e){
node.attr("cx", new js.FunctionProxy((d,i,e){
if (d.x > 0 && d.x<4000){print(d.x);}
return d.x;
}))
.attr("cy", new js.FunctionProxy((d,i,e){
return d.y;
})
);
}));
*/
}
I have to admit, I am a bit confused on how the whole proxy system works and Dart's async is giving me a bit of trouble too. I just don't see how the force.start function would even effect the previous console.log statement.
Overall, I just want my object to stop getting f'd up by the force layout. NAN's for x and why make it so my circles do not draw correctly.
EDIT: It seems to have to do with gravity. After adding .gravity(0) before .start() it does not compute x and y to nan. I did this in a hunch that one of the 2 forces of force layout was causing the issue. I still don't understand why gravity(0) fixes this.
You shouldn't mix package:js and dart:js.
remove import 'dart:js';.
replace new JsObject.jsify(word_map) with js.map(word_map)
Related
I have finally decided to saddle up and adopt d3 v5 syntax after years of using v3. After looking at some tutorials and examples, v5 syntax really struck me as sublime. The readability is far improved and it seems easier to integrate multiple data sources.
To my dismay, and despite my reverence of it, I couldn't quite build a visual from scratch with the new Promise syntax. Here is my simple graph: (note I'm using hard coded data for the sake of this post, and I have commented out the .csv() call that I'd actually use. It should still be functionally the same)
var margins = {top:50, right:50, bottom:50, left:50};
var width = window.innerWidth - margins.left - margins.right;
var height = window.innerHeight - margins.top - margins.bottom;
var sampleData = [
{'y':32, 'x':1},
{'y':20, 'x':2},
{'y':19, 'x':3},
{'y':12, 'x':4},
{'y':15, 'x':5},
{'y':19, 'x':6},
{'y':22, 'x':7},
{'y':26, 'x':8},
{'y':31, 'x':9},
{'y':36, 'x':10}
];
//var dataset = d3.csv("my-data.csv").then(function(data)
// {return data;
// });
var dataset = sampleData.then(function(data)
{return data;
});
var svg = d3.select('body').append('svg').attr('id','svg').attr('height','100%').attr('width','100%');
var myLine = dataset.then(function(data) {
Promise.all(data.map(function(d) {return {X:+d.x, Y:+d.y}}))//ensure numeric parsing
var xScale = d3.scaleLinear()
.domain(d3.extent(data, function(d) { return d.X; }))
.range([0,width]);
var yScale = d3.scaleLinear()
.domain(d3.extent(data, function(d) {return d.Y; }))
.range([height,0]);
var xAxis = d3.axisBottom(xScale);
var yAxis = d3.axisLeft(yScale);
var line = d3.line()
.x(function(d) {return xScale(d.x); })
.y(function(d) {return yScale(d.y); });
var svg = d3.select('body').append('svg').attr('id','svg').attr('height','100%').attr('width','100%');
var graphGroup = svg.append('g')
.attr('transform',"translate("+margins.left+","+margins.top+")");
graphGroup.append('path')
.attr('d', function(d) {return line(data); });
graphGroup.append('g')
.attr('class', 'axis x')
.attr('transform', "translate(0,"+height+")")
.call(xAxis);
graphgroup.append('g')
.attr('class', 'axis y')
.call(yAxis);
});
I get this error in the console:
Uncaught TypeError: sampleData.then is not a function
Question
I take the point that Promise.all() and .then() are not always favorable for really simple data visuals, but I'd still like to know why I can't make the above script output a minimal line graph. From then, hopefully, I can slowly take the training wheels off and find my stride with v5.
I'm particularly confused with how to cast to numbers using the unary + with Promise.
Although there are many twists and turns when it comes to using Promises, it turns out that the actual changes required to port code to make use of the d3-fetch module in favor of the deprecated d3-request module are strikingly minimal. Loosely speaking, to adapt your—or any pre-v5—code to use the new d3-fetch module you just move the callback from one method to another. Thus, the former
d3.dsv(url, callback);
now becomes
d3.dsv(url).then(callback);
The only thing to be aware of is to check if the callback's signature matches the one expected for .then. This only becomes relevant, though, if your callback used two parameters to handle errors:
function callback(error, data) {
// Handle error
if (error) throw error;
// Manipulate data
}
With Promises this is split into two separated methods:
function onFullfilled(data) {
// Manipulate data
}
function onRejected(error) {
// Handle error
}
These callback can be used in two ways:
// 1.
d3.dsv(url).then(onFullfilled, onRejected);
// 2.
d3.dsv(url).then(onFullfilled).catch(onRejected);
Another important point is that you cannot return data from your callback (beware of the infamous "How do I return the response from an asynchronous call?"!). d3.dsv now returns a Promise, not your data; you have to handle the data inside your callback. If you become more skilled using Promises you might have a look into the await operator, though, which allows you to wait for a Promise and its fulfilled value. Although this is ECMAScript 2017 (ES8) syntax it has already seen wide-spread browser support.
That being the general case, now for your code: sampleData is an Array object which, of course, does not have a .then() method and, hence, the error. To make the code work there is not much to do apart from uncommenting the lines featuring d3.dsv and putting the relevant code handling data inside the callback.
If you really want to do an offline simulation with hardcoded data you can use Promise.resolve() which will return a Promise already resolved with the given value. In your case instead of
d3.csv("my-data.csv")
.then(function(data) { });
you can use
Promise.resolve(sampleDate)
.then(function(data) { }); // Same handler as above
In D3 v4 with the force module, how do you update the parameters of the simulation once you have initialized the graph?
More precisely, I am trying to change the .forceLink and .forceManyBody of the force directed graph when the user clicks one of its nodes.
var node = svg
.append("g")
.attr("class", "gnodes")
.selectAll(".node")
.data(graph.nodes)
.enter()
.append("g")
.attr("class", "node")
.on('dblclick', connectedNodes); //calls for change in simulation parameters
So far I have been able to update it by duplicating the simulation under the connectedNodes function:
function connectedNodes() {
//new parameters
linkDistance = 5;
fCharge = -10;
//replicates the initial simulation code
simulation = d3.forceSimulation()
.force("link", d3.forceLink()
.id(function(d) {return d.id;})
.distance(linkDistance)
)
.force("collide", d3.forceCollide()
.radius(function(d){return d.r + 10})
.strength(1)
)
.force("charge", d3.forceManyBody()
.strength(fCharge)
)
.force("center", d3.forceCenter(width / 2, height / 2));
simulation.nodes(graph.nodes).on("tick", ticked);
simulation.force("link").links(graph.links);
Although this works it is very redundant. Is there a way in which the simulation can be refreshed with the new parameters? I have tried the following but it does not work
function connectedNodes() {
//new parameters
linkDistance = 5;
fCharge = -10;
//restart simulation with new parameters
simulation.restart();
}
You need a reference to the forces you would like to update. This can be done using either of two ways:
As pointed out by Cool Blue in their comment, you can easily get a reference to the force by calling simulation.force() passing in just the name of the force it was registered with in the first place. If we had, supposedly, created our simulation while passing in an anonymous, in-place force like so:
var simulation = d3.forceSimulation()
.force("link", d3.forceLink() // "link" is the name to register the force
.id(function(d) { return d.id; })
.distance(linkDistance)
);
Later on the force can be obtained by its name whenever needed:
var forceLink = simulation.force("link"); // Get the force by its name
Personally, I like this approach and would prefer it over the second one, whenever possible, because I do not like having to many references / variables around.
Keep a reference to the force when creating it.
var forceLink = d3.forceLink() // Keep a reference to the force
.id(function(d) { return d.id; })
.distance(linkDistance);
var simulation = d3.forceSimulation()
.force("link", forceLink ) // Pass the reference when creating the simulation
No matter, which way you chose, you may then update your force by doing something like
linkDistance += 10;
forceLink.distance(linkDistance);
This will take the new value into account once the next tick is calculated. If the simulation has already come to a stop or you just want to heat it up again you may call
simulation.alpha(1).restart();
I have set up an example which shows these live updates when you hover over the SVG. This will update the linkDistance and restart the force layout.
Introducing
I'm using Sequences Sunburst of d3.js for visualization of data.
I want to add a transition between two datasets (triggered by a button). I would like each arc to animate to display the new data.
Something like this: (1)Sunburst_Exemple, but without changing the accessor.
Research
In the (1)Sunburst_Example, the value accessor is modified. But I want to change the data, not the function that defines how to reach the data.
So, I searched a way for redefining data into path.
I was inspired by (2)Sunburst_Exemple, using each() method to store old values and attrTween() for transitions. But nothing is changing and I have the following error message:
Maximum call stack size exceeded . Maybe caused by the fact I have a method and I'm not in a global scope.
(2) link : _http://ninjapixel.io/StackOverflow/doughnutTransition.html
Then I have tried (3)Zoomable_Sunburst example, but nothing it happens in my case... .
(3) link : _http://bl.ocks.org/mbostock/4348373
My Example
Here is my example : JSFIDDLE
Problem is :
colors are lost
transition is not happening
I think I don't understand how transitioning is really working, I could have missed something that could help me in this case.
Any help ?
-
Listener of button call click method that redefined nodes and path.
/*CHANGING DATA*/
function click(d){
d3.select("#container").selectAll("path").remove();
var nodes = partition.nodes(d)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
}) ;
var path = vis.data([d]).selectAll("path")
.data(nodes)
.enter().append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return colors[d.name]; })
.style("opacity", 1)
.on("mouseover", mouseover)
.each(stash)
.transition()
.duration(750)
.attrTween("d", arcTween);
;
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
}
_
// Stash the old values for transition.
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
// Interpolate the arcs in data space.
function arcTween(a) {
var i = d3.interpolate({x: a.x0, dx: a.dx0}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
}
Data Characteristics :
the root node is the same for the two datasets.
the structure is globally the same, only the values are changing.
Some fields disappear but we can add them with value equal to 0.
I've recently passed from d3.v2 to d3.v3, and am trying to understand the differences in the transition mechanisms.
In the code underneath, I'm trying to make a bar graph that, when drawn, has bars that increase in height via a transition. This code works without issue in d3.v2, but in v3, the transition seems to happen "instantly" (the height is immediately set to the end value).
graph.enter()//for each bucket
.append('g')
.attr('transform',function(d,i){ return 'translate('+(xBand(i))+')';})
.attr('width',xBand.rangeBand())
.each(function(data,index){//here we are working on the selection for a single bucket
var $this=d3.select(this); //this refers to the group selection
var currentY=0;
var rects=$this.selectAll('rect')
.data(data.values);
rects.enter()
.insert('rect')
.attr('group-id',me.groupId)
.attr('y',Hats.accessor('y'))
.attr('width',xBand.rangeBand())
.attr('fill',(function(elt){ return me.colors(me.groupId(elt));}));
rects.transition()
.duration(750)
.attr('height',(function(elt){
var h=_.compose(heightScale,me.values)(elt);
d3.select(this).attr('y',currentY);
currentY+=h;
return h;
}));
});
Try setting a starting height in your enter selection:
rects.enter()
.insert('rect')
.attr('group-id',me.groupId)
.attr('y',Hats.accessor('y'))
.attr('width',xBand.rangeBand())
.attr('fill',(function(elt){ return me.colors(me.groupId(elt));}))
.attr('height', 0);
rects.transition()
.duration(750)
.attr('height',(function(elt){
var h=_.compose(heightScale,me.values)(elt);
d3.select(this).attr('y',currentY);
currentY+=h;
return h;
}));
I have a really simple line chart written using NVD3.js. I've written a simple redraw based on timer, pulled from examples I've seen, but I get the error
Uncaught TypeError: Cannot read property 'y' of undefined
The JS is
var data = [{
"key": "Long",
"values": getData()
}];
var chart;
nv.addGraph(function () {
chart = nv.models.cumulativeLineChart()
.x(function (d) { return d[0] })
.y(function (d) { return d[1] / 100 })
.color(d3.scale.category10().range());
chart.xAxis
.tickFormat(function (d) {
return d3.time.format('%x')(new Date(d))
});
chart.yAxis
.tickFormat(d3.format(',.1%'));
d3.select('#chart svg')
.datum(data)
.transition().duration(500)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
});
function redraw() {
d3.select('#chart svg')
.datum(data)
.transition().duration(500)
.call(chart);
}
function getData() {
var arr = [];
var theDate = new Date(2012, 01, 01, 0, 0, 0, 0);
for (var x = 0; x < 30; x++) {
arr.push([theDate.getTime(), Math.random() * 10]);
theDate.setDate(theDate.getDate() + 1);
}
return arr;
}
setInterval(function () {
var long = data[0].values;
var next = new Date(long[long.length - 1][0]);
next.setMonth(next.getMonth() + 1)
long.shift();
long.push([next.getTime(), Math.random() * 100]);
redraw();
}, 1500);
Second Answer (after comment)
I looked at source for cumulativeLineChart. You can see the display.y property get created during chart creation. It relies on a private method: "indexify". If some derivative of that method was made public, then perhaps you could do something like chart.reindexify() before redrawing.
As a temporary workaround, you could recreate the chart from scratch on every update. If you remove the transition, that seems to work okay. Example jsfiddle: http://jsfiddle.net/kaliatech/PGyKF/.
First Answer
I believe there is bug in cumulativeLineChart. It appears that the cumulativeLineChart adds a "display.y" property dynamically to data values in the series. However, it does not regenerate this property when new values are added to the series for a redraw. I don't know of anyway to make it do this, although I'm new to NVD3.
Do you really need a CumulativeLineChart, or would a normal line chart be sufficient? If so, I had to make the following changes to your code:
Change from cumulativeLineChart to lineChart
Change from using 2 dimension arrays of data, to using objects of data (with x,y properties)
(I'm not familiar enough with NVD3 to say what data formats is expects. The 2D array obviously works for initial loads, but I think it fails to work for subsequent redraws. This is likely related to the same issue you are having with cumulativeLineChart. I thought changing to objects would fix cumulativeLineChart as well, but it didn't seem to.)
I also changed the following, although not as important:
Modified your getData function to create a new instance of Date to avoid unexpected consequences of sharing a reference as the date gets incremented.
Modified the update interval function to generate new data in increments of days (not months) with y values in the same range as the getData function.
Here's a working jsfiddle with those changes:
http://jsfiddle.net/kaliatech/4TMMD/
I found what I think is a better solution. The problem occurs because the cumulative chart sets the y function during processing. Whenever your want to refresh the chart, first set it back to a default which returns the correct original y. In your redraw function do this before updating:
chart.y(function (d) { return d.y; });
Even better would be if the cumulative chart could do this for itself (store the original access function before setting the new one, and put it back before re-indexing). If I get a chance, I'll try to push a fix.
I ran into the same issue. I changed the y() function on the lines from
.y(function(d) { return d.display.y })
to
.y(function(d) { return d.display ? d.display.y : d.y })
This gets rid of the error. Obviously it won't be displaying the (non-existent) indexed value in the error case, but in my experience, the chart gets updated again with display defined, and it looks correct.