D3 Concentric/Nested Donut Chart - javascript

I have created a donut chart with D3 that uses two data sets and displays slightly different size rings for each. I would like to add labels to the data set(for a legend), but the selectAll("path") expects each data set to be a simple array of values, so I have been unable to add the labels.
Below is the code I have so far and a fiddle:
Fiddle
var dataset = {
apples: [13245, 28479, 11111, 11000, 3876],
oranges: [53245, 28479, 19697, 24037, 19654],
};
var width = d3.select('#duration').node().offsetWidth,
height = 300,
cwidth = 33;
var colorO = ['#1352A4', '#2478E5', '#5D9CEC', '#A4C7F4', '#DBE8FB'];
var colorA = ['#58A53B', '#83C969', '#A8D996', '#CDE9C3', '#E6F4E1'];
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select("#duration svg")
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var path = gs.selectAll("path")
.data(function(d, i) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i, j) {
if (j == 0) {
return colorO[i];
} else {
return colorA[i];
}
})
.attr("d", function(d, i, j) {
if (j == 0) {
return arc.innerRadius(75 + cwidth * j - 17).outerRadius(cwidth * (j + 2.9))(d);
} else {
return arc.innerRadius(75 + cwidth * j - 5).outerRadius(cwidth * (j + 2.5))(d);
}
});

expects each data set to be a simple array of values
This is not true. You can and should use an array of objects. Then use the value accessor to target a property of your object for the pie function. Here's how I'd re-factor your code:
var dataset = {
apples: [{
value: 13245,
color: '#1352A4',
label: 'one'
}, {
value: 28479,
color: '#5D9CEC',
label: 'two'
}, {
value: 11111,
color: '#1352A4',
label: 'three'
}, {
value: 11000,
color: '#A4C7F4',
label: 'four'
}, {
value: 3876,
color: '#DBE8FB',
label: 'five'
}],
oranges: [{
value: 53245,
color: '#58A53B',
label: 'one'
}, {
value: 28479,
color: '#83C969',
label: 'two'
}, {
value: 19697,
color: '#A8D996',
label: 'three'
}, {
value: 24037,
color: '#CDE9C3',
label: 'four'
}, {
value: 19654,
color: '#E6F4E1',
label: 'five'
}]
};
var width = d3.select('#duration').node().offsetWidth,
height = 300,
cwidth = 33;
var pie = d3.layout.pie()
.sort(null)
.value(function(d) {
return d.value;
})
var innerArc = d3.svg.arc()
.innerRadius(58)
.outerRadius(cwidth * 2.9);
var outerArc = d3.svg.arc()
.innerRadius(70 + cwidth)
.outerRadius(cwidth * 3.5);
var svg = d3.select("#duration svg")
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var en = gs.selectAll("path")
.data(function(d, i) {
return pie(d);
})
.enter();
en.append("path")
.attr("fill", function(d) {
return d.data.color;
})
.attr("d", function(d, i, j) {
return j === 0 ? innerArc(d) : outerArc(d);
});
en.append("text")
.text(function(d) {
return d.data.label;
})
.attr("transform", function(d, i, j) {
return j === 0 ? "translate(" + innerArc.centroid(d) + ")" : "translate(" + outerArc.centroid(d) + ")";
});
<script src="https://d3js.org/d3.v3.min.js"></script>
<div id="duration">
<svg style="height:300px;width:100%"></svg>
</div>

Related

d3 how to transitions image together with slice in pie chart

