Create a gantt chart using d3.js [closed] - javascript

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
This is the kind of data that I have:
[
{'TotalTime': 10, 'Hour': 0, 'Name': 'Sam'},
{'TotalTime': 15, 'Hour': 1, 'Name': 'Bob'},
{'TotalTime': 300, 'Hour': 2, 'Name': 'Tom'},
... and so on till,
{'TotalTime': 124, 'Hour': 23, 'Name': 'Jon'}
]
Data for every hour of the day. And I wish to create a Gantt Chart from it where size of bars is based on TotalTime.
Names on the y axis and Hour on the x.
Is it possible to make a Gantt chart without start time and end time on d3.js?

It's possible, but you'd need to draw it yourself if you're using D3.js. So If you've made bar charts before, along those lines, setup some axes, add them to an SVG and use them to convert the data into rectangles you'll put on the chart, then label the rects with the names from the data. D3.js does not include a layout for this. If you haven't done so already, go through the tutorials: Let’s Make a Bar Chart, Parts I, II & III, then move on to looking at a custom time axis example, and the related APIs.
There are many other libraries that build on D3.js, like C3 that provide prefab charts (like D3's layouts) but I'm not aware of one that does gantt style charts. There is one example Gantt chart out there (that randomly adds tasks into various scaled time views from 1 hour to 1 week) but I found it more confusing than my own time blocks below. YMMV.
I made a more calendar looking chart with d3js that you can read through here: https://github.com/dlamblin/timeblocks. You've got different input data formatting, but you could adapt this in a pinch and swap axes for the rotation. Assuming you're willing to urgently do this asap.
To make the aforementioned easier to read & view I took it apart into a JSfiddle example.
And here's just the JavaScript inlined into the answer (this is not a gantt chart, it's a vertical layout of scheduled blocks of time over a 7 day week):
var timeFmt = d3.time.format.utc('%H.%M'),
weekdaydef = 'Mon Tue Wed Thu Fri Sat Sun'.split(' '),
weekdayseq = 'Mon Tue Wed Thu Fri Sat Sun'.split(' '),
axes = [null, null, null, null];
function hm(i) {
var s = i.toFixed(2);
return timeFmt.parse((s.length < 4) ? '0' + s : s);
}
var timeData = [
{key: "m1","wday": 0,"begin": hm(6.00),"end": hm(7.00),
label: "Rising, dress etc\n\retc"},
{key: "m2","wday": 0,"begin": hm(7.00),"end": hm(7.30),
label: "Prep Sophie"},
{key: "m3","wday": 0,"begin": hm(7.30),"end": hm(8.00),
label: "Transit to School"
}, {
key: "t1",
"wday": 1,
"begin": hm(6.00),
"end": hm(7.00),
label: "Rising, dress etc"
}, {
key: "t2",
"wday": 1,
"begin": hm(17.00),
"end": hm(18.00),
label: "call"
},
{
key: "w1",
"wday": 2,
"begin": hm(6.00),
"end": hm(7.00),
'color': 0,
label: "Rising, dress etc"
}, {
key: "w2",
"wday": 2,
"begin": hm(7.00),
"end": hm(7.30),
'color': 0,
label: "Prep Sophie"
}, {
key: "w3",
"wday": 2,
"begin": hm(7.30),
"end": hm(8.00),
'color': 1,
label: "Transit to School"
}, {
key: "w4",
"wday": 2,
"begin": hm(8.00),
"end": hm(9.00),
'color': 2,
label: "Read Emails"
}, {
key: "w5",
"wday": 2,
"begin": hm(9.00),
"end": hm(10.00),
'color': 2,
label: "Write Emails"
}, {
key: "w6",
"wday": 2,
"begin": hm(10.00),
"end": hm(13.00),
'color': 3,
label: "Job"
}, {
key: "w7",
"wday": 2,
"begin": hm(13.00),
"end": hm(14.00),
'color': 4,
label: "Lunch & Meditation"
}, {
key: "w8",
"wday": 2,
"begin": hm(14.00),
"end": hm(15.00),
'color': 5,
label: "Pick Sophie & Home"
}, {
key: "w9",
"wday": 2,
"begin": hm(15.00),
"end": hm(18.00),
'color': 0,
label: "Clean"
}, {
key: "wa",
"wday": 2,
"begin": hm(18.00),
"end": hm(19.00),
'color': 0,
label: "Plan"
}, {
key: "wb",
"wday": 2,
"begin": hm(19.00),
"end": hm(20.00),
'color': 0,
label: "Wrap: Read Email & Clean"
},
{
key: "r1",
"wday": 3,
"begin": hm(6.00),
"end": hm(7.00),
label: "Rising, dress etc"
},
{
key: "f1",
"wday": 4,
"begin": hm(6.00),
"end": hm(7.00),
label: "Rising, dress etc"
}
];
timeData = timeData.sort(function(a, b) {
var o = d3.ascending(a.wday, b.wday);
return o === 0 ? d3.ascending(a.begin, b.begin) : o;
});
// Spacing out times by 5 minutes... see display
// var timeDataMap = d3.map(timeData, function(d) {return d.key;});
// timeDataMap.forEach(function(k,v) {v.end.setMinutes(v.end.getMinutes()-5);});
// timeData = timeDataMap.values();
var scale, colors = d3.scale.category10();
colors.range(d3.range(10).map(
function(i) {
return d3.rgb(colors(i)).brighter(1.25).toString();
}));
function d3UpdateScales() {
var svg = d3.select('#timeblock')[0][0],
margin = {
top: 25,
right: 80,
bottom: 25,
left: 80
},
width = svg.clientWidth - margin.left - margin.right,
height = svg.clientHeight - margin.top - margin.bottom;
return scale = {
margin: margin,
width: width,
height: height,
time: d3.time.scale.utc() // not d3.scale.linear()
.domain([d3.min(timeData, function(d) {
return d.begin
}),
d3.max(timeData, function(d) {
return d.end
})
])
.rangeRound([0, height]),
days: d3.scale.ordinal()
.domain(weekdayseq)
.rangePoints([0, width]),
week: d3.scale.ordinal()
.domain(weekdayseq)
.rangeRoundBands([0, width], 0.05),
}
}
function d3Update() {
var scale = d3UpdateScales();
// Update…
var svg = d3.select('#timeblock');
if (svg.select('g.view')[0][0] == null) {
svg.append('g').attr('class', 'view').attr('transform', 'translate(' + scale.margin.left + ',' + scale.margin.top + ')');
}
var g = svg.select('g.view').selectAll('g.data')
.data(timeData);
// Enter…
var ge = g.enter()
.append("g")
.attr('class', 'data');
ge.append("rect")
.attr("x", function(d) {
return scale.week(weekdaydef[d.wday]) + (scale.week.rangeBand() / 2)
})
.attr("y", function(d) {
var e = new Date(d.end);
e.setMinutes(e.getMinutes() - 5);
return scale.time(d.begin) + ((scale.time(e) - scale.time(d.begin)) / 2)
})
.attr("width", 0)
.attr("height", 0)
.attr("style", function(d) {
return ("color" in d) ? "fill:" + colors(d.color) : null
})
ge.append("text")
.attr("dy", "1.1em")
.text(function(d) {
return d.label;
});
// Exit…
g.exit().remove();
// Update…
g.select("rect")
.transition()
.attr("x", function(d) {
return scale.week(weekdaydef[d.wday])
})
.attr("y", function(d) {
return scale.time(d.begin)
})
.attr("width", function(d) {
return scale.week.rangeBand()
})
.attr("height", function(d) {
var e = new Date(d.end);
e.setMinutes(e.getMinutes() - 5);
return (scale.time(e) - scale.time(d.begin))
})
g.select("text")
.transition()
.attr("x", function(d) {
return scale.week(weekdaydef[d.wday]) + 5
})
.attr("y", function(d) {
return scale.time(d.begin)
});
axesAddOrUpdate(svg);
}
function axesAddOrUpdate(svg) {
var xaxis_t = d3.svg.axis().scale(scale.week).tickSize(13).orient('top'),
yaxis_r = d3.svg.axis().scale(scale.time).tickSize(7).orient('right'),
xaxis_b = d3.svg.axis().scale(scale.week).tickSize(13),
yaxis_l = d3.svg.axis().scale(scale.time).tickSize(7).orient('left');
// global axes array contains top, right, bottom, left axis.
if (null == axes[0]) {
axes[0] = svg.append("g").attr('class', 'axis')
.attr('transform', 'translate(' + String(scale.margin.left) + ',' + String(scale.margin.top) + ')')
.call(xaxis_t);
} else {
axes[0].transition().call(xaxis_t);
}
if (null == axes[2]) {
axes[2] = svg.append("g").attr('class', 'axis')
.attr('transform', 'translate(' + String(scale.margin.left) + ',' + String(scale.height + scale.margin.top) + ')')
.call(xaxis_b);
} else {
axes[2].transition().call(xaxis_b);
}
if (null == axes[3]) {
axes[3] = svg.append("g").attr('class', 'axis')
.attr('transform', 'translate(' + String(scale.margin.left - 5) + ',' + String(scale.margin.top) + ')')
.call(yaxis_l);
} else {
axes[3].transition().call(yaxis_l);
}
if (null == axes[1]) {
axes[1] = svg.append("g").attr('class', 'axis')
.attr('transform', 'translate(' + String(scale.margin.left + scale.width + 5) + ',' + String(scale.margin.top) + ')')
.call(yaxis_r);
} else {
axes[1].transition().call(yaxis_r);
}
}
window.onload = d3Update;
d3.select('#b_rotate').on("click", function(d, i, t) {
weekdayseq.push(weekdayseq.shift());
d3Update();
});

