I have a set of data in searchButton function that calls different numbers in an array like so: values[80,20,10] Then in my x variable I have this scale for d3js.
x = d3.scale.linear()
.domain([0, d3.max(values)])
.range([0, width]);
Now I have a function called grow and within it I grow a set of circles based on the values in searchbutton function.
this.grow = grow;
function grow(size) {
circle.attr("r",size / 5 + 10).transition().duration(2000);
}
when I set the grow function with size as the parameter it grows but transition is not working at all on it.
In order for a d3 transition to work, you need to animate a property starting from a default value to the desired value. What would I do is following:
this.grow = grow;
function grow(size) {
circle
.attr('r', 0)
.transition()
.ease('linear')
//.ease('cubic-in-out') // default
.duration(2000)
.attr('r', size / 5 + 10);
}
This will animate a circle from 0 radius to the desired radius. You may need to adjust your logic, maybe you don't want as default radius (before transition) a value of 0.
Could you make a fiddle or post your entire code? How are you calling the grow function?
Related
I would like to use the Collide force in D3 to prevent overlaps between nodes in a force layout, but my y-axis is time-based. I would like to only use the force on the nodes' x positions.
I have tried to combine the collide force with a forceY but if I increase the collide radius I can see that nodes get pushed off frame so the Y position is not preserved.
var simulation = d3.forceSimulation(data.nodes)
.force('links', d3.forceLink(data.links))
.force('x', d3.forceX(width/2))
.force('collision', d3.forceCollide().radius(5))
.force('y', d3.forceY( function(d) {
var date = moment(d.properties.date, "YYYY-MM-DD");
var timepos = y_timescale(date)
return timepos; }));
My hunch is that I could modify the source for forceCollide() and remove y but I am just using D3 with <script src="https://d3js.org/d3.v5.min.js"></script> and I'm not sure how to start making a custom version of the force.
Edit: I have added more context in response to the answer below:
- full code sample here
- screenshot here
Not quite enough code in the question to guarantee this is what you need, but making some assumptions:
Often when using a force layout you would allow the forces to calculate the positions and then reposition the node to a given [x,y] co-ordinate on tick e.g.
function ticked() {
nodeSelection.attr('cx', d => d.x).attr('cy', d => d.y);
}
Since you don't want the forces to affect the y co-ordinate just remove it from here i.e.
nodeSelection.attr('cx', d => d.x);
And set the y position on, say, enter:
nodeSelection = nodeSelection
.enter()
.append('circle')
.attr('class', 'node')
.attr('r', 2)
.attr('cy', d => {
// Set y position based on scale here
})
.merge(nodeSelection);
I'm trying to make a pie chart with d3.js that looks like this:
Note that the labels are placed along the edges of the pie chart. Initially, I am able to draw the pie charts and properly place the text nodes (the fiddle only displays one pie chart; assume, however, that they all have data that works and is appropriate, as this one does). However, when I go to adjust the data, I can't seem to .attr(translate, transform) them to the correct region along the edge of the pie chart (or do anything to them, for that matter):
changeFunctions[i] = function (data, i) {
path.data(pie(data))
.transition()
.duration(750)
.attrTween("d", arcTween);
text.each(function (d, num) {
d3.select(this)
.text(function (t) {
return t.data.name+". "+(100 * t.data.votes/totalVotes).toFixed(0) + "%";
})
/*
.attr("transform", function (d) {
//console.log("the d", d)
var c = arc.centroid(d),
x = c[0], y = c[1],
h = Math.sqrt(x * x + y * y);
return "translate(" + (x/h * 100) + ',' + (y/h * 100) + ")";
})*/
.attr("opacity", function (t) {
return t.data.votes == 0 ? 0 : 1;
});
})
}
I have omitted the general code to draw the pie chart; it's in the jsfiddle. Basically, I draw each of the pie charts in a for loop and store this function, changeFunctions[i], in a closure, so that I have access to variables like path and text.
The path.data part of this function works; the pie chart properly adjusts its wedges. The text.each part, however, does not.
How should I go about making the text nodes update both their values and locations?
fiddle
When updating the text elements, you also need to update the data that's bound to them, else nothing will happen. When you create the elements, you're binding the data to the g element that contains the arc segment and text. By then appending path and text, the data is "inherited" to those elements. You're exploiting this fact by referencing d when setting attributes for those elements.
Probably the best way to make it work is to use the same pattern on update. That is, instead of updating only the data bound to the path elements as you're doing at the moment, update the data for the g elements. Then you can .select() the descendant path and text elements, which will again inherit the new data to them. Then you can set the attributes in the usual manner.
This requires a few changes to your code. In particular, there should be a variable for the g element selection and not just for the paths to make things easier:
var g = svg.selectAll("g.arc")
.data(pie(data));
g.enter()
.append("g").attr("class", "arc");
var path = g.append("path");
The changeFunction code changes as follows:
var gnew = g.data(pie(data));
gnew.select("path")
.transition()
.duration(750)
.attrTween("d", arcTween);
Now, to update the text, you just need to select it and reset the attributes:
gnew.select("text")
.attr("transform", function(d) { ... });
Complete demo here.
I have a D3 bar chart with date/time on the X axis. This is working well now, but I'd like to add a smaller brushable chart below it and am having trouble due to some of the date manipulations I had to do to make the bars center over the X axis tick lines - see this post for details on that.
The brush does not seem to be calculating the X axis date range correctly.
Here is the x domain definition and brush function. JSFiddle for the full code.
main_x.domain([
d3.min(data.result, function(d) { return d.date.setDate(d.date.getDate() - 1); }),
d3.max(data.result, function(d) { return d.date.setDate(d.date.getDate() + 2); })
]);
function brushed() {
// I thought that re-defining the x domain without the date manipulations might work, but I
// was getting some odd results
//main_x.domain(d3.extent(data.result, function(d) { return d.date; }));
main_x.domain(brush.empty() ? main_x.domain() : brush.extent());
console.log(brush.extent());
bar.selectAll("rect")
.attr("width", function(d) { return main_width/len; })
.attr("x", function(d) { return main_x(d.date) - (main_width/len)/2; });
main.select(".x.axis").call(main_xAxis);
}
The problem is that you're using the same scale for the focus and the context chart. As soon as you change that, the range of the selection changes (same size, but different underlying scale). To fix, use two different scales.
I've done this here, introducing mini_x as the x scale for the context chart.
I am trying to create something similar to this
http://demo.joostkiens.com/het-parool-4g/
Where the plotted circles have outer lines throbbing outwards.
This is my demo so far.
http://jsfiddle.net/NYEaX/95/
I've plotted the circles with some dummy data. On top are red based circles. How do I invoke the animation and make it more vibrant per the alarm data .. eg. alarmLevel.
I am unsure as to how to create a looping animation with the radius bouncing off past the circumference and then fading out - having this variety based on the alarmLevel threshold
Would ideally need the transition to occur like this in a loop., http://jsfiddle.net/pnavarrc/udMUx/
var speedLineGroup = sampleSVG.append("g")
.attr("class", "speedlines");
speedLineGroup.selectAll("circle.dl-speed-static")
.data(dataset)
.enter().append("circle")
.style("stroke", "red")
.style("fill", "black")
.attr("r", function(d){
return d.value;
})
.attr("cx", function(d){
return d.xcoord;
})
.attr("cy", function(d){
return d.ycoord;
})
.attr("class", "dl-speed-static")
.attr("stroke-opacity", function (e) {
return 1;
//return var
})
.attr("fill-opacity", function (e) {
return 1;
//return var
})
.transition()
.ease("linear")
.duration(200)
.attr("r", function (e) {
return 1;
//return var
})
I've merged the ideas in the post. I've placed the ring creation in its own function and removed the time out. I've also started to try and hook into the alarm threshold per marker.
http://jsfiddle.net/NYEaX/102/
but the application still seems delayed/buggy - not very clear as in the prime example. How could this be improved further. Some of the alarm counts are low - but this method is causing the ring to throb too soon or flickery. Its almost like I need to invert the value to have a low alarm - create a slower response.
function makeRings() {
var datapoints = circleGroup.selectAll("circle");
var radius = 1;
function myTransition(circleData){
var transition = d3.select(this).transition();
speedLineGroup.append("circle")
.attr({"class": "ring",
"fill":"red",
"stroke":"red",
"cx": circleData.xcoord,
"cy": circleData.ycoord,
"r":radius,
"opacity": 0.4,
"fill-opacity":0.1
})
.transition()
.duration(function(){
return circleData.alarmLevel*100;
})
.attr("r", radius + 100 )
.attr("opacity", 0)
.remove();
transition.each('end', myTransition);
}
datapoints.each(myTransition);
}
This is the latest code..
makeRings()
var t = window.setInterval(makeRings, 10000);
function makeRings() {
var datapoints = mapSVG.selectAll("circle.location");
var radius = 1;
function myTransition(circleData){
console.log("circleData", circleData);
var transition = d3.select(this).transition();
speedLineGroup.append("circle")
.attr({"class": "ring",
"fill":"red",
"stroke":"red",
"cx": circleData.x * ratio,
"cy": circleData.y * ratio,
"r":radius,
"opacity": 0.4,
"fill-opacity":0.1
})
.transition()
.duration(function(){
return (circleData.redSum * 100);
})
.attr("r", radius + 30 )
.attr("opacity", 0)
.remove();
transition.each('end', myTransition);
}
datapoints.each(myTransition);
}
The example you linked to uses minified code, so it's a bit of a pain to figure out what they're doing. However, if you just watch the changes in the DOM inspector, you'll see that each ring is a new circle that gets added, grows in size and faces away, and then gets removed. The different points vary in how big the rings get before they fade away (and therefore in how many rings are visible at a time, since they all grow at the same speed).
The approach I would take to make this continue indefinitely is:
Use 'setInterval' to call a function on a regular basis (e.g., once or twice per second) that will create a new ring around each data circle.
Create the rings using an .each() call on your data circles, but add them to a different <g> element, and/or with different class names so there is no confusion between the rings and the data points.
Set the initial radius of the ring to be the same as the data point, but then immediately start a transition on it. Make the duration of the transition a function of the "intensity" data value for the associated data circle, and also make the final radius a function of that data value. Also transition the opacity to a value of 0.
Make the final line of the transition for the rings .remove() so that each ring removes itself after it has finished expanding.
Basic code:
window.setInterval(makeRings, 1000);
function makeRings() {
datapoints.each(function(circleData){
//datapoints is your d3 selection of circle elements
speedLineGroup.append("circle")
.attr({"class": "ring",
"fill":"red", //or use CSS to set fill and stroke styles
"stroke":"red",
"cx": circleData.xCoord,
"cy": circleData.yCoord,
//position according to this circle's position
"r":radius, //starting radius,
//set according to the radius used for data points
"opacity": 0.8, //starting opacity
"fill-opacity":0.5 //fill will always be half of the overall opacity
})
.transition()
.duration( intensityTimeScale(circleData.intensity) )
//Use an appropriate linear scale to set the time it takes for
//the circles to expand to their maximum radius.
//Note that you *don't* use function(d){}, since we're using the data
//passed to the .each function from the data point, not data
//attached to the ring
.attr("r", radius + intensityRadiusScale(circleData.intensity) )
//transition radius
//again, create an appropriate linear scale
.attr("opacity", 0) //transition opacity
.remove(); //remove when transition is complete
});
}
Because both the change in radius and the duration of the transition are linear functions of the intensity value, the change will have a constant speed for all the data points.
All you need to do to create looping transitions in d3 is to use the end callback on transitions. Create two functions, which each create a transition on your data, with one going from your start point to your end point, and the other going back, and have them call each other on completion, like so:
function myTransition(d){
var transition = d3.select(this).transition();
//Forward transition behavior goes here
//Probably create a new circle, expand all circles, fade out last circle
transition.each('end', myTransition); //This calls the backward transition
}
d3.select('myFlashingElement').each(myTransition);
This will encapsulate everything and keep looping at whatever the duration of your transition is. The next transition will always fire when the transition before it ends, so you don't have to worry about syncing anything.
I'm new to d3.js and still a beginner in javascript in general. I've got d3 correctly drawing a single SVG rectangle based on a single value in an array. When I increase the value of the number in the area via an input field and then call the reDraw function, the rectangle changes to the new size for just a second and then switches back to the initial size. I'm thinking I must have a scoping problem, but can't figure it out. Any ideas?
var dataset, h, reDraw, svg, w;
w = 300;
h = 400;
dataset = [1000];
// Create SVG element
svg = d3.select("#left").append("svg").attr("width", w).attr("height", h);
// set size and position of bar
svg.selectAll("rect").data(dataset).enter().append("rect").attr("x", 120).attr("y", function(d) {
return h - (d / 26) - 2;
}).attr("width", 60).attr("height", function(d) {
return d / 26;
}).attr("fill", "rgb(3, 100, 0)");
// set size and position of text on bar
svg.selectAll("text").data(dataset).enter().append("text").text(function(d) {
return "$" + d;
}).attr("text-anchor", "middle").attr("x", 150).attr("y", function(d) {
return h - (d / 26) + 14;
}).attr("font-family", "sans-serif").attr("font-size", "12px").attr("fill", "white");
// grab the value from the input, add to existing value of dataset
$("#submitbtn").click(function() {
localStorage.setItem("donationTotal", Number(localStorage.getItem("donationTotal")) + Number($("#donation").val()));
dataset.shift();
dataset.push(localStorage.getItem("donationTotal"));
return reDraw();
});
// redraw the rectangle to new size
reDraw = function() {
return svg.selectAll("rect").data(dataset).attr("y", function(d) {
return h - (d / 26) - 2;
}).attr("height", function(d) {
return d / 26;
});
};
You need to tell d3 how to match new data to existing data in the reDraw function. That is, you're selecting all rectangles in reDraw and then binding data to it without telling it the relation between the old and the new. So after the call to data(), the current selection (the one you're operating on) should be empty (not sure why something happens for you at all) while the enter() selection contains the new data and the exit() selection the old one.
You have several options to make this work. You could either operate on the enter() and exit() selections in your current setup, i.e. remove the old and add the new, or restructure your data such that you're able to match old and new. You could for example use an object with appropriate attributes instead of a single number. The latter has the advantage that you could add a transition from the old to the new size.
Have a look at this tutorial for some more information on data joins.
Edit:
Turns out that this was actually not the issue, see comments.