I have a project where I draw a doughnut/pie chart. Each slice in the chart consists of a group which in term has a path(the slice) and an image.
In order to transition the slices I have used the general update pattern to update the slices and added two attrTween methods to handle the transitions of the slices.
In order to add the images to the slices I have first put each slice into a group. Then I add an image to each group and use the arc.centroid method in order to position the images in the center of each slice. This works very well the first time I load the in the chart. But when the chart updates, the images stay in the center of their group as it was positioned previously.
I have console logged both the d element and the output of the arc.controid method with that d element. You can see clearly that the coordinates of the d elements don't change and that is why the images don't get an updated new position. But I do not get why they still get the old version of this d element and not the new updated one.
.append("svg:image")
.attr("transform", (d, i) => {
if (i === 1) {
console.log(d);
console.log(this.arc.centroid(d));
}
var x = this.arc.centroid(d)[0] - image_width / 2;
var y = this.arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
This is a snippet of the code I am using. I have tried to keep this as short as possible. But all the elements that are included are needed in order to get this snippet to work for this particular problem:
var margin = 1;
this.width = 250;
this.height = 250;
this.index = 0;
this.radius = Math.min(this.width, this.height) / 2 - margin;
this.svg = d3
.select(".canvas")
.append("svg")
.attr("width", this.width)
.attr("height", this.height)
.append("g")
.attr(
"transform",
"translate(" + this.width / 2 + "," + this.height / 2 + ")"
);
this.pie = d3
.pie()
.sort(null)
.value(d => d.value);
this.arc = d3
.arc()
.outerRadius(100)
.innerRadius(50);
const setSlicesOnDoughnut = (data) => {
this.arcs = this.svg.selectAll("path").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
.attr("class", "arc")
.attr("fill", "#206BF3")
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.each(function(d) {
this._current = d;
})
.transition()
.duration(1000)
.attrTween("d", arcTweenEnter);
},
update => {
update
.transition()
.duration(1000)
.attrTween("d", arcTweenUpdate);
},
exit => {
exit.remove();
}
);
}
const addImagesToSlices = () => {
var image_width = 20;
var image_height = 20;
this.svg.selectAll("image").remove();
this.svg
.selectAll("g")
.append("svg:image")
.attr("transform", (d, i) => {
if (i === 1) {
console.log(d);
console.log(this.arc.centroid(d));
}
var x = this.arc.centroid(d)[0] - image_width / 2;
var y = this.arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
.attr("class", "logo")
.attr("class", function(d) {
return `${d.data.key}-logo`;
})
.attr("href", d => d.data.icon)
.attr("width", image_width)
.attr("height", image_height)
.attr("opacity", 0)
.transition()
.duration(1500)
.attr("opacity", 1);
}
var data = [
[{
key: "One",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
],
[{
key: "One",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
]
]
const arcTweenEnter = (d) => {
var i = d3.interpolate(d.endAngle, d.startAngle);
return t => {
d.startAngle = i(t);
return this.arc(d);
};
}
const arcTweenUpdate = (d, i, n) => {
var interpolate = d3.interpolate(n[i]._current, d);
n[i]._current = d;
return t => {
return this.arc(interpolate(t));
};
}
setSlicesOnDoughnut(data);
addImagesToSlices();
const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
if (this.index === 0) this.index = 1;
else this.index = 0;
setSlicesOnDoughnut(data);
addImagesToSlices();
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>
The issue is that you never update the datum bound to the g. Let's look at how you enter and update the wedges:
this.arcs = this.svg.selectAll("path").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
...
},
update => {
update
.transition()
...
},
You select all the paths and bind data to them (selectAll("path")). On enter you append a g, which means the g gets a bound datum, as does the child path. On update however, as you've only selected paths, you only bind new data to the paths. The parent g is left with the original bound datum.
Instead, let's select the parent g with selectAll, and tweak our update function slightly to account for this:
// select parent g elements instead of paths:
this.arcs = this.svg.selectAll("g").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
...
},
update => {
update
.select("path") // select the child path.
.transition()
...
},
Now when we (re-)append the images, they are using the most recent datum bound to their parent g:
var margin = 1;
this.width = 250;
this.height = 250;
this.index = 0;
this.radius = Math.min(this.width, this.height) / 2 - margin;
this.svg = d3
.select(".canvas")
.append("svg")
.attr("width", this.width)
.attr("height", this.height)
.append("g")
.attr(
"transform",
"translate(" + this.width / 2 + "," + this.height / 2 + ")"
);
this.pie = d3
.pie()
.sort(null)
.value(d => d.value);
this.arc = d3
.arc()
.outerRadius(100)
.innerRadius(50);
const setSlicesOnDoughnut = (data) => {
this.arcs = this.svg.selectAll("g").data(this.pie(data[this.index]));
this.arcs.join(
enter => {
enter
.append("g")
.append("path")
.attr("class", "arc")
.attr("fill", "#206BF3")
.attr("stroke", "#2D3546")
.style("stroke-width", "2px")
.each(function(d) {
this._current = d;
})
.transition()
.duration(1000)
.attrTween("d", arcTweenEnter);
},
update => {
update
.select("path")
.transition()
.duration(1000)
.attrTween("d", arcTweenUpdate);
},
exit => {
exit.remove();
}
);
}
const addImagesToSlices = () => {
var image_width = 20;
var image_height = 20;
this.svg.selectAll("image").remove();
this.svg
.selectAll("g")
.append("svg:image")
.attr("transform", (d, i) => {
if (i === 1) {
console.log(this.arc.centroid(d));
}
var x = this.arc.centroid(d)[0] - image_width / 2;
var y = this.arc.centroid(d)[1] - image_height / 2;
return "translate(" + x + "," + y + ")";
})
.attr("class", "logo")
.attr("class", function(d) {
return `${d.data.key}-logo`;
})
.attr("href", d => d.data.icon)
.attr("width", image_width)
.attr("height", image_height)
.attr("opacity", 0)
.transition()
.duration(1500)
.attr("opacity", 1);
}
var data = [
[{
key: "One",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 10,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
],
[{
key: "One",
value: 30,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Two",
value: 15,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Three",
value: 20,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
},
{
key: "Four",
value: 100,
icon: "http://files.gamebanana.com/img/ico/sprays/4f68c8d10306a.png"
}
]
]
const arcTweenEnter = (d) => {
var i = d3.interpolate(d.endAngle, d.startAngle);
return t => {
d.startAngle = i(t);
return this.arc(d);
};
}
const arcTweenUpdate = (d, i, n) => {
var interpolate = d3.interpolate(n[i]._current, d);
n[i]._current = d;
return t => {
return this.arc(interpolate(t));
};
}
setSlicesOnDoughnut(data);
addImagesToSlices();
const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
if (this.index === 0) this.index = 1;
else this.index = 0;
setSlicesOnDoughnut(data);
addImagesToSlices();
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>

How to rotate text around its centroid (vertically flip) in SVG / D3?

I have text objects labeling points that are evenly spaced around a circle. Thanks to this article, I am able to correctly position both the points and text objects but the labels on the left hemisphere of the circle need to be rotated 180 degrees (flipped vertically) to be more legible.
I thought I could rotate the text object about its own origin before rotating it to the appropriate position around the circle but was unable to determine how to locate the center position of each text object.
How can I rotate text objects about their center for those on the left hemisphere of the circle (angle>= PI/2 && angle<=PI*1.5)? Or is there a better technique to use?
<style type="text/css">
* {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 13px;
}
circle {
fill: steelblue;
fill-opacity: .8;
}
circle:hover {
fill: orange;
fill-opacity: .8;
}
</style>
<div id="canvas"></div>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.min.js"></script>
<script type="text/javascript">
(function () {
var paddding = 250;
var createNodes = function () {
var nodeData = [
{ id: 0, label: 'AAA' },
{ id: 1, label: 'BBB' },
{ id: 2, label: 'CCC' },
{ id: 3, label: 'DDD' },
{ id: 4, label: 'EEE' },
{ id: 5, label: 'FFF' },
{ id: 6, label: 'GGG' },
{ id: 7, label: 'HHH' }
];
var radius = 100;
var nodes = [],
width = (radius * 2) + paddding,
height = (radius * 2) + paddding,
angle,
x,
y,
i;
var numNodes = nodeData.length;
for (i = 0; i < numNodes; i++) {
angle = (i / (numNodes / 2)) * Math.PI;
x = (radius * Math.cos(angle)) + (width / 2);
y = (radius * Math.sin(angle)) + (width / 2);
nodes.push({ 'id': i, 'x': x, 'y': y, 'label': nodeData[i].label, 'angle': angle });
}
return nodes;
}
var createSvg = function (radius, callback) {
d3.selectAll('svg').remove();
var svg = d3.select('#canvas').append('svg:svg')
.attr('width', (radius * 2) + paddding)
.attr('height', (radius * 2) + paddding);
callback(svg);
}
var createElements = function (svg, nodes, elementRadius) {
element = svg.selectAll('circle')
.data(nodes)
.enter().append('svg:circle')
.attr('r', elementRadius)
.attr('cx', function (d, i) { return d.x; })
.attr('cy', function (d, i) { return d.y; });
element = svg.selectAll('text')
.data(nodes)
.enter().append('svg:text')
.text(function (d, i) { return d.label + " - " + d.angle.toFixed(2) + ", " + (d.angle*180/Math.PI); })
.attr('x', function (d, i) { return nodes[0].x + 15; }) // add 15 for spacing off point
.attr('y', function (d, i) { return nodes[0].y; })
.attr("dy", ".35em")
.style("alignment-baseline","middle")
.style("text-anchor", "start")
.attr("transform", function(d,i) {
return "rotate(" + (d.angle * 180) / Math.PI + ", 225, 225)";})
;
}
var draw = function () {
var radius = 100;
var nodes = createNodes();
createSvg(radius, function (svg) {
createElements(svg, nodes, 10);
});
}
$(document).ready(function () {
draw();
});
})();
</script>
If you want to reverse the labels for those on the left side of the circle. You can achieve different ways. One way is by modifying three attributes of the text as you append it:
.attr('x', function (d, i) { return nodes[0].x + 15; })
.style("text-anchor", "start")
.attr("transform", function(d,i) {
return "rotate(" + (d.angle * 180) / Math.PI + ", 225, 225)"
})
If you modify only some of these, you might not get the results you are looking for.
Modification of text-end
This is needed as your text will start away from the point you are defining, and as the text may have variable length, defining a start point will be more complex than necessary. For points you need to flip, you'll need to use:
.style("text-anchor", "end")
Modification of the transform and x
The text needs to be rotated 180 degrees so that it is right way up; however, if you modify this function to add 180 degrees to any text, then the text will appear on the wrong side of the display. So, you'll need to set x to a new value too, so that it appears on the correct side of the display:
.attr('x', function (d, i) { return nodes[0].x - 215; }) // radius * 2, add 15 for spacing off point
.attr("transform", function(d,i) {
return "rotate(" + ((d.angle * 180) / Math.PI - 180) + ", 225, 225)"
})
All together, that looks like:
(function () {
var paddding = 250;
var createNodes = function () {
var nodeData = [
{ id: 0, label: 'AAA' },
{ id: 1, label: 'BBB' },
{ id: 2, label: 'CCC' },
{ id: 3, label: 'DDD' },
{ id: 4, label: 'EEE' },
{ id: 5, label: 'FFF' },
{ id: 6, label: 'GGG' },
{ id: 7, label: 'HHH' }
];
var radius = 100;
var nodes = [],
width = (radius * 2) + paddding,
height = (radius * 2) + paddding,
angle,
x,
y,
i;
var numNodes = nodeData.length;
for (i = 0; i < numNodes; i++) {
angle = (i / (numNodes / 2)) * Math.PI;
x = (radius * Math.cos(angle)) + (width / 2);
y = (radius * Math.sin(angle)) + (width / 2);
nodes.push({ 'id': i, 'x': x, 'y': y, 'label': nodeData[i].label, 'angle': angle });
}
return nodes;
}
var createSvg = function (radius, callback) {
d3.selectAll('svg').remove();
var svg = d3.select('#canvas').append('svg:svg')
.attr('width', (radius * 2) + paddding)
.attr('height', (radius * 2) + paddding);
callback(svg);
}
var createElements = function (svg, nodes, elementRadius) {
element = svg.selectAll('circle')
.data(nodes)
.enter().append('svg:circle')
.attr('r', elementRadius)
.attr('cx', function (d, i) { return d.x; })
.attr('cy', function (d, i) { return d.y; });
element = svg.selectAll('text')
.data(nodes)
.enter().append('svg:text')
.text(function (d, i) { return d.label + " - " + d.angle.toFixed(2) + ", " + (d.angle*180/Math.PI); })
.attr('x', function (d, i) { return nodes[0].x - 215; }) // radius * 2, add 15 for spacing off point
.attr('y', function (d, i) { return nodes[0].y; })
.attr("dy", ".35em")
.style("alignment-baseline","middle")
.style("text-anchor", "end")
.attr("transform", function(d,i) {
return "rotate(" + ((d.angle * 180) / Math.PI - 180) + ", 225, 225)";})
;
}
var draw = function () {
var radius = 100;
var nodes = createNodes();
createSvg(radius, function (svg) {
createElements(svg, nodes, 10);
});
}
$(document).ready(function () {
draw();
});
})();
* {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 13px;
}
circle {
fill: steelblue;
fill-opacity: .8;
}
circle:hover {
fill: orange;
fill-opacity: .8;
}
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.min.js"></script>
<div id="canvas"></div>
However, now the labels on the right are upside down. All that is left is to determine is whether a label falls on the right half or the left half and assign the appropriate attributes based on this.
Zero degrees points to the right, it is not the top of the diagram. Therefore, you need to ascertain if d.angle is less than 90 degrees (bottom right) or more than 270 degrees (top right), if so, your original code can be applied. If not, then you need to flip the label using the above code:
(function () {
var paddding = 250;
var createNodes = function () {
var nodeData = [
{ id: 0, label: 'AAA' },
{ id: 1, label: 'BBB' },
{ id: 2, label: 'CCC' },
{ id: 3, label: 'DDD' },
{ id: 4, label: 'EEE' },
{ id: 5, label: 'FFF' },
{ id: 6, label: 'GGG' },
{ id: 7, label: 'HHH' }
];
var radius = 100;
var nodes = [],
width = (radius * 2) + paddding,
height = (radius * 2) + paddding,
angle,
x,
y,
i;
var numNodes = nodeData.length;
for (i = 0; i < numNodes; i++) {
angle = (i / (numNodes / 2)) * Math.PI;
x = (radius * Math.cos(angle)) + (width / 2);
y = (radius * Math.sin(angle)) + (width / 2);
nodes.push({ 'id': i, 'x': x, 'y': y, 'label': nodeData[i].label, 'angle': angle });
}
return nodes;
}
var createSvg = function (radius, callback) {
d3.selectAll('svg').remove();
var svg = d3.select('#canvas').append('svg:svg')
.attr('width', (radius * 2) + paddding)
.attr('height', (radius * 2) + paddding);
callback(svg);
}
var createElements = function (svg, nodes, elementRadius) {
element = svg.selectAll('circle')
.data(nodes)
.enter().append('svg:circle')
.attr('r', elementRadius)
.attr('cx', function (d, i) { return d.x; })
.attr('cy', function (d, i) { return d.y; });
element = svg.selectAll('text')
.data(nodes)
.enter().append('svg:text')
.text(function (d, i) { return d.label + " - " + d.angle.toFixed(2) + ", " + (d.angle*180/Math.PI); })
.attr('x', function (d, i) {
if (d.angle > Math.PI/2 && d.angle < 1.5 * Math.PI) {
return nodes[0].x - 215 }
else {
return nodes[0].x + 15;
}
})
.attr('y', function (d, i) { return nodes[0].y; })
.attr("dy", ".35em")
.style("alignment-baseline","middle")
.style("text-anchor", function(d) {
if (d.angle > Math.PI/2 && d.angle < 1.5 * Math.PI) {
return "end"
}
else {
return "start";
}
})
.attr("transform", function(d,i) {
if (d.angle > Math.PI/2 && d.angle < 1.5 * Math.PI) {
return "rotate(" + ((d.angle * 180) / Math.PI - 180) + ", 225, 225)";
}
else {
return "rotate(" + ((d.angle * 180) / Math.PI) + ", 225, 225)"
}
})
;
}
var draw = function () {
var radius = 100;
var nodes = createNodes();
createSvg(radius, function (svg) {
createElements(svg, nodes, 10);
});
}
$(document).ready(function () {
draw();
});
})();
* {
font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
font-size: 13px;
}
circle {
fill: steelblue;
fill-opacity: .8;
}
circle:hover {
fill: orange;
fill-opacity: .8;
}
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script>
<script type="text/javascript" src="http://mbostock.github.com/d3/d3.v2.min.js"></script>
<div id="canvas"></div>

d3 javascript series chart

I am trying to create this particular d3 application where a series of data can be dynamically displayed like this. Each segment contains two pieces of data.
The first step is to print the circles so there is sufficient space between the series but also the largest circle is always under the smaller circle.
//version 3 -- with correct labels and legend--
http://jsfiddle.net/0ht35rpb/33/
//******version 2 fiddle******
http://jsfiddle.net/1oka61mL/10/
-- How to set the diagonal labels properly - same angles, aligned properly?
-- Add legend?
-- Mask the bottom pointers in an opposite color then continue the line in a different color?
//******Latest Jsfiddle******
http://jsfiddle.net/0ht35rpb/26/
var width = 600;
var height = 400;
var svg = d3.select('svg').attr("width", width).attr("height", height);
//Count
//Checkins
//Popularity
var data = [{
"name": "Twitter",
"items": [{
"id": 0,
"label": "Count",
"value": 200
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 30
}]
}, {
"name": "Facebook",
"items": [{
"id": 0,
"label": "Count",
"value": 500
}, {
"id": 1,
"label": "Checkins",
"value": 300
}, {
"id": 2,
"label": "Popularity",
"value": 740
}]
}, {
"name": "Ebay",
"items": [{
"id": 0,
"label": "Count",
"value": 4000
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 40
}]
}, {
"name": "Foursquare",
"items": [{
"id": 0,
"label": "Count",
"value": 2000
}, {
"id": 1,
"label": "Checkins",
"value": 3000
}, {
"id": 2,
"label": "Popularity",
"value": 4500
}]
}];
var outerRadius = [];
// organise the data.
// Insert indices and sort items in each series
// keep a running total of max circle size in each series
// for later positioning
var x = 0;
var totalWidth = d3.sum(
data.map(function(series) {
series.items.forEach(function(item, i) {
item.index = i;
});
series.items.sort(function(a, b) {
return b.value - a.value;
});
var maxr = Math.sqrt(series.items[0].value);
outerRadius.push(maxr);
x += maxr;
series.xcentre = x;
x += maxr;
return maxr * 2;
})
);
// make scales for position and colour
var scale = d3.scale.linear().domain([0, totalWidth]).range([0, width]);
//var colScale = d3.scale.category10();
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f"];
return colores_g[n % colores_g.length];
}
// add a group per series, position the group according to the values and position scale we calculated above
var groups = svg.selectAll("g").data(data);
groups.enter().append("g");
groups.attr("transform", function(d) {
return ("translate(" + d.xcentre + ",0)");
});
// then add circles per series, biggest first as items are sorted
// colour according to index (the property we inserted previously so we can
// keep track of their original position in the series)
var circles = groups.selectAll("circle").data(function(d) {
return d.items;
}, function(d) {
return d.index;
});
circles.enter().append("circle").attr("cy", height / 2).attr("cx", 0);
circles
.attr("r", function(d) {
return Math.sqrt(d.value);
})
.style("fill", function(d) {
return colores_google(d.index);
});
var labelsgroups = svg.selectAll("text").data(data);
labelsgroups.enter().append("text");
labelsgroups
.attr("y", function(d, i) {
d.y = 300;
d.cy = 200;
return 300;
})
.attr("x", function(d) {
d.x = d.xcentre;
d.cx = d.xcentre;
return d.xcentre;
})
.text(function(d) {
return d.name;
})
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y + 5;
})
.attr("text-anchor", "middle");
var pointersgroups = svg.selectAll("path.pointer").data(data);
pointersgroups.enter().append("path");
pointersgroups
.attr("class", "pointer")
.attr("marker-end", "url(#circ)");
pointersgroups
.attr("d", function(d) {
return "M" + (d.xcentre) + "," + (d.oy - 25) + "L" + (d.xcentre) + "," + (d.sy - 25) + " " + d.xcentre + "," + (d.cy);
})
function fetchValue(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return items[i].value;
}
}
}
function fetchRadius(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return Math.sqrt(items[i].value);
}
}
}
/*
var labels1groups = svg.selectAll(".label1").data(data);
labels1groups.enter().append("text");
labels1groups
.attr("class", "label1")
.attr("y", function(d, i) {
d.y = 100;
d.cy = 100;
return 100;
})
.attr("x", function(d) {
d.x = d.xcentre;
d.cx = d.xcentre+50;
return d.xcentre+50;
})
.text(function(d) {
return fetchValue(d.items, "Count");
})
.attr("transform", function(d, i) {
return "translate(" + (15 * i) + "," + (i * 45) + ") rotate(-45)";
})
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y ;
})
.attr("text-anchor", "left");
*/
var gridSize = 100;
var labels1groups = svg.selectAll(".label2")
.data(data)
.enter().append("text")
.text(function(d) {
return fetchValue(d.items, "Count");
//return d;
})
.attr("x", function(d, i) {
d.x = i * gridSize + 50;
d.cx = i * gridSize + 50;
return i * gridSize;
})
.attr("y", function(d, i) {
d.y = 105;
d.cy = 50;
return 0;
})
.attr("transform", function(d, i) {
return "translate(" + gridSize / 2 + ", -6)" +
"rotate(-45 " + ((i + 0.5) * gridSize) + " " + (-6) + ")";
})
.each(function(d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y;
})
.style("text-anchor", "end")
.attr("class", function(d, i) {
return ((i >= 8 && i <= 16) ?
"timeLabel mono axis axis-worktime" :
"timeLabel mono axis");
});
var pointers1groups = svg.selectAll("path.pointer1").data(data);
pointers1groups.enter().append("path");
pointers1groups
.attr("class", "pointer1")
.attr("marker-end", "url(#circ)");
pointers1groups
.attr("d", function(d, i) {
//d.y = outerRadius[i];
//d.y = d.oy - d.cy;
//fetchRadius(d.items, "Count");
//(d.xcentre+100)
// + " " + d.cx + "," + d.cy
//return "M "+ (d.xcentre) +" 25 ,L "+ dist +" 75";
return "M" + (d.xcentre) + "," + (d.y + d.oy - fetchRadius(d.items, "Count") - 10) + "L" + (d.xcentre + 80) + "," + d.cy;
})
//Older Jsfiddle
http://jsfiddle.net/59bunh8u/51/
var rawData = [{
"name": "Twitter",
"items" : [
{
"label" : "15 billion",
"unit" : "per day",
"value" : 1500
},
{
"label" : "450 checkins",
"unit" : "per day",
"value" : 450
}
]
},
{
"name": "Facebook",
"items" : [
{
"label" : "5 billion",
"unit" : "per day",
"value" : 5000
},
{
"label" : "2000 checkins",
"unit" : "per day",
"value" : 2000
}
]
}];
$.each(rawData, function(index, value) {
var total = 0;
var layerSet = [];
var ratios = [25, 100];
$.each(value["items"], function(i, v) {
total += v["value"];
});
value["total"] = total;
});
var w = $this.data("width");
var h = $this.data("height");
var el = $this;
var margin = {
top: 65,
right: 90,
bottom: 5,
left: 150
};
var svg = d3.select(el[0]).append("svg")
.attr("class", "series")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var defs = svg.append("svg:defs");
$.each(rawData, function(i, v) {
circleDraw(i, v["items"]);
});
//calculates where each element should be placed
function calculateDistance (d, i, items) {
var dcx = 0;
for (var k = 0; k < i; k++) {
dcx += Math.sqrt(items[k].value);
}
return dcx + 10 * i;
}
function getPercentage(value, total) {
return ((value / total) * 100);
}
function circleDraw(index, data){
data.sort(function(a, b) {
return parseFloat(b.value) - parseFloat(a.value);
});
var circlelayer = svg.append("g")
.attr("class", "circlelayer");
var circle = circlelayer.selectAll("circle")
.data(data);
circle.enter().append("circle")
.attr("class", function(d, i) {
if (i == 0) {
return "blue";
}
return "gold";
})
.attr("cy", 60)
.attr("cx", function(d, i) {
return calculateDistance(d, index, data);
})
.attr("r", function(d, i) {
return Math.sqrt(d.value);
});
circle.exit().remove();
}
Here is how you could drawn the lines:
groups.append("line")
.attr("class", "pointer1")
.attr("marker-end", "url(#circ)")
.attr("x1", 0)
.attr("y1", height / 2)
.attr("x2", 0)
.attr("y2",height - 10);
var linesG = svg.selectAll(".slines").data(data)
.enter().append("g")
.attr("transform", function(d) {
return ("translate(" + d.xcentre + "," + height/2 +") rotate(-45)");
});
linesG.append("line")
.attr("class", "pointer1")
.attr("marker-end", "url(#circ)")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 150)
.attr("y2", 0);
linesG.append("text")
.text(function(d) {
return fetchValue(d.items, "Count");
})
.attr("text-anchor", "end")
.attr("y", -5)
.attr("x", 150);
Updated jsfiddle
I've managed to get the diagonal markers and pointers in alignment, pointing to the correct circle colors to represent that set. I am keen to fine tune this chart and have more control over the padding and chart width/height parameters. The chart looks stable but would be keen to test it with different values and sized data sets.
/LATEST/
http://jsfiddle.net/0ht35rpb/33/
var width = 760;
var height = 400;
var svg = d3.select('#serieschart')
.append("svg:svg")
.attr("width", width)
.attr("height", height);
//Count
//Checkins
//Popularity
var data = [{
"name": "Twitter",
"items": [{
"id": 0,
"label": "Count",
"value": 200
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 30
}]
}, {
"name": "Facebook",
"items": [{
"id": 0,
"label": "Count",
"value": 500
}, {
"id": 1,
"label": "Checkins",
"value": 300
}, {
"id": 2,
"label": "Popularity",
"value": 740
}]
}, {
"name": "Ebay",
"items": [{
"id": 0,
"label": "Count",
"value": 4000
}, {
"id": 1,
"label": "Checkins",
"value": 1000
}, {
"id": 2,
"label": "Popularity",
"value": 40
}]
}, {
"name": "Foursquare",
"items": [{
"id": 0,
"label": "Count",
"value": 2000
}, {
"id": 1,
"label": "Checkins",
"value": 3000
}, {
"id": 2,
"label": "Popularity",
"value": 4500
}]
}];
var legend_group = svg.append("g")
.attr("class", "legend")
.attr("width", 80)
.attr("height", 100)
.append("svg:g")
.attr("class", "legendsection")
.attr("transform", "translate(0,30)");
var legend = legend_group.selectAll("circle").data(data[0].items);
legend.enter().append("circle")
.attr("cx", 70)
.attr("cy", function(d, i) {
return 15 * i;
})
.attr("r", 7)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {
return colores_google(i);
});
legend.exit().remove();
var legendtext = legend_group.selectAll("text").data(data[0].items);
legendtext.enter().append("text")
.attr("class", "labels")
.attr("dy", function(d, i) {
return 15 * i;
})
.attr("text-anchor", function(d) {
return "start";
})
.text(function(d) {
return d.label;
});
legendtext.exit().remove();
var m = [80, 20, 20, 10];
var w =+ width - m[0];
var h =+ height - m[1];
var chart = svg.append("g")
.attr("class", "serieschart")
.attr("width", w)
.attr("height", h);
var outerRadius = [];
// organise the data.
// Insert indices and sort items in each series
// keep a running total of max circle size in each series
// for later positioning
var x = 0;
var totalWidth = d3.sum(
data.map(function(series) {
series.items.forEach(function(item, i) {
item.index = i;
});
series.items.sort(function(a, b) {
return b.value - a.value;
});
var maxr = Math.sqrt(series.items[0].value);
outerRadius.push(maxr);
x += maxr;
series.xcentre = x;
x += maxr;
return maxr * 2;
})
);
// make scales for position and colour
var scale = d3.scale.linear().domain([0, totalWidth]).range([0, w]);
//var colScale = d3.scale.category10();
function colores_google(n) {
var colores_g = ["#f7b363", "#448875", "#c12f39", "#2b2d39", "#f8dd2f"];
return colores_g[n % colores_g.length];
}
function fetchValue(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return items[i].value;
}
}
}
function fetchRadius(items, label) {
for (i = 0; i <= items.length; i++) {
if (items[i].label == label) {
return Math.sqrt(items[i].value);
}
}
}
// add a group per series, position the group according to the values and position scale we calculated above
var groups = chart.selectAll("g.seriesGroup").data(data);
var newGroups = groups.enter().append("g").attr("class", "seriesGroup");
newGroups.append("text")
.attr("class", "seriesName")
.attr("text-anchor", "middle");
newGroups.append("line")
.attr("class", "seriesName")
.attr("y1", h - 40)
.attr("y2", h / 2);
newGroups.append("text")
.attr("class", "datumValue")
.attr("y", 10)
//.attr("transform", "rotate(-45)")
;
newGroups.append("g").attr("class", "circleGroup");
newGroups.append("g").attr("class", "datumLine")
.append("line")
.attr("class", "datumValue")
.attr("y2", 40);
var focus = "Count";
groups.attr("transform", function(d) {
return "translate(" + scale(d.xcentre) + ",0)";
});
groups.select("text.seriesName")
.text(function(d) {
return d.name;
})
.attr("y", h - 20);
groups.select("text.datumValue")
.text(function(d) {
return fetchValue(d.items, focus);
})
.attr("transform", function(d) {
return "translate(" + ((h / 2) - 20 - scale(fetchRadius(d.items, focus))) + ",20) rotate(-45)";
});
groups.select("line.datumValue")
.attr("y1", function(d) {
return (h / 2) - scale(fetchRadius(d.items, focus));
})
.attr("x2", function(d) {
return (h / 2) - scale(fetchRadius(d.items, focus) + 20);
});
// then add circles per series, biggest first as items are sorted
// colour according to index (the property we inserted previously so we can
// keep track of their original position in the series)
var circles = groups
.select(".circleGroup")
.selectAll("circle").data(function(d) {
return d.items;
}, function(d) {
return d.index;
});
circles.enter().append("circle").attr("cy", h / 2).attr("cx", 0);
circles
.attr("r", function(d) {
return scale(Math.sqrt(d.value));
})
.style("fill", function(d) {
return colores_google(d.index);
});