Related

Nest same object n times in array

Here is an example object array I'd like to transform into a much more complex structure:
[
{
"name": "Blue (3/8)"
},
{
"name": "Green (5/8)"
}
]
To give some context, I am trying to create a visual tree diagram demonstrating the probabilities of different-colored marbles being drawn out of a bag (that's why you see a number followed by a fraction). Below is the statistical-type tree I'm referring to.
I need to repeat adding this same array into an object in a nested format, assigning it to a key titled "children". This needs to be done n amount of times (based on the number of marble draws). The below object would be a result of nesting the same object array 3 times and then it would make a nice tree (using d3.js):
var treeData =
{
"children": [ // Level 1
{
"name": "Blue (3/8)",
"children": [ // Level 2
{ "name": "Blue (3/8)",
"children": [ // Level 3
{ "name": "Blue (3/8)" },
{ "name": "Green (5/8)" }
]
},
{ "name": "Green (5/8)",
"children": [ // Level 3
{ "name": "Blue (3/8)" },
{ "name": "Green (5/8)" }
]
}
]
},
{
"name": "Green (5/8)",
"children": [ // Level 2
{ "name": "Blue (3/8)",
"children": [ // Level 3
{ "name": "Blue (3/8)" },
{ "name": "Green (5/8)" }
]
},
{ "name": "Green (5/8)",
"children": [ // Level 3
{ "name": "Blue (3/8)" },
{ "name": "Green (5/8)" }
]
}
]
}
]
};
// set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
width = 660 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
// declares a tree layout and assigns the size
var treemap = d3.tree()
.size([height, width]);
// assigns the data to a hierarchy using parent-child relationships
var nodes = d3.hierarchy(treeData, function(d) {
return d.children;
});
// maps the node data to the tree layout
nodes = treemap(nodes);
// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom),
g = svg.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// adds the links between the nodes
var link = g.selectAll(".link")
.data( nodes.descendants().slice(1))
.enter().append("path")
.attr("class", "link")
.attr("d", function(d) {
return "M" + d.y + "," + d.x
+ "C" + (d.y + d.parent.y) / 2 + "," + d.x
+ " " + (d.y + d.parent.y) / 2 + "," + d.parent.x
+ " " + d.parent.y + "," + d.parent.x;
});
// adds each node as a group
var node = g.selectAll(".node")
.data(nodes.descendants())
.enter().append("g")
.attr("class", function(d) {
return "node" +
(d.children ? " node--internal" : " node--leaf"); })
.attr("transform", function(d) {
return "translate(" + d.y + "," + d.x + ")"; });
// adds the circle to the node
node.append("circle")
.attr("r", 10);
// adds the text to the node
node.append("text")
.attr("dy", ".35em")
.attr("x", function(d) { return d.children ? -13 : 13; })
.style("text-anchor", function(d) {
return d.children ? "end" : "start"; })
.text(function(d) { return d.data.name; });
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text { font: 12px sans-serif; }
.node--internal text {
text-shadow: 0 1px 0 #fff, 0 -1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
This happens to be the format of the object needed for d3.js to display the desired output in a tree. Here is the closest thing I could salvage to get my desired output:
var objectArray = [{
name: "Blue (5/8)"
}, {
name: "Green (3/8)"
}, {
name: "Blue (5/8)"
}, {
name: "Green (3/8)"
}, {
name: "Blue (5/8)"
}, {
name: "Green (3/8)"
}];
var newArr = objectArray.reverse().reduce((acc, item) => {
return [{
...item,
children: acc
}]
});
console.log(newArr);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
You could take a simple mapping with a look for the wanted depth.
const
create = (data, depth) => data.map((o, i) => ({
name: `${o.name} (${o.count}/${o.total})`,
...(depth > 1 ? { children: create(data.map(({ name, total, count }, j) => ({ name, count: count - (i === j), total: total - 1 })), depth - 1) } : {})
})),
data = [{ name: "Blue", count: 3, total: 8 }, { name: "Green", count: 5, total: 8 }],
result = create(data, 3);
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can use a recursive function:
const probabilityDrawingMarblesWithoutReplacement = ( marbles, remainingDraws ) => {
let totalMarbles = 0;
for ( let colour in marbles )
{
totalMarbles += Math.max( marbles[colour], 0 );
}
const output = [];
for ( let colour in marbles )
{
const amount = marbles[colour];
if ( amount <= 0 )
{
continue;
}
const colourOutput = {
"name": `${colour} (${amount}/${totalMarbles})`
}
if ( remainingDraws > 1 )
{
const remainingMarbles = Object.assign( {}, marbles );
remainingMarbles[colour]--;
colourOutput.children = probabilityDrawingMarblesWithoutReplacement( remainingMarbles, remainingDraws - 1 );
}
output.push( colourOutput );
}
return output;
}
console.log( probabilityDrawingMarblesWithoutReplacement( {"blue":3,"green":5}, 4 ) );
.as-console-wrapper { max-height: 100% !important; top: 0; }

D3.js line chart using the wrong keys when using general update pattern

I am using a D3.js to create a line chart using the general update pattern. I have two different types of data. The first type uses an abbreviated month key and the other the day of the month.
The problem I am experiencing is that the line doesn't transitions properly from one data type to the other. I have read some documentation and it stated that when updating d3 updates the lines using the index of each element. But you can change this by defining which variable D3 should watch when updating the chart.
So in order to accomplish this I declared inside the data function that D3 should use the key variable in the data arrays to check if two data points are the same. But in my snippet example at the bottom you can see that the update doesn't work properly. Instead of loading the full new line from the bottom. It transitions the first line into the second one but they clearly have a different key.
I have updated the code:
The problem wasn't explained correctly. I want to update the line where each point on the line should interpolate to the next point. Which in the snippet in the bottom is working. If it switches from the first to the second array, where all the keys are the same. The line should do as in the snippet and just interpolate.
But if I enter a completely new data with all new keys(like in the third array in the snippet), it should show the line which interpolates from the bottom(just like when entering the line the first time the application is loaded) of the chart and not interpolates from the its previous position. This is because in the project I am using the line also consists of points(circles) and these also transition from the bottom when using a new array.
this.area = this.area
.data([data], d => d.key)
new Vue({
el: "#app",
data() {
return {
index: 0,
data: [
[{
key: "Jan",
value: 5787
},
{
key: "Feb",
value: 6387
},
{
key: "Mrt",
value: 7375
},
{
key: "Apr",
value: 6220
},
{
key: "Mei",
value: 6214
},
{
key: "Jun",
value: 5205
},
{
key: "Jul",
value: 5025
},
{
key: "Aug",
value: 4267
},
{
key: "Sep",
value: 6901
},
{
key: "Okt",
value: 5800
},
{
key: "Nov",
value: 7414
},
{
key: "Dec",
value: 6547
}
],
[{
key: "Jan",
value: 4859
},
{
key: "Feb",
value: 5674
},
{
key: "Mrt",
value: 6474
},
{
key: "Apr",
value: 7464
},
{
key: "Mei",
value: 6454
},
{
key: "Jun",
value: 5205
},
{
key: "Jul",
value: 6644
},
{
key: "Aug",
value: 5343
},
{
key: "Sep",
value: 5363
},
{
key: "Okt",
value: 5800
},
{
key: "Nov",
value: 4545
},
{
key: "Dec",
value: 5454
}
],
[{
"key": 1,
"value": 4431
},
{
"key": 2,
"value": 5027
},
{
"key": 3,
"value": 4586
},
{
"key": 4,
"value": 7342
},
{
"key": 5,
"value": 6724
},
{
"key": 6,
"value": 6070
},
{
"key": 7,
"value": 5137
},
{
"key": 8,
"value": 5871
},
{
"key": 9,
"value": 6997
},
{
"key": 10,
"value": 6481
},
{
"key": 11,
"value": 5194
},
{
"key": 12,
"value": 4428
},
{
"key": 13,
"value": 4790
},
{
"key": 14,
"value": 5825
},
{
"key": 15,
"value": 4709
},
{
"key": 16,
"value": 6867
},
{
"key": 17,
"value": 5555
},
{
"key": 18,
"value": 4451
},
{
"key": 19,
"value": 7137
},
{
"key": 20,
"value": 5353
},
{
"key": 21,
"value": 5048
},
{
"key": 22,
"value": 5169
},
{
"key": 23,
"value": 6650
},
{
"key": 24,
"value": 5918
},
{
"key": 25,
"value": 5679
},
{
"key": 26,
"value": 5546
},
{
"key": 27,
"value": 6899
},
{
"key": 28,
"value": 5541
},
{
"key": 29,
"value": 7193
},
{
"key": 30,
"value": 5006
},
{
"key": 31,
"value": 6580
}
]
]
}
},
mounted() {
// set the dimensions and margins of the graph
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 30
},
width = 500 - margin.left - margin.right;
this.height = 200 - margin.top - margin.bottom;
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
this.svg = d3
.select("#my_dataviz")
.append("svg")
.attr(
"viewBox",
`0 0 ${width + margin.left + margin.right} ${this.height +
margin.top +
margin.bottom}`
)
.attr("preserveAspectRatio", "xMinYMin")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// set the ranges
this.xScale = d3
.scalePoint()
.range([0, width])
.domain(
this.data.map(function(d) {
return d.key;
})
)
.padding(0.5);
this.yScale = d3.scaleLinear().rangeRound([this.height, 0]);
this.yScale.domain([0, 7000]);
// Draw Axis
this.xAxis = d3.axisBottom(this.xScale);
this.xAxisDraw = this.svg
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${this.height})`);
this.yAxis = d3
.axisLeft(this.yScale)
.tickValues([0, 7000])
.tickFormat(d => {
if (d > 1000) {
d = Math.round(d / 1000);
d = d + "K";
}
return d;
});
this.yAxisDraw = this.svg.append("g").attr("class", "y axis");
this.update(this.data[this.index]);
},
methods: {
swapData() {
if (this.index === 2) this.index = 0;
else this.index++;
this.update(this.data[this.index]);
},
update(data) {
// Update scales.
this.xScale.domain(data.map(d => d.key));
this.yScale.domain([0, 7000]);
// Set up transition.
const dur = 1000;
const t = d3.transition().duration(dur);
// Update line.
this.line = this.svg.selectAll(".line")
this.line = this.line
.data([data], d => d.key)
.join(
enter => {
enter
.append("path")
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", "#206BF3")
.attr("stroke-width", 4)
.attr(
"d",
d3
.line()
.x(d => {
return this.xScale(d.key);
})
.y(() => {
return this.yScale(0);
})
)
.transition(t)
.attr(
"d",
d3
.line()
.x(d => {
return this.xScale(d.key);
})
.y(d => {
return this.yScale(d.value);
})
);
},
update => {
update.transition(t).attr(
"d",
d3
.line()
.x(d => {
return this.xScale(d.key);
})
.y(d => {
return this.yScale(d.value);
})
);
},
exit => exit.remove()
);
// Update Axes.
this.yAxis.tickValues([0, 7000]);
if (data.length > 12) {
this.xAxis.tickValues(
data.map((d, i) => {
if (i % 3 === 0) return d.key;
else return 0;
})
);
} else {
this.xAxis.tickValues(
data.map(d => {
return d.key;
})
);
}
this.yAxis.tickValues([0, 7000]);
this.xAxisDraw.transition(t).call(this.xAxis.scale(this.xScale));
this.yAxisDraw.transition(t).call(this.yAxis.scale(this.yScale));
}
}
})
<div id="app">
<button #click="swapData">Swap</button>
<div id="my_dataviz" class="flex justify-center"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://d3js.org/d3.v6.js"></script>
[UPDATE: According to the comments the code was updated to change with a new line starting from the bottom when the set of keys in the new data are different]
here is a contribution for a better understanding of the problem, and a possible answer.
There is some misuse of the key element. When you define the key of the line, it's for d3 to know that one line is binded to that key. In this case, your key is binded to the path.
When you add
this.line = this.line
.data([data], d => d.key)
d3 binds the selection to [data] and will generate exactly one element ([data].length = 1)
for this elements, d = data, hence d.key = null. This is the reason why you are not adding multiple lines, because your paths always got the key = null.
So, on the first time everything works as planned, you started a path as zero and then moves it to the final position with the transition.
This path has d attribute generate by the d3.line with a format like M x1 y1 L x2 y2 L x3 y3 ... L x12 y 12. Exactly 12 points for the first time.
When you swap the data, d3 will check the key (null again) and will consider this as an update.
So, it will interpolate the current path to a new one with the new data.
The issue here is that there are no keys to bind the values. As you have now 31 points, it will interpolate the first 12 points (which is the part that you see moving) and add the remaining points (13 to 31). Of course, these last points don't have transition, because they didn't exist.
A possible solution for your case is to use a custom interpolator (that you can build) and use an attrTween to do the interpolation.
Fortunately, someone built one already: https://unpkg.com/d3-interpolate-path/build/d3-interpolate-path.min.js
SO here is a working solution
new Vue({
el: "#app",
data() {
return {
index: 0,
data: [
[{
key: "Jan",
value: 5787
},
{
key: "Feb",
value: 6387
},
{
key: "Mrt",
value: 7375
},
{
key: "Apr",
value: 6220
},
{
key: "Mei",
value: 6214
},
{
key: "Jun",
value: 5205
},
{
key: "Jul",
value: 5025
},
{
key: "Aug",
value: 4267
},
{
key: "Sep",
value: 6901
},
{
key: "Okt",
value: 5800
},
{
key: "Nov",
value: 7414
},
{
key: "Dec",
value: 6547
}
],
[{
"key": 1,
"value": 4431
},
{
"key": 2,
"value": 5027
},
{
"key": 3,
"value": 4586
},
{
"key": 4,
"value": 7342
},
{
"key": 5,
"value": 6724
},
{
"key": 6,
"value": 6070
},
{
"key": 7,
"value": 5137
},
{
"key": 8,
"value": 5871
},
{
"key": 9,
"value": 6997
},
{
"key": 10,
"value": 6481
},
{
"key": 11,
"value": 5194
},
{
"key": 12,
"value": 4428
},
{
"key": 13,
"value": 4790
},
{
"key": 14,
"value": 5825
},
{
"key": 15,
"value": 4709
},
{
"key": 16,
"value": 6867
},
{
"key": 17,
"value": 5555
},
{
"key": 18,
"value": 4451
},
{
"key": 19,
"value": 7137
},
{
"key": 20,
"value": 5353
},
{
"key": 21,
"value": 5048
},
{
"key": 22,
"value": 5169
},
{
"key": 23,
"value": 6650
},
{
"key": 24,
"value": 5918
},
{
"key": 25,
"value": 5679
},
{
"key": 26,
"value": 5546
},
{
"key": 27,
"value": 6899
},
{
"key": 28,
"value": 5541
},
{
"key": 29,
"value": 7193
},
{
"key": 30,
"value": 5006
},
{
"key": 31,
"value": 6580
}
]
]
}
},
mounted() {
// set the dimensions and margins of the graph
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 30
},
width = 500 - margin.left - margin.right;
this.height = 200 - margin.top - margin.bottom;
// append the svg obgect to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
this.svg = d3
.select("#my_dataviz")
.append("svg")
.attr(
"viewBox",
`0 0 ${width + margin.left + margin.right} ${this.height +
margin.top +
margin.bottom}`
)
.attr("preserveAspectRatio", "xMinYMin")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// set the ranges
this.xScale = d3
.scalePoint()
.range([0, width])
.domain(
this.data.map(function(d) {
return d.key;
})
)
.padding(0.5);
this.yScale = d3.scaleLinear().rangeRound([this.height, 0]);
this.yScale.domain([0, 7000]);
// Draw Axis
this.xAxis = d3.axisBottom(this.xScale);
this.xAxisDraw = this.svg
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${this.height})`);
this.yAxis = d3
.axisLeft(this.yScale)
.tickValues([0, 7000])
.tickFormat(d => {
if (d > 1000) {
d = Math.round(d / 1000);
d = d + "K";
}
return d;
});
this.yAxisDraw = this.svg.append("g").attr("class", "y axis");
this.update(this.data[this.index]);
},
methods: {
swapData() {
if (this.index === 0) this.index = 1;
else this.index = 0;
this.update(this.data[this.index]);
},
update(data) {
// Update scales.
this.xScale.domain(data.map(d => d.key));
this.yScale.domain([0, 7000]);
// Set up transition.
const dur = 1000;
const t = d3.transition().duration(dur);
const line = d3
.line()
.x(d => {
return this.xScale(d.key);
})
.y((d) => {
return this.yScale(d.value);
});
// Update line.
this.line = this.svg.selectAll(".line")
this.line = this.line
.data([data], d => d.reduce((key, elem) => key + '_' + elem.key, ''))
.join(
enter => {
enter
.append("path")
.attr("class", "line")
.attr("fill", "none")
.attr("stroke", "#206BF3")
.attr("stroke-width", 4)
.attr(
"d",
d3
.line()
.x(d => {
return this.xScale(d.key);
})
.y(() => {
return this.yScale(0);
})
)
.transition(t)
.attr(
"d", (d) => line(d)
);
},
update => {
update
.transition(t)
.attrTween('d', function(d) {
var previous = d3.select(this).attr('d');
var current = line(d);
return d3.interpolatePath(previous, current);
});
},
exit => exit.remove()
);
// Update Axes.
this.yAxis.tickValues([0, 7000]);
if (data.length > 12) {
this.xAxis.tickValues(
data.map((d, i) => {
if (i % 3 === 0) return d.key;
else return 0;
})
);
} else {
this.xAxis.tickValues(
data.map(d => {
return d.key;
})
);
}
this.yAxis.tickValues([0, 7000]);
this.xAxisDraw.transition(t).call(this.xAxis.scale(this.xScale));
this.yAxisDraw.transition(t).call(this.yAxis.scale(this.yScale));
}
}
})
<div id="app">
<button #click="swapData">Swap</button>
<div id="my_dataviz" class="flex justify-center"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://d3js.org/d3.v6.js"></script>
<script src="https://unpkg.com/d3-interpolate-path/build/d3-interpolate-path.min.js"></script>
I'm not directly answering your question yet (sorry!) because this might be a better solution. It's possible to interpolate between lines with a different number of points, which may provide a better experience?
There's a d3-interpolate-path plugin that can handle a different number of points being present in the path, but still create a reasonably smooth animation by inserting placeholder points into the line.
There's a really good explanation of how this works, as well as some examples of it working https://bocoup.com/blog/improving-d3-path-animation .
Answer
If you really do want to animate from zero each time, then you need to check the keys match the last set of keys.
Create a d3 local store
const keyStore = d3.local();
Get the keys from last render (element wants to be your line)
const oldKeys = keyStore.get(element);
Determine if the keys match:
const newKeys = data.map(d => d.key);
// arraysEqual - https://stackoverflow.com/a/16436975/21061
const keysMatch = arraysEqual(oldKeys, newKeys);
Change your interpolation on keysMatch (see previous ternary):
update.transition(t)
.attrTween('d', function(d) {
var previous = keysMatch ? d3.select(this).attr('d') : 0;
var current = line(d);
return d3.interpolatePath(previous, current);
});

