D3 Chained Transitions With Access to Object Data, Transition Count - d3.js

I want to visualize state changes over time, and I'm not sure how to do it using d3 transitions. Simple chained transitions are pretty straightforward. But in this case, for each transition I need access to the object's data and I also need a way to keep track of the count of the transition I'm currently on (so I know which state change to use).
It's easy to do a simple brute force version:
http://bl.ocks.org/aschneiderman/4f7a8824c96f33aa7ad48c729b739409
var members = [
{ id: 1, state: [1, 3, 1, 2, 3] },
{ id: 2, state: [1, 2, 2, 1, 1] },
{ id: 7, state: [2, 3, 1, 2, 3] },
{ id: 112, state: [2, 2, 2, 1, 3] },
{ id: 127, state: [3, 3, 1, 2, 1] },
[...]
{ id: 296, state: [2, 1, 1, 2, 1] }
];
var Distance = 200;
d3.select("svg").selectAll("rect")
.data(members)
.enter().append("rect")
.attr("x",0)
.attr("y",function(d,i) { return 80 + ( 500 - ((d.state[0] % 3) * 250) ) + i*25} )
.attr("width",10)
.attr("height",10)
.style("fill", "Crimson")
.transition()
.ease(d3.easeLinear)
.duration(600)
.delay(0)
.attr("x", 60 + (1 * Distance))
.attr("y", function(d,i) { return 80 + ( 500 - ((d.state[1] % 3) * 250) ) + i*25} )
.transition()
.ease(d3.easeLinear)
.duration(600)
.delay(0)
.attr("x", 60 + (2 * Distance))
.attr("y", function(d,i) { return 80 + ( 500 - ((d.state[2] % 3) * 250) ) + i*25} )
.transition()
.ease(d3.easeLinear)
.duration(600)
.delay(0)
.attr("x", 60 + (3 * Distance))
.attr("y", function(d,i) { return 80 + ( 500 - ((d.state[3] % 3) * 250) ) + i*25} ) ;
How do I modify the code so I can run n transitions on each rectangle, where n = the number of state changes? Any pointers or d3 examples would be greatly appreciated.

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>

DC JS Limiting brushing height

Is there a way to limit the brushing height - say 50% of y axis (only from Y axis 0 - 250, brushing should work) ? Example fiddle
JS Code:
var hitslineChart = dc.barChart("#chart-line-hitsperday");
var data = [
{date: "12/27/2012", http_404: 2, http_200: 190, http_302: 100},
{date: "12/28/2012", http_404: 2, http_200: 10, http_302: 100},
{date: "12/29/2012", http_404: 1, http_200: 300, http_302: 200},
{date: "12/30/2012", http_404: 2, http_200: 90, http_302: 0},
{date: "12/31/2012", http_404: 2, http_200: 90, http_302: 0},
{date: "01/01/2013", http_404: 2, http_200: 90, http_302: 0},
{date: "01/02/2013", http_404: 1, http_200: 10, http_302: 1},
{date: "01/03/2013", http_404: 2, http_200: 90, http_302: 0},
{date: "01/04/2013", http_404: 2, http_200: 90, http_302: 0},
{date: "01/05/2013", http_404: 2, http_200: 90, http_302: 0},
{date: "01/06/2013", http_404: 2, http_200: 200, http_302: 1},
{date: "01/07/2013", http_404: 1, http_200: 200, http_302: 100}
];
var ndx = crossfilter(data);
var parseDate = d3.time.format("%m/%d/%Y").parse;
data.forEach(function(d) {
d.date = Date.parse(d.date);
d.total= d.http_404+d.http_200+d.http_302;
});
var dateDim = ndx.dimension(function(d) {return d.date;});
var hits = dateDim.group().reduceSum(function(d) {return d.total;});
var minDate = dateDim.bottom(1)[0].date;
var maxDate = dateDim.top(1)[0].date;
hitslineChart.width(500)
.height(200)
.dimension(dateDim)
.group(hits)
.x(d3.time.scale().domain([minDate,maxDate]));
dc.renderAll();
Thanks,
Arun
Although your example uses dc.js 1.7.0, I'm going to answer for dc.js 2.0, since it's a lot newer and a few APIs have changed.
The technique is to override the functions from the coordinateGridMixin which size the brush. This gets a little hairy, but it's possible.
It turns out we'll have to override three undocumented functions which render the brush, renderBrush, setBrushY, and (unfortunately) resizeHandlePath.
The reason this gets hairy is that we really want to override brushHeight, but that one is a private function.
We'll define our own like this:
function height_over_2() {
return (hitslineChart._xAxisY() - hitslineChart.margins().top)/2;
}
For renderBrush, we need to shift the brush down by height_over_2(). We'll pass through the call first, then modify the transform:
dc.override(hitslineChart, 'renderBrush', function(g) {
hitslineChart._renderBrush(g);
var gBrush = hitslineChart.select('g.brush')
.attr('transform', 'translate(' + hitslineChart.margins().left + ',' + (hitslineChart.margins().top + height_over_2()) + ')')
});
setBrushY we'll replace entirely (we could just assign to it, but we'll use dc.override for consistency):
dc.override(hitslineChart, 'setBrushY', function(gBrush) {
gBrush.selectAll('rect')
.attr('height', height_over_2());
gBrush.selectAll('.resize path')
.attr('d', hitslineChart.resizeHandlePath);
});
Finally, resizeHandlePath also uses the height, and here we (ugh) have to copy a big chunk of code out of dc.js, which was itself copied from the crossfilter demo:
dc.override(hitslineChart, 'resizeHandlePath', function (d) {
var e = +(d === 'e'), x = e ? 1 : -1, y = height_over_2() / 3;
return 'M' + (0.5 * x) + ',' + y +
'A6,6 0 0 ' + e + ' ' + (6.5 * x) + ',' + (y + 6) +
'V' + (2 * y - 6) +
'A6,6 0 0 ' + e + ' ' + (0.5 * x) + ',' + (2 * y) +
'Z' +
'M' + (2.5 * x) + ',' + (y + 8) +
'V' + (2 * y - 8) +
'M' + (4.5 * x) + ',' + (y + 8) +
'V' + (2 * y - 8);
});
Fork of your fiddle: http://jsfiddle.net/gordonwoodhull/anz9gfy0/13/

