Different states in D3/Coffee Bubble chart - d3.js

I want to use this (http://vallandingham.me/vis/gates/) Bubble Chart (made in D3):
...to walk through some different scenarios. In short, I want to visualize election data. How many votes did parties get, and what scenario's are possible to form a government?
At the data level, it's quite obvious: Name, number of seats in parliament, state1, state2, state3, etc. State1 is a 1 or 2. 1 is a place in government, 2 is opposition. Pretty straightforward.
But the example only shows two states: All Grants and Grants By year. What I want, is more states like Grants By Year. But me being not a very good programmer can't figure out how to make this work. The visualisation doesn't work when I add a new state.
Here's the code (Coffee) which controls states.
class BubbleChart
constructor: (data) ->
#data = data
#width = 940
#height = 600
#tooltip = CustomTooltip("gates_tooltip", 240)
# locations the nodes will move towards
# depending on which view is currently being
# used
#center = {x: #width / 2, y: #height / 2}
#year_centers = {
"2008": {x: #width / 3, y: #height / 2},
"2009": {x: #width / 2, y: #height / 2},
"2010": {x: 2 * #width / 3, y: #height / 2}
}
# used when setting up force and
# moving around nodes
#layout_gravity = -0.01
#damper = 0.1
# these will be set in create_nodes and create_vis
#vis = null
#nodes = []
#force = null
#circles = null
# nice looking colors - no reason to buck the trend
#fill_color = d3.scale.ordinal()
.domain(["low", "medium", "high"])
.range(["#d84b2a", "#beccae", "#7aa25c"])
# use the max total_amount in the data as the max in the scale's domain
max_amount = d3.max(#data, (d) -> parseInt(d.total_amount))
#radius_scale = d3.scale.pow().exponent(0.5).domain([0, max_amount]).range([2, 85])
this.create_nodes()
this.create_vis()
# create node objects from original data
# that will serve as the data behind each
# bubble in the vis, then add each node
# to #nodes to be used later
create_nodes: () =>
#data.forEach (d) =>
node = {
id: d.id
radius: #radius_scale(parseInt(d.total_amount))
value: d.total_amount
name: d.grant_title
org: d.organization
group: d.group
year: d.start_year
x: Math.random() * 900
y: Math.random() * 800
}
#nodes.push node
#nodes.sort (a,b) -> b.value - a.value
# create svg at #vis and then
# create circle representation for each node
create_vis: () =>
#vis = d3.select("#vis").append("svg")
.attr("width", #width)
.attr("height", #height)
.attr("id", "svg_vis")
#circles = #vis.selectAll("circle")
.data(#nodes, (d) -> d.id)
# used because we need 'this' in the
# mouse callbacks
that = this
# radius will be set to 0 initially.
# see transition below
#circles.enter().append("circle")
.attr("r", 0)
.attr("fill", (d) => #fill_color(d.group))
.attr("stroke-width", 2)
.attr("stroke", (d) => d3.rgb(#fill_color(d.group)).darker())
.attr("id", (d) -> "bubble_#{d.id}")
.on("mouseover", (d,i) -> that.show_details(d,i,this))
.on("mouseout", (d,i) -> that.hide_details(d,i,this))
# Fancy transition to make bubbles appear, ending with the
# correct radius
#circles.transition().duration(2000).attr("r", (d) -> d.radius)
# Charge function that is called for each node.
# Charge is proportional to the diameter of the
# circle (which is stored in the radius attribute
# of the circle's associated data.
# This is done to allow for accurate collision
# detection with nodes of different sizes.
# Charge is negative because we want nodes to
# repel.
# Dividing by 8 scales down the charge to be
# appropriate for the visualization dimensions.
charge: (d) ->
-Math.pow(d.radius, 2.0) / 8
# Starts up the force layout with
# the default values
start: () =>
#force = d3.layout.force()
.nodes(#nodes)
.size([#width, #height])
# Sets up force layout to display
# all nodes in one circle.
display_group_all: () =>
#force.gravity(#layout_gravity)
.charge(this.charge)
.friction(0.9)
.on "tick", (e) =>
#circles.each(this.move_towards_center(e.alpha))
.attr("cx", (d) -> d.x)
.attr("cy", (d) -> d.y)
#force.start()
this.hide_years()
# Moves all circles towards the #center
# of the visualization
move_towards_center: (alpha) =>
(d) =>
d.x = d.x + (#center.x - d.x) * (#damper + 0.02) * alpha
d.y = d.y + (#center.y - d.y) * (#damper + 0.02) * alpha
# sets the display of bubbles to be separated
# into each year. Does this by calling move_towards_year
display_by_year: () =>
#force.gravity(#layout_gravity)
.charge(this.charge)
.friction(0.9)
.on "tick", (e) =>
#circles.each(this.move_towards_year(e.alpha))
.attr("cx", (d) -> d.x)
.attr("cy", (d) -> d.y)
#force.start()
this.display_years()
# move all circles to their associated #year_centers
move_towards_year: (alpha) =>
(d) =>
target = #year_centers[d.year]
d.x = d.x + (target.x - d.x) * (#damper + 0.02) * alpha * 1.1
d.y = d.y + (target.y - d.y) * (#damper + 0.02) * alpha * 1.1
# Method to display year titles
display_years: () =>
years_x = {"2008": 160, "2009": #width / 2, "2010": #width - 160}
years_data = d3.keys(years_x)
years = #vis.selectAll(".years")
.data(years_data)
years.enter().append("text")
.attr("class", "years")
.attr("x", (d) => years_x[d] )
.attr("y", 40)
.attr("text-anchor", "middle")
.text((d) -> d)
# Method to hide year titiles
hide_years: () =>
years = #vis.selectAll(".years").remove()
show_details: (data, i, element) =>
d3.select(element).attr("stroke", "black")
content = "<span class=\"name\">Title:</span><span class=\"value\"> #{data.name}</span><br/>"
content +="<span class=\"name\">Amount:</span><span class=\"value\"> $#{addCommas(data.value)}</span><br/>"
content +="<span class=\"name\">Year:</span><span class=\"value\"> #{data.year}</span>"
#tooltip.showTooltip(content,d3.event)
hide_details: (data, i, element) =>
d3.select(element).attr("stroke", (d) => d3.rgb(#fill_color(d.group)).darker())
#tooltip.hideTooltip()
root = exports ? this
$ ->
chart = null
render_vis = (csv) ->
chart = new BubbleChart csv
chart.start()
root.display_all()
root.display_all = () =>
chart.display_group_all()
root.display_year = () =>
chart.display_by_year()
root.toggle_view = (view_type) =>
if view_type == 'year'
root.display_year()
else
root.display_all()
d3.csv "data/gates_money.csv", render_vis

On the index page itself, it has the code for toggle_view(view_type):
<script type="text/javascript">
$(document).ready(function() {
$(document).ready(function() {
$('#view_selection a').click(function() {
var view_type = $(this).attr('id');
$('#view_selection a').removeClass('active');
$(this).toggleClass('active');
toggle_view(view_type);
return false;
});
});
});
</script>
And in the code you provided, you have the code for that function:
root.toggle_view = (view_type) =>
if view_type == 'year'
root.display_year()
else
root.display_all()
So it seems as though to add another state, you need to:
Add an appropriate id to a link My Type
Add an else if with that id, direct to a function
Write the function
So like this
root.toggle_view = (view_type) =>
if view_type == 'year'
root.display_year()
else if view_type == 'my_type'
root.display_my_type()
else
root.display_all()
display_my_type = () =>
# Whatever needs to be done

Related

Automatic choosing of scales (linear, power, logarithmic) for legends

This is more a data-science question than d3.js but I guess other people must have thought about that too.
I have a dataset with daily updating values. The set also contains the historical data of all or several days. Basically like this:
{data: [
"ItemA" : {
"24.10.2020" : 123,
"25.10.2020" : 134,
"26.10.2020" : 145,
"27.10.2020" : 156,
"28.10.2020" : 167
},
"ItemB" : {
"24.10.2020" : 123,
"25.10.2020" : 234,
"26.10.2020" : 456,
"27.10.2020" : 567,
"28.10.2020" : 678
},
"ItemC" : {
"24.10.2020" : 123,
"25.10.2020" : 136,
"26.10.2020" : 149,
"27.10.2020" : 152,
"26.10.2020" : 165,
"28.10.2020" : 178
},
]}
As you see ItemB is an outlier with values growing much faster than those of the other Items.
Setting up a scale for a Legend to display the growth over time was easy as long as the values grew at almost the same rate. A d3.scaleLinear().domain([0, upperBoundValues]) was fine. While the values grew the user could still differentiate between smaller an higher values.
Since one Item grew faster, the one with slower growth get pushed in one part of the scale. So if I had a color range like d3.interpolateTurbo suddenly most values get displayed as the colors near to black and one always to the red.
Manually I'd switch to a power scale or a log scale. Especially because I'd have to check daily for what happens.
I'd prefer to have a function that tests for such developments and automatically switches the scale if the values. Even better it would be nice to choose a fitting scale (Basically choosing the best fitting exponent of a power scale and/or base for the log scales).
I don't need a base10 log scale if my values will never grow to ultra large digits.
Is there any approximation function/algorithm I can implement that makes choosing easier, or that returns a value upon I could choose the scale (Like: 0...1 -> Linear // 1...n -> Log)
As an extension of my comment, consider the following, which uses scaleThreshold with the decile values. I've drawn 5 circles, and the first one's value increases much faster than the others. But you'll still see enough difference between them, because of the threshold scale.
const data = d3.range(5).map(i => {
let values = [1];
d3.range(50).forEach(() => {
// Either a multiplier [0.9, 1.2], or (if it's the first one, [1.2, 1.5]
const multiplier = (i === 0 ? 1.2 : 0.9) + (Math.random() * 0.3);
values.push(values[values.length - 1] * multiplier);
});
return {
x: 50 + i * 100,
y: 50,
r: 40,
values: values,
};
});
const allValues = data.map(d => d.values).flat().sort((a, b) => a - b);
const colours = d3.scaleThreshold()
.domain([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9].map(i => d3.quantile(allValues, i)))
.range(d3.schemeSpectral[10]);
const svg = d3.select("svg")
.attr("width", 500);
const colourbar = svg.append("g");
colourbar
.selectAll("rect")
.data(colours.range())
.enter()
.append("rect")
.attr("x", (d, i) => i * 50)
.attr("y", 100)
.attr("height", 20)
.attr("width", 50)
.attr("fill", d => d);
colourbar
.selectAll("text")
.data(colours.domain())
.enter()
.append("text")
.attr("x", (d, i) => (i + 1) * 50)
.attr("y", 135)
.text(d => d.toFixed(1));
const circles = svg.append("g")
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("r", d => d.r);
const labels = svg
.append("g")
.selectAll("text")
.data(data)
.enter()
.append("text")
.style("fill", "white")
.attr("dy", 5)
.attr("x", d => d.x)
.attr("y", d => d.y);
let counter = -1;
function colour() {
counter = (counter + 1) % 50;
labels.text(d => d.values[counter].toFixed(1));
circles
.transition()
.duration(1000)
.ease(d3.easeLinear)
.attr("fill", d => colours(d.values[counter]))
.filter((d, i) => i === 0)
.on("end", colour);
}
colour();
text {
text-anchor: middle;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>

D3.js shape gets translated in display after transition as if coordinate system had changed

This animation tries to illustrate balls following a curved line "falling" into a bucket:
(1) https://bl.ocks.org/max-l/ddfef6f8415675878baba32080d6a874/bae06bead60551cdae7488faccaa0d9c5624455c
For a reason that I can't understand, in (1), the balls get "teleported" outside the rectangle, it's as if the display suddenly had changed coordinate system.
The following code illustrates what should happen at the end of the transition: the balls should bounce in the rectangle that represents a bucket:
(2) https://bl.ocks.org/max-l/cda07bafcf7970e724b3aa00aefe9a02/8230c5db14e666efcb833c6c41c3c941f836729f
Why do the circles get "teleported" on the display, while the x,y coordinate shows no such "teleportation" ?
function redraw(data){
var circle = svg.selectAll("circle")
.data(data)
circle.enter().append("circle")
.attr("r", radius)
.transition()
.ease(d3.easeQuad)
.delay(rndDelay)
.duration(2000)
.attrTween("transform", translateAlong(path.node()))
.on("end", d => {
const lastP = faucet[2]
d.state = 1
d.x = lastP[0]
d.y = lastP[1]
console.log("a1",[d.x,d.y])
})
circle.filter(d => d.state == 1)
.attr("r", radius)
.attr("cx", d => d.x)
.attr("cy", d => {
console.log("a2",[d.x,d.y])
return d.y
})
}
After the transition is complete, you are both transforming with translate and positioning with cx/cy, which results in the position being off.
During the transition you set the transform for each circle:
.attrTween("transform", translateAlong(path.node()))
Afterwards you position by:
.attr("cx", d => d.x)
.attr("cy", d => d.y)
But this is added to the end transition point/translation (the end of the faucet). This is why everything appears normal except off by a fixed amount.
Just reset the transform after the transition.
Example
Or alternatively, update the translate with the new x/y values rather than using cx/cy.

d3.js nested selection with 2d array

Say I have the following JSON array:
var json = [
[1,2,3],
[1,2,3],
[1,2,3]
];
How can I render a grid of SVG rect nodes without performing 2 selectAll calls? In this fiddle I was able to make the grid, but I had to render each row inside of an intermediate g node. Is there a way to write it without the first selection? If I try:
svg.data(json)
.selectAll('rect')
.data(function(data) { return data; })
.enter()
.append('rect')
.attr('x', function(data, x, y) {
return (x * size) + (x * spacing);
})
.attr('y', function(data, x, y) {
return (y * size) + (y * spacing);
})
.attr('width', size)
.attr('height', size);
it only renders the first row of the 2D array.
Do you mean that you'd rather not have the g nodes in the hierarchy, and instead, you want to make all the rects direct siblings?
This is doable, but you have to first flatten the json into a 1-dimensional array with 9 elements (not shown here; can use Array.reduce()). Then you would only select and bind once:
var flatJson = [1,2,3,1,2,3,1,2,3]
svg
.selectAll('rect')
.data(flatJson)`
and position based on i:
.enter()
.append('rect')
.attr('x', function(d, i) {
return (i % numColumns) * (size + spacing);
})
.attr('y', function(d, i) {
return Math.floor(i/numColumns) * (size + spacing);
})
.attr('width', size)
.attr('height', size);
The trade off is that there needs to be a sense of the number of columns numColumns = 3.
The flattened json could be turned into an array of objects, and each object could have a column and row. For eample:
var flatJson = [
{ row:0, col:0, value: 1},
{ row:0, col:1, value: 2},
{ row:0, col:2, value: 3},
{ row:1, col:0, value: 1},
...
]

why won't my d3 arc transitions work?

I've looked at a bunch of examples of the net and can't seem to get my arc transitions to have a nice smooth animation. I've tried two different ways to implement arcTween, both of which correctly redraw my pie charts on update, but neither of which have a smooth animation.
render: (oldData, newData) ->
return if _.compact(newData).length is 0
#toggleWidth()
pieData = #layout(newData)
oldPieData = #layout(oldData)
#arcGroup.selectAll("path").data(pieData)
.enter().append("path")
.attr("fill", (d,i) => #color(i) )
.transition()
.duration(250)
.attr("d", (d) =>
#arc()(d)
)
.each((d) =>
this._current = d
)
#arcGroup.selectAll("path").data(pieData)
.transition()
.attr("fill", (d,i) => #color(i) )
.transition()
.duration(250)
.attrTween("d", (d,i) =>
#arcTween(oldPieData, d,i));
#arcGroup.selectAll("path").data(pieData)
.exit()
.remove()
.transition()
.duration(3000)
.attrTween("d", (d,i) =>
#arcTween(oldPieData, d,i));
the comments on the arcTween method are from a different way that I was implementing this method.
arcTween: (oldData, d, i) =>
# if oldData[i]
# s0 = oldData[i].startAngle
# e0 = oldData[i].endAngle
# else
# s0 = 0
# e0 = 0
# i = d3.interpolate({startAngle: s0, endAngle: e0}, {startAngle: d.startAngle, endAngle: d.endAngle})
i = d3.interpolate(this._current, d)
(t) =>
b = i(t)
#arc()(d)
what am I missing? thanks!
The last line of your arcTween function should read
#arc()(b)
instead of
#arc()(d)
You need to return the interpolator instead of the datum.

D3: How do I chain transitions for nested data?

I'm attempting to make a visualization with a multistage animation. Here's a contrived fiddle illustrating my problem (code below).
In this visualization the boxes in each row should turn green when the entire group has finished moving to the right column. IOW, when the first row (containing 3 boxes) is entirely in the right column, all the boxes should turn from black to green, but the second row, having only partially moved to the right column at this point, would remain black until it, too, is completely in the right column.
I'm having a hard time designing this transition.
Basic chaining without a delay immediately turns each box green once its finished moving (this is how it's working currently). Not good enough.
On the other hand creating a delay for the chain is difficult, since the effective delay per group is based on the number of boxes it has and I don't think this count is available to me.
It's like I need the transition to happen at mixed levels of granularity.
How should I go about doing this?
The fiddle (code below)
var data = [
["x", "y", "z"],
["a", "b", "c", "d", "e"]
];
var svg = d3.select("svg");
var group = svg.selectAll("g").data(data)
.enter()
.append("g")
.attr("transform", function(d, i) {
return "translate(0, " + (40 * i) + ")";
});
var box = group.selectAll("rect")
.data(function(d) { return d; });
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) { return 60 + 30 * i; })
.transition()
.delay(function(d, i) { return 250 + 500 * i; })
.attr("x", function(d, i) { return 300 + 30 * i; })
.transition()
.attr("style", "fill:green");
// I probably need a delay here but it'd be based off the
// number of elements in the nested data and I don't know
// how to get that count
.attr("style", "fill:green");
I manage to get the effect you want, it's a little tricky though. You can customize the behavior of a transition at the begining and end of a transition. If you add a function to the end of the transition that detects if the transitioned element is the last in the group, you select all the rectangles in the group and apply the change to them.
box.enter()
.append("rect")
.attr("width", 30)
.attr("height", 30)
.attr("x", function(d, i) { return 60 + 30 * i; })
.transition()
.delay(function(d, i) { return 250 + 500 * i; })
.attr("x", function(d, i) { return 300 + 30 * i; })
.each('end', function(d, i) {
var g = d3.select(d3.select(this).node().parentNode),
n = g.selectAll('rect')[0].length;
if (i === n - 1) {
g.selectAll('rect').attr('fill', 'green');
}
});
More details in the transitions here, a working fiddle here.

Resources