Setting up my D3 project, which will have a number of shapes on the screen being animated (Org class). I am trying to maintain an object-oriented paradigm, and take advantage of the D3 animations (https://jrue.github.io/coding/2014/lesson07/).
Consider the code below:
function test() {
class Org {
constructor(_width, _height) {
this.width = _width;
this.height = _height;
}
}
var orgs = [];
var canvas = d3.select('body')
.append('svg')
.attr('width', screen.width)
.attr('height', screen.height);
for (var x = 0; x < 100; x++) {
var circle = new Org(Math.random()*screen.width, Math.random()*screen.height);
orgs.push(circle);
canvas.append('circle')
.attr('cx', circle.width)
.attr('cy', circle.height)
.attr('r', 5)
.attr('fill', 'pink');
}
for (var b = 0; b < orgs.length; b++) {
circle.transition().attr('cx', 0); //DOES NOT WORK
}
}
Obviously, the commented line throws an error because transition() belongs to D3, not my class. How can I select these objects and animate them?
Your code is not very D3-ish, which makes it cumbersome to achieve your goal. Keep in mind, that D3 by its nature is about data-driven documents and, thus, data binding is at its very core. It is essential to understand this concept to get the most out of this library. When refactoring your code accordingly, the solution becomes almost obvious.
That said, it always looks suspicious using for-loops when dealing with D3. Only rarely is there a need to put those loops to use as this is taken care of by D3's internal workings. Without breaking your OO-approach you can bind your orgs array to a selection and take advantage of D3 doing its magic:
var circles = canvas.selectAll("circle")
.data(orgs)
.enter().append('circle')
.attr('cx', d => d.width )
.attr('cy', d => d.height )
.attr('r', 5)
.attr('fill', 'pink');
This will append circles to your selection corresponding to all Org instances in your orgs array which was bound to the selection using .data(orgs). The above statement also keeps a reference to the selection containing all newly appended circles in the circles variable, which you can use for later manipulation.
This reference comes in handy, when doing the transition:
circles
.transition()
.attr('cx', 0);
Have a look at the following snippet which is equivalent to your approach, but does it the D3 way.
class Org {
constructor(_width, _height) {
this.width = _width;
this.height = _height;
}
}
var orgs = d3.range(100).map(function() {
return new Org(Math.random() * screen.width, Math.random() * screen.height);
});
var canvas = d3.select('body')
.append('svg')
.attr('width', screen.width)
.attr('height', screen.height);
var circles = canvas.selectAll("circle")
.data(orgs)
.enter().append('circle')
.attr('cx', d => d.width )
.attr('cy', d => d.height )
.attr('r', 5)
.attr('fill', 'pink');
circles
.transition()
.attr('cx', 0);
<script src="https://d3js.org/d3.v4.js"></script>
You might want to have a look at some tutorials about this concept for a more in-depth introduction:
Three Little Circles
Let’s Make a Bar Chart, Parts I, II & III
Thinking with Joins
How Selections Work (advanced)
Perhaps give them an id and select them with d3?
First give the circles an id:
for (var x = 0; x < 100; x++) {
var circle = new Org(Math.random()*screen.width, Math.random()*screen.height);
orgs.push(circle);
canvas.append('circle')
.attr('id', "myCircle_" + x)
.attr('cx', circle.width)
.attr('cy', circle.height)
.attr('r', 5)
.attr('fill', 'pink');
}
Then select them by id:
for (var b = 0; b < orgs.length; b++) {
d3.select('#myCircle_'+b).transition().attr('cx', 0);
}
Related
I am trying to create a scale which contains the svg elements as range. E.g. a linear continous scale with a range containing circles with radius ranging from 0 to 100 and which can also be queried for those circles. The purpose of this is to pass the scale to a legend maker which uses the nice properties of d3 scales to construct a legend.
I am able to create circles that I see in the browser's page inspector but they are not displayed where it matters. Why is that? If append() accepts a dom element why isn't it displayed?
let canvas = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 200);
let domFun = function(scale, range) {
scale.range(range);
return function(d) {
let template = document.createElement("template");
template.innerHTML = scale(d);
let dom = template.content.childNodes[0];
return dom;
}
};
let cScale = domFun(d3.scaleLinear(), ["<circle r = 0>", "<circle r = 100>"]);
let data = [0.2, 0.3, 0.6, 1];
canvas.selectAll("circle").data(data).enter()
.append(d => cScale(d))
.attr("cy", 100)
.attr("cx", (d, i) => (i + 0.5) * 200)
.attr("fill", "red");
Grateful for any help/input here.
While the other answer is the typical D3 approach to this sort of task, you can make this work with one modification.
You need to specify an SVG namespace when creating the elements. This might look like:
return function(d) {
let template = document.createElementNS(d3.namespaces.svg, "svg")
template.innerHTML = scale(d);
let dom = template.firstChild;
return dom;
}
Altogether that looks something like:
let svg = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 200);
let domFun = function(scale, range) {
scale.range(range);
return function(d) {
let template = document.createElementNS(d3.namespaces.svg, "svg")
template.innerHTML = scale(d);
let dom = template.firstChild;
return dom;
}
};
let cScale = domFun(d3.scaleLinear(), ["<circle r = 0>", "<circle r = 100>"]);
let data = [0.2, 0.3, 0.6, 1];
svg.selectAll("circle").data(data).enter()
.append(d => cScale(d))
.attr("cy", 100)
.attr("cx", (d, i) => (i + 0.5) * 200)
.attr("fill", "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Why use the scale to construct some tags and use a function to extract it from inside another tag?
Just use the scale to calculate the radius and the circles are visible
let canvas = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 200);
let cScale = d3.scaleLinear().range([0, 100]);
let data = [0.2, 0.3, 0.6, 1];
canvas.selectAll("circle").data(data).enter()
.append("circle")
.attr("r", d => cScale(d))
.attr("cy", 100)
.attr("cx", (d, i) => (i + 0.5) * 200)
.attr("fill", "red");
Edit
Instead of using the string interpolator and constructing a DOM element it can be done with the Object interpolator. This allows you to also interpolate colors.
let canvas = d3.select("body").append("svg")
.attr("width", 800)
.attr("height", 200);
let icScale = d3.scaleLinear().range([{r:10, fill:"red", cx:100}, {r:100, fill:"yellow", cx:700, cy:100, shape:"circle"}]);
let data = icScale.ticks(5);
let shape = icScale(data[0]).shape;
canvas.selectAll(shape).data(data).enter()
.append(shape)
.attrs(d => icScale(d));
you need to add
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
this to your HTML page to use the .attrs().
Only the second object needs to have the shape attribute, and also because here cy does not change, it only needs to be in the second object.
If you need to keep the object returned by the scale be aware that you have to make a copy.
This one has got to be easy but I can't for the life of me figure out why it's not working.
I've got some D3 code to plot some circles. I've nested arrays of six numbers inside a single variable (called 'dataset'). I'm trying to access those values to use a a y-value for the circle.
var width = 600;
var height = 400;
var dataset = [[16.58, 17.90, 17.11, 17.37, 13.68, 13.95], [20.26,1 3.40, 18.63, 19.28, 20,92, 18.95], [16.32, 23.16, 21.05, 28.16, 23.68, 23.42], [31.32, 30.80, 29.37, 28.16, 32.37, 27.63], [41.32, 39.74, 29.37, 35.00, 35.53, 30.00], [25.83, 38.27, 43.33, 45.83, 44.17, 41.25]];
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d, i) {
return i * 100 + 50;
})
.attr("cy", function(d, i) {
for (var j = 0; j<6; j++){
//console.log(i,j);
return d[i][j]; //THIS IS WHERE I'M FAILING
}
})
.attr("r", 15);
So the x values are just 100 px intervals (I basically want each set in its own column). The y value of each circle should then be the j'th term in the i'th array. So why doesn't d[i][j] return that?
I've got a console.log statement commented out. If I un-commennt that everything logs just as I would expect, but the double bracket notation is clearly not accessing the numbers. If I go straight to the console in the browser and type dataset[0][1], it returns '17.90', so why doesn't it work in this implementation?
So confused.
Thanks
I am trying get a few html elements to follow each other along a SVG path. I would like them to stay the same distance apart as they go around the path. I would also like the SVG image to scale to the container that holds it.
I have created a codepen that demonstrates what I have so far:
http://codepen.io/mikes000/pen/GIJab
The problem I am having is that when the elements move along the X axis they seem to get further apart than they do on the Y axis.
Is there a way to make them stay the same distance as they travel along the line?
Thanks!
Update**
After some further fiddling I have discovered that the distance variation seems to be caused by the aspect ratio of the SVG viewbox being increased for X greater than it is for Y. When it is stretched along the X axis 1px down the line may become 3px on the screen.
The position of the red squares is being set by moving them in front and behind by half the width of the black box. When traveling along the line if the viewbox aspect ratio is changed the distance between each point on the line increase or decreases based off of this.
I have tried creating a similar SVG with the exact viewbox of the size of the container div and the red dots are exactly on the ends of the black box all the way down the line. This doesn't solve problem because I would like the SVG with the line to scale to any size container it is placed inside.
I think if there is a way to calculate how many pixels the size of the black box is in relation to how many pixels down the line it covers the red dots would line up exactly.
Any ideas how to accomplish this or any ideas on a better way to approach this problem?
Take a look at http://jsfiddle.net/4LzK4/
var svg = d3.select("#line").append("svg:svg").attr("width", "100%").attr("height", "100%");
var data = d3.range(50).map(function(){return Math.random()*10})
var x = d3.scale.linear().domain([0, 10]).range([0, 700]);
var y = d3.scale.linear().domain([0, 10]).range([10, 290]);
var line = d3.svg.line()
.interpolate("cardinal")
.x(function(d,i) {return x(i);})
.y(function(d) {return y(d);})
var path = svg.append("svg:path").attr("d", line(data));
var circle =
svg.append("circle")
.attr("cx", 100)
.attr("cy", 350)
.attr("r", 3)
.attr("fill", "red");
var circleBehind =
svg.append("circle")
.attr("cx", 50)
.attr("cy", 300)
.attr("r", 3)
.attr("fill", "blue");
var circleAhead =
svg.append("circle")
.attr("cx", 125)
.attr("cy", 375)
.attr("r", 3)
.attr("fill", "green");
var pathEl = path.node();
var pathLength = pathEl.getTotalLength();
var BBox = pathEl.getBBox();
var scale = pathLength/BBox.width;
var offsetLeft = document.getElementById("line").offsetLeft;
var randomizeButton = d3.select("button");
svg.on("mousemove", function() {
var x = d3.event.pageX - offsetLeft;
var beginning = x, end = pathLength, target;
while (true) {
target = Math.floor((beginning + end) / 2);
pos = pathEl.getPointAtLength(target);
if ((target === end || target === beginning) && pos.x !== x) {
break;
}
if (pos.x > x) end = target;
else if (pos.x < x) beginning = target;
else break; //position found
}
circle
.attr("opacity", 1)
.attr("cx", x)
.attr("cy", pos.y);
posBehind = pathEl.getPointAtLength(target-10);
circleBehind
.attr("opacity", 1)
.attr("cx", posBehind.x)
.attr("cy", posBehind.y);
posAhead = pathEl.getPointAtLength(target+10);
circleAhead
.attr("opacity", 1)
.attr("cx", posAhead.x)
.attr("cy", posAhead.y);
});
randomizeButton.on("click", function(){
data = d3.range(50).map(function(){return Math.random()*10});
circle.attr("opacity", 0)
path
.transition()
.duration(300)
.attr("d", line(data));
});
Instead of calculating the positions of the circles behind and ahead on your own, use getPointAtLength relative to the centre of object that has to stay in the middle.
Inspired by: http://bl.ocks.org/duopixel/3824661
I am attempting to plot a simple dataset consisting of an array of javascript objects. Here is the array in JSON format.
[{"key":"FITC","count":24},{"key":"PERCP","count":16},{"key":"PERCP-CY5.5","count":16},{"key":"APC-H7","count":1},{"key":"APC","count":23},{"key":"APC-CY7","count":15},{"key":"ALEXA700","count":4},{"key":"E660","count":1},{"key":"ALEXA647","count":17},{"key":"PE-CY5","count":4},{"key":"PE","count":38},{"key":"PE-CY7","count":18}]
Each object simply contains a String: "key", and a Integer: "count".
Now, I am plotting these in D3 as follows.
function key(d) {
return d.key;
}
function count(d) {
return parseInt(d.count);
}
var w = 1000,
h = 300,
//x = d3.scale.ordinal()
//.domain([count(lookup)]).rangePoints([0,w],1);
//y = d3.scale.ordinal()
//.domain(count(lookup)).rangePoints([0,h],2);
var svg = d3.select(".chart").append("svg")
.attr("width", w)
.attr("height", h);
var abs = svg.selectAll(".little")
.data(lookup)
.enter().append("circle")
.attr("cx", function(d,i){return ((i + 0.5)/lookup.length) * w;})
.attr("cy", h/2).attr("r", function(d){ return d.count * 1.5})
Here is what this looks like thus far.
What I am concerned about is how I am mapping my "cx" coordinates. Shouldn't the x() scaling function take care of this automatically, as opposed to scaling as I currently handle it? I've also tried .attr("cx", function(d,i){return x(i)}).
What I eventually want to do is label these circles with their appropriate "keys". Any help would be much appreciated.
Update:
I should mention that the following worked fine when I was dealing with an array of only the counts, as opposed to an array of objects:
x = d3.scale.ordinal().domain(nums).rangePoints([0, w], 1),
y = d3.scale.ordinal().domain(nums).rangePoints([0, h], 2);
Your code is doing what you want...I just added the text part. Here is the FIDDLE.
var txt = svg.selectAll(".txt")
.data(lookup)
.enter().append("text")
.attr("x", function (d, i) {
return ((i + 0.5) / lookup.length) * w;
})
.attr("y", h / 2)
.text(function(d) {return d.key;});
I commented out the scales, they were not being used...as already noted by you.
I am creating a rectangle using d3.js, inside that rectangle i am creating 10 smaller rectangles`.
i want to replicate whole thing into another svg element on mouse click.
jsfiddle link of the code : http://jsfiddle.net/nikunj2512/XK585/
Here is the code:
var svgContainer = d3.select("body").append("svg")
.attr("width", 200)
.attr("height", 200);
//Draw the Rectangle
var rectangle = svgContainer.append("rect")
.attr("x", 10)
.attr("y", 10)
.attr("fill", "red")
.attr("width", 200)
.attr("height", 200);
var bigRectContainer = d3.select('#bigRectContainer').append('svg')
.attr('width', 200)
.attr('height', 200);
var dim = 20;
var x = 0;
var y = 0;
for (i = 1; i < 11; i++) {
x = 10 + (i-1)*dim;
//alert(x);
y = 10;
svgContainer.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", 20)
.attr("height", 20)
.style("fill", "black");
}
var bigRectContainer = svgContainer.append("g");
svgContainer.selectAll("rect").on("click", function () {
var littleRect = d3.select(this);
console.log(littleRect)
var bigRect = bigRectContainer.selectAll("rect")
.data(littleRect)
.enter()
.append("rect");
});
Please tell me where i made the mistake...
I'm not entirely certain what you're trying to do with the code you've posted, but I thought that duplicating an entire SVG node was interesting. It turns out it's quite easy to do with selection#html - this doesn't work on the SVG node, but it does work on its container, so it's easy to clone the whole node:
function addAnother() {
var content = d3.select(this.parentNode).html();
var div = d3.select('body').append('div')
.html(content);
div.selectAll('svg').on('click', addAnother);
}
svg.on('click', addAnother);
See working fiddle here. Note that this only works if the SVG node is the only child of its parent - otherwise, you might need to wrap it somehow.
D3 doesn't provide cloning functionality, probably because of the native cloneNode method that already exists on DOM elements, including SVG nodes.
This method includes a boolean parameter to deep copy (i.e. copy all descendants) instead of just cloning the node it is called on. You would probably want to do something like bigRectContainer.node().cloneNode(true) to copy the entire DOM branch of rectangles.