JSFiddle: http://jsfiddle.net/kKvtJ/2/
Right now the groups are 20px wide. When clicked, I want the selected group to expand to 40px wide, with the groups to the right shifting over 20px more.
Current:
Expected:
Can I can set a transform on all the groups like this? I couldn't figure this out.
var clicked_index = 3; // how to get currently clicked `g` index?
d3.selectAll('g')
.attr('transform',function(d,i){ return 'translate('+(i>clicked_index?40:0)+',0)' });
I have marked what I want to accomplish in the code below, in // pseudocode.
JSFiddle: http://jsfiddle.net/kKvtJ/2/
code
var data = [13, 11, 10, 8, 6];
var width = 200;
var height = 200;
var chart_svg = d3.select("#chart")
.append("svg")
.append("g");
y_scale = d3.scale.linear().domain([0, 15]).range([200, 0]);
h_scale = d3.scale.linear().domain([0, 15]).range([0,200]);
x_scale = d3.scale.linear().domain([0, 10]).range([0, 200]);
var nodes = chart_svg.selectAll('g').data(data);
var nodes_enter = nodes.enter().append('g')
.attr('transform', function (d, i) {
return 'translate(' + (i * 30) + ',0)'
})
.attr('fill', d3.rgb('#3f974e'));
nodes_enter.on('click', function() {
d3.selectAll('line')
.attr('opacity',0);
d3.selectAll('text')
.style('fill','white')
.attr('x',0);
d3.select(this).select('line')
.attr('opacity',1);
d3.select(this).selectAll('text')
.style('fill','black')
.attr('x',40);
// pseudocode
// d3.select(this).nextAll('g')
// .attr('transform','translate(20,0)');
});
nodes_enter.append('rect')
.attr('y', function (d) { return y_scale(d) })
.attr('height', function (d) { return h_scale(d) })
.attr('width', 20);
nodes_enter.append('text')
.text(function (d) { return d })
.attr('y', function (d) { return y_scale(d) + 16 })
.style('fill', 'white');
nodes_enter.append('line')
.attr('x1', 0)
.attr('y1', function(d) { return y_scale(d) })
.attr('x2', 40)
.attr('y2', function(d) { return y_scale(d) })
.attr('stroke-width', 1)
.attr('stroke','black')
.attr('opacity', 0);
You can do this by selecting all the g elements, shifting them if the respective index is larger than the one of the bar you clicked on, and selecting all the rect elements and adjusting the width depending on whether the index is the one you clicked on. Updated jsfiddle here, relevant code below. Note that I assigned the class "bar" to the relevant g elements to be able to distinguish them from the others.
nodes_enter.on('click', function(d, i) {
d3.selectAll("g.bar")
.attr('transform', function (e, j) {
return 'translate(' + (j * 30 + (j > i ? 20 : 0)) + ',0)';
});
d3.selectAll("g.bar > rect")
.attr("width", function(e, j) { return j == i ? 40 : 20; });
});
Related
I have a vertical bar chart in my Ember application and I am struggling to attach text labels to the top of the bars.
The chart is broken up into the following functions:
Drawing the static elements of the chart:
didInsertElement() {
let svg = select(this.$('svg')[0]);
this.set('svg', svg);
let height = 325
let width = 800
let padding = {
top: 10,
bottom: 30,
left: 40,
right: 0
};
this.set('barsContainer', svg.append('g')
.attr('class', 'bars')
.attr('transform', `translate(${padding.left}, ${padding.top})`)
);
let barsHeight = height - padding.top - padding.bottom;
this.set('barsHeight', barsHeight);
let barsWidth = width - padding.left - padding.right;
// Y scale & axes
let yScale = scaleLinear().range([barsHeight, 0]);
this.set('yScale', yScale);
this.set('yAxis', axisLeft(yScale));
this.set('yAxisContainer', svg.append('g')
.attr('class', 'axis axis--y axisWhite')
.attr('transform', `translate(${padding.left}, ${padding.top})`)
);
// X scale & axes
let xScale = scaleBand().range([0, barsWidth]).paddingInner(0.15);
this.set('xScale', xScale);
this.set('xAxis', axisBottom(xScale));
this.set('xAxisContainer', svg.append('g')
.attr('class', 'axis axis--x axisWhite')
.attr('transform', `translate(${padding.left}, ${padding.top + barsHeight})`)
);
// Color scale
this.set('colorScale', scaleLinear().range(COLORS[this.get('color')]));
this.renderChart();
this.set('didRenderChart', true);
},
This re-draws the chart when the model changes:
didUpdateAttrs() {
this.renderChart();
},
This handles the drawing of the chart:
renderChart() {
let data = this.get('data');
let counts = data.map(data => data.count);
// Update the scales
this.get('yScale').domain([0, Math.max(...counts)]);
this.get('colorScale').domain([0, Math.max(...counts)]);
this.get('xScale').domain(data.map(data => data.label));
// Update the axes
this.get('xAxis').scale(this.get('xScale'));
this.get('xAxisContainer').call(this.get('xAxis')).selectAll('text').attr("y", 0)
.attr("x", 9)
.attr("dy", ".35em")
.attr("transform", "rotate(40)")
.style("text-anchor", "start");
this.get('yAxis').scale(this.get('yScale'));
this.get('yAxisContainer').call(this.get('yAxis'));
let barsUpdate = this.get('barsContainer').selectAll('rect').data(data, data => data.label);
// Enter
let barsEnter = barsUpdate.enter()
.append('rect')
.attr('opacity', 0);
let barsExit = barsUpdate.exit();
let div = select('body')
.append("div")
.attr("class", "vert-tooltip");
// Update
let rafId;
barsEnter
.merge(barsUpdate)
.transition()
.attr('width', `${this.get('xScale').bandwidth()}px`)
.attr('height', data => `${this.get('barsHeight') - this.get('yScale')(data.count)}px`)
.attr('x', data => `${this.get('xScale')(data.label)}px`)
.attr('y', data => `${this.get('yScale')(data.count)}px`)
.attr('fill', data => this.get('colorScale')(data.count))
.attr('opacity', data => {
let selected = this.get('selectedLabel');
return (selected && data.label !== selected) ? '0.5' : '1.0';
})
.on('start', (data, index) => {
if (index === 0) {
(function updateTether() {
Tether.position()
rafId = requestAnimationFrame(updateTether);
})();
}
})
.on('end interrupt', (data, index) => {
if (index === 0) {
cancelAnimationFrame(rafId);
}
});
// Exit
barsExit
.transition()
.attr('opacity', 0)
.remove();
}
I have stripped some tooltip and click events to maintain clarity.
To add the labels I have tried to add the following in the renderChart() function:
barsEnter.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function (d) { return d.count; })
.attr("x", function (d) { return xScale(d.label) + xScale.bandwidth() / 2; })
.attr("y", function (d) { return yScale(d.count) + 12; })
.style("fill", "white");
with the above code I receive an error to say that xScale and yScale are not found because they are not within this functions scope. If I use:
.attr("x", function (d) { return this.get('xScale')(d.label) + this.get('xScale').bandwidth() / 2; })
.attr("y", function (d) { return this.get('yScale')(d.count) + 12; })
I generate 'this.get' is not a function errors and the context of 'this' becomes the an object with the value of (d).
If I add the X and Y scales as variables to this function like:
let xScale = this.get('xScale')
let yScale = this.get('ySCale')
...
.attr("x", function (d) { return xScale(d.label) + xScale.bandwidth() / 2; })
.attr("y", function (d) { return yScale(d.count) + 12; })
Then the x and y attrs are returned as undefined. Please let me know if I have missed anything out.
Converting the function() {} syntax into arrow functions will allow you to maintain the this.
So:
function(d) { return this.get('xScale'); }
becomes
(d) => this.get('xScale')
or
(d) => {
return this.get('xScale');
}
For more information on arrow functions:
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
- https://hackernoon.com/javascript-es6-arrow-functions-and-lexical-this-f2a3e2a5e8c4
I am trying to add Text/Label to my bars in a bar chart using D3.Js.
My texts are appending but from the second index data first index is skipped I dont know why it is doing like this .I have debugged the dat ,data is coming correctly..
I have been doing as below:
function revenueBar(localDataJson) {
var w = 400;
var h = 400;
var barPadding = 1;
var maxRevenue = 0;
var maxTurnOver = 0;
var padding = {
left: 45, right: 10,
top: 40, bottom: 60
}
var maxWidth = w - padding.left - padding.right;
var maxHeight = h - padding.top - padding.bottom;
for (var j = 0; j < localDataJson.length; j++) {
if (localDataJson[j].Revenue > maxRevenue) {
maxRevenue = localDataJson[j].Revenue;
}
}
for (var j = 0; j < localDataJson.length; j++) {
if (localDataJson[j].TurnOver > maxTurnOver) {
maxTurnOver = localDataJson[j].TurnOver;
}
}
var convert = {
x: d3.scale.ordinal(),
y: d3.scale.linear()
};
// Define your axis
var axis = {
x: d3.svg.axis().orient('bottom')
//y: d3.svg.axis().orient('left')
};
// Define the conversion function for the axis points
axis.x.scale(convert.x);
// axis.y.scale(convert.y);
// Define the output range of your conversion functions
convert.y.range([maxHeight, 0]);
convert.x.rangeRoundBands([0, maxWidth]);
convert.x.domain(localDataJson.map(function (d) {
return d.Country;
})
);
convert.y.domain([0, maxRevenue]);
$('#chartBar').html("");
var svg = d3.select("#chartBar")
.append("svg")
.attr("width", w)
.attr("height", h);
// The group node that will contain all the other nodes
// that render your chart
$('.bar-group').html("");
var chart = svg.append('g')
.attr({
class: 'container',
transform: function (d, i) {
return 'translate(' + padding.left + ',' + padding.top + ')';
}
});
chart.append('g') // Container for the axis
.attr({
class: 'x axis',
transform: 'translate(0,' + maxHeight + ')'
})
.call(axis.x)
.selectAll("text")
.attr("x", "-.8em")
.attr("y", ".15em")
.style("text-anchor", "end")
.attr("transform", "rotate(-65)");// Insert an axis inside this node
$('.axis path').css("fill", "none");
chart.append('g') // Container for the axis
// .attr({
// class: 'y axis',
// height: maxHeight,
// })
//.call(axis.y);
var bars = chart
.selectAll('g.bar-group')
.data(localDataJson)
.enter()
.append('g') // Container for the each bar
.attr({
transform: function (d, i) {
return 'translate(' + convert.x(d.Country) + ', 1)';
},
class: 'bar-group'
});
var color = d3.scale.ordinal()
.range(['#f1595f', '#79c36a', '#599ad3', '#f9a65a', '#9e66ab','#cd7058']);
bars.append('rect')
.attr({
y: maxHeight,
height: 0,
width: function (d) { return convert.x.rangeBand(d) - 3; },
class: 'bar'
})
.transition()
.duration(1500)
.attr({
y: function (d, i) {
return convert.y(d.Revenue);
},
height: function (d, i) {
return maxHeight - convert.y(d.Revenue);
}
})
.attr("fill", function (d, i) {
return color(i);
})
var svgs = svg.selectAll("g.container")
// svgs.selectAll("text")
.data(localDataJson)
.enter()
.append("text")
//.transition() // <-- This is new,
// .duration(5000)
.text(function (d) {
return (d.Revenue);
})
.attr("text-anchor", "middle")
//// Set x position to the left edge of each bar plus half the bar width
.attr("x", function (d, i) {
return (i * (w / localDataJson.length)) + ((w / localDataJson.length - barPadding) / 2);
})
.attr({
y: function (d, i) {
return convert.y(d.Revenue) +70;
},
height: function (d, i) {
return maxHeight - convert.y(d.Revenue);
}
})
.attr("font-family", "sans-serif")
.attr("font-size", "13px")
.attr("fill", "white")
}
My Data is:
localdatajson=[
{"Country";"USA","Revenue":"12","TurnOver":"16"},
{"Country";"Brazil","Revenue":"4.5","TurnOver":"16"},
{"Country";"Belzium","Revenue":"4.8","TurnOver":"16"},
{"Country";"Britain","Revenue":"20","TurnOver":"16"},
{"Country";"Canada","Revenue":"6.5","TurnOver":"16"},
{"Country";"DenMark","Revenue":"7.5","TurnOver":"16"}
]
The problem is text is appending but after first one i.e., it is escaping Revenue 12.and appending from second one "4.5"
Please help.
The problem is text is appending but after first one i.e., it is
escaping Revenue 12.and appending from second one "4.5"
This is because your current block that adds the text elements has
var svgs = svg.selectAll("g.container")
.data(localDataJson)
.enter()
...
which means that it searches for g.container elements within svg and tries to link each one to corresponding localDataJson elements (adding new ones for extra localDataJson elements for which it can't find a corresoponding g.container element).
Since you have exactly one g.container element, it will link the first element to that and then adds new text elements for the remaining.
You want to be doing this
var svgs = svg.select("g.container").selectAll("text.label")
.data(localDataJson)
.enter()
.append("text")
.classed("label", true)
...
instead i.e. match text elements in g.container to the data array and add a new one for each extra one.
Notice that we use .label and added the class label - this is because we want to match it to the text elements for the data labels (not say, the ones we add for the x axis labels)
While this solves the problem, you'll probably need a few more corrections in your x and y coordinates for the labels and you don't actually need to set a width for the labels
...
.attr("x", function (d, i) {
return convert.x(d.Country) + (convert.x.rangeBand(d) - 3) / 2;
})
.attr("y", function (d, i) {
return maxHeight;
})
...
I set it to maxHeight just to show it works - the bar height actually goes offchart because there's something wrong with your y scale.
I think I'm missing something very obvious here. Basically what I am trying to do is create a treemap that on button click will go to the server and retrieve the next level into the treemap...This is necessary because the treemap structure is too large and takes too long to calculate so jumping one level at a time is the only option we have.
[Note to IE users, in this example the treemap node names don't appear to be working. Try using Chrome]
http://plnkr.co/edit/simVGU
This code is taken almost exactly from
http://bost.ocks.org/mike/treemap/
I'm using vizData1.json for the "first" level and on mouse click I'm using vizData2.json as the "second" level. You can see that the two end up overlapping. I've tried to do svg.exit() as well as svg.clear() without any luck.
I should also note that I have already tried the sticky(false) suggestion from this post
Does the d3 treemap layout get cached when a root node is passed to it?
UPDATE:
To continue my hunt I have found an example that successfully adds new nodes to an existing treemap. However I am having trouble adapting this logic as the treemap I am attempting to fit this logic into has been heavily customized by Michael Bostock - #mbostock to allow for the nice breadcrumb trail bar at the top.
Code snippet that proves appending to existing treemap nodes is possible:
http://jsfiddle.net/WB5jh/3/
Also, Stackoverflow is forcing me to post code because I'm linking to plnkr so I have dumped my script.js here for those who would rather not interact with plunker
$(function() {
var margin = { top: 20, right: 0, bottom: 0, left: 0 },
width = 960,
height = 500,
formatNumber = d3.format(",d"),
transitioning;
var x = d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y = d3.scale.linear()
.domain([0, height])
.range([0, height]);
var treemap = d3.layout.treemap()
.children(function (d, depth) { return depth ? null : d._children; })
.sort(function (a, b) { return a.value - b.value; })
.ratio(height / width * 0.5 * (1 + Math.sqrt(5)))
.round(false)
.sticky(false);
var svg = d3.select("#treemap")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.style("margin-left", -margin.left + "px")
.style("margin.right", -margin.right + "px")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.style("shape-rendering", "crispEdges");
var grandparent = svg.append("g")
.attr("class", "grandparent");
grandparent.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", margin.top);
grandparent.append("text")
.attr("x", 6)
.attr("y", 6 - margin.top)
.attr("dy", ".75em");
d3.json("vizData1.json", function (root) {
initialize(root);
accumulate(root);
layout(root);
display(root);
});
function initialize(root) {
root.x = root.y = 0;
root.dx = width;
root.dy = height;
root.depth = 0;
}
// Aggregate the values for internal nodes. This is normally done by the
// treemap layout, but not here because of our custom implementation.
// We also take a snapshot of the original children (_children) to avoid
// the children being overwritten when when layout is computed.
function accumulate(d) {
return (d._children = d.children)
? d.value = d.children.reduce(function (p, v) { return p + accumulate(v); }, 0)
: d.value;
}
// Compute the treemap layout recursively such that each group of siblings
// uses the same size (1×1) rather than the dimensions of the parent cell.
// This optimizes the layout for the current zoom state. Note that a wrapper
// object is created for the parent node for each group of siblings so that
// the parent’s dimensions are not discarded as we recurse. Since each group
// of sibling was laid out in 1×1, we must rescale to fit using absolute
// coordinates. This lets us use a viewport to zoom.
function layout(d) {
if (d._children) {
treemap.nodes({ _children: d._children });
d._children.forEach(function (c) {
c.x = d.x + c.x * d.dx;
c.y = d.y + c.y * d.dy;
c.dx *= d.dx;
c.dy *= d.dy;
c.parent = d;
layout(c);
});
}
}
function display(d) {
console.log(d);
grandparent
.datum(d.parent)
.on("click", transition)
.select("text")
.text(name(d));
var g1 = svg.insert("g", ".grandparent")
.datum(d)
.attr("class", "depth");
var g = g1.selectAll("g")
.data(d._children)
.enter().append("g");
g.filter(function (d) { return d._children; })
.classed("children", true)
.on("click", transition);
g.selectAll(".child")
.data(function (d) { return d._children || [d]; })
.enter().append("rect")
.attr("class", "child")
.call(rect);
g.append("rect")
.attr("class", "parent")
.call(rect)
.append("title")
.text(function (d) { return formatNumber(d.value); });
g.append("foreignObject")
.call(rect)
.attr("class", "foreignobj")
.append("xhtml:div")
.attr("dy", ".75em")
.html(function (d) { return d.name; })
.attr("class", "textdiv");
function transition(d) {
if (transitioning || !d) return;
transitioning = true;
d3.json("vizData2.json", function (root) {
initialize(root);
accumulate(root);
layout(root);
display(root);
});
var g2 = display(d),
t1 = g1.transition().duration(750),
t2 = g2.transition().duration(750);
// Update the domain only after entering new elements.
x.domain([d.x, d.x + d.dx]);
y.domain([d.y, d.y + d.dy]);
// Enable anti-aliasing during the transition.
svg.style("shape-rendering", null);
// Draw child nodes on top of parent nodes.
svg.selectAll(".depth").sort(function (a, b) { return a.depth - b.depth; });
// Fade-in entering text.
g2.selectAll("text").style("fill-opacity", 0);
g2.selectAll("foreignObject div").style("display", "none"); /*added*/
// Transition to the new view.
t1.selectAll("text").call(text).style("fill-opacity", 0);
t2.selectAll("text").call(text).style("fill-opacity", 1);
t1.selectAll("rect").call(rect);
t2.selectAll("rect").call(rect);
t1.selectAll(".textdiv").style("display", "none"); /* added */
t1.selectAll(".foreignobj").call(foreign);
t2.selectAll(".textdiv").style("display", "block"); /* added */
t2.selectAll(".foreignobj").call(foreign); /* added */
// Remove the old node when the transition is finished.
t1.remove().each("end", function () {
svg.style("shape-rendering", "crispEdges");
transitioning = false;
});
}
return g;
}
function text(text) {
text.attr("x", function (d) { return x(d.x) + 6; })
.attr("y", function (d) { return y(d.y) + 6; });
}
function rect(rect) {
rect.attr("x", function (d) { return x(d.x); })
.attr("y", function (d) { return y(d.y); })
.attr("width", function (d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function (d) { return y(d.y + d.dy) - y(d.y); });
}
function foreign(foreign) { /* added */
foreign.attr("x", function (d) { return x(d.x); })
.attr("y", function (d) { return y(d.y); })
.attr("width", function (d) { return x(d.x + d.dx) - x(d.x); })
.attr("height", function (d) { return y(d.y + d.dy) - y(d.y); });
}
function name(d) {
return d.parent
? name(d.parent) + "." + d.name
: d.name;
}
});
I've got a fairly simple reusable chart built in D3.js -- some circles and some text.
I'm struggling to figure out how to update the chart with new data, without redrawing the entire chart.
With the current script, I can see that the new data is bound to the svg element, but none of the data-driven text or attributes is updating. Why isn't the chart updating?
Here's a fiddle: http://jsfiddle.net/rolfsf/em5kL/1/
I'm calling the chart like this:
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 224,
Clusters: [
['Other', 45],
['FooBars', 30],
['Foos', 50],
['Bars', 124],
['BarFoos', 0]
]
})
.call( clusterChart() );
When the button is clicked, I'm simply calling the chart again, with different data:
$("#doSomething").on("click", function(){
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 122,
Clusters: [
['Other', 14],
['FooBars', 60],
['Foos', 22],
['Bars', 100],
['BarFoos', 5]
]
})
.call( clusterChart() );
});
The chart script:
function clusterChart() {
var width = 450,
margin = 0,
radiusAll = 72,
maxRadius = radiusAll - 5,
r = d3.scale.linear(),
padding = 1,
height = 3 * (radiusAll*2 + padding),
startAngle = Math.PI / 2,
onTotalMouseOver = null,
onTotalClick = null,
onClusterMouseOver = null,
onClusterClick = null;
val = function(d){return d};
function chart(selection) {
selection.each(function(data) {
var cx = width / 2,
cy = height / 2,
stepAngle = 2 * Math.PI / data.Clusters.length,
outerRadius = 2*radiusAll + padding;
r = d3.scale.linear()
.domain([0, d3.max(data.Clusters, function(d){return d[1];})])
.range([50, maxRadius]);
var svg = d3.select(this).selectAll("svg")
.data([data])
.enter().append("svg");
//enter
var totalCircle = svg.append("circle")
.attr("class", "total-cluster")
.attr('cx', cx)
.attr('cy', cy)
.attr('r', radiusAll)
.on('mouseover', onTotalMouseOver)
.on('click', onTotalClick);
var totalName = svg.append("text")
.attr("class", "total-name")
.attr('x', cx)
.attr('y', cy + 16);
var totalValue = svg.append("text")
.attr("class", "total-value")
.attr('x', cx)
.attr('y', cy + 4);
var clusters = svg.selectAll('circle.cluster')
.data(data.Clusters)
.enter().append('circle')
.attr("class", "cluster");
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-value');
var clusterNames = svg.selectAll("text.cluster-name")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-name');
clusters .attr('cx', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('cy', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius; })
.attr("r", "10")
.on('mouseover', function(d, i, j) {
if (onClusterMouseOver != null) onClusterMouseOver(d, i, j);
})
.on('mouseout', function() { /*do something*/ })
.on('click', function(d, i){ onClusterClick(d); });
clusterNames
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius + 16; });
clusterValues
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius - 4; });
//update with data
svg .selectAll('text.total-value')
.text(val(data.Value));
svg .selectAll('text.total-name')
.text(val(data.Name));
clusters
.attr('class', function(d, i) {
if(d[1] === 0){ return 'cluster empty'}
else {return 'cluster'}
})
.attr("r", function (d, i) { return r(d[1]); });
clusterValues
.text(function(d) { return d[1] });
clusterNames
.text(function(d, i) { return d[0] });
$(window).resize(function() {
var w = $('.cluster-chart').width(); //make this more generic
svg.attr("width", w);
svg.attr("height", w * height / width);
});
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onClusterClick = function(_) {
if (!arguments.length) return onClusterClick;
onClusterClick = _;
return chart;
};
return chart;
}
I have applied the enter/update/exit pattern across all relevant svg elements (including the svg itself). Here is an example segment:
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters,function(d){ return d[1];});
clusterValues.exit().remove();
clusterValues
.enter().append('text')
.attr('class', 'cluster-value');
...
Here is a complete FIDDLE with all parts working.
NOTE: I tried to touch your code as little as possible since you have carefully gone about applying a re-usable approach. This the reason why the enter/update/exit pattern is a bit different between the total circle (and text), and the other circles (and text). I might have gone about this using a svg:g element to group each circle and associated text.
I've got a simple donut chart in d3.js, which will only be used to compare 2 or 3 items.
http://jsfiddle.net/Ltqu2/
I want to combine the legend and values as text in the center of the chart.
In the current implementation, the text alignment is ok for 3 items, but for 2 items it doesn't adjust. The alignment is somewhat hard coded:
var l = svg.selectAll('.legend')
.data(data)
.enter().append('g')
.attr('class', 'legend');
l.append('text')
.attr('x', 0)
.attr('y', function(d, i) { return i * 40 - radius / 2 + 10; })
.attr('class', function(d, i){return 'legend-label data-label value-' + (i+1)})
.text(function(d, i) { return d + '%'; });
l.append('text')
.attr('x', 0)
.attr('y', function(d, i) { return i * 40 - radius / 2 + 22; })
.attr('class', function(d, i){return 'legend-label units-label value-' + (i+1)})
.text(function(d, i) { return legend[i]; });
How can I make the text alignment more flexible, so it distributes evenly for both 2 and 3 items? Is there something I can use .rangeround for?
Here's the full script
/**
* Donut chart for d3.js
*/
function donutChart() {
var width = 420,
height = 420,
radius = 0,
factor = 0.7;
var legend = ['Low', 'Medium', 'High'];
function chart(selection) {
selection.each(function(data) {
if (radius == 0) {
radius = Math.min(width, height) / 2 - 10;
}
var arc = d3.svg.arc()
.innerRadius(radius * factor)
.outerRadius(radius);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d; });
var svg = d3.select(this).append('svg')
.attr('width', width)
.attr('height', height)
.attr('class', 'donut')
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var g = svg.selectAll('.arc')
.data(pie(data))
.enter().append('g')
.attr('class', 'arc');
g.append('path')
.attr('d', arc)
.attr('class', function(d, i){return 'value-' + (i+1)})
.style('stroke', '#fff');
var l = svg.selectAll('.legend')
.data(data)
.enter().append('g')
.attr('class', 'legend');
l.append('text')
.attr('x', 0)
.attr('y', function(d, i) { return i * 40 - radius / 2 + 10; })
.attr('class', function(d, i){return 'legend-label data-label value-' + (i+1)})
.text(function(d, i) { return d + '%'; });
l.append('text')
.attr('x', 0)
.attr('y', function(d, i) { return i * 40 - radius / 2 + 22; })
.attr('class', function(d, i){return 'legend-label units-label value-' + (i+1)})
.text(function(d, i) { return legend[i]; });
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
chart.radius = function(_) {
if (!arguments.length) return radius;
radius = _;
return chart;
};
chart.factor = function(_) {
if (!arguments.length) return factor;
factor = _;
return chart;
};
chart.legend = function(_) {
if (!arguments.length) return legend;
legend = _;
return chart;
};
return chart;
}
d3.select('#chart')
.datum([78, 20])
.call(donutChart()
.width(220)
.height(220)
.legend(['This Thing', 'That Thing'])
);
d3.select('#chart2')
.datum([63, 20, 15])
.call(donutChart()
.width(220)
.height(220)
.legend(['This Thing', 'That Thing', 'Another Thing'])
);
I would put all the legend text elements in a common <g> element which can then be translated to center vertically. Determining actual text sizes in SVG is always a pain, you can look into getBBox() for more information on that. But using a predetermined text height will work fine. Here's a bit of calculation (separated into many steps for clarity):
// hardcoding 36 here, you could use getBBox to get the actual SVG text height using sample text if you really wanted.
var legendItemHeight = 36;
// calculated height of all legend items together
var actualLegendHeight = data.length * legendItemHeight;
// inner diameter
var availableLegendHeight = radius * factor * 2;
// y-coordinate of first legend item (relative to the center b/c the main svg <g> element is translated
var legendOffset = (availableLegendHeight - actualLegendHeight) / 2 - (radius*factor);
And then create a <g> element to hold the <text> elements:
// append all the legend items to a common group which is translated
var l = svg.selectAll('.legend')
.data(data)
.enter().append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
return 'translate(0, '+ (legendOffset + i * legendHeight)+')';
});
Modified jsFiddle: http://jsfiddle.net/rshKs/2/