d3.js v4: How to access parent group's datum index?

The description of the selection.data function includes an example with multiple groups (link) where a two-dimensional array is turned into an HTML table.
In d3.js v3, for lower dimensions, the accessor functions included a third argument which was the index of the parent group's datum:
td.text(function(d,i,j) {
return "Row: " + j;
});
In v4, this j argument has been replaced by the selection's NodeList. How do I access the parent group's datum index now?
Well, sometimes an answer doesn't provide a solution, because the solution may not exist. This seems to be the case.
According to Bostock:
I’ve merged the new bilevel selection implementation into master and also simplified how parents are tracked by using a parallel parents array.
A nice property of this new approach is that selection.data can
evaluate the values function in exactly the same manner as other
selection functions: the values function gets passed {d, i, nodes}
where this is the parent node, d is the parent datum, i is the parent
(group) index, and nodes is the array of parent nodes (one per group).
Also, the parents array can be reused by subselections that do not
regroup the selection, such as selection.select, since the parents
array is immutable.
This change restricts functionality—in the sense that you cannot
access the parent node from within a selection function, nor the
parent data, nor the group index — but I believe this is ultimately A
Good Thing because it encourages simpler code.
(emphasis mine)
Here's the link: https://github.com/d3/d3-selection/issues/47
So, it's not possible to get the index of the parent's group using selection (the parent's group index can be retrieved using selection.data, as this snippet bellow shows).
var testData = [
[
{x: 1, y: 40},
{x: 2, y: 43},
{x: 3, y: 12},
{x: 6, y: 23}
], [
{x: 1, y: 12},
{x: 4, y: 18},
{x: 5, y: 73},
{x: 6, y: 27}
], [
{x: 1, y: 60},
{x: 2, y: 49},
{x: 3, y: 16},
{x: 6, y: 20}
]
];
var svg = d3.select("body")
.append("svg")
.attr("width", 300)
.attr("height", 300);
var g = svg.selectAll(".groups")
.data(testData)
.enter()
.append("g");
var rects = g.selectAll("rect")
.data(function(d, i , j) { console.log("Data: " + JSON.stringify(d), "\nIndex: " + JSON.stringify(i), "\nNode: " + JSON.stringify(j)); return d})
.enter()
.append("rect");
<script src="https://d3js.org/d3.v4.min.js"></script>
My workaround is somewhat similar to Dinesh Rajan's, assuming the parent index is needed for attribute someAttr of g.nestedElt:
v3:
svg.selectAll(".someClass")
.data(nestedData)
.enter()
.append("g")
.attr("class", "someClass")
.selectAll(".nestedElt")
.data(Object)
.enter()
.append("g")
.attr("class", "nestedElt")
.attr("someAttr", function(d, i, j) {
});
v4:
svg.selectAll(".someClass")
.data(nestedData)
.enter()
.append("g")
.attr("class", "someClass")
.attr("data-index", function(d, i) { return i; }) // make parent index available from DOM
.selectAll(".nestedElt")
.data(Object)
.enter()
.append("g")
.attr("class", "nestedElt")
.attr("someAttr", function(d, i) {
var j = +this.parentNode.getAttribute("data-index");
});
I ended up defining an external variable "j" and then increment it whenever "i" is 0
example V3 snippet below.
rowcols.enter().append("rect")
.attr("x", function (d, i, j) { return CalcXPos(d, j); })
.attr("fill", function (d, i, j) { return GetColor(d, j); })
and in V4, code converted as below.
var j = -1;
rowcols.enter().append("rect")
.attr("x", function (d, i) { if (i == 0) { j++ }; return CalcXPos(d, j); })
.attr("fill", function (d, i) { return GetColor(d, j); })
If j is the nodeList...
j[i] is the current node (eg. the td element),
j[i].parentNode is the level-1 parent (eg. the row element),
j[i].parentNode.parentNode is the level-2 parent (eg. the table element),
j[i].parentNode.parentNode.childNodes is the array of level-1 parents (eg. array of row elements) including the original parent.
So the question is, what is the index of the parent (the row) with respect to it's parent (the table)?
We can find this using Array.prototype.indexOf like so...
k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode);
You can see in the snippet below that the row is printed in each td cell when k is returned.
var testData = [
[
{x: 1, y: 1},
{x: 1, y: 2},
{x: 1, y: 3},
{x: 1, y: 4}
], [
{x: 2, y: 1},
{x: 2, y: 2},
{x: 2, y: 3},
{x: 2, y: 4}
], [
{x: 3, y: 4},
{x: 3, y: 4},
{x: 3, y: 4},
{x: 3, y: 4}
]
];
var tableData =
d3.select('body').selectAll('table')
.data([testData]);
var tables =
tableData.enter()
.append('table');
var rowData =
tables.selectAll('table')
.data(function(d,i,j){
return d;
});
var rows =
rowData.enter()
.append('tr');
var eleData =
rows.selectAll('tr')
.data(function(d,i,j){
return d;
});
var ele =
eleData.enter()
.append('td')
.text(function(d,i,j){
var k = Array.prototype.indexOf.call(j[i].parentNode.parentNode.childNodes,j[i].parentNode);
return k;
});
<script src="https://d3js.org/d3.v4.min.js"></script>
Reservations
This approach is using DOM order as a proxy for data index. In many cases, I think this is a viable band-aid solution if this is no longer possible in D3 (as reported in this answer).
Some extra effort in manipulating the DOM selection to match data might be needed. As an example, filtering j[i].parentNode.parentNode.childNodes for <tr> elements only in order to determine the row -- generally speaking the childNodes array may not match the selection and could contain extra elements/junk.
While this is not a cure-all, I think it should work or could be made to work in most cases, presuming there is some logical connection between DOM and data that can be leveraged which allows you to use DOM child index as a proxy for data index.
Here's an example of how to use the selection.each() method. I don't think it's messy, but it did slow down the render on a large matrix. Note the following code assumes an existing table selection and a call to update().
update(matrix) {
var self = this;
var tr = table.selectAll("tr").data(matrix);
tr.exit().remove();
tr.enter().append("tr");
tr.each(addCells);
function addCells(data, rowIndex) {
var td = d3.select(this).selectAll("td")
.data(function (d) {
return d;
});
td.exit().remove();
td.enter().append("td");
td.attr("class", function (d) {
return d === 0 ? "dead" : "alive";
});
td.on("click", function(d,i){
matrix[rowIndex][i] = d === 1 ? 0 : 1; // rowIndex now available for use in callback.
});
}
setTimeout(function() {
update(getNewMatrix(matrix))
}, 1000);
},
Assume you want to do a nested selectiom, and your
data is some array where each element in turn
contains an array, let's say "values". Then you
have probably some code like this:
var aInnerSelection = oSelection.selectAll(".someClass") //
.data(d.values) //
...
You can replace the array with the values by a new array, where
you cache the indices within the group.
var aInnerSelection = oSelection.selectAll(".someClass") //
.data(function (d, i) {
var aData = d.values.map(function mapValuesToIndexedValues(elem, index) {
return {
outerIndex: i,
innerIndex: index,
datum: elem
};
})
return aData;
}, function (d, i) {
return d.innerIndex;
}) //
...
Assume your outer array looks like this:
[{name "X", values: ["A", "B"]}, {name "y", values: ["C", "D"]}
With the first approach, the nested selection brings you from here
d i
------------------------------------------------------------------
root dummy X {name "X", values: ["A", "B"]} 0
dummy Y {name "Y", values: ["C", "D"]} 1
to here.
d i
------------------------------------------------------------------
root X A "A" 0
B "B" 1
Y C "C" 2
D "D" 3
With the augmented array, you end up here instead:
d i
------------------------------------------------------------------
root X A {datum: "A", outerIndex: 0, innerIndex: 0} 0
B {datum: "B", outerIndex: 0, innerIndex: 1} 1
Y C {datum: "C", outerIndex: 1, innerIndex: 0} 2
D {datum: "D", outerIndex: 1, innerIndex: 1} 3
So you have within the nested selections, in any function(d,i), all
information you need.
Here's a snippet I crafter after re-remembering this usage of .each for nesting, I thought it may be useful to others who end up here. This examples creates two layers of circles, and the parent group index is used to determine the color of the circles - white for the circles in the first layer, and black for the circles in the top layer (only two layers in this case).
const nested = nest().key(layerValue).entries(data);
let layerGroups = g.selectAll('g.layer').data(nested);
layerGroups = layerGroups.enter().append('g').attr('class', 'layer')
.merge(layerGroups);
layerGroups.each(function(layerEntry, j) {
const circles = select(this)
.selectAll('circle').data(layerEntry.values);
circles.enter().append('circle')
.merge(circles)
.attr('cx', d => xScale(xValue(d)))
.attr('cy', d => yScale(yValue(d)))
.attr('r', d => radiusScale(radiusValue(d)))
.attr('fill', j === 0 ? 'white' : 'black'); // <---- Access parent index.
});
My solution was to embed this information in the data provided to d3js
data = [[1,2,3],[4,5,6],[7,8,9]]
flattened_data = data.reduce((acc, v, i) => {
v.forEach((d, j) => {
data_item = { i, j, d };
acc.push(data_item);
});
return acc;
}, []);
Then you can access i, j and d from the data arg of the function
td.text(function(d) {
// Can access i, j and original data here
return "Row: " + d.j;
});

d3 drawing arrows tips

In this example :
http://jsfiddle.net/maxl/mNmYH/2/
If I enlarge the circles, ex:
var radius = 30; // (is 6 in the jsFiddle)
var circle = svg.append("svg:g").selectAll("circle")
.data(force.nodes())
.enter().append("svg:circle")
.attr("r", radius)
What is the best way to properly adjust the drawing of the arrow
so that it points to the radius of the circle ?
Thanks
You asked for the "best way to properly adjust the drawing of the arrow ".
I cannot claim the following approach is the "best" way, and I look forward to other answers, but here is one method to tackle this issue.
http://jsfiddle.net/Y9Qq3/2/
Relevant updates are noted below.
...
var w = 960,
h = 500
markerWidth = 6,
markerHeight = 6,
cRadius = 30, // play with the cRadius value
refX = cRadius + (markerWidth * 2),
refY = -Math.sqrt(cRadius),
drSub = cRadius + refY;
...
svg.append("svg:defs").selectAll("marker")
.data(["suit", "licensing", "resolved"])
.enter().append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -5 10 10")
.attr("refX", refX)
.attr("refY", refY)
.attr("markerWidth", markerWidth)
.attr("markerHeight", markerHeight)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-5L10,0L0,5");
...
function tick() {
path.attr("d", function (d) {
var dx = d.target.x - d.source.x,
dy = (d.target.y - d.source.y),
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + (dr - drSub) + "," + (dr - drSub) + " 0 0,1 " + d.target.x + "," + d.target.y;
});
...

Different states in D3/Coffee Bubble chart

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

Resources