D3 multiple Axis interpolation

I am working on a project to plot a stream of csv/Json data (bar chart) where the order of arrival of the data is important.
The Y axis is unique, but there are multiple X axes that correspond to different measures of the data.
I am having trouble producing a nice graph that looks like this, given the following data:
x0,x1,x2,y,idx
-1,z,w2,10,0
0,z,w2,9,1
1,z,w2,8,2
-1,k,w2,11,3
0,k,5q,5,4
1,k,5q,8,5
idx represent the order the data arrives in.
this is what I get
X=["idx","x0","x1","x2"];
Y=["y"];
var margin = {
top: 80,
right: 180,
bottom: 180,
left: 180
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = [],
x = [];
var x_uid = d3.scale.ordinal()
.rangeRoundPoints([0, width]);
for (var idx = 0; idx < X.length; idx++) {
x[idx] = d3.scale.ordinal()
.rangeRoundPoints([0, width]);
xAxis[idx] = d3.svg.axis()
.scale(x[idx])
.orient("bottom");
}
var yAxis = d3.svg.axis()
.scale(y)
.orient("left");
// .ticks(8, "%");
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var data = [{
x0:-1,
x1:z,
x2:w2,
y:10,
idx:0
},
{
x0:0,
x1:z,
x2:w2,
y:10,
idx:1
},
{
x0:1,
x1:z,
x2:w2,
y:10,
idx:2
},
{
x0:-1,
x1:j,
x2:w2,
y:10,
idx:3
},
{
x0:0,
x1:j,
x2:5q,
y:10,
idx:4
},
{
x0:1,
x1:j,
x2:5q,
y:10,
idx:5
}]
if(data) {
for (var idx = 0; idx < X.length; idx++) {
x[idx].domain(data.map(function(d) {
return d[X[idx]];
}));
}
x_uid.domain(data.map(function(d) {
return d.idx;
}));
y.domain([0, d3.max(data, function(d) {
d.value = d[Y[0]];
return d.value;
})]);
for (var idx = 0; idx < X.length; idx++)
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (height + idx * 25) + ")")
.call(xAxis[idx]);
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function(d) {
return x_uid(d.idx);
})
.attr("width", 1)
.attr("y", function(d) {
return y(d.value);
})
.attr("height", function(d) {
return height - y(d.value);
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.6/d3.min.js"></script>
<div id="chart"></div>
Offsetting the ticks' text is not an issue, but I am having problems with the interpolation due to the multiplicities of the values:
e.g. width of w2 > width of 5q
e.g. x0 axis should be -1 0 1 -1 0 1 but d3 interpolates as -1 0 1
I tried using rangeRoundBand instead of rangeRoundPoint but the issue is similar.
I also tried playing around with tickValues but to no avail.
I tried doing my own interpolation using linear scales instead of ordinal, but it becomes very messy very quickly because is forces me to manually calculate and adjust all the ticks' positions and texts while taking into account the d3.behavior zoom level etc...
function adjustTickPosition(selection, count, scale, translate, rotate) {
//selection = axis
//count = multiplicity of each tick
//scale = d3.behavior.zoom scale
//translate = d3.behavior.zoom translation
//rotate = irrelevent here (additional styling)
console.info( selection.selectAll("g.tick"))
// cancel previous position
//
// /!\ For some reason there is always 100 ticks instead of the appropriate number
//
selection.selectAll("g.tick")
.attr("transform", "translate(0,0)");
// align tick marks
selection.selectAll("g.tick line")
.attr('transform', function (d, k) {
if (k <= count.length - 1) {
var newPosition = scaleTranslate(count[k]);
if (newPosition > width || newPosition < 0) {
d3.select(this.parentNode).style("visibility", "hidden");
} else
d3.select(this.parentNode).style("visibility", "visible");
return 'translate(' + newPosition + ',0)';
} else
return 'translate(0,0)';
});
// offset tick label compared to tick marks
selection.selectAll("g.tick text")
.attr('transform', function (d, k) {
if (k <= count.length - 1) {
var pos, transform;
if (k > 0) pos = (count[k - 1] + count[k]) / 2;
else pos = count[k] / 2;
var newPosition = scaleTranslate(pos);
if (newPosition > width || newPosition < 0) {
d3.select(this.parentNode).style("visibility", "hidden");
} else
d3.select(this.parentNode).style("visibility", "visible");
var transform = 'translate(' + newPosition + ',0)';
if (rotate) transform += ' rotate(-65)';
return transform;
} else
return 'translate(0,0)';
});
if (rotate) selection.selectAll("g.tick text").style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", ".15em");
return selection;
function scaleTranslate(v) {
return v / count[count.length - 1] * width * scale + translate[0];
}
}
Could someone please show me how to properly use axes ticks for this kind of purpose?
Thank you in advance
I made my own class/object because d3 was apparently not meant for this kind of graph
function chartAxis(key, args) {
//***************************
// PRIVATE
//***************************
var _direction = args ? (args.direction || "x") : "x";
var _width = args ? (args.width || 500) : 500;
var _alignTicks = args ? (args.alignTicks || false) : false;
var _tickSize = args ? (args.tickSize || 0) : 0;
var _numTicks = args ? (args.numTicks || 10) : 10;
var _offset = args ? (args.offset || 25) : 25;
var _zoom = args ? (args.zoom || {
s: 1,
t: 0
}) : {
s: 1,
t: 0
};
var _totalLength;
function consecutiveReduction(list, key) {
var Bin = function (val, cnt) {
return {
value: val,
count: cnt,
cumulativeCount: 0,
center: 0,
position: 0
};
};
var result = list.map(function (d) {
return key ? d[key] : d;
}).reduce(function (acc, d) {
var currentBin = acc[acc.length - 1];
if ((acc.length > 0) && d === currentBin.value) {
//add to current bin
currentBin.count++;
} else {
//create new bin
acc.push(new Bin(d, 1));
}
return acc;
}, []);
result.forEach(accumulate);
result.forEach(positionTick);
return result;
}
function positionTick(d) {
d.position = ApplyZoom(d.cumulativeCount);
d.center = _alignTicks ? d.position : ApplyZoom(d.cumulativeCount - d.count / 2);
function ApplyZoom(val) {
var translate;
if (_zoom.t.length > 1)
translate = (_direction == "x") ? _zoom.t[0] : _zoom.t[1];
else
translate = _zoom.t;
return val / _totalLength * _width * _zoom.s + translate;
}
}
function accumulate(d, i, arr) {
d.cumulativeCount = d.count;
if (i > 0) d.cumulativeCount += arr[i - 1].cumulativeCount;
}
//***************************
// PUBLIC
//***************************
var xAxis = function (selection) {
selection.each(function (data) {
// calculate
_totalLength = data.length;
var tickData = consecutiveReduction(data, key);
console.log(tickData.map(function (d) {
return d.count
}))
console.table(data,key)
//create parent axis with clip-path
var axis = d3.select(this)
.attr("id", key);
axis.selectAll("#clipAxis-" + key).data([1]).enter()
.append("clipPath")
.attr("id", "clipAxis-" + key)
.append("svg:rect")
.attr("x", 0)
.attr("y", _offset - _tickSize)
.attr("width", _width)
.attr("height", 25 + _tickSize);
// Axis line and label
var axisLine = axis.selectAll(".axisLine").data([1]).enter();
axisLine.append("line").attr({
x1: 0,
y1: _offset,
x2: _width,
y2: _offset,
class: "axisLine"
});
axisLine.append("text")
.text(key)
.attr({
x: _width + 10,
y: _offset
}).style("text-anchor", "start");
// tick on the axis
var ticks = axis.selectAll("g.tick")
.data(tickData);
// ENTER
var newticks = ticks.enter().append("g").attr("class", "tick");
newticks.append("line");
newticks.append("text");
// UPDATE
ticks.attr("clip-path", "url(#clipAxis-" + key + ")");
ticks.select(".tick line")
.attr("x1", function (d) {
return d.position
})
.attr("x2", function (d) {
return d.position
})
.attr("y1", function (d) {
return _offset - _tickSize
})
.attr("y2", function (d) {
return _offset + 5
});
ticks.select(".tick text")
.text(function (d) {
return d.value;
})
.attr("x", function (d) {
return d.center;
})
.attr("y", function (d) {
return _offset + 10;
})
.style("text-anchor", "middle")
.style("text-length", function (d) {
return (0.6 * 2 * (d.position - d.center)) + "px";
});
// EXIT
ticks.exit().remove();
})
};
var yAxis = function (selection) {
selection.each(function (data) {
// calculate
_totalLength = data.length;
var tickData = d3.extent(data, function (d) {
return d[key];
});
var tickRange = (tickData[1] - tickData[0]) / (_numTicks - 4 + 1); // -4 -> [0.85*min min ... max 1.15*max]
console.log(tickData.map(function (d) {
return d.count
}))
console.log(_tickSize)
//create parent axis with clip-path
var axis = axisLine = d3.select(this)
.attr("id", key);
axis.selectAll("#clipAxis-" + key).data([1]).enter()
.append("clipPath")
.attr("id", "clipAxis-" + key)
.append("svg:rect")
.attr("x", _offset)
.attr("y", 0)
.attr("width", _width)
.attr("height", 25 + _tickSize);
// Axis line and label
axisLine = axis.selectAll(".axisLine").data([1]).enter();
axisLine.append("line").attr({
x1: _offset,
y1: 0,
x2: _offset,
y2: _width,
class: "axisLine"
});
axisLine.append("text")
.text(key)
.attr({
x: _offset,
y: -10
}).style("text-anchor", "start");
// tick on the axis
var ticks = axis.selectAll("g.tick")
.data(tickData);
// ENTER
var newticks = ticks.enter().append("g").attr("class", "tick");
newticks.append("line");
newticks.append("text");
// UPDATE
ticks.attr("clip-path", "url(#clipAxis-" + key + ")");
ticks.select(".tick line")
.attr("x1", function (d) {
return _offset - 5
})
.attr("x2", function (d) {
return _offset + _tickSize
})
.attr("y1", function (d) {
return d.position
})
.attr("y2", function (d) {
return d.position
});
ticks.select(".tick text")
.text(function (d) {
return d.value;
})
.attr("x", function (d) {
return _offset + 10;
})
.attr("y", function (d) {
return d.center;
})
.style("text-anchor", "middle")
.style("text-length", function (d) {
return (0.6 * 2 * (d.position - d.center)) + "px";
});
// EXIT
ticks.exit().remove();
}); // end select.foreach
}; // end yAxis
xAxis.BindToZoom = function (zoomObject) {
_zoom = zoomObject;
return xAxis;
}
yAxis.BindToZoom = function (zoomObject) {
_zoom = zoomObject;
return yAxis;
}
return (_direction == "x") ? xAxis : yAxis;
}
Usage:
function chartAxis(key, args) {
//***************************
// PRIVATE
//***************************
var _direction = args ? (args.direction || "x") : "x";
var _width = args ? (args.width || 500) : 500;
var _alignTicks = args ? (args.alignTicks || false) : false;
var _tickSize = args ? (args.tickSize || 0) : 0;
var _numTicks = args ? (args.numTicks || 10) : 10;
var _offset = args ? (args.offset || 25) : 25;
var _zoom = args ? (args.zoom || {
s: 1,
t: 0
}) : {
s: 1,
t: 0
};
var _totalLength;
function consecutiveReduction(list, key) {
var Bin = function(val, cnt) {
return {
value: val,
count: cnt,
cumulativeCount: 0,
center: 0,
position: 0
};
};
var result = list.map(function(d) {
return key ? d[key] : d;
}).reduce(function(acc, d) {
var currentBin = acc[acc.length - 1];
if ((acc.length > 0) && d === currentBin.value) {
//add to current bin
currentBin.count++;
} else {
//create new bin
acc.push(new Bin(d, 1));
}
return acc;
}, []);
result.forEach(accumulate);
result.forEach(positionTick);
return result;
}
function positionTick(d) {
d.position = ApplyZoom(d.cumulativeCount);
d.center = _alignTicks ? d.position : ApplyZoom(d.cumulativeCount - d.count / 2);
function ApplyZoom(val) {
var translate;
if (_zoom.t.length > 1)
translate = (_direction == "x") ? _zoom.t[0] : _zoom.t[1];
else
translate = _zoom.t;
return val / _totalLength * _width * _zoom.s + translate;
}
}
function accumulate(d, i, arr) {
d.cumulativeCount = d.count;
if (i > 0) d.cumulativeCount += arr[i - 1].cumulativeCount;
}
//***************************
// PUBLIC
//***************************
var xAxis = function(selection) {
selection.each(function(data) {
// calculate
_totalLength = data.length;
var tickData = consecutiveReduction(data, key);
//create parent axis with clip-path
var axis = d3.select(this)
.attr("id", key);
axis.selectAll("#clipAxis-" + key).data([1]).enter()
.append("clipPath")
.attr("id", "clipAxis-" + key)
.append("svg:rect")
.attr("x", 0)
.attr("y", _offset - _tickSize)
.attr("width", _width)
.attr("height", 25 + _tickSize);
// Axis line and label
var axisLine = axis.selectAll(".axisLine").data([1]).enter();
axisLine.append("line").attr({
x1: 0,
y1: _offset,
x2: _width,
y2: _offset,
class: "axisLine"
});
axisLine.append("text")
.text(key)
.attr({
x: _width + 10,
y: _offset
}).style("text-anchor", "start");
// tick on the axis
var ticks = axis.selectAll("g.tick")
.data(tickData);
// ENTER
var newticks = ticks.enter().append("g").attr("class", "tick");
newticks.append("line");
newticks.append("text");
// UPDATE
ticks.attr("clip-path", "url(#clipAxis-" + key + ")");
ticks.select(".tick line")
.attr("x1", function(d) {
return d.position
})
.attr("x2", function(d) {
return d.position
})
.attr("y1", function(d) {
return _offset - _tickSize
})
.attr("y2", function(d) {
return _offset + 5
});
ticks.select(".tick text")
.text(function(d) {
return d.value;
})
.attr("x", function(d) {
return d.center;
})
.attr("y", function(d) {
return _offset + 10;
})
.style("text-anchor", "middle")
.style("text-length", function(d) {
return (0.6 * 2 * (d.position - d.center)) + "px";
});
// EXIT
ticks.exit().remove();
})
};
var yAxis = function(selection) {
selection.each(function(data) {
// calculate
_totalLength = data.length;
var tickData = consecutiveReduction(data, key);
//create parent axis with clip-path
var axis = axisLine = d3.select(this)
.attr("id", key);
axis.selectAll("#clipAxis-" + key).data([1]).enter()
.append("clipPath")
.attr("id", "clipAxis-" + key)
.append("svg:rect")
.attr("x", _offset)
.attr("y", 0)
.attr("width", _width)
.attr("height", 25 + _tickSize);
// Axis line and label
axisLine = axis.selectAll(".axisLine").data([1]).enter();
axisLine.append("line").attr({
x1: _offset,
y1: 0,
x2: _offset,
y2: _width,
class: "axisLine"
});
axisLine.append("text")
.text(key)
.attr({
x: _offset,
y: -10
}).style("text-anchor", "start");
// tick on the axis
var ticks = axis.selectAll("g.tick")
.data(tickData);
// ENTER
var newticks = ticks.enter().append("g").attr("class", "tick");
newticks.append("line");
newticks.append("text");
// UPDATE
ticks.attr("clip-path", "url(#clipAxis-" + key + ")");
ticks.select(".tick line")
.attr("x1", function(d) {
return _offset - 5
})
.attr("x2", function(d) {
return _offset + _tickSize
})
.attr("y1", function(d) {
return d.position
})
.attr("y2", function(d) {
return d.position
});
ticks.select(".tick text")
.text(function(d) {
return d.value;
})
.attr("x", function(d) {
return _offset + 10;
})
.attr("y", function(d) {
return d.center;
})
.style("text-anchor", "middle")
.style("text-length", function(d) {
return (0.6 * 2 * (d.position - d.center)) + "px";
});
// EXIT
ticks.exit().remove();
}); // end select.foreach
}; // end yAxis
xAxis.BindToZoom = function(zoomObject) {
_zoom = zoomObject;
return xAxis;
}
yAxis.BindToZoom = function(zoomObject) {
_zoom = zoomObject;
return yAxis;
}
return (_direction == "x") ? xAxis : yAxis;
}
var data = [{
"a": 1,
"b": 3,
c: 1
}, {
"a": 1,
"b": 3,
c: 2
}, {
"a": 1,
"b": 2,
c: 3
}, {
"a": 1,
"b": 3,
c: 4
}, {
"a": 2,
"b": 3,
c: 5
}, {
"a": 3,
"b": "a",
c: 6
}, {
"a": 1,
"b": "a",
c: 7
}];
X = ["b", "a", "c"];
var axesDOM = d3.select("svg")
.selectAll(".axis")
.data(X).enter()
.append("g").attr("class", "axis");
axesDOM.each(function(x, i) {
d3.select(this).datum(data)
.call(new chartAxis(x, {
width: 200,
offset: 25 + i * 25,
direction: "x"
}));
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width="200px" height="200px"></svg>

d3.js dougnut pie chart legend toggling

I'm developing a legend toggling d3.js pie chart application using this jsfiddle as my latest version http://jsfiddle.net/Qh9X5/3328/ .
I am aiming to get a streamlined working example where the legend can toggle the slices, trying to deactivate all slices - resorts in a reset which reactivates all the slices. Splitting up presentation and application layer logic.
Tweening needs improvement too - as the slices pop into existence then re-tween smoothly.
How do I improve/fix the various bugs in this code base?
onLegendClick: function(dt, i){
//_toggle rectangle in legend
var completeData = jQuery.extend(true, [], methods.currentDataSet);
newDataSet = completeData;
if(methods.manipulatedData){
newDataSet = methods.manipulatedData;
}
d3.selectAll('rect')
.data([dt], function(d) {
return d.data.label;
})
.style("fill-opacity", function(d, j) {
var isActive = Math.abs(1-d3.select(this).style("fill-opacity"));
if(isActive){
newDataSet[j].total = completeData[j].total;
}else{
newDataSet[j].total = 0;
}
return isActive;
});
//animate slices
methods.animateSlices(newDataSet);
//stash manipulated data
methods.manipulatedData = newDataSet;
}
Here is the entire js code - I've used the tidyup. I wasn't sure about using the shortcuts as I'm not sure the values will be correct. The latest fiddle - http://jsfiddle.net/Qh9X5/3340/
$(document).ready(function () {
var pieChart = {
el: "",
init: function (el, options) {
var clone = jQuery.extend(true, {}, options["data"]);
pieChart.el = el;
pieChart.setup(clone, options["width"], options["height"], options["r"], options["ir"]);
},
getArc: function (radius, innerradius) {
var arc = d3.svg.arc()
.innerRadius(innerradius)
.outerRadius(radius);
return arc;
},
setup: function (dataset, w, h, r, ir) {
var padding = 80;
this.width = w;
this.height = h;
this.radius = r
this.innerradius = ir;
this.color = d3.scale.category20();
this.pie = d3.layout.pie()
.sort(null)
.value(function (d) {
return d.total;
});
this.arc = this.getArc(this.radius, this.innerradius);
this.svg = d3.select(pieChart.el["selector"]).append("svg")
.attr("width", this.width + padding)
.attr("height", this.height + padding);
this.holder = this.svg.append("g")
.attr("transform", "translate(" + ((this.width / 2) + (padding / 2)) + "," + ((this.height / 2) + (padding / 2)) + ")");
this.piec = this.holder.append("g")
.attr("class", "piechart");
this.segments = this.holder.append("g")
.attr("class", "segments");
this.labels = this.holder.append("g")
.attr("class", "labels");
this.pointers = this.holder.append("g")
.attr("class", "pointers");
this.legend = this.svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(" + -(this.width / 4) + "," + this.height + ")");
},
oldPieData: "",
pieTween: function (r, ir, d, i) {
var that = this;
var theOldDataInPie = pieChart.oldPieData;
// Interpolate the arcs in data space
var s0;
var e0;
if (theOldDataInPie[i]) {
s0 = theOldDataInPie[i].startAngle;
e0 = theOldDataInPie[i].endAngle;
} else if (!(theOldDataInPie[i]) && theOldDataInPie[i - 1]) {
s0 = theOldDataInPie[i - 1].endAngle;
e0 = theOldDataInPie[i - 1].endAngle;
} else if (!(theOldDataInPie[i - 1]) && theOldDataInPie.length > 0) {
s0 = theOldDataInPie[theOldDataInPie.length - 1].endAngle;
e0 = theOldDataInPie[theOldDataInPie.length - 1].endAngle;
} else {
s0 = 0;
e0 = 0;
}
var i = d3.interpolate({
startAngle: s0,
endAngle: e0
}, {
startAngle: d.startAngle,
endAngle: d.endAngle
});
return function (t) {
var b = i(t);
return pieChart.getArc(r, ir)(b);
};
},
removePieTween: function (r, ir, d, i) {
var that = this;
s0 = 2 * Math.PI;
e0 = 2 * Math.PI;
var i = d3.interpolate({
startAngle: d.startAngle,
endAngle: d.endAngle
}, {
startAngle: s0,
endAngle: e0
});
return function (t) {
var b = i(t);
return pieChart.getArc(r, ir)(b);
};
},
animateSlices: function (dataSet) {
var r = $(pieChart.el["selector"]).data("r");
var ir = $(pieChart.el["selector"]).data("ir");
this.piedata = pieChart.pie(dataSet);
//__slices
this.path = pieChart.segments.selectAll("path.pie")
.data(this.piedata, function (d) {
return d.data.label
});
this.path.enter().append("path")
.attr("class", "pie")
.attr("fill", function (d, i) {
return pieChart.color(i);
})
.attr("stroke", "#ffffff")
.transition()
.duration(300)
.attrTween("d", function (d, i) {
return pieChart.pieTween(r, ir, d, i);
});
this.path.transition()
.duration(300)
.attrTween("d", function (d, i) {
return pieChart.pieTween(r, ir, d, i);
});
this.path.exit()
.transition()
.duration(300)
.attrTween("d", function (d, i) {
return pieChart.removePieTween(r, ir, d, i);
})
.remove();
//__slices
//__labels
var labels = pieChart.labels.selectAll("text")
.data(this.piedata, function (d) {
return d.data.label
});
labels.enter()
.append("text")
.attr("text-anchor", "middle")
labels.attr("x", function (d) {
var a = d.startAngle + (d.endAngle - d.startAngle) / 2 - Math.PI / 2;
d.cx = Math.cos(a) * (ir + ((r - ir) / 2));
return d.x = Math.cos(a) * (r + 20);
})
.attr("y", function (d) {
var a = d.startAngle + (d.endAngle - d.startAngle) / 2 - Math.PI / 2;
d.cy = Math.sin(a) * (ir + ((r - ir) / 2));
return d.y = Math.sin(a) * (r + 20);
})
.attr("opacity", function (d) {
var opacityLevel = 1;
if (d.value == 0) {
opacityLevel = 0;
}
return opacityLevel;
})
.text(function (d) {
return d.data.label;
})
.each(function (d) {
var bbox = this.getBBox();
d.sx = d.x - bbox.width / 2 - 2;
d.ox = d.x + bbox.width / 2 + 2;
d.sy = d.oy = d.y + 5;
})
.transition()
.duration(300)
labels.transition()
.duration(300)
labels.exit().remove();
//__labels
//__pointers
pieChart.pointers.append("defs").append("marker")
.attr("id", "circ")
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("refX", 3)
.attr("refY", 3)
.append("circle")
.attr("cx", 3)
.attr("cy", 3)
.attr("r", 3);
var pointers = pieChart.pointers.selectAll("path.pointer")
.data(this.piedata, function (d) {
return d.data.label
});
pointers.enter()
.append("path")
.attr("class", "pointer")
.style("fill", "none")
.style("stroke", "black")
.attr("marker-end", "url(#circ)");
pointers.attr("d", function (d) {
if (d.cx > d.ox) {
return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
} else {
return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
}
})
.attr("opacity", function (d) {
var opacityLevel = 1;
if (d.value == 0) {
opacityLevel = 0;
}
return opacityLevel;
})
.transition()
.duration(300)
pointers.transition()
.duration(300)
pointers.exit().remove();
},
onToggle: function (sliceData, index) {
//_toggle rectangle in legend
//_toggle slice
var completeData = jQuery.extend(true, [], pieChart.currentDataSet);
var dataLength = completeData.length;
var newDataSet = completeData;
if (pieChart.manipulatedData) {
newDataSet = pieChart.manipulatedData;
}
d3.selectAll('rect')
.data([sliceData], function (d) {
return d.data.label;
})
.style("fill-opacity", function (d) {
var isActive = Math.abs(1 - d3.select(this).style("fill-opacity"));
if (isActive) {
newDataSet[index].total = completeData[index].total;
newDataSet[index].value = completeData[index].value;
} else {
newDataSet[index].total = 0;
newDataSet[index].value = 0;
}
return isActive;
});
//if all elements are to be not shown - reset to show all slices again.
//animate slices
pieChart.animateSlices(newDataSet);
//stash manipulated data
pieChart.manipulatedData = newDataSet;
},
update: function (el, dataSet) {
var that = this;
pieChart.el = el;
pieChart.svg = d3.select(pieChart.el["selector"] + " .piechart");
pieChart.segments = d3.select(pieChart.el["selector"] + " .segments");
pieChart.labels = d3.select(pieChart.el["selector"] + " .labels");
pieChart.pointers = d3.select(pieChart.el["selector"] + " .pointers");
pieChart.legend = d3.select(pieChart.el["selector"] + " .legend");
dataSet.forEach(function (d) {
d.total = +d.value;
});
pieChart.currentDataSet = dataSet;
pieChart.animateSlices(dataSet);
//__legends
var w = 200;
// add legend
var legend = pieChart.legend; //.append("g")
var legendRects = legend.selectAll('rect')
.data(this.piedata, function (d) {
return d.data.label
});
legendRects.enter()
.append("rect")
.attr("x", w - 65)
.attr("y", function (d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", function (d, i) {
return pieChart.color(i);
})
.style("stroke", function (d, i) {
return pieChart.color(i);
})
.on('click', function(d, i){
pieChart.onToggle(d, i);
})
.transition()
.duration(300)
legendRects.style("fill", function (d, i) {
return pieChart.color(i);
})
.style("stroke", function (d, i) {
return pieChart.color(i);
})
.transition()
.duration(300)
legendRects.exit().remove();
var legendText = legend.selectAll('text.label')
.data(this.piedata, function (d) {
return d.data.label
});
legendText.enter()
.append("text")
.attr("class", "label")
.attr("x", w - 52)
.attr("y", function (d, i) {
return i * 20 + 9;
})
.text(function (d) {
return d.data.label;
})
.transition()
.duration(300)
legendText.text(function (d) {
return d.data.label;
})
.transition()
.duration(300)
legendText.exit().remove();
var legendTextVals = legend.selectAll('text.vals')
.data(this.piedata, function (d) {
return d.data.label
});
legendTextVals.enter()
.append("text")
.attr("class", "vals")
.attr("x", w + 20)
.attr("y", function (d, i) {
return i * 20 + 9;
})
.text(function (d) {
return d.data.value;
})
.transition()
.duration(300)
legendTextVals.text(function (d) {
return d.data.value;
})
.transition()
.duration(300)
legendTextVals.exit().remove();
//__pointers
this.oldPieData = this.piedata;
}
};
var dataCharts = [{
"data": [{
"segments": [{
"label": "apple",
"value": 53245
}, {
"label": "cherry",
"value": 145
}, {
"label": "pear",
"value": 2245
}, {
"label": "bananana",
"value": 15325
}]
}]
}, {
"data": [{
"segments": [{
"label": "milk",
"value": 122
}, {
"label": "cheese",
"value": 44
}, {
"label": "grapes",
"value": 533
}]
}]
}, {
"data": [{
"segments": [{
"label": "pineapple",
"value": 1532
}, {
"label": "orange",
"value": 1435
}, {
"label": "grapes",
"value": 22
}]
}]
}, {
"data": [{
"segments": [{
"label": "lemons",
"value": 133
}, {
"label": "mango",
"value": 435
}, {
"label": "melon",
"value": 2122
}]
}]
}];
var clone = jQuery.extend(true, {}, dataCharts);
//__invoke concentric
$('[data-role="piechart"]').each(function (index) {
var selector = "piechart" + index;
$(this).attr("id", selector);
var options = {
data: clone[0].data,
width: $(this).data("width"),
height: $(this).data("height"),
r: $(this).data("r"),
ir: $(this).data("ir")
}
pieChart.init($("#" + selector), options);
pieChart.update($("#" + selector), clone[0].data[0].segments);
});
$(".testers a").on("click", function (e) {
e.preventDefault();
var clone = jQuery.extend(true, {}, dataCharts);
var min = 0;
var max = 3;
//__invoke pie chart
$('[data-role="piechart"]').each(function (index) {
pos = Math.floor(Math.random() * (max - min + 1)) + min;
pieChart.update($("#" + $(this).attr("id")), clone[pos].data[0].segments);
});
});
});

Categories