To create this DOM:
<g>
<rect></rect>
<circle></circle>
</g>
from .enter() selection, I tried:
someUpdate.enter()
.append('g')
.attr('class', 'my-group')
.append('rect')
.attr('class', 'my-rect')
// I'd like to get the .parent() here
.append('cicle')
.attr('class', 'my-circle')
This doesn't work since .append('rect') changes the selection to rect.
Breaking this to:
const update = someUpdate.enter()
.append('g')
.attr('class', 'my-group')
update
.append('rect')
.attr('class', 'my-rect')
update
.append('cicle')
.attr('class', 'my-circle')
works.
But, I wonder if there is a cleaner way?
There are no methods in D3 for traversing the DOM like e.g. jQuery's .parent(). Hence, the way you broke this down into separate statements will be the correct approach.
On the other hand, it is not completely impossible to do it the way you first suggested. Just yesterday I posted an answer to "D3.js - what is selection.call() returning?" explaining how selection.call() will return exactly the selection it was called upon to allow for method chaining. Keeping that in mind you could something like the following:
d3.select("svg").selectAll("g")
.data([1])
.enter().append('g')
.call((parent) => parent.append('rect')
.attr("fill", "red")
.attr("width", 100).attr("height", 100))
.call((parent) => parent.append('circle')
.attr("fill", "blue").attr("r", 50));
<script src="https://d3js.org/d3.v4.js"></script>
<svg></svg>
Both functions invoked by .call() will be passed the same selection of previously entered <g> elements, which happens to be the parent element in this case.
Although it is possible to do it this way, the solution has its drawbacks. First, it will look somewhat strange and awkward to any seasoned D3 developer, which might complicate matters if you want to share or discuss your code with others. Second, even though I named the parameter parent, which it is in this particular case, it is still not really an equivalent to jQuery's .parent() method. It will just pass in and return the very same selection be it a parent selection or something else.
Agree with the others that your second code snippet is the correct way to do what you want but I want to play to, how about:
d3.select("svg").selectAll("g")
.data([1])
.enter()
.append('g')
.each(function() {
var p = d3.select(this);
p.append('rect')
.attr("fill", "red")
.attr("width", 100).attr("height", 100);
p.append('circle')
.attr("fill", "blue").attr("r", 50);
});
<script src="https://d3js.org/d3.v4.js"></script>
<svg></svg>
#altocumulus said in his answer:
Although it is possible to do it this way, the solution has its drawbacks. First, it will look somewhat strange and awkward to any seasoned D3 developer.
Well, just as him, I have to say that OP's original code (appending the rectangle and the circle to the group separately) is the standard way, applied by the majority of D3 coders.
But, just for fun and to participate in this strange competition, here is another way to do it, using the less famous third argument:
d3.select("svg").selectAll("g")
.data([1])
.enter()
.append('g')
.attr("foo", function(d, i, p) {
d3.select(p[0]).append('rect')
.attr("fill", "red")
.attr("width", 100).attr("height", 100);
return null;
})
.attr("foo", function(d, i, p) {
d3.select(p[0]).append('circle')
.attr("fill", "blue").attr("r", 50);
return null;
});
<script src="https://d3js.org/d3.v4.js"></script>
<svg></svg>
I bet that this wins the "awkward code" competition.
Related
I'am working on a bubble-chart using d3. Now there should be an arrow as a graphical asset below the text elements. What I wan to achieve is a dynamic positioning of the arrow-group having a defined gap between itself and the last text:
I've already tried to position it with a percentage value:
arr.append("g").attr("class", "arrow").style('transform', 'translate(-1%, 5%)');
Which does not give the effect I want to.
I also tried to insert a dynamic value based on the radius of the circles which is simply not working and I don't know why:
arr.append("g")
.attr("class", "arrow")
.attr('transform', function(d, i) { return "translate(20," + d.r / 2 + ")");});
OR
arr.append("g")
.attr("class", "arrow")
.style('transform', (d, i) => 'translate(0, ${d.r/2})');
Please find my pen here.
Thank you very much in advance!
Ok.. solved it! For everyone who is interested or having the same trouble:
My last attempt was nearly correct but I was not able to transform via .style(...). I had to use .attr(...) like this:
arr.append("g")
.attr("class", "arrow")
.attr('transform', (d, i) => translate(0, ${d.r/2})');
I draw a line chart using D3 with some data from our database, I got some data for the entire year to calculate what would be our trendline (taking some values for 8am, 12m, 4pm and 9pm), I'm drawing this in the chart with path and values for each X (time).
Now my problem is the domain of the trendline is of course bigger than our current values (lets say its 2 pm and my trendline will always go to 9 pm). The closes I got was setting the trendline's domain to my current data domain, which returns this:
Test1
xTrend.domain(d3.extent(trendData, function (d) { return d.date; }));
How can I cut it so it doesn't go beyond the SVGs width? I tried setting the width attribute and it doesn't work, so my guess is it has something to do with the domain.
If I set the trendline's domain to its data, I get this:
Test2
xTrend.domain(d3.extent(data, function (d) { return d.date; }));
PS: While we are on this, if anyone can point me on how I could see if my line is above-below my trendline it would be great ;)
Update:
Thanks to #lhoworko
I added this
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", height);
And this to my line path
.attr("clip-path", "url(#clip)")
Take a look at d3 clip paths. It will make sure the line beyond the end of the chart isn't displayed.
Take a look here.
https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath
I have 2 charts on a page (A and B) and wish for some custom behavior to be added to chart B when a brush filter on chart A is performed.
I thought I could achieve this by doing something like;
charta.on('postRender', function(){
...
d3.selectAll('#chartb svg')
.data(nested, function(d){ return d.key})
.enter()
.append("g")
.attr("class", "aclass")... more code....
But this #chartb selector doesn't seem to work - when I inspect the DOM it has appended the <g> attributes to the <html> element and not the svg element I wanted to append to.
Is what I am trying to achieve possible?
If you are just adding stuff to the other chart, something like this should be possible. I don't think you will be able to select the generated items of the other chart and then apply a d3 join to it, because it is already joined.
I believe the problem with the code above is that d3.select is what you use to choose the context for a join, and d3.selectAll is what you use to actually make the data join. See
http://bost.ocks.org/mike/join/
So your code is trying to join to the chart and svg elements, which would have the effect you are describing. Instead you'll want to d3.select the svg and then d3.selectAll the elements you want to add - even though they don't exist yet! Yes, it's kind of a mind-bender; take a look at the above and the linked articles to get a better idea of it.
Note: there are dc convenience methods on the chart object which will execute the selects in the right context.
I got this working in the end by replacing the .enter() with repeated calls to datum() instead. A bit of a hack, but it works; If anyone can suggest a more d3ish way of acheiving this, I would be very grateful.
var svg = chart.svg();
nested.forEach(function(withValues) {
_(withValues.values).filter(function(d){return d.value < threshold}).forEach(function(timesMatchingThreshold){
svg.datum(timesMatchingThreshold)
.append("rect")
.style("opacity", 0.6)
.attr("transform", "translate(" + 30 + ", " + (-30) + ")")
.attr("class", "belowThreshold")
.attr("x", function(d) {return x(d.date)})
.attr("y", function(d) {return 200 - y(d.value)})
.attr("width", 3)
.attr("height", function(d) {return y(d.value)});
});
I want to make a bar chart in d3.js that has both positive and negative bars for each item or row, like this:
it's somewhat like the google finance "Sector Summary" chart (http://google.com/finance)
Can anyone point me to a d3.js example of this kind of chart? I am aware of the "bar chart with negative values" example here: http://bl.ocks.org/mbostock/2368837
If there is a relatively easy way to explain how to modify that example to accomplish what I want, that could work too.
Thanks!
Here is what I could do:
The basis is to have two values per item. To simplify things we can say that all values have to be positive, the first one will be displayed on the right, the second one on the left.
From the example you provided, we will just plot the second value we add:
data = [
{name: "A",value: 1,value2: 2},
...
]
We will also fix the domain (you can write a function to do it nicely afterwards):
x.domain([-10,10])
Finally, we will draw the second bars (on the left):
svg.selectAll(".bar2")
.data(data)
.enter().append("rect")
.attr("class", "bar2")
.attr("x", function (d) {
return x(Math.min(0, -d.value2));
})
.attr("y", function (d) {
return y(d.name);
})
.attr("width", function (d) {
return Math.abs(x(-d.value2) - x(0));
})
.attr("height", y.rangeBand());
This code is just copy paste from the example where I replaced d.value by -d.value1 and .bar by .bar2 for the class.
You will also have to modify the x-axis format for having "75, 50, 25, 0, 25, 50, 75".
jsFiddle: http://jsfiddle.net/chrisJamesC/vgZ6E/
I try to use d3.scale.category.20b() to generate a color scale, problem is whatever number of the list I ask for, it always returns first element of the list.
var color = d3.scale.category20b();
console.log(color(X));
OR
console.log(d3.scale.category20b()(X);
No matter what X is, it always logs #393b79 which is the first elements, according to the d3 API
This can happen because categorical scales in d3 append to the domain as new data comes in. If every enter() creates a new categorical scale, the domain of the categorical scale remains the same.
As an example, please consider this jFiddle: http://jsfiddle.net/seldomawake/MV55j/1/
Here, we see that as data enters, we append to a categorical scale in the global namespace, $colorScale (specific code below).
function redraw(theData) {
var localColorScale = d3.scale.category20c(); //< NOT USED HERE
var svg = d3.select("svg");
var circles = svg.selectAll("circle")
.data(theData).enter().append("circle")
var circleAttributes = circles.attr("cx", getRandomInt(50, 450))
.attr("cy", getRandomInt(50, 450))
.attr("r", function (d) { return d.value; })
.style("fill", function () { return $colorScale(getRandomInt(0, 19)); });
}
However, if we were to replace return $colorScale(getRandomInt(0, 19)) with return localColorScale(getRandomInt(0, 19)), we would no longer have the data append to the range of the categorical scale, and which would result in a single-color output.
Edit: fixing URL to jsfiddle.
At first I thought this would have been a bug with D3.js so created this jsfiddle which works fine.
var data = d3.range(0,20);
var color = d3.scale.category20b();
d3.select('.target').selectAll('div')
.data(data)
.enter()
.append('div')
.text(function(d){return color(d);})
.attr('style', function(d){return "background-color:"+ color(d) + ";" ;})
It had been raised by others about version of D3 you are using. This looks unlikely to be the cause of your issue as the code in question has hardly been touched. If the code has not been touched much and others have no issue it raises the question of browser compatibly. I sent my jsfiddle to browsershots and did not see any browser output a single block of color instead of the expected pretty color stripes.
After all this it seams there is not enough information to properly answer your problem. I suggest you have a look to see if X is really changing by making a small change to the code console.log({'color':color(X), 'x':X}).
Which version of D3 are you using? I wrote a jsFiddle (D3 3.0.4), the colors are shown normally:
var color = d3.scale.category20b();
var svg = d3.select('#chart').append('svg')
.attr('width', 200)
.attr('height', 100);
svg.append('rect')
.attr('width', 100)
.attr('height', 100)
.attr('fill', color(0));
svg.append('rect')
.attr('x', 100)
.attr('width', 100)
.attr('height', 100)
.attr('fill', color(1));
The result is: