I'm new to learning d3 and so any advice on improving any inefficiencies in my charts would be greatly appreciated. I am using d3js to produce a horizontal stacked bar chart with two color scales.
In this example, there are two teams and a total of 8 players. We are charting the players first based on total points, then points per game. Then, I am also using two different color scales to show which team they belong to.
I did this using this stack bar example as reference: http://bl.ocks.org/mbostock/3886208
... but in order to color each player (the y axis) using the color scale associated with the team, I had to first remove the fill that was being applied to each of the wrapping groups made for each game. Then I had to add the key for each stack() series to each data array in that series.
When drawing the individual s I had to do an if and hard code the if condition that tells it which color scale to use in which case.
Here is my block: https://bl.ocks.org/Ognami/2128772d2d7c1d2708d973b9401e8e1f
My question, most importantly, is there an easier way to pass this key along to all of the data arrays in each series? And is there a way around the conditionals for choosing which scale?
Here's some small optimizations. Store the color scales in a object:
var z = {
'a': d3.scaleOrdinal()
.range(["#8cb6d9", "#4787ba", "#216297"]),
'b': d3.scaleOrdinal()
.range(["#ff5555", "#ff1122", "#990022"])
};
Assign there domains as:
for (key in z){
z[key].domain(key);
}
Just pass the data down to subselection and get the "key" from the parent:
g.append("g")
.selectAll("g")
.data(d3.stack().keys(keys)(data))
.enter().append("g")
.selectAll("rect")
.data(d => d)
.enter().append("rect")
.attr("fill", function(d, i) {
var key = d3.select(this.parentNode).datum().key;
return z[d.data.team](key);
});
Now as the number of teams increases or decreases, you only need to edit the z to include the the colors for that team.
Full code here.
Related
I'm looking for a way to plug in groups to my force-directed graph visualization. I've found three related examples so far:
Cola.js which would require adding another library and possibly retro-fitting my code to fit this different library.
This block, which is pretty hard to untangle.
This slide from mbostock's slide deck, which isn't what I want but on the right path...
What I'd like most is a simple way of adding something very close to the structure from the first link, but without too much overhead.
Right now I have a pretty standard setup:
var link = g.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style(...
var node = g.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("id", function(d) { return d.id; })
I was hoping to just grab the d3 code out of cola.js and mess with it, but that library seems fairly complicated so it wouldn't be too easy. I'm hoping it isn't too hard to get something kind of like this in straight d3:
Thanks!
I'm following the title "visualize groups of nodes" more than the suggested picture, but I think it wouldn't be that hard to tweak my answer to show bounding boxes as in the image
There's probably a few d3 only solutions, all of them almost certainly require tweaking the node positions manually to keep nodes grouped properly. The end result won't strictly be typical of a force-layout because links and node positions must be manipulated to show grouping in addition to connectivity - consquently, the end result will be a compromise between each force - node charge, length strength and length, and group.
The easiest way to accomplish your goal may be to:
Weaken link strength when links link different groups
On each tick, calculate each group's centroid
Adjust each node's position to move it closer to the group's centroid
Use a voronoi diagram to show the groupings
For my example here, I'll use Mike's canonical force layout.
Weaken links when links link different groups
Using the linked example, we can dampen the link strength when link target and link source have different groups. The specified strength will likely need to be altered depending on the nature of the force layout - more inter-connected groups will likely need to have weaker intergroup link strength.
To change the link strength depending on if we have an intergroup link or not, we might use:
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(function(link) {
if (link.source.group == link.source.target) {
return 1; // stronger link for links within a group
}
else {
return 0.1; // weaker links for links across groups
}
}) )
.force("charge", d3.forceManyBody().strength(-20))
.force("center", d3.forceCenter(width / 2, height / 2));
On Each Tick, Calculate Group Centroids
We want to force group nodes together, to do so we need to know the centroid of the group. The data structure of simulation.nodes() isn't the most amenable to calculating centroids, so we need to do a bit of work:
var nodes = this.nodes();
var coords ={};
var groups = [];
// sort the nodes into groups:
node.each(function(d) {
if (groups.indexOf(d.group) == -1 ) {
groups.push(d.group);
coords[d.group] = [];
}
coords[d.group].push({x:d.x,y:d.y});
})
// get the centroid of each group:
var centroids = {};
for (var group in coords) {
var groupNodes = coords[group];
var n = groupNodes.length;
var cx = 0;
var tx = 0;
var cy = 0;
var ty = 0;
groupNodes.forEach(function(d) {
tx += d.x;
ty += d.y;
})
cx = tx/n;
cy = ty/n;
centroids[group] = {x: cx, y: cy}
}
Adjust each node's position to move it closer to its group's centroid:
We don't need to adjust every node - just those that are straying fairly far from their centroids. For those that are sufficiently far we can nudge them closer using a weighted average of the centroid and the node's current position.
I modify the minimum distance used to determine if a node should be adjusted as the visualization cools. For the majority of the time when the visualization is active, when alpha is high, the priority is grouping, so most nodes will be forced towards the grouping centroid. As alpha drops towards zero, nodes should be grouped already, and the need to coerce their position is less important:
// don't modify points close the the group centroid:
var minDistance = 10;
// modify the min distance as the force cools:
if (alpha < 0.1) {
minDistance = 10 + (1000 * (0.1-alpha))
}
// adjust each point if needed towards group centroid:
node.each(function(d) {
var cx = centroids[d.group].x;
var cy = centroids[d.group].y;
var x = d.x;
var y = d.y;
var dx = cx - x;
var dy = cy - y;
var r = Math.sqrt(dx*dx+dy*dy)
if (r>minDistance) {
d.x = x * 0.9 + cx * 0.1;
d.y = y * 0.9 + cy * 0.1;
}
})
Use a Voronoi Diagram
This allows the easiest grouping of nodes - it ensures that there is no overlap between group shells. I haven't built in any verification to ensure that a node or set of node's aren't isolated from the rest of their group - depending on the visualization's complexity you might need this.
My initial thought was using a hidden canvas to calculate if shells overlapped, but with a Voronoi you could probably calculate if each group is consolidated using neighboring cells. In the event of non-consolidated groups you could use a stronger coercion on stray nodes.
To apply the voronoi is fairly straightforward:
// append voronoi
var cells = svg.selectAll()
.data(simulation.nodes())
.enter().append("g")
.attr("fill",function(d) { return color(d.group); })
.attr("class",function(d) { return d.group })
var cell = cells.append("path")
.data(voronoi.polygons(simulation.nodes()))
And update on each tick:
// update voronoi:
cell = cell.data(voronoi.polygons(simulation.nodes())).attr("d", renderCell);
Results
Altogether, this looks like this during the grouping phase:
And as the visualization finally stops:
If the first image is preferable, then remove the part the changes the minDistance as alpha cools down.
Here's a block using the above method.
Further Modification
Rather than using the centroid of each group's nodes, we could use another force diagram to position the ideal centroid of each group. This force diagram would have a node for each group, the strength of links between each group would correspond to te number of links between the nodes of the groups. Using this force diagram, we could coerce the original nodes towards our idealized centroids - the nodes of the second force layout.
This approach may have advantages in certain situations, such as by separating groups by greater amounts. This approach might give you something like:
I've included an example here, but hope that the code is commented sufficiently to understand without a breakdown like the above code.
Block of second example.
The voronoi is easy, but not always the most aesthetic, you could use a clip path to keep clip the polygons to some sort of oval, or use a gradient overlay to fade the polygons out as they reach the edges. One option that is likely possible depending on graph complexity is using a minimum convex polygon instead, though this won't work well with groups with less than three nodes. Bounding box's probably won't work in most instances, unless you really keep the coercion factor high (eg: keep minDistance very low the entire time). The trade off will always be what do you want to show more: connections or grouping.
I'm interested in graphing live-ish data in D3js. Now, when I say "live-ish" I mean that I'll be collecting data every 200ms +/- 10ms, but there may be several minute long periods of inactivity. Fortunately, the input data is time-stamped!
What I have so far: I've followed some line drawing in d3 guides (eg: this) and I have a Y axis with the value range/domain I want. I have an X axis with the range I want and a moving domain as per a standard time-series fixed-width graph. That is, if my graph's x axis domain is (0:15, 0:35) in 5 seconds it will be (0:20, 0:40). This transitions nicely as it's using linear easing.
I have mock-data being output each iteration of the graph tick. My domain is set up as such that new points are just out of the x-axis domain such as to allow the smooth effect as per 1. All in all, it looks great.
So where do I go from here? My desired result: data comes in asynchronously and is placed precisely at its x-axis time-stamped location. If data is up to date, it gets placed juuust outside the x-axis domain and has a smooth transition in. If data doesn't arrive in time, the graph continues without drawing any new points until data is received, at which time it adds each point at its appropriate time-stamp retroactively. If data for the missing period doesn't arrive at all, we just continue with a gap in the graph. I can emulate this by calling...
d3.select(window).on("click", .. )
Effectively, I can click to add random data at the current time-stamp using some anonymous function which allows me to mimic the data / event structure my code should handle.
I think my current confusion is due to how I add data and draw the path from it.
var line = d3.svg.line()
.interpolate("basis-open")
.x(function(d, i) { return x(now - (n-1-i)*duration); })
.y(function(d, i) { return y(d); });
var axis = svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(x.axis = d3.svg.axis().scale(x).orient("bottom"));
svg.append("g")
.attr("class", "y axis")
.call(d3.svg.axis().scale(y).orient("left"));
var path = svg.append("g")
.attr("clip-path", "url(#clip)")
.append("path")
.datum(data)
.attr("class", "line")
The big question: my y-values correspond to the path.datum(data) (data is just an array of values) appropriately, but when I push to the data array to draw the line, it always places each point graphically at equal distance apart. How do I break out of the mono-variable graph without destroying my time-series smooth scrolling animation? I could record a second array of timestamps alongside my data array, but how to I integrate those into the line? Ideally, I'd have them both be a part of the same array so I could sort by timestamp so when I call data.shift() truly the oldest data is gone. I tried changing the "duration" of the transition function but it made the graph accelerate weirdly and didn't actually break the equidistance of points on x.
How do I set up the y-axis graph to also take into account x-location without breaking my graph?
Alright, so I figured out a pretty straightforward way of doing exactly what I wanted while still using .datum(data) instead of .data(data) (where 'data' is my array). Instead of passing in an array of values to .datum, I pass in an array of objects. Or, what was once data = [value1, value2, ...] is now data = [{gx:timestamp1, gy:value1}, {gx:timestamp2, gy:value2}, ...].
My line x-axis / y-axis functions are now...
.x(function(d, i) { return x(d.gx); })
.y(function(d, i) { return y(d.gy); });
Which ends up being a bit neater than my initial run at it. My transition functions didn't have to change.
The final puzzle piece was what to do about data that comes in time-stamped but out of order. Fortunately, my display domain isn't very large and thus, I don't need to store many values in the array. As such, when new data comes in I simply sort it to make the line not a wobbly-bobbly mess.
data.sort(function(a,b){ return a.gx - b.gx});
And voila! If the size of the array is beyond the boundary, shift off data. This gives us the effect of a sliding timeseries window of the past n seconds where time-stamped data points may be arbitrarily dumped on and displayed properly.
I'm trying to get a stacked bar chart to animate correctly as bars come and go. There's probably a good example of this somewhere (maybe I'll ask as a separate question), but the examples I'm finding don't show transitions with individual stack elements exiting and entering I want to make sure that as bars are exiting, they drag down the bars above them, and as they're entering, they push up the bars above them. And I don't want any gaps or overlaps midway through the transition.
Can anyone point me to an example that does this?
Correcting my wrong-headed question:
Ashitaka answered the question with a helpful jsfiddle. His answer prompted me to look at the d3 stack layout more closely, where I read:
In the simplest case, layers is a two-dimensional array of values. All of the 2nd-dimensional arrays must be the same length.
So, I concluded I was going about this all wrong. I shouldn't have been trying to remove stack bars at all. If bars in my data were going to disappear, I should leave them in the data and change their height to zero. That way the transitions work great. I haven't yet had to deal with new bars appearing.
One confusing aspect of transitioning stacked charts (and working with SVG in general) is that the coordinate system origin is at the top-left corner, which means that y increases downwards.
First, our data should have 2 y related attributes:
y, the height of the bar
And y0, the baseline or the y position of the bar when it's on top of other bars. This should be calculated by d3.layout.stack().
Then, we should create 2 scales:
One for height, which works exactly as expected:
var heightScale = d3.scale.linear()
.domain([0, maxStackY])
.range([0, height]);
And one for the y position, which works in the reverse way:
var yScale = d3.scale.linear()
.domain([0, maxStackY])
.range([height, 0]);
With these two scales, we can create some functions to calculate the appropriate y positions and heights of our bars:
var barBaseY = function (d) { return yScale(d.y0); };
var barTopY = function (d) { return yScale(d.y0 + d.y); };
var barHeight = function (d) { return heightScale(d.y); };
Next, it's critical that we create a key function so that elements are bound to the correct data:
var joinKey = function (d) { return d.name; };
Without this function D3 would join the data using its index, which would break everything.
Now, to remove or add a set of bars from the stack, we take these steps:
Recalculate the stack:
var newStack = stack(enabledSeries());
Join the new stack with the current selection of layers with the data function:
layers = layers.data(newStack, joinKey);
With our key function, D3 determines the bars that are to be added, removed or updated.
Access the appropriate bars:
layers.enter() contains the "enter selection", that is, the new set of bars to be added.
layers.exit() contains the "exit selection", that is, the set of bars to be removed.
And simply layers contains the "update selection", that is, the bars that are to be updated. However, after enter.append the "update selection" is modified to contain both entering and updating elements. This has changed in D3 v4 though.
Animate the bars:
For added bars, we create them with height 0 and y position barBaseY.
Then we animate all the bars' height and y attributes.
For removed bars, we animate them to height 0 and y position barBaseY, the exact opposite of adding bars. Then we animate all the remaining bars' height and y attributes. D3 is smart enough to render all these animations at the same time.
Here's a pared down version of the stacked chart I linked to in my first comment.
And here's a visual explanation of why you have to animate both y and height attributes to simulate a bar diminishing in size "going down".
I am attempting to create a vertical timeline using d3.js that is linked to a map so that any item(s) contained in the brush will also be displayed in the map. Kind of like http://code.google.com/p/timemap/ but with d3 instead of SIMILE and a vertical timeline rather than horizontal.
I can successfully create an svg with vertical bars representing time ranges, legend, ticks, and a brush. The function handling brush events is getting called and I can obtain the extent which contains the y-axis start and stop of the brush. So far so good...
How does one obtain the datums covered by the brush? I could iterate over my initial data set looking for items within the extent range but that feels hacky. Is there a d3 specific way of getting the datums highlighted by a brush?
var data = [
{
start: 1375840800,
stop: 1375844400,
lat: 0.0,
lon: 0.0
}
];
var min = 1375833600; //Aug 7th 00:00:00
var max = 1375919999; //Aug 7th 23:59:59
var yScale = d3.time.scale.utc().domain([min, max]).range([0, height])
var brush = d3.svg.brush().y(yScale).on("brush", brushmove);
var timeline = d3.select("#myDivId").append("svg").attr("width", width).attr("height", height);
timeline.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("x", function(datum, index) {return index * barSize})
.attr("y", function(datum, index) {return yScale(datum.start)})
.attr("height", function(datum, index) {return yScale(datum.end) - yScale(datum.start)})
.attr("width", function() {return barSize})
timeline.append("g")
.attr("class", "brush")
.call(brush)
.selectAll("rect")
.attr("width", width);
function brushmove() {
var extent = brush.extent();
//How do I get the datums contained inside the extent????
}
You'll need to do some kind of iteration to figure out what points live inside the brush extent. D3 doesn't automatically do this for you, probably because it can't know what shapes you're using to represent your data points. How detailed you get about what is considered "selected" and what isn't is quite application specific.
There are a few ways you can go about this:
As you suggest, you can iterate your data. The downside to this is that you would need to derive the shape information from the data again the same way you did when you created the <rect> elements.
Do a timeline.selectAll("rect") to grab all elements you potentially care about and use selection.filter to pare it down based on the x, y, height and width attributes.
If performance is a concern because you have an very large number of nodes, you can use the Quadtree helper to partition the surface and reduce the number of points that need to be looked at to find the selected ones.
Or try Crossfilter, there you pass the extent from the brush to a dimension filter and then you fetch filtered and sorted data by dimension.top(Infinity).
(A bit late answer, buy maybe useful for others, too.)
I need to create a d3 bar chart that can have negative values. Ideally the axis zero position should be calculated based on the extent of the data, but I'd settle for a solution that assumes symmetric positive and negative extent, i.e. that it would be always in the middle of the chart.
Here's an example of what I'd like to achieve.
OK, let's say you have an array of numbers as your dataset, and this includes some positive and negative values:
var data = [-15, -20, -22, -18, 2, 6, -26, -18];
You'll want two scales to construct a bar chart. You need one quantitative scale (typically a linear scale) to compute the bar positions along the x-axis, and a second ordinal scale to compute the bar positions along the y-axis.
For the quantitative scale, you typically need to compute the domain of your data, which is based on the minimum and maximum value. An easy way to do that is via d3.extent:
var x = d3.scale.linear()
.domain(d3.extent(data))
.range([0, width]);
You might also want to nice the scale to round the extent slightly. As another example, sometimes you want the zero-value to be centered in the middle of the canvas, in which case you'll want to take the greater of the minimum and maximum value:
var x0 = Math.max(-d3.min(data), d3.max(data));
var x = d3.scale.linear()
.domain([-x0, x0])
.range([0, width])
.nice();
Alternatively, you can hard-code whatever domain you want.
var x = d3.scale.linear()
.domain([-30, 30])
.range([0, width]);
For the y-axis, you'll want to use rangeRoundBands to divide the vertical space into bands for each bar. This also lets you specify the amount of padding between bars. Often an ordinal scale is used with some identifying data—such as a name or a unique id. However, you can also use ordinal scales in conjunction with the data's index:
var y = d3.scale.ordinal()
.domain(d3.range(data.length))
.rangeRoundBands([0, height], .2);
Now that you've got your two scales, you can create the rect elements to display the bars. The one tricky part is that in SVG, rects are positioned (the x and y attributes) based on their top-left corner. So we need to use the x- and y-scales to compute the position of the top-left corner, and that depends on whether the associated value is positive or negative: if the value is positive, then the data value determines the right edge of the bar, while if it's negative, it determines the left edge of the bar. Hence the conditionals here:
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d, i) { return x(Math.min(0, d)); })
.attr("y", function(d, i) { return y(i); })
.attr("width", function(d, i) { return Math.abs(x(d) - x(0)); })
.attr("height", y.rangeBand());
Lastly, you can add an axis to display tick marks on top. You might also compute a fill style (or even a gradient) to alter the differentiate the appearance of positive and negative values. Putting it all together:
Bar Chart with Negative Values