I have created a calendar heatmap, but the problem is that the data that falls into the brake "over 4500" is marked in black instead of the indicated color #3d3768. It looks like the color cannot be found for this category. Why?
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<title>Data Calendar</title>
<style>
.month {
fill: none;
stroke: #000;
stroke-width: 2px;
}
.day {
fill: #fff;
stroke: #ccc;
}
text {
font-family:sans-serif;
font-size:1.5em;
}
.dayLabel {
fill:#aaa;
font-size:0.8em;
}
.monthLabel {
text-anchor:middle;
font-size:0.8em;
fill:#aaa;
}
.yearLabel {
fill:#aaa;
font-size:1.2em;
}
.key {font-size:0.5em;}
</style>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script>
var breaks=[1500,2500,3500,4500,4550];
var colours=["#e2dff1","#b7b1dd","#8b82c8","#584f95","#3d3768"];
//general layout information
var cellSize = 17;
var xOffset=20;
var yOffset=60;
var calY=50;//offset of calendar in each group
var calX=25;
var width = 960;
var height = 163;
var parseDate = d3.time.format("%d/%m/%y").parse;
format = d3.time.format("%d-%m-%Y");
toolDate = d3.time.format("%d/%b/%y");
d3.csv("data.csv", function(error, data) {
//set up an array of all the dates in the data which we need to work out the range of the data
var dates = new Array();
var values = new Array();
//parse the data
data.forEach(function(d) {
dates.push(parseDate(d.date));
values.push(d.value);
d.date=parseDate(d.date);
d.value=d.value;
d.year=d.date.getFullYear();//extract the year from the data
});
var yearlyData = d3.nest()
.key(function(d){return d.year;})
.entries(data);
var svg = d3.select("body").append("svg")
.attr("width","90%")
.attr("viewBox","0 0 "+(xOffset+width)+" 540")
//title
svg.append("text")
.attr("x",xOffset)
.attr("y",20)
.text(title);
//create an SVG group for each year
var cals = svg.selectAll("g")
.data(yearlyData)
.enter()
.append("g")
.attr("id",function(d){
return d.key;
})
.attr("transform",function(d,i){
return "translate(0,"+(yOffset+(i*(height+calY)))+")";
})
var labels = cals.append("text")
.attr("class","yearLabel")
.attr("x",xOffset)
.attr("y",15)
.text(function(d){return d.key});
//create a daily rectangle for each year
var rects = cals.append("g")
.attr("id","alldays")
.selectAll(".day")
.data(function(d) { return d3.time.days(new Date(parseInt(d.key), 0, 1), new Date(parseInt(d.key) + 1, 0, 1)); })
.enter().append("rect")
.attr("id",function(d) {
return "_"+format(d);
//return toolDate(d.date)+":\n"+d.value+" dead or missing";
})
.attr("class", "day")
.attr("width", cellSize)
.attr("height", cellSize)
.attr("x", function(d) {
return xOffset+calX+(d3.time.weekOfYear(d) * cellSize);
})
.attr("y", function(d) { return calY+(d.getDay() * cellSize); })
.datum(format);
//create day labels
var days = ['Su','Mo','Tu','We','Th','Fr','Sa'];
var dayLabels=cals.append("g").attr("id","dayLabels")
days.forEach(function(d,i) {
dayLabels.append("text")
.attr("class","dayLabel")
.attr("x",xOffset)
.attr("y",function(d) { return calY+(i * cellSize); })
.attr("dy","0.9em")
.text(d);
})
//let's draw the data on
var dataRects = cals.append("g")
.attr("id","dataDays")
.selectAll(".dataday")
.data(function(d){
return d.values;
})
.enter()
.append("rect")
.attr("id",function(d) {
return format(d.date)+":"+d.value;
})
.attr("stroke","#ccc")
.attr("width",cellSize)
.attr("height",cellSize)
.attr("x", function(d){return xOffset+calX+(d3.time.weekOfYear(d.date) * cellSize);})
.attr("y", function(d) { return calY+(d.date.getDay() * cellSize); })
.attr("fill", function(d) {
if (d.value<breaks[0]) {
return colours[0];
}
for (i=0;i<breaks.length+1;i++){
if (d.value>=breaks[i]&&d.value<breaks[i+1]){
return colours[i];
}
}
if (d.value>breaks.length-1){
return colours[breaks.length]
}
})
//append a title element to give basic mouseover info
dataRects.append("title")
.text(function(d) { return toolDate(d.date)+":\n"+d.value+units; });
//add montly outlines for calendar
cals.append("g")
.attr("id","monthOutlines")
.selectAll(".month")
.data(function(d) {
return d3.time.months(new Date(parseInt(d.key), 0, 1),
new Date(parseInt(d.key) + 1, 0, 1));
})
.enter().append("path")
.attr("class", "month")
.attr("transform","translate("+(xOffset+calX)+","+calY+")")
.attr("d", monthPath);
//retreive the bounding boxes of the outlines
var BB = new Array();
var mp = document.getElementById("monthOutlines").childNodes;
for (var i=0;i<mp.length;i++){
BB.push(mp[i].getBBox());
}
var monthX = new Array();
BB.forEach(function(d,i){
boxCentre = d.width/2;
monthX.push(xOffset+calX+d.x+boxCentre);
})
//create centred month labels around the bounding box of each month path
//create day labels
var months = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
var monthLabels=cals.append("g").attr("id","monthLabels")
months.forEach(function(d,i) {
monthLabels.append("text")
.attr("class","monthLabel")
.attr("x",monthX[i])
.attr("y",calY/1.2)
.text(d);
})
//create key
var key = svg.append("g")
.attr("id","key")
.attr("class","key")
.attr("transform",function(d){
return "translate("+xOffset+","+(yOffset-(cellSize*1.5))+")";
});
key.selectAll("rect")
.data(colours)
.enter()
.append("rect")
.attr("width",cellSize)
.attr("height",cellSize)
.attr("x",function(d,i){
return i*130;
})
.attr("fill",function(d){
return d;
});
key.selectAll("text")
.data(colours)
.enter()
.append("text")
.attr("x",function(d,i){
return cellSize+5+(i*130);
})
.attr("y","1em")
.text(function(d,i){
if (i<colours.length-1){
return "up to "+breaks[i] + "%";
} else {
return "over "+breaks[i-1] + "%";
}
});
});//end data load
//pure Bostock - compute and return monthly path data for any year
function monthPath(t0) {
var t1 = new Date(t0.getFullYear(), t0.getMonth() + 1, 0),
d0 = t0.getDay(), w0 = d3.time.weekOfYear(t0),
d1 = t1.getDay(), w1 = d3.time.weekOfYear(t1);
return "M" + (w0 + 1) * cellSize + "," + d0 * cellSize
+ "H" + w0 * cellSize + "V" + 7 * cellSize
+ "H" + w1 * cellSize + "V" + (d1 + 1) * cellSize
+ "H" + (w1 + 1) * cellSize + "V" + 0
+ "H" + (w0 + 1) * cellSize + "Z";
}
</script>
</body>
</html>
Your color selection code could be reduced somewhat in size, and the use of a d3 scale to chose your color would make this much cleaner as well. But, ultimately, your problem lies here:
if (d.value>breaks.length-1){
return colours[breaks.length]
}
This section of code is only called when a color has not yet been returned, that is, for those values that are greater than 4550. While I am not sure why the if statement in this code block is needed, your arrays breaks and colours are the same length. Essentially you are returning array[array.length], which will not return a value as arrays are zero indexed. You probably want to return colours[colours.length-1] as you want the last specified colour.
Edit: Also, if you assign a color for those below your lowest break (which it appears you do) and those above your highest, you will need one more color than break value. Eg: A one break scale has two colors for those above/below the break. However, you may have intentionally applied colours[0] to both: values under breaks[0] and values between breaks[0] and breaks[1]:
if (d.value<breaks[0]) {
return colours[0];
}
for (i=0;i<breaks.length+1;i++){
if (d.value>=breaks[i]&&d.value<breaks[i+1]){
return colours[i];
}
}
Related
I'm using d3js to move circular dots from right to left with respect to current time. I have a couple of issues:
1. .exit().remove() don't work. Node doesn't get removed once it goes out of the view.
2. Transition of circles are a bit jumpy
var circles = g.selectAll('path')
circles.data(data)
.enter()
.append('path')
.attr("d", symbol.type(d3.symbolCircle))
.merge(circles)
.attr("transform", (d) => "translate(" + x(d.x) + "," + y(d.y) + ")");
circles.exit().remove();
You can see my full code here: http://jsfiddle.net/hsdhott/3tdhuLgm/
Besides your enter-exit-update pattern not being correct (please check the snippet bellow), the big problem here is the data:
selection.exit() method won't magically select — normally for using remove() later — an element based on any arbitrary criterion, such as "getting out of the view". It's based on the data only. And the problem is that your data never stops increasing:
if (count % 10 === 0) {
var point = {
x: globalX,
y: ((Math.random() * 200 + 50) >> 0)
};
data.push(point);
}
So, a very quick solution in this case is removing the data points based on your x domain:
data = data.filter(function(d) {
return d.x > globalX - 10000;
});
This is just a suggestion, use the logic you want for removing the objects from the data array. However, regardless the logic you use, you have to remove them.
Regarding the jumpy transition, the problem is that you're using selection.transition and setInterval. That won't work, chose one of them.
Here is your updated code:
var data = [];
var width = 500;
var height = 350;
var globalX = new Date().getTime();
/* var globalX = 0; */
var duration = 250;
var step = 10;
var count = 0;
var chart = d3.select('#chart')
.attr('width', width + 50)
.attr('height', height + 50);
var x = d3.scaleTime()
.domain([globalX, (globalX - 10000)])
.range([0, width]);
var y = d3.scaleLinear()
.domain([0, 300])
.range([300, 0]);
// -----------------------------------
// Draw the axis
var xAxis = d3.axisBottom()
.scale(x)
.ticks(10)
.tickFormat(formatter);
var axisX = chart.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0, 300)')
.call(xAxis);
// Append the holder for line chart and circles
var g = chart.append('g');
function formatter(time) {
if ((time.getSeconds() % 5) != 0) {
return "";
}
return d3.timeFormat('%H:%M:%S')(time);
}
function createData() {
// Generate new data
var point = {
x: globalX,
y: ((Math.random() * 200 + 50) >> 0)
};
data.push(point);
}
function callInterval() {
count++;
if (count % 3 === 0) createData();
}
// Main loop
function tick() {
// Generate new data
if (count % 10 === 0) {
var point = {
x: globalX,
y: ((Math.random() * 200 + 50) >> 0)
};
data.push(point);
}
data = data.filter(function(d) {
return d.x > globalX - 10000;
});
count++;
globalX = new Date().getTime();
var timer = new Date().getTime();
var symbol = d3.symbol().size([100]),
color = d3.schemeCategory10;
var circles = g.selectAll('path')
.data(data);
circles = circles.enter()
.append('path')
.attr("d", symbol.type(d3.symbolCircle))
.merge(circles)
.attr("transform", (d) => "translate(" + x(d.x) + "," + y(d.y) + ")");
circles.exit().remove();
// Shift the chart left
x.domain([timer - 10000, timer]);
axisX.call(xAxis);
g.attr('transform', null)
.attr('transform', 'translate(' + x(globalX - 10000) + ')');
// Remote old data (max 50 points)
if (data.length && (data[data.length - 1].x < (globalX - 10000))) data.shift();
}
tick();
setInterval(tick, 10);
.axis {
font-family: sans-serif;
font-size: 12px;
}
.line {
fill: none;
stroke: #f1c40f;
stroke-width: 3px;
}
.circle {
stroke: #e74c3c;
stroke-width: 3px;
fill: #FFF;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg id="chart"></svg>
drawAll is a two dimensional array where drawAll[i] is an element/drawing containing all the strokes in that drawing. A stroke, represented as a string, is a series of 2D points that can be used to create a <path>.
I am trying to make each .element draggable to anywhere else in the #canvas svg. I have followed this answer, but my elements are not getting dragged easily---I have to try dragging multiple times before the drag events are getting fired. I'm not sure what is going wrong or how the elements could be dragged easily. Thanks in advance. :)
drawAll example with two elements (containing only one stroke per element):
var drawAll = [["M6,239 C6,239 46,149 88,67 127,6 135,0 140,19 143,74 151,129 158,156 185,221 184,243 166,251 127,255 52,252 0,246"],["M113,15 C113,15 45,95 28,114 23,115 0,148 48,166 114,184 255,184 208,125 178,75 147,12 122,0"]];
HTML:
<div id="wrapper">
<svg id="canvas" preserveAspectRatio="none" height="400" width="400" viewbox="0 0 2000 2000"></svg>
</div>
JS:
function getData(){
return new Promise(function(resolve, reject){
var data;
$.getJSON(Flask.url_for("data"))
.done(function(json){
data = json;
var drawAll = [];
var i, j;
var temp1 = data;
for (i in temp1){
var temp2 = temp1[i];
var drawOne = [];
for (j in temp2){
var temp3 = temp2[j][0];
var temp4 = temp2[j][1];
var points = "";
for (k in temp3){
if(k == 0)
{
points = points + "M" + temp3[k] + ",";
points = points + temp4[k] + " ";
points = points + "C" + temp3[k] + ",";
points = points + temp4[k] + " ";
}
else
{
points = points + temp3[k] + ",";
points = points + temp4[k] + " ";
}
}
drawOne.push(points);
}
drawAll.push(drawOne);
}
resolve(drawAll);
reject("Error in fetching JSON");
});
});
}
function draw(drawAll){
var width = 2000;
var spacing = 300;
var x = -spacing;
var temp = -spacing;
var y = 0;
var id = 0;
var elements = d3.select("#canvas");
var element = elements.selectAll("svg.element")
.data(drawAll)
.enter()
.append("svg")
.attr("class", "element")
.attr("id", function(){
return id++;
})
.attr("x", function(){
if(x + spacing > width)
x = -spacing;
x = x + spacing;
return x;
})
.attr("y", function(){
if(temp + spacing > width)
{
temp = -spacing;
y = y + spacing;
}
temp = temp + spacing;
return y;
})
.call(d3.drag()
.on("start", function(d){
d3.select(this).raise();
})
.on("drag", function(d){
d3.select(this)
.attr("x", d.x = d3.event.x)
.attr("y", d.y = d3.event.y);
})
.on("end", function(d){
}));
element.selectAll("path .stroke")
.data(function(d){
return d;
})
.enter()
.append("path")
.attr("class", "stroke")
.attr("d", function(d){
return d;
})
.attr("style", "fill: none; stroke: black; stroke-width: 2;");
return new Promise(function(resolve, reject){
resolve(drawAll);
reject("Error in drawing JSON");
});
}
function runner(){
return getData()
.then(draw)
.catch(function(msg){
console.log(msg);
});
}
runner().catch(function(msg){
console.log(msg);
});
Take a look at the pointer-events presentation attribute to tweak where events can be raised.
To hit a narrow stroke, you could also combine it with a wider, transparent line that is only there to catch the events:
var group = element.selectAll("g .stroke")
.data(function(d){
return d;
})
.enter()
.append("g")
.attr("style", "fill: none; stroke: black")
.attr("class", "stroke");
group.append("path")
.attr("d", function(d){
return d;
})
.attr("style", "stroke-width: 10; stroke-linecap: round; opacity: 0");
group.append("path")
.attr("d", function(d){
return d;
})
.attr("style", "stroke-width: 2;");
Legend Color not matching Screenshot I am Working on the d3js sequence sunburst chart.
My Sample Data is:
Shift-Mode-Machineid,MachineDuration
DayShift-auto-1091,1
DayShift-auto-1100,5
DayShift-auto-1100,117
DayShift-manual-1111,4
DayShift-manual-1112,6
DayShift-manual-1120,43
NightShift-auto-1150,9
NightShift-auto-1150,85
NightShift-auto-1150,6
NightShift-manual-1120,16
NightShift-manual-1120,1
NightShift-manual-1120,4
Please suggest how to match the legend color and the chart color. The text inside the legend is perfect but the color is a mismatch. I have to put correct colors. Please help.
// Dimensions of sunburst.[Screenshot of wrong color in legend][1]
var width = 750;
var height = 600;
var radius = Math.min(width, height) / 2;
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
var b = {
w: 75, h: 30, s: 3, t: 10
};
// Mapping of step names to colors.
var colors = {};
var color = d3.scale.category20b();
/*{
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
}*/;
// Total size of all segments; we set this later, after loading the data.
var totalSize = 0;
var vis = d3.select("#chart").append("svg:svg")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
// Use d3.text and d3.csv.parseRows so that we do not need to have a header
// row, and can receive the csv as an array of arrays.
d3.text("Data.csv", function(text) {
var csv = d3.csv.parseRows(text);
var json = buildHierarchy(csv);
/*After getting proper json, we are making colors variable to hold our data keys
*/
json.children.forEach(function(root, ind){
colors[root.name]=color(ind);
});
createVisualization(json);
});
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Basic setup of page elements.
initializeBreadcrumbTrail();
drawLegend();
d3.select("#togglelegend").on("click", toggleLegend);
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("svg:circle")
.attr("r", radius)
.style("opacity", 0);
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
//.style("fill", function(d) { return colors[d.name]; })
.style("fill", function(d) { return color((d.parent ? d : d.children).name); })
.style("opacity", 1)
.on("mouseover", mouseover);
// Add the mouseleave handler to the bounding circle.
d3.select("#container").on("mouseleave", mouseleave);
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
};
// Fade all but the current sequence, and show it in the breadcrumb trail.
function mouseover(d) {
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 0.1) {
percentageString = "< 0.1%";
}
d3.select("#percentage")
.text(percentageString);
d3.select("#explanation")
.style("visibility", "");
var sequenceArray = getAncestors(d);
updateBreadcrumbs(sequenceArray, percentageString);
// Fade all the segments.
d3.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
function mouseleave(d) {
// Hide the breadcrumb trail
d3.select("#trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
d3.selectAll("path")
.transition()
.duration(1000)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", mouseover);
});
d3.select("#explanation")
.style("visibility", "hidden");
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
function getAncestors(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
function initializeBreadcrumbTrail() {
// Add the svg area.
var trail = d3.select("#sequence").append("svg:svg")
.attr("width", width)
.attr("height", 50)
.attr("id", "trail");
// Add the label at the end, for the percentage.
trail.append("svg:text")
.attr("id", "endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
function breadcrumbPoints(d, i) {
var points = [];
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// Update the breadcrumb trail to show the current sequence and percentage.
function updateBreadcrumbs(nodeArray, percentageString) {
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select("#trail")
.selectAll("g")
.data(nodeArray, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("svg:g");
entering.append("svg:polygon")
.attr("points", breadcrumbPoints)
// .style("fill", function(d) { return colors[d.name]; });
.style("fill", function(d) { return color((d.parent ? d : d.children).name); });
entering.append("svg:text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select("#trail").select("#endlabel")
.attr("x", (nodeArray.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(percentageString);
// Make the breadcrumb trail visible, if it's hidden.
d3.select("#trail")
.style("visibility", "");
}
function drawLegend() {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
var legend = d3.select("#legend").append("svg:svg")
.attr("width", li.w)
.attr("height", d3.keys(colors).length * (li.h + li.s));
var g = legend.selectAll("g")
.data(d3.entries(colors))
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return d.value; });
g.append("svg:text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.key; });
}
function toggleLegend() {
var legend = d3.select("#legend");
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
}
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
function buildHierarchy(csv) {
var root = {"name": "root", "children": []};
for (var i = 0; i < csv.length; i++) {
var sequence = csv[i][0];
var size = +csv[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split("-");
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
};
After doing some analysis, I could suggest you below code, try it once.
// Dimensions of sunburst.
var width = 750;
var height = 600;
var radius = Math.min(width, height) / 2;
// Breadcrumb dimensions: width, height, spacing, width of tip/tail.
var b = {
w: 75, h: 30, s: 3, t: 10
};
// Mapping of step names to colors.
var colors = {};/*{
"home": "#5687d1",
"product": "#7b615c",
"search": "#de783b",
"account": "#6ab975",
"other": "#a173d1",
"end": "#bbbbbb"
}*/;
// Total size of all segments; we set this later, after loading the data.
var totalSize = 0;
var vis = d3.select("#chart").append("svg:svg")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.size; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
// Use d3.text and d3.csv.parseRows so that we do not need to have a header
// row, and can receive the csv as an array of arrays.
d3.text("visit-sequences.csv", function(text) {
var csv = d3.csv.parseRows(text);
var json = buildHierarchy(csv);
/*After getting proper json, we are making colors variable to hold our data keys
*/
json.children.forEach(function(root, ind){
colors[root.name]=color(root.name); //Changed ind to root.name
});
createVisualization(json);
});
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Basic setup of page elements.
initializeBreadcrumbTrail();
drawLegend();
d3.select("#togglelegend").on("click", toggleLegend);
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("svg:circle")
.attr("r", radius)
.style("opacity", 0);
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json)
.filter(function(d) {
return (d.dx > 0.005); // 0.005 radians = 0.29 degrees
});
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("svg:path")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return colors[d.name]; })
.style("opacity", 1)
.on("mouseover", mouseover);
// Add the mouseleave handler to the bounding circle.
d3.select("#container").on("mouseleave", mouseleave);
// Get total size of the tree = value of root node from partition.
totalSize = path.node().__data__.value;
};
// Fade all but the current sequence, and show it in the breadcrumb trail.
function mouseover(d) {
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 0.1) {
percentageString = "< 0.1%";
}
d3.select("#percentage")
.text(percentageString);
d3.select("#explanation")
.style("visibility", "");
var sequenceArray = getAncestors(d);
updateBreadcrumbs(sequenceArray, percentageString);
// Fade all the segments.
d3.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
vis.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
// Restore everything to full opacity when moving off the visualization.
function mouseleave(d) {
// Hide the breadcrumb trail
d3.select("#trail")
.style("visibility", "hidden");
// Deactivate all segments during transition.
d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
d3.selectAll("path")
.transition()
.duration(1000)
.style("opacity", 1)
.each("end", function() {
d3.select(this).on("mouseover", mouseover);
});
d3.select("#explanation")
.style("visibility", "hidden");
}
// Given a node in a partition layout, return an array of all of its ancestor
// nodes, highest first, but excluding the root.
function getAncestors(node) {
var path = [];
var current = node;
while (current.parent) {
path.unshift(current);
current = current.parent;
}
return path;
}
function initializeBreadcrumbTrail() {
// Add the svg area.
var trail = d3.select("#sequence").append("svg:svg")
.attr("width", width)
.attr("height", 50)
.attr("id", "trail");
// Add the label at the end, for the percentage.
trail.append("svg:text")
.attr("id", "endlabel")
.style("fill", "#000");
}
// Generate a string that describes the points of a breadcrumb polygon.
function breadcrumbPoints(d, i) {
var points = [];
points.push("0,0");
points.push(b.w + ",0");
points.push(b.w + b.t + "," + (b.h / 2));
points.push(b.w + "," + b.h);
points.push("0," + b.h);
if (i > 0) { // Leftmost breadcrumb; don't include 6th vertex.
points.push(b.t + "," + (b.h / 2));
}
return points.join(" ");
}
// Update the breadcrumb trail to show the current sequence and percentage.
function updateBreadcrumbs(nodeArray, percentageString) {
// Data join; key function combines name and depth (= position in sequence).
var g = d3.select("#trail")
.selectAll("g")
.data(nodeArray, function(d) { return d.name + d.depth; });
// Add breadcrumb and label for entering nodes.
var entering = g.enter().append("svg:g");
entering.append("svg:polygon")
.attr("points", breadcrumbPoints)
.style("fill", function(d) { return colors[d.name]; });
entering.append("svg:text")
.attr("x", (b.w + b.t) / 2)
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; });
// Set position for entering and updating nodes.
g.attr("transform", function(d, i) {
return "translate(" + i * (b.w + b.s) + ", 0)";
});
// Remove exiting nodes.
g.exit().remove();
// Now move and update the percentage at the end.
d3.select("#trail").select("#endlabel")
.attr("x", (nodeArray.length + 0.5) * (b.w + b.s))
.attr("y", b.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(percentageString);
// Make the breadcrumb trail visible, if it's hidden.
d3.select("#trail")
.style("visibility", "");
}
function drawLegend() {
// Dimensions of legend item: width, height, spacing, radius of rounded rect.
var li = {
w: 75, h: 30, s: 3, r: 3
};
var legend = d3.select("#legend").append("svg:svg")
.attr("width", li.w)
.attr("height", d3.keys(colors).length * (li.h + li.s));
var g = legend.selectAll("g")
.data(d3.entries(colors))
.enter().append("svg:g")
.attr("transform", function(d, i) {
return "translate(0," + i * (li.h + li.s) + ")";
});
g.append("svg:rect")
.attr("rx", li.r)
.attr("ry", li.r)
.attr("width", li.w)
.attr("height", li.h)
.style("fill", function(d) { return d.value; });
g.append("svg:text")
.attr("x", li.w / 2)
.attr("y", li.h / 2)
.attr("dy", "0.35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.key; });
}
function toggleLegend() {
var legend = d3.select("#legend");
if (legend.style("visibility") == "hidden") {
legend.style("visibility", "");
} else {
legend.style("visibility", "hidden");
}
}
// Take a 2-column CSV and transform it into a hierarchical structure suitable
// for a partition layout. The first column is a sequence of step names, from
// root to leaf, separated by hyphens. The second column is a count of how
// often that sequence occurred.
function buildHierarchy(csv) {
var root = {"name": "root", "children": []};
for (var i = 0; i < csv.length; i++) {
var sequence = csv[i][0];
var size = +csv[i][1];
if (isNaN(size)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split("-");
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode["children"];
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k]["name"] == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {"name": nodeName, "children": []};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {"name": nodeName, "size": size};
children.push(childNode);
}
}
}
return root;
};
Try this code n tell me.
We need to change
json.children.forEach(function(root, ind){
colors[root.name]=color(ind); //Changed ind to root.name
});
so that
json.children.forEach(function(root, ind){
colors[root.name]=color(root.name);
});
Hope this is causing colors not to match.
Try n tell me now.
I have a d3 layout that is intended to produce 3 charts:
Custom item layout w/ transitions
Pie chart for sales by category (with transitions at some point)
Bar chart for top 5 performing items w/ transitions.
1 & 2 work OK but when I add the third chart, I see some strange behavior. The intention is to create a bar chart where the width of each bar is tied to the sales metric for the top N items determined like so:
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
topData = salesDimension.top(5);
The problem is that instead of drawing the bar chart, my code somehow overwrites the position of 5 items in chart 1 and moves them back to the origin. If I change 5 to 10 above then 10 items are overwritten and so on.
I double checked the scope of my variables and even tried changing the names of everything in my drawTopItems() which made no difference. I suspect that I am doing something incorrectly when it comes to selecting the svg element or applying classes to the svg group elements that I want to modify but I can't for the life of me see what. Can anyone tell me what I might be doing wrong?
Here is my issue in a fiddle: https://jsfiddle.net/Sledge/4eggpd5e/12/.
Here is my javascript code:
var item_width = 40, item_height = 60;
var margin = {top: 50, right: 50, bottom: 75, left: 40},
width = 700 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scaleLinear().range([0, width]);
var y = d3.scaleLinear().range([0, height]);
var colorScale = d3.scaleLinear().domain([500,3000]).range(["white","#4169e1"]);
// Pie Chart parameters
var pieWidth = 300, pieHeight = 300;
var outerRadius = Math.min(pieWidth, pieHeight) / 2,
innerRadius = outerRadius * .50;
var pieColor = d3.scaleOrdinal(['#42b9f4','#3791f2','#374ff1','#25b22e','#107222']); // custom color scale
var legendRectSize = 18; // NEW
var legendSpacing = 4; // NEW
// Top Item Parameters
var topItemMargin = {top:25, right:25, bottom: 25, left: 25};
var topItemWidth = 300 - topItemMargin.left - topItemMargin.right,
topItemHeight = 300 - topItemMargin.top - topItemMargin.bottom;
var topItemXScale = d3.scaleLinear().range([0, topItemWidth]);
var barHeight = 20, barSpacing = 5;
/* SVG */
var svgItemLayout = d3.select("#item_layout")
.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 svgPieChart = d3.select("#pie_chart")
.append("svg")
.attr("width", pieWidth)
.attr("height", pieHeight)
.append("g")
.attr("transform", "translate(" + outerRadius + "," + outerRadius + ")") ;
var svgTopItems = d3.select("#top_items")
.append("svg")
.attr("width", topItemWidth)
.attr("height", topItemHeight)
.append("g")
.attr("transform", "translate(" + topItemMargin.left + "," + topItemMargin.top + ")");
/* DRAW FUNCTIONS */
// a single function to draw
function drawItemLayout(data, someDate){
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var x_offset = 5, y_offset = 5;
x.domain(d3.extent(data, function(d) { return d.x_pos; })); // set the x domain
y.domain(d3.extent(data, function(d) { return d.y_pos; })); // set the y domain
// create an update selection with a key function
var g_sel = svgItemLayout.selectAll("g")
.data(data, function(d){
return d.item_name;
});
// get rid of those leaving the update
g_sel.exit().remove();
// our entering g
var g_ent = g_sel.enter()
.append("g");
// add our rects to our g
g_ent.append("rect")
.attr("class", "dot") // wonder if I really need this class?
.attr("width", item_width)
.attr("height", item_height)
.attr("rx", 3)
.attr("ry", 3)
.style("fill", function(d){ return colorScale(d.sales); }) // color factor variable
.style("fill-opacity", 0.5);
// add our text to our g
g_ent.append("text")
.attr("font-size", 10)
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("dx", item_width/2)
.attr("dy", item_height/2)
.text(function(d){ return d.item_name; });
// UPDATE + ENTER selection
g_sel = g_ent.merge(g_sel);
// move them into position with transition
g_sel
.transition()
.duration(1200)
.delay(function(d, i) { return i *40; })
.attr("transform", function(d){
return "translate(" + (x(d.x_pos) + x_offset) + "," + (y(d.y_pos) + y_offset) + ")";
});
}
function drawPieChart(data, someDate) {
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
var categoryDimension = cf.dimension(function(d){ return d.category; });
var categoryGroup = categoryDimension.group();
function reduceInitial(p, v) {
return {
sales : 0,
count : 0
};
}
function reduceAdd(p, v) {
p.sales = p.sales + v.sales;
p.count = p.count + 1;
return p;
}
function reduceRemove(p, v) {
p.sales = p.sales - v.sales;
p.count = p.count - 1;
return p;
}
categoryAggregated = categoryGroup.reduce(reduceAdd, reduceRemove, reduceInitial).all();
var arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
var pie = d3.pie()
.value(function(d) { return d.value.sales; })
.sort(null);
var path = svgPieChart.selectAll('path')
.data(pie(categoryAggregated))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) { return pieColor(i);});
// Add a legend:
var legend = svgPieChart.selectAll('.legend')
.data(pieColor.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * pieColor.domain().length / 2;
var horz = -3 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', pieColor)
.style('stroke', pieColor);
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return categoryAggregated[d].key; }); // returns text based on data index
}
function drawTopItems (data, someDate) {
data.forEach(function(d) {
d.x_pos = +d.x_pos;
d.y_pos = +d.y_pos;
d.sales = +d.sales;
});
// pre-filter data
data = data.filter(function(d){ return d.date === someDate});
var cf = crossfilter(data);
var salesDimension = cf.dimension(function(d){ return d.sales; });
topData = salesDimension.top(5);
topItemXScale.domain(d3.extent(topData, function(d) { return d.sales; })); // set the x domain
var f_sel = svgTopItems.selectAll("g")
.data(topData,function(d){ return d.item_name; }).enter();
f_sel.exit().remove();
var f_ent = f_sel.enter().append("g");
f_ent.append("rect")
.attr("class", "dot") // wonder if I really need this class?
.attr("width", function(d){ return d.sales })
.attr("height", barHeight)
.style("fill","#351eff") // color factor variable
.style("fill-opacity", 0.75);
// add our text to our g
f_ent.append("text")
.attr("font-size", 10)
.attr("text-anchor", "left")
.attr("fill", "black")
.attr("dx", item_width/2)
.attr("dy", item_height/2)
.text(function(d){ return d.item_name});
// UPDATE + ENTER selection
f_sel = f_ent.merge(f_sel);
f_sel.transition()
.duration(1200)
.delay(function(d, i) { return i *40; })
.attr("transform", function(d, i){
return "translate( 0, "+ i*25 +")" + ")";
});
}
/* MAIN */
var data = d3.csvParse( d3.select("pre#data").text());
drawItemLayout(data, '1-20-2017');
drawPieChart(data, '1-20-2017');
drawTopItems(data, '1-20-2017');
/* UPDATE DATA */
function updateData(date) {
//d3.csv("http://localhost:8080/udacity_test_vis_1/output_fixture_data.csv", function(data) {
var data = d3.csvParse( d3.select("pre#data").text());
drawItemLayout (data, date);
drawPieChart(data, date);
drawTopItems(data, date);
}
/* GET SELECTION */
$("#select_params").change(function(){
var date = $("#select_params :selected").val();
console.log(date);
updateData(date);
})
Just three problems:
You are repeating the enter() function:
var f_sel = svgTopItems.selectAll("g")
.data(topData,function(d){ return d.item_name; }).enter();
//this is an enter selection...
var f_ent = f_sel.enter().append("g");
//and you have enter() again here
So, remove the first enter: f_sel should be just the data-binding selection.
Move your merged selection to before appending the rectangles
Your translate has an extra parenthesis:
return "translate( 0, "+ i*25 +")" + ")";
With that problems corrected, this is your updated fiddle: https://jsfiddle.net/utf5hva2/
I am using the below example and wanted to have the legend outside the Pie chart and also have the Polyline for the Text and the count and Percentage for each slice.
With the current code I have Pie inside the pie and Text and Percentage are showing when I mouse over the slice.
Appreciate the help a lot.Thanks
Can some one please help as I am unable to move forward.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<link rel="stylesheet" href="normalize.css">
<style>
#chart {
height: 360px;
margin: 0 auto; /* NEW */
position: relative;
width: 360px;
}
.tooltip {
background: #eee;
box-shadow: 0 0 5px #999999;
color: #333;
display: none;
font-size: 12px;
left: 130px;
padding: 10px;
position: absolute;
text-align: center;
top: 95px;
width: 80px;
z-index: 10;
}
.legend {
font-size: 12px;
}
rect {
cursor: pointer; /* NEW */
stroke-width: 2;
}
rect.disabled { /* NEW */
fill: transparent !important; /* NEW */
}
/* NEW */
h1 { /* NEW */
font-size: 14px; /* NEW */
text-align: center; /* NEW */
}
/* NEW */
</style>
</head>
<body>
<div id="chart"></div>
<script src="Scripts/d3.v3.min.js"></script>
<script>
(function(d3) {
'use strict';
var width = 360;
var height = 360;
var radius = Math.min(width, height) / 2;
var donutWidth = 75;
var legendRectSize = 18;
var legendSpacing = 4;
var color = d3.scale.category20(); //builtin range of colors
var svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (height / 2) + ')');
var arc = d3.svg.arc()
.innerRadius(radius - donutWidth)
.outerRadius(radius);
var pie = d3.layout.pie()
.value(function(d) { return d.count; })
.sort(null);
var tooltip = d3.select('#chart')
.append('div')
.attr('class', 'tooltip');
tooltip.append('div')
.attr('class', 'label');
tooltip.append('div')
.attr('class', 'count');
tooltip.append('div')
.attr('class', 'percent');
d3.csv('weekdays.csv', function(error, dataset) {
dataset.forEach(function(d) {
d.count = +d.count;
d.enabled = true; // NEW
});
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.label);
}) // UPDATED (removed semicolon)
.each(function(d) { this._current = d; }); // NEW
path.on('mouseover', function(d) {
var total = d3.sum(dataset.map(function(d) {
return (d.enabled) ? d.count : 0; // UPDATED
}));
var percent = Math.round(1000 * d.data.count / total) / 10;
tooltip.select('.label').html(d.data.label);
tooltip.select('.count').html(d.data.count);
tooltip.select('.percent').html(percent + '%');
tooltip.style('display', 'block');
});
path.on('mouseout', function() {
tooltip.style('display', 'none');
});
/* OPTIONAL
path.on('mousemove', function(d) {
tooltip.style('top', (d3.event.pageY + 10) + 'px')
.style('left', (d3.event.pageX + 10) + 'px');
});
*/
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = -2 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color) // UPDATED (removed semicolon)
.on('click', function(label) { // NEW
var rect = d3.select(this); // NEW
var enabled = true; // NEW
var totalEnabled = d3.sum(dataset.map(function(d) { // NEW
return (d.enabled) ? 1 : 0; // NEW
})); // NEW
if (rect.attr('class') === 'disabled') { // NEW
rect.attr('class', ''); // NEW
} else { // NEW
if (totalEnabled < 2) return; // NEW
rect.attr('class', 'disabled'); // NEW
enabled = false; // NEW
} // NEW
pie.value(function(d) { // NEW
if (d.label === label) d.enabled = enabled; // NEW
return (d.enabled) ? d.count : 0; // NEW
}); // NEW
path = path.data(pie(dataset)); // NEW
path.transition() // NEW
.duration(750) // NEW
.attrTween('d', function(d) { // NEW
var interpolate = d3.interpolate(this._current, d); // NEW
this._current = interpolate(0); // NEW
return function(t) { // NEW
return arc(interpolate(t)); // NEW
}; // NEW
}); // NEW
}); // NEW
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return d; });
});
})(window.d3);
</script>
</body>
</html>
You can place the legends where ever you wish by making the legends in a group and placing it using the translate
First Make SVG:
var s = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height);
Now make a legend group:
var legend_group = s.append('g').attr('transform',
'translate(' + (width / 3) + ',' + (height / 1.4) + ')');
Use translate it to a place of your choice. I have moved it to (width/3, height/1.4)
Make a group in which the pie chart will be drawn.
var svg = s.append('g')
.attr('transform', 'translate(' + (width / 2) +
',' + (radius) + ')');
Lets make a polyline for each slice:
This function will make as many polylines as the dataset length.
function makePolyLines() {
var polyline = svg.selectAll("polyline")
.data(pie(dataset), key);
polyline.enter()
.append("polyline");
//hide polyline for which value is 0, a case when legend is clicked.
svg.selectAll("polyline").style("display", function(d) {
if (d.value == 0) {
return "none";
} else {
return "block";
}
});
polyline.transition().duration(1000)
.attrTween("points", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
var d2 = interpolate(t);
var pos = outerArc.centroid(d2);
pos[0] = radius * 0.95 * (midAngle(d2) < Math.PI ? 1 : -1);
return [arc.centroid(d2), outerArc.centroid(d2), pos];
};
});
polyline.exit()
.remove();
}
Similarly make text for labels.
function makeTexts() {
var text = svg.selectAll(".labels")
.data(pie(dataset), key);
text.enter()
.append("text")
.attr("dy", ".35em")
.classed("labels", true)
.text(function(d) {
return d.data.label + " (" + d.data.count + ")";
});
//hide text for which value is 0, a case when legend is clicked.
svg.selectAll(".labels").style("display", function(d) {
if (d.value == 0) {
return "none";
} else {
return "block";
}
});
text.transition().duration(1000)
.attrTween("transform", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
var d2 = interpolate(t);
var pos = outerArc.centroid(d2);
pos[0] = radius * (midAngle(d2) < Math.PI ? 1 : -1);
return "translate(" + pos + ")";
};
})
.styleTween("text-anchor", function(d) {
this._current = this._current || d;
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
var d2 = interpolate(t);
return midAngle(d2) < Math.PI ? "start" : "end";
};
});
text.exit()
.remove();
}
finally call these two functions.
1) initially after the data is fetched.
2) whenever legend is clicked and the piechart is updated.
Working code here
First, you need to make the svg element wider. Currently it's var width = 360;, you can change it to var width = 700; for example.
After you gained some more space, determine the width of the legend, for the example let's use 300px. Declare a new variable: var legendWidth = 300;
Now, when the legend is being declared:
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = (-2 * legendRectSize);
var vert = i * height - offset;
return 'translate(' + (horz) + ',' + vert + ')';
});
When calculation to horizontal translation, we need to take the legendWidth into consideration:
var horz = (-2 * legendRectSize) - legendWidth;
Note: You will need to fix the left and top CSS properties for the .tooltip element.
Another note: If you want to take this solution to the next level, you can implement it in a dynamic way instead of having the "magic number" of var legendWidth = 300.