d3 - dougnut bubble pie chart

I am trying to create a merged pie/bubble chart.
-- looking for a starting bubble chart base - maybe this one.
http://jsfiddle.net/xsafy/
^ need to cluster these bubbles - maybe sub bubbles per slice.
//bubble chart base.
http://jsfiddle.net/NYEaX/1450/
(function() {
var diameter = 250;
var svg = d3.select('#graph').append('svg')
.attr('width', diameter)
.attr('height', diameter);
var bubble = d3.layout.pack()
.size([diameter, diameter])
.value(function(d) {
return d.size;
})
.padding(3);
var color = d3.scale.ordinal()
.domain(["Lorem ipsum", "dolor sit", "amet", "consectetur", "adipisicing"])
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56"]);
function randomData() {
var data1 = {
"children": [
{
name: "AA",
className: "aa",
size: 170
},
{
name: "BB",
className: "bb",
size: 393
},
{
name: "CC",
className: "cc",
size: 293
},
{
name: "DD",
className: "dd",
size: 89
}
]
};
var data2 = {
"children": [
{
name: "AA",
className: "aa",
size: 120
},
{
name: "BB",
className: "bb",
size: 123
},
{
name: "CC",
className: "cc",
size: 193
},
{
name: "DD",
className: "dd",
size: 289
}
]
};
var j = Math.floor((Math.random() * 2) + 1);
console.log("j", j);
if (j == 1) {
return data1;
} else {
return data2;
}
}
change(randomData());
d3.select(".randomize")
.on("click", function() {
change(randomData());
});
function change(data) {
console.log("data", data);
// generate data with calculated layout values
var nodes = bubble.nodes(data)
.filter(function(d) {
return !d.children;
}); // filter out the outer bubble
var vis = svg.selectAll('circle')
.data(nodes);
vis.enter()
.insert("circle")
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
.attr('r', function(d) {
return d.r;
})
.style("fill", function(d) {
return color(d.name);
})
.attr('class', function(d) {
return d.className;
});
vis
.transition().duration(1000)
vis.exit()
.remove();
};
})();
//doughnut chart base.
derived from these examples.
http://bl.ocks.org/dbuezas/9306799
https://bl.ocks.org/mbostock/1346410
http://jsfiddle.net/NYEaX/1452/
var svg = d3.select("#graph")
.append("svg")
.append("g")
svg.append("g")
.attr("class", "slices");
svg.append("g")
.attr("class", "labels");
svg.append("g")
.attr("class", "lines");
var width = 560,
height = 450,
radius = Math.min(width, height) / 2;
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.value;
});
var arc = d3.svg.arc()
.outerRadius(radius * 0.85)
.innerRadius(radius * 0.83);
var outerArc = d3.svg.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9);
svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var key = function(d) {
return d.data.label;
};
var color = d3.scale.ordinal()
.domain(["Lorem ipsum", "dolor sit", "amet", "consectetur", "adipisicing"])
.range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56"]);
function randomData() {
var data1 = [{
"label": "AA",
"value": 0.911035425558026
}, {
"label": "BB",
"value": 0.08175111844879179
}, {
"label": "CC",
"value": 0.25262439557273275
}, {
"label": "DD",
"value": 0.8301366989535612
}, {
"label": "EE",
"value": 0.0517762265780517
}];
var data2 = [{
"label": "AA",
"value": 0.243879179
}, {
"label": "BB",
"value": 0.243879179
}, {
"label": "CC",
"value": 0.2342439557273275
}, {
"label": "DD",
"value": 0.2349535612
}, {
"label": "EE",
"value": 0.2345780517
}];
var j = Math.floor((Math.random() * 2) + 1);
if (j == 1) {
return data1;
} else {
return data2;
}
}
change(randomData());
d3.select(".randomize")
.on("click", function() {
change(randomData());
});
function change(data) {
/* ------- PIE SLICES -------*/
var slice = svg.select(".slices").selectAll("path.slice")
.data(pie(data), key);
slice.enter()
.insert("path")
.style("fill", function(d) {
return color(d.data.label);
})
.attr("class", "slice");
slice
.transition().duration(1000)
.attrTween("d", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
})
slice.exit()
.remove();
};
This is as close I have got so far.. I've merged the two charts together as such - although there is a bug still trying to update the bubbles -- at least get them to scale/morph/move/animate. Ideally I want them to stick close to their group segments..
here is the fiddle.
https://jsfiddle.net/tk5xog0g/1/
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>doughpie</title>
<link rel="stylesheet" href="css/generic.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<button class="randomize">randomize</button>
<div data-role="doughpie" data-width="450" data-height="450" id="graph"></div>
<script>
//__invoke pie chart
$('[data-role="doughpie"]').each(function(index) {
createDoughnut(this);
});
function bubbledata(data){
//loop through data -- and MERGE children
var childs = [];
$.each(data, function( index, value ) {
childs.push(value.children);
});
var merged = [].concat.apply([], childs);//flatterns multidimensional array
return $.extend(true, {}, {"children": merged});// return deep clone
}
function createDoughnut(el){
var width = $(el).data("width"),
height = $(el).data("height"),
radius = Math.min(width, height) / 2;
var svg = d3.select($(el)[0])
.append("svg")
.attr("width", width)
.attr("height", height)
//_create doughpie shell
var doughpie = svg.append("g")
.attr("class", "doughpie");
doughpie.append("g")
.attr("class", "slices");
doughpie.append("g")
.attr("class", "labels");
doughpie.append("g")
.attr("class", "lines");
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.value;
});
var arc = d3.svg.arc()
.outerRadius(radius * 0.85)
.innerRadius(radius * 0.83);
var outerArc = d3.svg.arc()
.innerRadius(radius * 0.9)
.outerRadius(radius * 0.9);
doughpie.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var key = function(d) {
return d.data.label;
};
var color = d3.scale.ordinal()
.range(["#46a2de", "#7b3cce", "#31d99c", "#de5942", "#ffa618"]);
//_create doughpie shell
//_create bubble
var diameter = width/2;//take half/width
var bubs = svg.append("g")
.attr("class", "bubs");
bubs.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
var bubble = d3.layout.pack()
.size([diameter, diameter])
.value(function(d) {
return d.size;
})
.padding(3);
//_create bubble
function randomData() {
var data1 = [{
"label": "AA",
"value": 0.911035425558026,
"children": [
{
name: "some text aa",
group: "AA",
size: 120
}
]
}, {
"label": "BB",
"value": 0.08175111844879179,
"children": [
{
name: "some text bb",
group: "BB",
size: 123
}
]
}, {
"label": "CC",
"value": 0.25262439557273275,
"children": [
{
name: "some text cc",
group: "CC",
size: 193
}
]
}, {
"label": "DD",
"value": 0.8301366989535612,
"children": [
{
name: "some text dd",
group: "DD",
size: 29
},
{
name: "some text dd",
group: "DD",
size: 289
}
]
}, {
"label": "EE",
"value": 0.0517762265780517,
"children": [
{
name: "some text ee",
group: "EE",
size: 389
},
{
name: "some text ee",
group: "EE",
size: 89
}
]
}];
var data2 = [{
"label": "AA",
"value": 0.243879179,
"children": [
{
name: "some text aa",
group: "AA",
size: 120
}
]
}, {
"label": "BB",
"value": 0.243879179,
"children": [
{
name: "some text bb",
group: "BB",
size: 123
}
]
}, {
"label": "CC",
"value": 0.2342439557273275,
"children": [
{
name: "some text cc",
group: "CC",
size: 193
}
]
}, {
"label": "DD",
"value": 0.2349535612,
"children": [
{
name: "some text dd",
group: "DD",
size: 29
},
{
name: "some text dd",
group: "DD",
size: 289
}
]
}, {
"label": "EE",
"value": 0.2345780517,
"children": [
{
name: "some text ee",
group: "EE",
size: 389
},
{
name: "some text ee",
group: "EE",
size: 89
}
]
}];
var j = Math.floor((Math.random() * 2) + 1);
if (j == 1) {
return data1;
} else {
return data2;
}
}
change(randomData());
d3.select(".randomize")
.on("click", function() {
change(randomData());
});
function change(data) {
/* ------- ANIMATE PIE SLICES -------*/
var slice = doughpie.select(".slices").selectAll("path.slice")
.data(pie(data), key);
slice.enter()
.insert("path")
.style("fill", function(d) {
return color(d.data.label);
})
.style("transform", function(d, i){
//return "translate(0, 0)";
})
.attr("class", "slice");
slice
.transition().duration(1000)
.attrTween("d", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
})
slice.exit()
.remove();
/* ------- ANIMATE PIE SLICES -------*/
/* ------- ANIMATE BUBBLES -------*/
// generate data with calculated layout values
var data = bubbledata(data);
var nodes = bubble.nodes(data)
.filter(function(d) {
return !d.children;
}); // filter out the outer bubble
var bubbles = bubs.selectAll('circle')
.data(nodes);
bubbles.enter()
.insert("circle")
.attr('transform', function(d) {
return 'translate(' + d.x + ',' + d.y + ')';
})
.attr('r', function(d) {
return d.r;
})
.style("fill", function(d) {
return color(d.group);
});
bubbles
.transition().duration(1000)
bubbles.exit()
.remove();
/* ------- ANIMATE BUBBLES -------*/
};
}
</script>
</body>
</html>
I thought I will try and first plot mid dot points on the arcs -- and maybe from THOSE -- start to get a batch of zone coordinates to master control the coloured bubbles.
I've been trying to follow this -- How to get coordinates of slices along the edge of a pie chart?
https://jsfiddle.net/tk5xog0g/28/
/*placing mid dots*/
var midDotsArrayCooridnates = [];
slice
.each(function(d) {
x = 0 + (radius * 0.85) * Math.cos(d.startAngle);
y = 0 + (radius * 0.85) * Math.sin(d.startAngle);
var obj = {
"x": x,
"y": y
}
midDotsArrayCooridnates.push(obj);
});
$.each(midDotsArrayCooridnates, function(index, value) {
var dot = doughpie.select(".slicedots").append('circle')
.attr('cx', value.x)
.attr('cy', value.y)
.attr('r', 5)
.style("fill", "red")
.attr('class', "ccc")
});
/*placing mid dots*/

