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.
Related
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; }
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);
});
I have constructed a tree graph using html5, css3 but the nodes are static. Now I wanna make that graph dynamic. Here dynamic means suppose the node count increases and there are multiple children, now the graph will be generated with the new nodes and children.
This is just an example using vanilla JavaScript. I've taken a lot of inspiration from d3, including how to store the data as a self-referencing tree of nodes and how to traverse the tree breadth-first.
I've tried to comment the code as well as possible, I hope this gives you some inspiration. I'd try to improve the positioning of the labels a bit, and/or increase the margins so they're better visible.
const data = [{
id: 0,
label: "Command Sequence Starting",
},
{
id: 1,
parents: [0],
label: "W_SCMadl_refresh",
status: "done",
},
{
id: 2,
parents: [1],
label: "W_adl_photo",
status: "done",
},
{
id: 3,
parents: [2],
label: "W_adl_collect",
status: "done",
},
{
id: 4,
parents: [3],
label: "W_adl_collect_cr",
status: "done",
},
{
id: 5,
parents: [4],
label: "W_adl_sync",
status: "aborted",
},
{
id: 6,
parents: [5],
label: "W_adl_attach",
status: "waiting",
},
{
id: 7,
parents: [6],
label: "W_adl_attach",
status: "waiting",
},
{
id: 8,
parents: [7],
label: "W_adl_publish",
status: "waiting",
},
{
id: 9,
parents: [8],
label: "W_adl_ds_ws",
status: "waiting",
},
{
id: 10,
parents: [9],
label: "W64_Shared_Preq_mkdir",
status: "waiting",
},
{
id: 11,
parents: [10, 12],
label: "W64_mkCopyPreq",
status: "waiting",
},
{
id: 12,
parents: [0],
label: "WIN64_MCCMon",
status: "done",
},
];
// Make the data array a self-referencing tree, where each node has pointers to their
// parents and their children nodes
data
.filter(d => d.parents !== undefined)
.forEach(d => {
d.parents = data.filter(p => d.parents.includes(p.id));
d.parents.forEach(p => {
if (p.children === undefined) {
p.children = [];
}
p.children.push(d);
});
});
const root = data.find(d => d.parents === undefined);
// Breadth first traversal of the tree, excuting `fn` for every node
const forEach = (root, fn) => {
const stack = [root];
while (stack.length) {
const current = stack.shift();
if (current.children) {
stack.push(...current.children);
}
fn(current);
}
};
const svg = document.querySelector(".mv-sequence svg");
const margin = {
top: 20,
bottom: 20,
right: 20,
left: 20,
};
const width = +svg.getAttribute("width") - margin.left - margin.right;
const stepHeight = 40;
const namespace = "http://www.w3.org/2000/svg";
const gContainer = document.createElementNS(namespace, "g");
gContainer.setAttribute("transform", `translate(${margin.left},${margin.top})`);
svg.appendChild(gContainer);
const linksContainer = document.createElementNS(namespace, "g");
gContainer.appendChild(linksContainer);
const nodesContainer = document.createElementNS(namespace, "g");
gContainer.appendChild(nodesContainer);
// Give node a level. First complete this loop, then start drawing, because we want to
// be robust against not all parents having a level yet
forEach(
root,
d => {
if (d === root) {
d.level = 0;
return;
}
d.level = Math.max(...d.parents.map(p => p.level)) + 1;
}
);
forEach(
root,
d => {
// Position the node based on the number of siblings.
const siblings = data.filter(n => n.level === d.level);
// If the node is an only child. The root should be in the centre,
// any other node should be in the average of it's parents
if (siblings.length === 1) {
if (d.parents === undefined) {
d.x = width / 2;
} else {
d.x = d.parents.map(p => p.x).reduce((s, v) => s + v, 0) / d.parents.length;
}
return;
}
// Otherwise, divide the space evenly for all sibling nodes
const siblingIndex = siblings.indexOf(d);
const stepWidth = width / (siblings.length - 1);
if (siblings.length % 2 === 0) {
// Even number of siblings
d.x = stepWidth * siblingIndex;
} else {
// Odd number of siblings, the center one must be in the middle
d.x = width / 2 + stepWidth * (siblingIndex - (siblings.length - 1) / 2);
}
}
);
forEach(
root,
d => {
// Append a circle and `text` for all new nodes
d.y = d.level * stepHeight;
const nodeContainer = document.createElementNS(namespace, "g");
nodeContainer.setAttribute("transform", `translate(${d.x}, ${d.y})`);
nodeContainer.classList.add("mv-command", d.status);
nodesContainer.appendChild(nodeContainer);
const circle = document.createElementNS(namespace, "circle");
circle.setAttribute("r", stepHeight / 4);
nodeContainer.appendChild(circle);
const label = document.createElementNS(namespace, "text");
label.setAttribute("dx", stepHeight / 4 + 5);
label.textContent = d.label;
nodeContainer.appendChild(label);
// Append a link from every parent to this node
(d.parents || []).forEach(p => {
const link = document.createElementNS(namespace, "path");
let path = `M${p.x} ${p.y}`;
let dx = d.x - p.x;
let dy = d.y - p.y;
if (dy > stepHeight) {
// Move down to the level of the child node
path += `v${dy - stepHeight}`;
dy = stepHeight;
}
path += `s0 ${dy / 2}, ${dx / 2} ${dy / 2}`;
path += `s${dx / 2} 0, ${dx / 2} ${dy / 2}`;
link.setAttribute("d", path)
linksContainer.appendChild(link);
})
}
);
// Finally, set the height to fit perfectly
svg.setAttribute("height", Math.max(...data.map(d => d.level)) * stepHeight + margin.top + margin.bottom);
.mv-command.done {
fill: #477738;
}
.mv-command.aborted {
fill: #844138;
}
.mv-command.waiting {
fill: #808080;
}
.mv-command.disabled {
fill: #80808080;
}
.mv-command.running {
fill: #005686;
animation: mymove 2s infinite;
}
.mv-command>text {
dominant-baseline: middle;
font-size: 12px;
}
path {
fill: none;
stroke: darkgreen;
stroke-width: 3px;
}
<div class="mv-sequence">
<svg width="200"></svg>
</div>
I am currently working through this d3js force layout tutorial, with some custom modifications. Mostly, my interest is to introduce an abstraction layer between my back-end and d3.js visualization, which I am doing using JS classes. Right now I am mostly throwing away some attributes I have in my mock .json file, but this logic could grow to be more complex.
Here is a MWE that I cannot seem to get to work:
index.html:
<!DOCTYPE html>
<html>
<meta charset="utf-8">
<head>
</head>
<body>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="js/model.js"></script>
<script src="js/viz.js"></script>
</body>
</html>
model.js:
class Node {
constructor(id, name) {
this.id = id;
this.name = name;
this.outEdges = {};
this.inEdges = {};
}
}
class Edge {
constructor(node1, node2) {
this.source = node1;
this.target = node2;
}
}
class Graph {
constructor(nodes, edges) {
this.nodes = {}
for (let node of nodes) {
this.nodes[node.id] = node;
}
for (let edge of edges) {
this.nodes[edge.source].outEdges[edge.target] = edge;
this.nodes[edge.target].inEdges[edge.source] = edge;
}
this.edges = edges;
}
}
viz.js:
function loadJson() {
path = "mockup2.json";
$.getJSON(path, function(json) {
var nodes = [];
var edges = [];
for (let node of json.nodes) {
var temp = new Node(node.index, node.name);
nodes.push(temp);
}
for (let edge of json.edges) {
var temp = new Edge(edge.node1, edge.node2);
edges.push(temp);
}
var graph = new Graph(nodes, edges);
drawGraph(graph);
});
}
function drawGraph(graph) {
width = "90%";
height = "90%";
// set up svg and layout
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var force = d3.layout.force()
.gravity(.1)
.distance(20)
.charge(-30)
.size([width, height]);
// prep nodes and links for d3 viz
var d3nodes = [];
var temp = {};
for (var key in graph.nodes) {
node = graph.nodes[key]
temp = {}
temp.x = Math.random() * 100;
temp.y = Math.random() * 100;
temp.index = node.id;
d3nodes.push(temp);
}
d3edges = [];
var temp = {}
for (var key in graph.edges) {
edge = graph.edges[key];
temp = {}
temp.source = edge.source;
temp.target = edge.target;
temp.timediff = edge.timediff;
d3edges.push(temp);
}
// draw links and nodes
var svglinks = svg.selectAll('.link')
.data(d3edges)
.enter().append('line')
.attr('class', 'link');
var svgnodes = svg.selectAll('.node')
.data(d3nodes)
.enter().append('g')
.attr('class', 'node')
.call(force.drag);
force.nodes(d3nodes)
.links(d3edges)
.linkDistance(width/3)
.start();
// test svglinks values
svglinks.forEach(function(d) {
console.log(JSON.stringify(d));
});
force.on("tick", function() {
svglinks.attr("x1", function(d) {
console.log(JSON.stringify(d));
return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
svgnodes.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
// avoid console spam
force.stop();
});
}
$(document).ready(function() {
loadJson();
});
mockup2.json:
{
"nodes": [
{"index": 0, "name": 0, "timestamp": 1, "group": 1},
{"index": 1, "name": 0, "timestamp": 0, "group": 2},
{"index": 2, "name": 0, "timestamp": 0, "group": 3},
{"index": 3, "name": 0, "timestamp": 2, "group": 1},
{"index": 4, "name": 0, "timestamp": 0, "group": 2},
{"index": 5, "name": 0, "timestamp": 3, "group": 3},
{"index": 6, "name": 0, "timestamp": 2, "group": 3},
{"index": 7, "name": 0, "timestamp": 5, "group": 2},
{"index": 8, "name": 0, "timestamp": 8, "group": 2},
{"index": 9, "name": 0, "timestamp": 3, "group": 1}
],
"edges": [
{"node1": 1, "node2": 2},
{"node1": 2, "node2": 4},
{"node1": 4, "node2": 8},
{"node1": 4, "node2": 8},
{"node1": 8, "node2": 2},
{"node1": 5, "node2": 9}
]
}
The most damning thing to me is that the svglinks variable seems to be fine before outside of the force.on('tick') stuff, displaying sane values for x, y, px and py - I have included a console.log() call for you to check this too. However, printing the very same object inside of that function shows me null values for all these attributes, which make it possible for d3.js to render the graph correctly (at least in my intuition).
I also have a more "standard" example which uses the more standard d3.json() call to load a .json file which is preformatted with source and target attributes; this works perfectly. So my intuition is that I am not properly building the d3nodes and d3links variables above; however, comparing them across examples has shown me that they look exactly the same. I am a little dumbfounded here, I will admit!
Thanks in advance to everyone who is going to take the time to give this a go.
Here i am try to update json data and its D3 wheel view but i Don't know why its not update the D3 sunbrust and update the data
This is my code
// in html body
//css code
svg{
width: 100% !important;
background:#ffffff;
}
.slice {
cursor: pointer;
}
.slice .main-arc {
stroke: #fff;
stroke-width: 1px;
}
.slice .hidden-arc {
fill: none;
}
.slice text {
pointer-events: none;
dominant-baseline: middle;
text-anchor: middle;
}
#tooltip { background-color: white;
padding: 3px 5px;
border: 1px solid black;
text-align: center;}
div.tooltip {
position: absolute;
text-align: left;
width: auto;
height: auto;
padding: 8px;
font: 12px sans-serif;
background: #0a2538;
border: #0a2538 1px solid;
border-radius: 0px;
pointer-events: none;
color: white;
}
//D3 Script code for https://d3js.org/d3.v4.min.js
const initItems ={
"name": "Core root",
"id":3,
"description":"Lorem ipsum dolor sit amet",
"children": [
{
"name": "VM.HSN.5.A",
"children": [
{
"name": "eiusmod",
"children": [
{"name": "G.8.1", "id": 5},
{"name": "G.8.2", "id": 4}
]
},
{
"name": "F.8.0",
"children": [
{"name": "F.8.4", "id": 1},
{"name": "F.8.5", "id":1}
]
},
{
"name": "EE.8.5-CLX",
"children": [
{"name": "EE.8.5-CLX2", "id": 1}
]
}
]
},
{
"name": "NS.8.2-CLX",
"children": [
{"name": "NS.8.2-CLX4", "id": 1},
{"name": "NS.8.2-CLX5", "id": 1},
{
"name": "NS.8.0",
"children": [
{"name": "NS.8.2", "id": 1},
{"name": "NS.8.3", "id": 1}
]
},
{"name": "NS.8.1-CLX1", "id":1},
{"name": "NS.8.1-CLX2", "id": 1},
{"name": "NS.8.1-CLX3", "id":1},
{"name": "NS.8.1-CLX4", "id": 1},
{"name": "NS.8.1-CLX5", "id": 1}
]
}
]
}
var newItems = {
"name": "Second Root",
"children": [{
"name": "A2",
"children": [{
"name": "B4",
"size": 40
}, {
"name": "B5",
"size": 30
}, {
"name": "B6",
"size": 10
}]
}, {
"name": "A3",
"children": [{
"name": "B7",
"size": 50
}, {
"name": "B8",
"size": 15
}
]
}]
}
const width = window.innerWidth,
height = window.innerHeight,
maxRadius = (Math.min(width, height) / 2) - 20;
const formatNumber = d3.format(',d');
const x = d3.scaleLinear()
.range([0, 2 * Math.PI])
.clamp(true);
const y = d3.scaleSqrt()
.range([maxRadius*.1, maxRadius]);
const color = d3.scaleOrdinal(d3.schemeCategory20);
const partition = d3.partition();
const arc = d3.arc()
.startAngle(d => x(d.x0))
.endAngle(d => x(d.x1))
.innerRadius(d => Math.max(0, y(d.y0)))
.outerRadius(d => Math.max(0, y(d.y1)));
const middleArcLine = d => {
const halfPi = Math.PI/2;
const angles = [x(d.x0) - halfPi, x(d.x1) - halfPi];
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const middleAngle = (angles[1] + angles[0]) / 2;
const invertDirection = middleAngle > 0 && middleAngle < Math.PI; // On lower quadrants write text ccw
if (invertDirection) { angles.reverse(); }
const path = d3.path();
path.arc(0, 0, r, angles[0], angles[1], invertDirection);
return path.toString();
};
const textFits = d => {
const CHAR_SPACE = 6;
const deltaAngle = x(d.x1) - x(d.x0);
const r = Math.max(0, (y(d.y0) + y(d.y1)) / 2);
const perimeter = r * deltaAngle;
return d.data.name.length * CHAR_SPACE < perimeter;
};
const svg = d3.select('#vdata').append('svg')
.style('width', '100vw')
.style('height', '100vh')
.attr('viewBox', `${-width / 2} ${-height / 2} ${width} ${height}`)
.on('click', () => focusOn()); // Reset zoom on canvas click
var updateChart = function (items) {
// var root = items;
console.log(items);
//d3.json('dummy4.json', (error, root) => {
///if (error) throw error;
//start custom code
var root = d3.hierarchy(items, function(d) { return d.children })
.sum( function(d) {
if(d.children) {
return 0
} else {
return 2
}
});
//end custom code
//root = d3.hierarchy(root);
//var ad =root.sum(d => d.depth);
//root.sum(d => d.id);
const slice = svg.selectAll('g.slice')
.data(partition(root).descendants());
slice.exit().remove();
const newSlice = slice.enter()
.append('g').attr('class', 'slice')
.on('click', d => {
console.log(d.data.name);
d3.event.stopPropagation();
focusOn(d);
});
newSlice.append('title')
//.text(d => d.data.name + '\n' + d.data.id);
//.on("mouseover", mouseover);
newSlice.append('path')
.attr('class', 'main-arc')
.style('fill',colour)
//.style('fill', d => color((d.children ? d : d.parent).data.name))
//console.log(data.id)
.on("mouseover", mouseover)
.on("mouseout", mouseOutArc)
.attr('d', arc);
newSlice.append('path')
.attr('class', 'hidden-arc')
.attr('id', (_, i) => `hiddenArc${i}`)
.attr('d', middleArcLine);
const text = newSlice.append('text')
.attr('display', d => textFits(d) ? null : 'none');
// Add white contour
text.append('textPath')
.attr('startOffset','50%')
.attr('xlink:href', (_, i) => `#hiddenArc${i}` )
.text(function(d) {
return d.data.name;
})
.style('fill', 'none')
.style('stroke', '#fff')
.style('stroke-width', 5)
.style('stroke-linejoin', 'round');
text.append('textPath')
.attr('startOffset','50%')
.attr('xlink:href', (_, i) => `#hiddenArc${i}` )
.text(function(d) {
return d.data.name;
});
//});
}
// tooltip
var tooltip = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
function colour(d) {
//if (d.id==5) {
var colours;
console.log(d.data.id);
if(d.data.id==5)
{
// There is a maximum of two children!
colours ='#b00';
}else if(d.data.name=='Apps'){
colours='#cc0001';
}
else{
colours=color((d.children ? d : d.parent).data.name)
} // L*a*b* might be better here...
// return d3.hsl((a.h + b.h) / 2, a.s * 1.2, a.l / 1.2);
//}
return colours;
}
function mouseover(d) {
d3.select(this).style("cursor", "pointer")
var descript;
if(d.data.description!=null)
{
descript= "<br/><br/>"+d.data.description;
}else{
descript='';
}
tooltip.html(d.data.name + descript)
.style("opacity", 0.8)
.style("left", (d3.event.pageX) + 0 + "px")
.style("top", (d3.event.pageY) - 0 + "px");
}
function mouseOutArc(){
d3.select(this).style("cursor", "default")
tooltip.style("opacity", 0);
}
function focusOn(d = { x0: 0, x1: 1, y0: 0, y1: 1 }) {
// Reset to top-level if no data point specified
const transition = svg.transition()
.duration(750)
.tween('scale', () => {
const xd = d3.interpolate(x.domain(), [d.x0, d.x1]),
yd = d3.interpolate(y.domain(), [d.y0, 1]);
return t => { x.domain(xd(t)); y.domain(yd(t)); };
});
transition.selectAll('path.main-arc')
.attrTween('d', d => () => arc(d));
transition.selectAll('path.hidden-arc')
.attrTween('d', d => () => middleArcLine(d));
transition.selectAll('text')
.attrTween('display', d => () => textFits(d) ? null : 'none');
moveStackToFront(d);
//
function moveStackToFront(elD) {
svg.selectAll('.slice').filter(d => d === elD)
.each(function(d) {
this.parentNode.appendChild(this);
if (d.parent) { moveStackToFront(d.parent); }
})
}
}
updateChart(initItems);
I am trying to update json data and regenerate the D3 sunburst wheel but it not update.
On developer console it show the json data updated but it not update the text in wheel and not properly regenerate the D3 sunburst wheel.
Please help me out.!
Thanks in advance.
I used this code and it works for me great..!
setTimeout(function () {d3.selectAll("svg > *").remove(); updateChart(newItems); }, 2000);