I am experiencing issues when trying to load D3 v4.8 and word cloud layout component (https://github.com/jasondavies/d3-cloud) using the RequireJS. While both D3.js and d3.layout.cloud.js are being downloaded to the browser, an exception is being thrown indicating the d3.layout.cloud is not a function.
Here is how I am configuring the RequireJS:
require.config({
waitSeconds: 10,
baseUrl: './scripts',
paths: {
d3: 'd3.min',
jquery: 'jquery-2.1.0.min',
cloud: 'd3.layout.cloud'
},
shim: {
cloud: {
deps:['jquery', 'd3']
}
}
});
The line of code that throws an exception is d3.layout.cloud().size([width, height]) and can be found in the function below:
function wordCloud(selector) {
var width = $(selector).width();
var height = $(selector).height();
//var fill = d3.scale.category20();
//Construct the word cloud's SVG element
var svg = d3.select(selector).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate("+ width/2 +","+ height/2 +")")
var fill = d3.scale.category20();
//Draw the word cloud
function draw(words) {
var cloud = svg.selectAll("g text")
.data(words, function(d) { return d.text; })
//Entering words
cloud.enter()
.append("text")
.style("font-family", "Impact")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr('font-size', 1)
.style("cursor", "hand")
.text(function(d) { return d.text; })
.on("click", function (d, i){
window.open("http://www.google.com/?q=" + d.text, "_blank");
});
//Entering and existing words
cloud
.transition()
.duration(600)
.style("font-size", function(d) { return d.size + "px"; })
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.style("fill-opacity", 1);
//Exiting words
cloud.exit()
.transition()
.duration(200)
.style('fill-opacity', 1e-6)
.attr('font-size', 1)
.remove();
}
//Use the module pattern to encapsulate the visualisation code. We'll
// expose only the parts that need to be public.
return {
//Recompute the word cloud for a new set of words. This method will
// asycnhronously call draw when the layout has been computed.
//The outside world will need to call this function, so make it part
// of the wordCloud return value.
update: function(words) {
// min/max word size
var minSize = d3.min(words, function(d) { return d.size; });
var maxSize = d3.max(words, function(d) { return d.size; });
var textScale = d3.scale.linear()
.domain([minSize,maxSize])
.range([15,30]);
d3.layout.cloud().size([width, height])
.words(words.map(function(d) {
return {text: d.text, size: textScale(d.size) };
}))
.padding(5)
.rotate(function() { return ~~(Math.random() * 2) * 90; })
.font("Impact")
.fontSize(function(d) { return d.size; })
.on("end", draw)
.start();
}
}
}
The latest version of d3-cloud only depends on d3-dispatch so it should work fine with any version of D3. I think the issue here is that you're using RequireJS (AMD) to reference the d3.layout.cloud.js file, but you're not using RequireJS to use the library (configured as cloud in your example). See the following example:
requirejs.config({
baseUrl: ".",
paths: {
cloud: "d3.layout.cloud" // refers to ./d3.layout.cloud.js
}
});
requirejs(["cloud"], function(cloud) { // depends on "cloud" defined above
cloud()
.size([100, 100])
.words([{text: "test"}])
.on("word", function() {
console.log("it worked!");
})
.start();
});
If you prefer to use CommonJS-style require(…) you can also use this with RequireJS if you use the appropriate define(…) syntax, like in this quick example.
While d3-cloud itself is compatible with both D3 V3 and V4, most examples you will find are not, and your current code is not, it can only work with V3.
To make it work with V4 you need to replace all references to d3.scale.
For example, d3.scale.category20() becomes d3.scaleOrdinal(d3.schemeCategory20).
For a small runnable example that is compatible with both, see Fiddle #1. It uses RequireJS to load d3, d3-cloud, and jQuery. Try changing the D3 version in require config at top of JS part.
Let's settle on V3 for now, since your code relies on it. There are still a few issues:
Your must use objects for d3 & d3cloud & jQuery that are obtained via a call to require. And with RequireJS this is asynchronous (because it needs to fetch the JS scripts programmatically, and in a browser that can only be done asynchronously). You put your code within a callback function (for more info, see RequireJS docs):
[ Edit: see Jason's answer for alternative syntax. ]
require(['d3', 'd3cloud', 'jQuery'], function(d3, d3cloud, $) {
/* Your code here.
Inside "require" function that you just called,
RequireJS will fetch the 3 named dependencies,
and at the end will invoke this callback function,
passing the 3 dependencies as arguments in required order.
*/
});
In this context (and versions) d3 cloud main function is not available under d3.layout.cloud(), so you need to replace that with d3cloud(), assuming that's the name of the argument passed as callback to require.
You must make sure you never pass width or height of 0 to d3cloud.size([width, height]), or it will enter an infinite loop. Unfortunately it can easily happen if you use $(selector).height(), depending on content of your page, and possible "accidents". I suggest var height = $(selector).height() || 10; for example.
Not a programming problem, but the function you pass to .rotate() is taken from an example, and maybe you want to change that: it yields only 2 possible values, 0 or 90, I find this monotonous, the default one is prettier. So I removed the line entirely in the example below. Maybe you'll want it back, just add your .rotate(...) line.
Fiddle #2: a complete working example based on your piece of code.
Related
Does someone know of a way to 'flush' a transition.
I have a transition defined as follows:
this.paths.attr('transform', null)
.transition()
.duration(this.duration)
.ease(d3.easeLinear)
.attr('transform', 'translate(' + this.xScale(translationX) + ', 0)')
I am aware I can do
this.paths.interrupt();
to stop the transition, but that doesn't finish my animation. I would like to be able to 'flush' the transition which would immediately finish the animation.
If I understand correctly (and I might not) there is no out of the box solution for this without going under the hood a bit. However, I believe you could build the functionality in a relatively straightforward manner if selection.interrupt() is of the form you are looking for.
To do so, you'll want to create a new method for d3 selections that access the transition data (located at: selection.node().__transition). The transition data includes the data on the tweens, the timer, and other transition details, but the most simple solution would be to set the duration to zero which will force the transition to end and place it in its end state:
The __transition data variable can have empty slots (of a variable number), which can cause grief in firefox (as far as I'm aware, when using forEach loops), so I've used a keys approach to get the non-empty slot that contains the transition.
d3.selection.prototype.finish = function() {
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].duration = 0;
})
}
If working with delays, you can also trigger the timer callback with something like: if(slots[d]) slots[d].timer._call();, as setting the delay to zero does not affect the transition.
Using this code block you call selection.finish() which will force the transition to its end state, click a circle to invoke the method:
d3.selection.prototype.finish = function() {
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].timer._call();
})
}
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var circle = svg.selectAll("circle")
.data([1,2,3,4,5,6,7,8])
.enter()
.append("circle")
.attr("cx",50)
.attr("cy",function(d) { return d * 50 })
.attr("r",20)
.on("click", function() { d3.select(this).finish() })
circle
.transition()
.delay(function(d) { return d * 500; })
.duration(function(d) { return d* 5000; })
.attr("cx", 460)
.on("end", function() {
d3.select(this).attr("fill","steelblue"); // to visualize end event
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.12.0/d3.min.js"></script>
Of course, if you wanted to keep the method d3-ish, return the selection so you can chain additional methods on after. And for completeness, you'll want to ensure that there is a transition to finish. With these additions, the new method might look something like:
d3.selection.prototype.finish = function() {
// check if there is a transition to finish:
if (this.node().__transition) {
// if there is transition data in any slot in the transition array, call the timer callback:
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].timer._call();
})
}
// return the selection:
return this;
}
Here's a bl.ock of this more complete implementation.
The above is for version 4 and 5 of D3. To replicate this in version 3 is a little more difficult as timers and transitions were reworked a bit for version 4. In version three they are a bit less friendly, but the behavior can be achieved with slight modification. For completeness, here's a block of a d3v3 example.
Andrew's answer is a great one. However, just for the sake of curiosity, I believe it can be done without extending prototypes, using .on("interrupt" as the listener.
Here I'm shamelessly copying Andrew code for the transitions and this answer for getting the target attribute.
selection.on("click", function() {
d3.select(this).interrupt()
})
transition.on("interrupt", function() {
var elem = this;
var targetValue = d3.active(this)
.attrTween("cx")
.call(this)(1);
d3.select(this).attr("cx", targetValue)
})
Here is the demo:
var svg = d3.select("svg")
var circle = svg.selectAll("circle")
.data([1, 2, 3, 4, 5, 6, 7, 8])
.enter()
.append("circle")
.attr("cx", 50)
.attr("cy", function(d) {
return d * 50
})
.attr("r", 20)
.on("click", function() {
d3.select(this).interrupt()
})
circle
.transition()
.delay(function(d) {
return d * 500;
})
.duration(function(d) {
return d * 5000;
})
.attr("cx", 460)
.on("interrupt", function() {
var elem = this;
var targetValue = d3.active(this)
.attrTween("cx")
.call(this)(1);
d3.select(this).attr("cx", targetValue)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="500"></svg>
PS: Unlike Andrew's answer, since I'm using d3.active(node) here, the click only works if the transition had started already.
I want to draw a pie chart for every point on the map instead of a circle.
The map and the points are displaying well but the pie chart is not showing over the map points. There is no error also. I can see the added pie chart code inside map also.
Below is the code snippet .
var w = 600;
var h = 600;
var bounds = [[78,30], [87, 8]]; // rough extents of India
var proj = d3.geo.mercator()
.scale(800)
.translate([w/2,h/2])
.rotate([(bounds[0][0] + bounds[1][0]) / -2,
(bounds[0][1] + bounds[1][1]) / -2]); // rotate the project to bring India into view.
var path = d3.geo.path().projection(proj);
var map = d3.select("#chart").append("svg:svg")
.attr("width", w)
.attr("height", h);
var india = map.append("svg:g")
.attr("id", "india");
var gDataPoints = map.append("g"); // appended second
d3.json("data/states.json", function(json) {
india.selectAll("path")
.data(json.features)
.enter().append("path")
.attr("d", path);
});
d3.csv("data/water.csv", function(csv) {
console.log(JSON.stringify(csv))
gDataPoints.selectAll("circle")
.data(csv)
.enter()
.append("circle")
.attr("id", function (d,i) {
return "chart"+i;
})
.attr("cx", function (d) {
return proj([d.lon, d.lat])[0];
})
.attr("cy", function (d) {
return proj([d.lon, d.lat])[1];
})
.attr("r", function (d) {
return 3;
})
.each(function (d,i) {
barchart("chart"+i);
})
.style("fill", "red")
//.style("opacity", 1);
});
function barchart(id){
var data=[15,30,35,20];
var radius=30;
var color=d3.scale.category10()
var svg1=d3.select("#"+id)
.append("svg").attr('width',100).attr('height',100);
var group=svg1.append('g').attr("transform","translate(" + radius + "," + radius + ")");
var arc=d3.svg.arc()
.innerRadius('0')
.outerRadius(radius);
var pie=d3.layout.pie()
.value(function(d){
return d;
});
var arcs=group.selectAll(".arc")
.data(pie(data))
.enter()
.append('g')
.attr('class','arc')
arcs.append('path')
.attr('d',arc)
.attr("fill",function(d,i){
return color(d.data);
//return colors[i]
});
}
water.csv:
lon,lat,quality,complaints
80.06,20.07,4,17
72.822,18.968,2,62
77.216,28.613,5,49
92.79,87.208,4,3
87.208,21.813,1,12
77.589,12.987,2,54
16.320,75.724,4,7
In testing your code I was unable to see the pie charts rendering, at all. But, I believe I still have a solution for you.
You do not need a separate pie chart function to call on each point. I'm sure that there are a diversity of opinions on this, but d3 questions on Stack Overflow often invoke extra functions that lengthen code while under-utilizing d3's strengths and built in functionality.
Why do I feel this way in this case? It is hard to preserve the link between data bound to svg objects and your pie chart function, which is why you have to pass the id of the point to your function. This will be compounded if you want to have pie chart data in your csv itself.
With d3's databinding and selections, you can do everything you need with much simpler code. It took me some time to get the hang of how to do this, but it does make life easier once you get the hang of it.
Note: I apologize, I ported the code you've posted to d3v4, but I've included a link to the d3v3 code below, as well as d3v4, though in the snippets the only apparent change may be from color(i) to color[i]
In this case, rather than calling a function to append pie charts to each circle element with selection.each(), we can append a g element instead and then append elements directly to each g with selections.
Also, to make life easier, if we initially append each g element with a transform, we can use relative measurements to place items in each g, rather than finding out the absolute svg coordinates we would need otherwise.
d3.csv("water.csv", function(error, water) {
// Append one g element for each row in the csv and bind data to it:
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("id", function (d,i) { return "chart"+i; })
.append("g").attr("class","pies");
// Add a circle to it if needed
points.append("circle")
.attr("r", 3)
.style("fill", "red");
// Select each g element we created, and fill it with pie chart:
var pies = points.selectAll(".pies")
.data(pie([0,15,30,35,20]))
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
Now, what if we wanted to show data from the csv for each pie chart, and perhaps add a label. This is now done quite easily. In the csv, if there was a column labelled data, with values separated by a dash, and a column named label, we could easily adjust our code to show this new data:
d3.csv("water.csv", function(error, water) {
var points = gDataPoints.selectAll("g")
.data(water)
.enter()
.append("g")
.attr("transform",function(d) { return "translate("+projection([d.lon,d.lat])+")" })
.attr("class","pies")
points.append("text")
.attr("y", -radius - 5)
.text(function(d) { return d.label })
.style('text-anchor','middle');
var pies = points.selectAll(".pies")
.data(function(d) { return pie(d.data.split(['-'])); })
.enter()
.append('g')
.attr('class','arc');
pies.append("path")
.attr('d',arc)
.attr("fill",function(d,i){
return color[i];
});
});
The data we want to display is already bound to the initial g that we created for each row in the csv. Now all we have to do is append the elements we want to display and choose what properties of the bound data we want to show.
The result in this case looks like:
I've posted examples in v3 and v4 to show a potential implementation that follows the above approach for the pie charts:
With one static data array for all pie charts as in the example: v4 and v3
And by pulling data from the csv to display: v4 and v3
I have this working code. where the d3 part is basically:
var bar = chart.append("div").attr("class", "chart")
.selectAll('div')
.data(scope.data.sort().reverse()).enter().append("div")
.transition().ease("elastic")
.style("width", function(d) { return (d[0]/sum)*attrs.chartWidth + "px"; })//This is where I base the width as a precentage from the sum and calculate it according to the chart-width attribute
.style("background-color",function(){i++;if (i<=colors.length-1){return colors[i-1]} else {return colors[(i-1)%colors.length]}}).text(function(d) { return d[1] ; })
but when I try to append("span") in the chaining so the text would be on the span and not in the parent div. the text just disappears and the dev console shows no clue of both the span and the text. Also tried insert("span") and even replacing the .text for .html(function(d){return "<span>"+d[1]+"</span>"}
neither work.
any clues? Thanks!
The problem is that you are starting a transition in the chain. The transition object provides many functions just like a normal d3.selection including .remove, .text and .html, but does not allow .append operation.
You should re-factor the code to read:
var bar = chart.append("div").attr("class", "chart")
.selectAll('div')
.data(scope.data.sort().reverse()).enter().append("div");
bar
.transition().ease("elastic")
.style("width", function(d) { return (d[0]/sum)*attrs.chartWidth + "px"; })//This is where I base the width as a precentage from the sum and calculate it according to the chart-width attribute
.style("background-color",function(){i++;if (i<=colors.length-1){return colors[i-1]} else {return colors[(i-1)%colors.length]}}) })
bar.append('span')
.text(function(d) { return d[1] });
Demo
As a side note, while selecting the background-color, you do not need to maintain the index variable yourself, d3 passes the data d and the index i to the setter function you provide to .style:
.style("background-color",
function(d, i){ // <-- 'd' and 'i' are passed by d3
if (i<=colors.length-1)
{return colors[i-1]}
else {return colors[(i-1)%colors.length]}})
})
I was able to incorporate the Pan/Zoom example fine. The problem is I also want to be able to drag various groups of elements in addition to zooming. What I'm doing now, is disabling the "pan" part of the zoom/drag behavior, and having a separate drag event listener on the groups I want the user to be able to drag. The problem now is that when I drag a group, then go to zoom in, the group jumps. I was wondering what the best way to go about fixing this problem might be? I was thinking about changing all of the individual elements positions' directly instead of just translating the whole group, but that seems really brittle and inefficient to me.
EDIT: Here is my code. I've commented the pertinent parts, but left the whole directive for context
window.angular.module('ngff.directives.board', [])
.directive('board', ['socket',
function(socket) {
var linker = function(scope, elem, attrs) {
var nodeDragging = false;
scope.svg = d3.select(elem[0]).append("svg")
.attr('width', elem.parent().width())
.attr('height', elem.parent().height())
.attr('class', 'boardBackground')
.on('mouseup', function(){
nodeDragging = false;
})
.append('g')
.call(d3.behavior.zoom().on('zoom', zoom))
.append('g')
scope.svg.append("rect")
.attr("class", "overlay")
.attr("width", elem.parent().width())
.attr("height", elem.parent().height());
scope.$on('addFormation', function(event, formation) {
var group = formation.select('g');
scope.svg.node().appendChild(group.node())
var pieces = group.selectAll('circle')
.on('mousedown', function(){
//Here I set a flag so I can check for node dragging when my zoom function is called
nodeDragging = true;
})
.call(d3.behavior.drag().on('drag', move));
})
function move() {
var dragTarget = d3.select(this);
dragTarget
.attr('cx', function() {
return d3.event.dx + parseInt(dragTarget.attr('cx'))
})
.attr('cy', function() {
return d3.event.dy + parseInt(dragTarget.attr('cy'))
})
}
//******I return here if the user is dragging the node to keep all of the elements from being translated
function zoom() {
if(nodeDragging)return
console.log('zoom')
scope.svg
.attr("transform","translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
}
}
return {
restrict: 'E',
link: linker
}
}
])
I'm just starting out with D3.js. I've created a simple enough donut chart using this example. My problem is, if I have an array of objects as my data source - data points for ex. would be a1.foo or a1.bar - and I want to switch between them, how would i go about doing this? My current solution looks ugly and it can't be the proper way of doing it - code below.
//Call on window change event
//Based on some parameter, change the data for the document
//vary d.foo to d.bar and so on
var donut = d3.layout.pie().value(function(d){ return d.foo})
arcs = arcs.data(donut(data)); // update the data
Is there a way I can set the value accessor at run time other than defining a new pie function?
Generally to switch the data that is being displayed you would create a redraw() function that would then update the data for the chart. In the redraw you'll need to make sure to handle the three cases - what should be done when data elements are modified, what should be done when new data elements are added, and what should be done when data elements are removed.
It usually looks something like this (this example changes the data set through panning, but it doesn't really matter). See the full code at http://bl.ocks.org/1962173.
function redraw () {
var rects, labels
, minExtent = d3.time.day(brush.extent()[0])
, maxExtent = d3.time.day(brush.extent()[1])
, visItems = items.filter(function (d) { return d.start < maxExtent && d.end > minExtent});
...
// upate the item rects
rects = itemRects.selectAll('rect')
.data(visItems, function (d) { return d.id; }) // update the data
.attr('x', function(d) { return x1(d.start); })
.attr('width', function(d) { return x1(d.end) - x1(d.start); });
rects.enter().append('rect') // draw the new elements
.attr('x', function(d) { return x1(d.start); })
.attr('y', function(d) { return y1(d.lane) + .1 * y1(1) + 0.5; })
.attr('width', function(d) { return x1(d.end) - x1(d.start); })
.attr('height', function(d) { return .8 * y1(1); })
.attr('class', function(d) { return 'mainItem ' + d.class; });
rects.exit().remove(); // remove the old elements
}