d3 node sub structure/styling based on data attribute - javascript

I'd like to create multiple functions which "style" a rectangle differently based on the data (i.e. call different .attr() or .style() functions or even appending other elements based on the data).
I managed to use .call(myDecorator) to dispatch the styling to a separate function, but here I am a little struck. In rectDecorator, I wish to call different sub-selections based on d.type. At the moment (.style('fill'...).style(...).style(...)) shows just one of those possible ways to decorate the rectangle, but ideally, I'd like to "branch" to different chained commands based on d.type. Any idea how I could accomplish that?
function rectDecorator(selection) {
// call different selection.xyz functions to style rect differently based on `d.type`
return selection
.style('fill', function(d) {
return d.color;
})
.style('fill-opacity', 1)
.style('stroke-width', 0);
}
d3.select('svg')
.selectAll('g')
.data(data)
.join(
enter => enter
.append('g')
.append('rect')
.attr('x', ...)
.attr('y', ...)
.attr('height', 20)
.attr('width', 40)
.call(rectDecorator)
update => update(),
exit => exit.remove()

There are multiple possible solutions to this, although, at some point, you will have to iterate through the selection to check each element's individual type to find the specific decorator. You could set up a Map whereby mapping the types to their corresponding decorators.
const decorators = new Map([
["type1", selection => selection.attr("attr1", "...")],
["type2", selection => selection.attr("attr2", "...")]
]);
Afterwards, you can easily loop through the selection using .each() and get the suitable decorator from the map based on the datum's type property:
.each(function(d) {
d3.select(this).call(decorators.get(d.type));
})
For a working demo have a look at the following snippet:
const decorators = new Map([
["red", selection => selection.attr("fill", "red")],
["green", selection => selection.attr("fill", "none").attr("stroke", "green")]
]);
const data = [{ type: "green" }, { type: "red" }];
d3.select('body').append("svg")
.selectAll('g')
.data(data)
.join(
enter => enter
.append('g')
.append('rect')
.attr('x', (_, i) => i * 50 + 50)
.attr('y', 50)
.attr('height', 20)
.attr('width', 40)
.each(function(d) {
d3.select(this).call(decorators.get(d.type));
})
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.15.0/d3.js"></script>

Related

Angular Typscript Argument of type 'string' is not assignable to parameter of type 'Element'

Typescript in angular for d3js is getting 3 errors for element. I was creating a mouseover for it to display the tag and value data that corresponds with the data for the bar graph. I have tried declaring the strings. I even added an ("noImplicitAny": false) to the tsconfig.js file. I'm trying to figure out where the issue is at. The code for my question is here:
// object.enteries: creates arrays of an element
svg.selectAll("bars")
.data(sortedata)
.enter()
.append("rect")
.attr('x', function(d):any {return xAxis(d[0])})
.attr('y', function(d){return yAxis(Number(d[1]))} )
.attr('width', xAxis.bandwidth())
.attr("height", (d) => this.height - yAxis(Number(d[1])))
.attr("fill", "#d04a35")
.on('mouseover', mouseover)
const tip = d3.select(".tooltip")
function mouseover(event,d){
tip
style('left', `${event.clientX + 15}px`)
style('top' , `${event.clientX + 15}px`)
style('opacity', 0.88)
tip.select("h3").html("tag:" +d[0]),
tip.select("h4").html("value:" +d[1]);
}
Here is also a visualization of the errors just incase.
I believe you need a . before the style keyword.
tip
.style('left', `${event.clientX + 15}px`)
.style('top' , `${event.clientX + 15}px`)
.style('opacity', 0.88)
However, if you want to set several syles at once, the d3 docs give the example
If you want to set several style properties at once, use an object literal. For example:
selection.style({stroke: "black", "stroke-width": "2px"});
Link to docs

Graphs update only when I write enter() at the end

I tried to toggle the data using
d3.interval and render the data as bar graphs on my svg.
As below.
interval--
d3.interval(function(){update(data);
flag =! flag;
},1000)
rendering--
function update(input){
var value = flag ? 'revenue':'profit'
x.domain(input.map(function(d){return d.month}));
y.domain([0,d3.max(input, function(d){return d[value]})])
var xAxisCall = d3.axisBottom(x);
var yAxisCall = d3.axisLeft(y).tickFormat(function(d){return '$'+d})
xAxisGroup.call(xAxisCall.tickSizeOuter(0))
yAxisGroup.call(yAxisCall.tickSizeOuter(0))
var rects = svg.selectAll('rect').data(data)
rects.exit().remove()
rects.enter().append('rect').attr('y', (d)=>{return height-y.range([0,height])(d[value])})
.attr('x', (d)=>{ return 50+x(d.month)})
.attr('height',(d)=>{return y(d[value])})
.attr('width',x.bandwidth())
.attr('fill','grey')
}
However,
it doesn't toggle the graph depending on the data fed into the rectangles.
It only worked when I put .append() at the end of rectangle rendering like below.
rects.attr('y', (d)=>{return height-y.range([0,height])(d[value])})
.attr('x', (d)=>{ return 50+x(d.month)})
.attr('height',(d)=>{return y(d[value])})
.attr('width',x.bandwidth())
.attr('fill','grey')
.enter().append('rect')
Why is it that I need to add enter and append at the end?
The reason that I don't get this is
Even if I put the .enter().append('rect') at the beginning,
the properties of the rect can be redefined and updated.
However, the properties of the rectangles weren't updated once I put the enter() argument beforehand.
I tested out by adding 'console.log' like below.
rects.enter().append('rect').attr('fill',function(){return flag? "rgba(100,0,0,0.2)":'rgba(0,100,0,0.2)'}).attr('y', (d)=>{return height-y.range([0,height])(d[value])})
.attr('x', (d)=>{ return 50+x(d.month)})
.attr('height',(d)=>{console.log(d[value])
;return y(d[value])})
.attr('width',x.bandwidth())
It doesn't log anything, it logs only when I put enter at the end.
Why all the arguments don't run?
In your first case you have an enter selection but nothing changing your update selection. In your second case you're changing your update selection while doing nothing to your enter selection.
Write your update and enter selections clearly and separately. This normally leads to duplicated code... if you don't like those duplications, use merge():
var rects = svg.selectAll('rect').data(data)
rects.exit().remove()
rects = rects.enter()
.append('rect')
.attr('fill', 'grey')
.merge(rects)
.attr('y', (d) => {
return height - y.range([0, height])(d[value])
})
.attr('x', (d) => {
return 50 + x(d.month)
})
.attr('height', (d) => {
return y(d[value])
})
.attr('width', x.bandwidth())
D3 uses the data join, which doens't act on elements directly, but describes their transformations. When you do:
var rects = svg.selectAll('rect').data(data)
you're not selecting all the rectangles, but only those in the "update" selection. Here's a more detailled explaination by the author of D3.
Nowadays, it's recommended to use the selection.join() method which makes selection-based code clearer:
svg.selectAll('rect')
.data(data)
.join(
enter => enter.append('rect')
.attr('fill', 'grey'),
update => update
.attr('x', (d) => 50 + x(d.month))
.attr('y', (d) => height-y.range([0,height])(d[value]))
.attr('width', x.bandwidth())
.attr('height', (d) => y(d[value])),
exit => exit.remove()
)

D3 - forEach is not a function when upgrading from v3 to v4

I am trying to upgrade this stackable bar chart to v4.
Everything works except for one thing.
When I filter one category the bars don't drop to the start of the x-axis. I get an error which says:
state.selectAll(...).forEach is not a function
I've tried multiple things but I can't figure this one out.
This is the broken code:
function plotSingle(d) {
class_keep = d.id.split("id").pop();
idx = legendClassArray.indexOf(class_keep);
//erase all but selected bars by setting opacity to 0
d3.selectAll(".bars:not(.class" + class_keep + ")")
.transition()
.duration(1000)
.attr("width", 0) // use because svg has no zindex to hide bars so can't select visible bar underneath
.style("opacity", 0);
//lower the bars to start on x-axis
state.selectAll("rect").forEach(function(d, i) {
//get height and y posn of base bar and selected bar
h_keep = d3.select(d[idx]).attr("height");
y_keep = d3.select(d[idx]).attr("y");
h_base = d3.select(d[0]).attr("height");
y_base = d3.select(d[0]).attr("y");
h_shift = h_keep - h_base;
y_new = y_base - h_shift;
//reposition selected bars
d3.select(d[idx])
.transition()
.ease("bounce")
.duration(1000)
.delay(750)
.attr("y", y_new);
})
}
I find it strange that this works flawlessly in D3 v3, why wouldn't this work in v4?
In d3 v3 selectAll returned an array, in d3 v4 it returns an object.
From the v3 notes:
Selections are arrays of elements—literally (maybe not literally...).
D3 binds additional methods to the array so that you can apply
operators to the selected elements, such as setting an attribute on
all the selected elements.
Where as changes in v4 include:
Selections no longer subclass Array using prototype chain injection;
they are now plain objects, improving performance. The internal fields
(selection._groups, selection._parents) are private; please use the
documented public API to manipulate selections. The new
selection.nodes method generates an array of all nodes in a selection.
If you want to access each node in v4 try:
selection.nodes().forEach( function(d,i) { ... })
But, this is just the node, to get the data you would need to select each node:
var data = [0,1,2];
var svg = d3.select("body").append("svg")
.attr("width",500)
.attr("height",200)
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d,i) { return i * 20 + 50 })
.attr("cy", 50)
.attr("r", 4);
circles.nodes().forEach(function(d,i) {
console.log(d3.select(d).data());
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
But, if you need the data or to modify the selection properties, it could be easier to use selection.each(). d3.each iterates through each element of a d3 selection itself, and allows you to invoke a function for each element in a selection (see API docs here):
var data = [0,1,2];
var svg = d3.select("body").append("svg")
.attr("width",500)
.attr("height",200)
var circles = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d,i) { return i * 20 + 50 })
.attr("cy", 50)
.attr("r", 4);
circles.each( function() {
console.log(d3.select(this).data());
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
In v3 of this bar chart, in the forEach loop
`states.selectAll("rect").forEach(function(d,i) {`
d is an array of nodes (the rectangles in each .g).
But, in v4 d3 selections aren't arrays, you can't use a forEach loop in the same way. But you can still get the nodes in it without much modification using selection.nodes() and than get the childNodes to replicate the array in the v3 version:
state.nodes().forEach(function(d, i) {
var nodes = d.childNodes;
Here we go through each element/node in state and get the child rects, returned as an array. Here's an updated fiddle.

How to update the scattergraph?

I have this table and chart with scattergraph:
https://jsfiddle.net/horacebury/bygscx8b/6/
And I'm trying to update the positions of the scatter dots when the values in the second table column change.
Based on this SO I thought I could just use a single line (as I'm not changing the number of points, just their positions):
https://stackoverflow.com/a/16071155/71376
However, this code:
svg.selectAll("circle")
.data(data)
.transition()
.duration(1000)
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
});
Is giving me this error:
Uncaught TypeError: svg.selectAll(...).data is not a function
The primary issue is that:
svg.selectAll("circle") is not a typical selection as you have redefined svg to be a transition rather than a generic selection:
var svg = d3.select("#chart").transition();
Any selection using this svg variable will return a transition (from the API documentation), for example with transition.selectAll():
For each selected element, selects all descendant elements that match
the specified selector string, if any, and returns a transition on the
resulting selection.
For transitions, the .data method is not available.
If you use d3.selectAll('circle') you will have more success. Alternatively, you could drop the .transition() when you define svg and apply it only to individual elements:
var svg = d3.select('#chart');
svg.select(".line").transition()
.duration(1000).attr("d", valueline(data));
...
Here is an updated fiddle taking the latter approach.
Also, for your update transition you might want to change scale and values you are using to get your new x,y values (to match your variable names):
//Update all circles
svg.selectAll("circle")
.data(data)
.transition()
.duration(1000)
.attr("cx", function(d) {
return x(d.date);
})
.attr("cy", function(d) {
return y(d.close);
});
}

Restart D3 bar chart animation

I am working on a widget that shows several D3 bar charts with different values, one after the other, in a sliding carousel.
When the page loads, the bar chart animate as it should, but when the page goes on to the next chart - whether it be on click or by itself - I would like it to restart the animation again each time.
I have tried calling animateChart() in the console but this doesn't work.
I am looking for a function that I can call from the console or from another function, like animateChart(), that will reload the D3 bar chart animation.
Here is a link to my widget:
http://jsfiddle.net/alocdk/oa5tg1qu/1/
I've found where you could enhance your animateChart function.
In fact you were modifying only data that were enterring your graph.
By calling :
d3.select(svg)
.selectAll("rect")
.data(data)
.enter().append("rect")
[...]
Everything following this, will only apply on the new data.
You may want to read these to understand the pattern to follow with data update in D3.
General Update Pattern, I
General Update Pattern, II
General Update Pattern, III
Here is my shot now http://jsfiddle.net/uknynmqa/1/
I've removed the loop you were doing on all your svg, because I assumed you wanted to only animate the current one.
And your function is updating all of the data, and not only those enterring thanks to :
// Update the data for all
var join = d3.select(svg)
.selectAll("rect")
.data(data);
// Append new data.
join.enter().append("rect")
.attr("class", function (d, i) {
var low = ""
i == minIndex ? low = " low" : "";
return "bar" + " " + "index_" + i + low;
})
// Update everyone.
join.attr("width", barWidth)
.attr("x", function (d, i) {
return barWidth * i + barSpace * i;
})
.attr("y", chartHeight)
.attr("height", 0)
.transition()
.delay(function (d, i) {
return i * 100;
})
.attr("y", function (d, i) {
return chartHeight - y(d);
})
.attr("height", function (d) {
return y(d);
});
D3 is following a really specific data update pattern.
Depending on what you want to do, you can follow this. It's up to you what you want to animate or not.
// Link data to your graph and get the join
var join = svg.selectAll('rect')
.data(data);
// Update data already there
join.attr('x', 0);
// Append the new data
join.enter().append('rect')
.attr('x', 0);
// Remove exiting elements not linked to any data anymore
join.exit().remove();
// Update all resulting elements
join.attr('x', 0);

Categories

Resources