Centering stacked bars vertically in d3

I'm trying to visualize sold items from timeseries. I'm using Nick Rabinowitz's alluvial chart as a basis but have made few modifications to it. Everything else looks good but I would like to center the stacked bars vertically.
This is what my chart looks like at the moment:
/*Original code obtained from http://nickrabinowitz.com/projects/d3/alluvial/alluvial.html*/
var data = {
"times": [
[{
"id": "item1",
"nodeName": "Item 1 50/2015",
"nodeValue": 9,
"incoming": []
}, {
"id": 1,
"nodeName": "Item 2 50/2015",
"nodeValue": 6,
"incoming": []
}, {
"id": 2,
"nodeName": "Item 3 50/2015",
"nodeValue": 3,
"incoming": []
}],
[{
"id": "item12",
"nodeName": "Item 1 51/2015",
"nodeValue": 8,
"incoming": []
}, {
"id": 4,
"nodeName": "Item 2 51/2015",
"nodeValue": 2,
"incoming": []
}, {
"id": 5,
"nodeName": "Item 3 51/2015",
"nodeValue": 5,
"incoming": []
}],
[{
"id": 6,
"nodeName": "Item 1 52/2015",
"nodeValue": 1,
"incoming": []
}, {
"id": 7,
"nodeName": "Item 2 52/2015",
"nodeValue": 7,
"incoming": []
}, {
"id": 8,
"nodeName": "Item 3 50/2015",
"nodeValue": 4,
"incoming": []
}]
],
"links": [{
"source": "item1",
"target": "item12",
"outValue": 9,
"inValue": 8
}, {
"source": "item12",
"target": 6,
"outValue": 8,
"inValue": 1
}, {
"source": 1,
"target": 4,
"outValue": 6,
"inValue": 2
}, {
"source": 4,
"target": 7,
"outValue": 2,
"inValue": 7
}, {
"source": 2,
"target": 5,
"outValue": 3,
"inValue": 5
}
/*,
{
"source": 5,
"target": 8,
"outValue": 5,
"inValue": 4
}*/
]
};
/* Process Data */
// make a node lookup map
var nodeMap = (function() {
var nm = {};
data.times.forEach(function(nodes) {
nodes.forEach(function(n) {
nm[n.id] = n;
// add links and assure node value
n.links = [];
n.incoming = [];
n.nodeValue = n.nodeValue || 0;
})
});
console.log(nm);
return nm;
})();
// attach links to nodes
data.links.forEach(function(link) {
console.log(link);
nodeMap[link.source].links.push(link);
nodeMap[link.target].incoming.push(link);
});
// sort by value and calculate offsets
data.times.forEach(function(nodes) {
var nCumValue = 0;
nodes.sort(function(a, b) {
return d3.descending(a.nodeValue, b.nodeValue)
});
nodes.forEach(function(n, i) {
n.order = i;
n.offsetValue = nCumValue;
nCumValue += n.nodeValue;
// same for links
var lInCumValue;
var lOutCumValue;
// outgoing
if (n.links) {
lOutCumValue = 0;
n.links.sort(function(a, b) {
return d3.descending(a.outValue, b.outValue)
});
n.links.forEach(function(l) {
l.outOffset = lOutCumValue;
lOutCumValue += l.outValue;
});
}
// incoming
if (n.incoming) {
lInCumValue = 0;
n.incoming.sort(function(a, b) {
return d3.descending(a.inValue, b.inValue)
});
n.incoming.forEach(function(l) {
l.inOffset = lInCumValue;
lInCumValue += l.inValue;
});
}
})
});
data = data.times;
// calculate maxes
var maxn = d3.max(data, function(t) {
return t.length
}),
maxv = d3.max(data, function(t) {
return d3.sum(t, function(n) {
return n.nodeValue
})
});
/* Make Vis */
// settings and scales
var w = 960,
h = 500,
gapratio = .5,
padding = 7,
x = d3.scale.ordinal()
.domain(d3.range(data.length))
.rangeBands([0, w], gapratio),
y = d3.scale.linear()
.domain([0, maxv])
.range([0, h - padding * maxn]),
area = d3.svg.area()
.interpolate('monotone');
// root
var vis = d3.select("#alluvial")
.append("svg:svg")
.attr("width", w)
.attr("height", h);
// time slots
var times = vis.selectAll('g.time')
.data(data)
.enter().append('svg:g')
.attr('class', 'time')
.attr("transform", function(d, i) {
return "translate(" + x(i) + ",0)"
});
// node bars
var nodes = times.selectAll('g.node')
.data(function(d) {
return d
})
.enter().append('svg:g')
.attr('class', 'node');
nodes.append('svg:rect')
.attr('fill', 'steelblue')
.attr('y', function(n, i) {
return y(n.offsetValue) + i * padding;
})
.attr('width', x.rangeBand())
.attr('height', function(n) {
return y(n.nodeValue)
})
.append('svg:title')
.text(function(n) {
return n.nodeName
});
// links
var links = nodes.selectAll('path.link')
.data(function(n) {
return n.links || []
})
.enter().append('svg:path')
.attr('class', 'link')
.attr('d', function(l, i) {
var source = nodeMap[l.source];
var target = nodeMap[l.target];
var gapWidth = x(0);
var bandWidth = x.rangeBand() + gapWidth;
var sourceybtm = y(source.offsetValue) +
source.order * padding +
y(l.outOffset) +
y(l.outValue);
var targetybtm = y(target.offsetValue) +
target.order * padding +
y(l.inOffset) +
y(l.inValue);
var sourceytop = y(source.offsetValue) +
source.order * padding +
y(l.outOffset);
var targetytop = y(target.offsetValue) +
target.order * padding +
y(l.inOffset);
var points = [
[x.rangeBand(), sourceytop],
[x.rangeBand() + gapWidth / 5, sourceytop],
[bandWidth - gapWidth / 5, targetytop],
[bandWidth, targetytop],
[bandWidth, targetybtm],
[bandWidth - gapWidth / 5, targetybtm],
[x.rangeBand() + gapWidth / 5, sourceybtm],
[x.rangeBand(), sourceybtm]
];
return area(points);
});
body {
margin: 3em;
}
.node {
stroke: #fff;
stroke-width: 2px;
}
.link {
fill: #000;
stroke: none;
opacity: .3;
}
.node {
stroke: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div id="alluvial"></div>
Here is a JSFiddle if you like to play around with the code.
The solution probably lies somewhere in counting the full height of the bar and calculating the node offsets from the centerpoint.
The way the original code is structured looks like to be calculating offsets per node and then using these offsets to calculate node positions. I probably need to be able to modify this calculated offset in someway at somepoint but I just can't figure out how and where. If it is even possible.
If that isn't possible, is there another way in d3 to achieve visually similar results?
You could try calculated the maximum full height using (I've just added the lines that change, the rest is the same):
//calculate the max full height
var maxHeight=0;
data.times.forEach(function(nodes,p) {
var curHeight=0;
nodes.forEach(function(n) {
curHeight+=n.nodeValue;
});
if(curHeight > maxHeight) maxHeight=curHeight
});
And then adding (maxHeight/2 - curHeight/2) to the offset, curHeight being the total height of the nodes for each band.
To do this you can add a couple lines to the loop calculating the offset:
// sort by value and calculate offsets
data.times.forEach(function(nodes,p) {
var nCumValue = 0;
nodes.sort(function(a, b) {
return d3.descending(a.nodeValue, b.nodeValue)
});
var bandHeight = 0;
nodes.forEach(function(n) {
bandHeight+=n.nodeValue;
});
nodes.forEach(function(n, i) {
n.order = i;
n.offsetValue = nCumValue + (maxHeight/2-bandHeight/2);
Here's a JSFiddle with these changes.

d3 transition with percentages using styleTween

I have a treemap rendered with d3. Since I want to be responsive and economical (not running js if I do not really have to) I am using percentages for the divs. But the transitions are some kind of wired using percentages. After reading this issue I have tried several styleTweens but I do not have any luck ...
How can I use transitions for percentage values in d3?
Here is a fiddle of the below code: http://jsfiddle.net/0z7p68wb/ (just click somewhere on the treemap to start the animation)
var target = d3.select("#target")
render = function(data, oldData) {
// our custom d3 code
console.log("render!", data, oldData);
// draw rectangles
var margin = {margin: 0.2, padding: 2},
width = 100 - margin.margin * 2,
height = 100 - margin.margin * 2;
var treemap = d3.layout.treemap()
.size([100, 100])
//.sticky(true)
.value(function(d) { return d.size; });
// bind data
var nodes = target.datum(data)
.selectAll(".node")
.data(treemap.nodes);
// transform existing nodes
if (data !== oldData)
nodes.transition()
.duration(1500)
.call(position);
// append new nodes
nodes.enter().append("div")
.attr("class", "node")
.style("position", "absolute")
.style("display", function(d,i) { return i==0 ? "none" : "block"})
.style("background-color", "silver")
.call(position)
;
// remove obsolete nodes
nodes.exit().remove();
// set position of nodes
function position() {
this.style("left", function(d) { return d.x + "%"; })
.style("top", function(d) { return d.y + "%"; })
.style("width", function(d) { return Math.max(0, d.dx) + "%"; })
.style("height", function(d) { return Math.max(0, d.dy) + "%"; })
}
}
tree1 = {
name: "tree",
children: [
{ name: "Word-wrapping comes for free in HTML", size: 16000 },
{ name: "animate makes things fun", size: 8000 },
{ name: "data data everywhere...", size: 5220 },
{ name: "display something beautiful", size: 3623 },
{ name: "flex your muscles", size: 984 },
{ name: "physics is religion", size: 6410 },
{ name: "query and you get the answer", size: 2124 }
]
};
tree2 = {
name: "tree",
children: [
{ name: "Word-wrapping comes for free in HTML", size: 8000 },
{ name: "animate makes things fun", size: 10000 },
{ name: "data data everywhere...", size: 2220 },
{ name: "display something beautiful", size: 6623 },
{ name: "flex your muscles", size: 1984 },
{ name: "physics is religion", size: 3410 },
{ name: "query and you get the answer", size: 2124 }
]
};
tree = tree1;
render(tree, tree);
d3.select("#target").on("click", function(){
console.log("click");
tree = tree == tree1 ? tree2 : tree1;
render(tree, {});
});
Got it!
// transform existing nodes
if (data !== oldData)
nodes.transition()
.duration(1500)
.call(position)
.styleTween('left', function(d,i,a){
return d3.interpolateString(this.style.left, d.x + "%")
})
.styleTween('top', function(d,i,a){;
return d3.interpolateString(this.style.top, d.y + "%")
})
.styleTween('width', function(d,i,a){;
return d3.interpolateString(this.style.width, Math.max(0, d.dx) + "%")
})
.styleTween('height', function(d,i,a){;
return d3.interpolateString(this.style.height, Math.max(0, d.dy) + "%")
})
;

Categories