Related
I have created a parallel coordinate in d3 V4 (with a lot of pain)which has both numerical and ordinal axis, with basic features like axis dragging, brushing, brush snapping.
Here is the working sample
http://plnkr.co/edit/dCNuBsaDNBwr7CrAJUBe?p=preview
I am looking to have multiple brushes in an axis, (for example I want to brush 0.2 to 0.5 and 0.7 to 0.9 of column1 in my example at the same time). So basically based on multiple brush areas the corresponding lines should be highlighted.
Please suggest some way to do this.
Thanks in advance
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
font: 10px sans-serif;
}
.background path {
fill: none;
stroke: #ddd;
stroke-opacity: .4;
shape-rendering: crispEdges;
}
.foreground path {
fill: none;
stroke: steelblue;
stroke-opacity: .7;
}
.brush .extent {
fill-opacity: .3;
stroke: #fff;
shape-rendering: crispEdges;
}
.axis line,
.axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text {
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
cursor: move;
}
</style>
<body>
<script src="http://d3js.org/d3.v4.min.js"></script>
<script>
var margin = {top: 30, right: 10, bottom: 10, left: 10},
width = 600 - margin.left - margin.right,
height = 200 - margin.top - margin.bottom;
var x = d3.scalePoint().rangeRound([0, width]).padding(1),
y = {},
dragging = {};
var line = d3.line(),
//axis = d3.axisLeft(x),
background,
foreground,
extents;
var container = d3.select("body").append("div")
.attr("class", "parcoords")
.style("width", width + margin.left + margin.right + "px")
.style("height", height + margin.top + margin.bottom + "px");
var svg = container.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 quant_p = function(v){return (parseFloat(v) == v) || (v == "")};
d3.json("convertcsv.json", function(error, cars) {
dimensions = d3.keys(cars[0]);
x.domain(dimensions);
dimensions.forEach(function(d) {
var vals = cars.map(function(p) {return p[d];});
if (vals.every(quant_p)){
y[d] = d3.scaleLinear()
.domain(d3.extent(cars, function(p) {
return +p[d]; }))
.range([height, 0])
console.log(y[d]);
}
else{
vals.sort();
y[d] = d3.scalePoint()
.domain(vals.filter(function(v, i) {return vals.indexOf(v) == i;}))
.range([height, 0],1);
}
})
extents = dimensions.map(function(p) { return [0,0]; });
// Add grey background lines for context.
background = svg.append("g")
.attr("class", "background")
.selectAll("path")
.data(cars)
.enter().append("path")
.attr("d", path);
// Add blue foreground lines for focus.
foreground = svg.append("g")
.attr("class", "foreground")
.selectAll("path")
.data(cars)
.enter().append("path")
.attr("d", path);
// Add a group element for each dimension.
var g = svg.selectAll(".dimension")
.data(dimensions)
.enter().append("g")
.attr("class", "dimension")
.attr("transform", function(d) { return "translate(" + x(d) + ")"; })
.call(d3.drag()
.subject(function(d) { return {x: x(d)}; })
.on("start", function(d) {
dragging[d] = x(d);
background.attr("visibility", "hidden");
})
.on("drag", function(d) {
dragging[d] = Math.min(width, Math.max(0, d3.event.x));
foreground.attr("d", path);
dimensions.sort(function(a, b) { return position(a) - position(b); });
x.domain(dimensions);
g.attr("transform", function(d) { return "translate(" + position(d) + ")"; })
})
.on("end", function(d) {
delete dragging[d];
transition(d3.select(this)).attr("transform", "translate(" + x(d) + ")");
transition(foreground).attr("d", path);
background
.attr("d", path)
.transition()
.delay(500)
.duration(0)
.attr("visibility", null);
}));
// Add an axis and title.
var g = svg.selectAll(".dimension");
g.append("g")
.attr("class", "axis")
.each(function(d) { d3.select(this).call(d3.axisLeft(y[d]));})
//text does not show up because previous line breaks somehow
.append("text")
.attr("fill", "black")
.style("text-anchor", "middle")
.attr("y", -9)
.text(function(d) { return d; });
// Add and store a brush for each axis.
g.append("g")
.attr("class", "brush")
.each(function(d) {
if(y[d].name == 'r'){
// console.log(this);
d3.select(this).call(y[d].brush = d3.brushY().extent([[-8, 0], [8,height]]).on("brush start", brushstart).on("brush", go_brush).on("brush", brush_parallel_chart).on("end", brush_end));
}
else if(y[d].name == 'n')
d3.select(this).call(y[d].brush = d3.brushY().extent([[-8, 0], [15,height]]).on("brush start", brushstart).on("brush", go_brush).on("brush", brush_parallel).on("end", brush_end_ordinal));
})
.selectAll("rect")
.attr("x", -8)
.attr("width", 16);
}); // closing
function position(d) {
var v = dragging[d];
return v == null ? x(d) : v;
}
function transition(g) {
return g.transition().duration(500);
}
// Returns the path for a given data point.
function path(d) {
return line(dimensions.map(function(p) { return [position(p), y[p](d[p])]; }));
}
function go_brush() {
d3.event.sourceEvent.stopPropagation();
}
invertExtent = function(y) {
return domain.filter(function(d, i) { return y === range[i]; });
};
function brushstart(selectionName) {
foreground.style("display", "none")
//console.log(selectionName);
var dimensionsIndex = dimensions.indexOf(selectionName);
//console.log(dimensionsIndex);
extents[dimensionsIndex] = [0, 0];
foreground.style("display", function(d) {
return dimensions.every(function(p, i) {
if(extents[i][0]==0 && extents[i][0]==0) {
return true;
}
return extents[i][1] <= d[p] && d[p] <= extents[i][0];
}) ? null : "none";
});
}
// Handles a brush event, toggling the display of foreground lines.
function brush_parallel_chart() {
for(var i=0;i<dimensions.length;++i){
if(d3.event.target==y[dimensions[i]].brush) {
//if (d3.event.sourceEvent.type === "brush") return;
extents[i]=d3.event.selection.map(y[dimensions[i]].invert,y[dimensions[i]]);
}
}
foreground.style("display", function(d) {
return dimensions.every(function(p, i) {
if(extents[i][0]==0 && extents[i][0]==0) {
return true;
}
return extents[i][1] <= d[p] && d[p] <= extents[i][0];
}) ? null : "none";
});
}
function brush_end(){
if (!d3.event.sourceEvent) return; // Only transition after input.
if (!d3.event.selection) return; // Ignore empty selections.
for(var i=0;i<dimensions.length;++i){
if(d3.event.target==y[dimensions[i]].brush) {
extents[i]=d3.event.selection.map(y[dimensions[i]].invert,y[dimensions[i]]);
extents[i][0] = Math.round( extents[i][0] * 10 ) / 10;
extents[i][1] = Math.round( extents[i][1] * 10 ) / 10;
d3.select(this).transition().call(d3.event.target.move, extents[i].map(y[dimensions[i]]));
}
}
}
// brush for ordinal cases
function brush_parallel() {
for(var i=0;i<dimensions.length;++i){
if(d3.event.target==y[dimensions[i]].brush) {
var yScale = y[dimensions[i]];
var selected = yScale.domain().filter(function(d){
// var s = d3.event.target.extent();
var s = d3.event.selection;
return (s[0] <= yScale(d)) && (yScale(d) <= s[1])
});
var temp = selected.sort();
extents[i] = [temp[temp.length-1], temp[0]];
}
}
foreground.style("display", function(d) {
return dimensions.every(function(p, i) {
if(extents[i][0]==0 && extents[i][0]==0) {
return true;
}
//var p_new = (y[p].ticks)?d[p]:y[p](d[p]);
//return extents[i][1] <= p_new && p_new <= extents[i][0];
return extents[i][1] <= d[p] && d[p] <= extents[i][0];
}) ? null : "none";
});
}
function brush_end_ordinal(){
console.log("hhhhh");
if (!d3.event.sourceEvent) return; // Only transition after input.
if (!d3.event.selection) return; // Ignore empty selections.
for(var i=0;i<dimensions.length;++i){
if(d3.event.target==y[dimensions[i]].brush) {
var yScale = y[dimensions[i]];
var selected = yScale.domain().filter(function(d){
// var s = d3.event.target.extent();
var s = d3.event.selection;
return (s[0] <= yScale(d)) && (yScale(d) <= s[1])
});
var temp = selected.sort();
extents[i] = [temp[temp.length-1], temp[0]];
if(selected.length >1)
d3.select(this).transition().call(d3.event.target.move, extents[i].map(y[dimensions[i]]));
}
}
}
</script>
An implementation of multi brush in d3 V4 is there in https://github.com/BigFatDog/parcoords-es
But I guess demo examples are not there.
Here's a Plunkr using multiple brushes in d3 v4 and of course working on your parallel chart:
Code Plunkr for multi brush
https://github.com/shashank2104/d3.svg.multibrush (I'll suggest the v4 patch to the guy who has written the v3 version once it's completely done)
There's one thing that's bothering me which is the brush clicking, as of now, the brushing works with CLICK to start, MOUSEMOVE and CLICK to end. Just play around with the brushing and you'll notice the difference.
The new brushing function is the default one (as follows);
// Handles a brush event, toggling the display of foreground lines.
function brush() {
var actives = dimensions.filter(function(p) { return !y[p].brush.empty(); }),
extents = actives.map(function(p) { return y[p].brush.extent(); });
foreground.style("display", function(d) {
return actives.every(function(p, i) {
return extents[i].some(function(e){
return e[0] <= d[p] && d[p] <= e[1];
});
}) ? null : "none";
});
}
I'll be working on it and hopefully it'll be ready by morning.
Until then, ordinal axis shouldn't be a problem to fix.
I'll keep you posted and edit the same post. (It'd be nice if you could not accept the answer as the right one until it's completely fixed. It'll be on my plate to fix)
Let me know if you come across any questions.
Shashank thanks a lot for the working example, Its like first working example of multi brush in V4. I observed the points you mentioned about brushing.Its a bit different from the traditional brushing method. But the multi brush library from V3 has the conventional brushing way , right.
I have placed the code for ordinal brushing also in your (default) brush function.
function brush() {
var actives = dimensions.filter(function(p) { return !y[p].brush.empty(); }),
extents = actives.map(function(p) { return y[p].brush.extent(); });
foreground.style("display", function(d) {
return actives.every(function(p, i) {
var p_new = (y[p].ticks)?d[p]:y[p](d[p]);
return extents[i].some(function(e){
//return e[0] <= d[p] && d[p] <= e[1];
return e[0] <= p_new && p_new <= e[1];
});
}) ? null : "none";
});
}
In my plunkr example, I have used d3.brushY() functions which are in V4 and thats why I have to implement invert functions (for numerical axis , referred from Appended text not showing in d3 v4) and refeered http://bl.ocks.org/chrisbrich/4173587 for ordinal axis. With these combinations I was able to do brush snapping also.
Also Is there a way that I can do brush snapping also (both numerical and ordinal axis) with your multi brush plunkr example.
I know I am asking more, but basically two points:
1) Is there a way to do multi brushing like that in V3, like as click- drag - leave !
2) Brush snapping (numerical and ordinal) in your multi brush example ?
Thanks again for all your effort and time , means a lot. I am making points so that once you suggest V4 patch, its ll be like fully good.:)
I'm trying to get this block working with d3 v4 and can't figure out what I'm doing wrong. I've placed my code in a fiddle, there is an issue with the setInterval and brushing - the extent is expecting a date object but I'm not sure how to get it.
function realTimeChart() {
var version = "0.1.0",
datum, initialData, data,
maxSeconds = 300, pixelsPerSecond = 10,
svgWidth = 700, svgHeight = 300,
margin = { top: 20, bottom: 20, left: 50, right: 30, topNav: 10, bottomNav: 20 },
dimension = { chartTitle: 20, xAxis: 20, yAxis: 20, xTitle: 20, yTitle: 20, navChart: 70 },
barWidth = 3,
maxY = 100, minY = 0,
chartTitle, yTitle, xTitle,
drawXAxis = true, drawYAxis = true, drawNavChart = true,
border,
selection,
barId = 0;
// create the chart
var chart = function(s) {
selection = s;
if (selection == undefined) {
console.error("selection is undefined");
return;
};
// process titles
chartTitle = chartTitle || "";
xTitle = xTitle || "";
yTitle = yTitle || "";
// compute component dimensions
var chartTitleDim = chartTitle == "" ? 0 : dimension.chartTitle;
var xTitleDim = xTitle == "" ? 0 : dimension.xTitle;
var yTitleDim = yTitle == "" ? 0 : dimension.yTitle;
var xAxisDim = !drawXAxis ? 0 : dimension.xAxis;
var yAxisDim = !drawYAxis ? 0 : dimension.yAxis;
var navChartDim = !drawNavChart ? 0 : dimension.navChart;
// compute chart dimension and offset
var marginTop = margin.top + chartTitleDim;
var height = svgHeight - marginTop - margin.bottom - chartTitleDim - xTitleDim - xAxisDim - navChartDim + 30;
var heightNav = navChartDim - margin.topNav - margin.bottomNav;
var marginTopNav = svgHeight - margin.bottom - heightNav - margin.topNav;
var width = svgWidth - margin.left - margin.right;
var widthNav = width;
// append the svg
var svg = selection.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.style("border", function(d) {
if (border) return "1px solid lightgray";
else return null;
});
// create main group and translate
var main = svg.append("g")
.attr("transform", "translate (" + margin.left + "," + marginTop + ")");
// define clip-path
main.append("defs").append("clipPath")
.attr("id", "myClip")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
// create chart background
main.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.style("fill", "#f5f5f5");
// note that two groups are created here, the latter assigned to barG;
// the former will contain a clip path to constrain objects to the chart area;
// no equivalent clip path is created for the nav chart as the data itself
// is clipped to the full time domain
var barG = main.append("g")
.attr("class", "barGroup")
.attr("transform", "translate(0, 0)")
.attr("clip-path", "url(#myClip")
.append("g");
// add group for x axis
var xAxisG = main.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
// add group for y axis
var yAxisG = main.append("g")
.attr("class", "y axis");
// in x axis group, add x axis title
xAxisG.append("text")
.attr("class", "title")
.attr("x", width / 2)
.attr("y", 25)
.attr("dy", ".71em")
.text(function(d) {
var text = xTitle == undefined ? "" : xTitle;
return text;
});
// in y axis group, add y axis title
yAxisG.append("text")
.attr("class", "title")
.attr("transform", "rotate(-90)")
.attr("x", - height / 2)
.attr("y", -35)
.attr("dy", ".71em")
.text(function(d) {
var text = yTitle == undefined ? "" : yTitle;
return text;
});
// in main group, add chart title
main.append("text")
.attr("class", "chartTitle")
.attr("x", width / 2)
.attr("y", -20)
.attr("dy", ".71em")
.text(function(d) {
var text = chartTitle == undefined ? "" : chartTitle;
return text;
});
// define main chart scales
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().domain([minY, maxY]).range([height, 0]);
// define main chart axis
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
// add nav chart
var nav = svg.append("g")
.attr("transform", "translate (" + margin.left + "," + marginTopNav + ")");
// add nav background
nav.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", heightNav)
.style("fill", "#F5F5F5")
.style("shape-rendering", "crispEdges")
.attr("transform", "translate(0, 0)");
// add group to hold line and area paths
var navG = nav.append("g")
.attr("class", "nav");
// add group to hold nav x axis
var xAxisGNav = nav.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + heightNav + ")");
// define nav scales
var xNav = d3.scaleTime().range([0, widthNav]);
var yNav = d3.scaleLinear().domain([minY, maxY]).range([heightNav, 0]);
// define nav axis
var xAxisNav = d3.axisBottom(xNav);
// define function that will draw the nav area chart
var navArea = d3.area()
.x(function (d) { return xNav(d.time); })
.y1(function (d) { return yNav(d.value); })
.y0(heightNav);
// define function that will draw the nav line chart
var navLine = d3.line()
.x(function (d) { return xNav(d.time); })
.y(function (d) { return yNav(d.value); });
// compute initial time domains...
var ts = new Date().getTime();
// first, the full time domain
var endTime = new Date(ts);
var startTime = new Date(endTime.getTime() - maxSeconds * 1000);
var interval = endTime.getTime() - startTime.getTime();
// then the viewport time domain (what's visible in the main chart
// and the viewport in the nav chart)
var endTimeViewport = new Date(ts);
var startTimeViewport = new Date(endTime.getTime() - width / pixelsPerSecond * 1000);
var intervalViewport = endTimeViewport.getTime() - startTimeViewport.getTime();
var offsetViewport = startTimeViewport.getTime() - startTime.getTime();
// set the scale domains for main and nav charts
x.domain([startTimeViewport, endTimeViewport]);
xNav.domain([startTime, endTime]);
// update axis with modified scale
xAxis.scale(x)(xAxisG);
yAxis.scale(y)(yAxisG);
xAxisNav.scale(xNav)(xAxisGNav);
// create brush (moveable, changable rectangle that determines
// the time domain of main chart)
var sel;
var viewport = d3.brush()
// .x(xNav)
.extent([startTimeViewport, endTimeViewport])
.on("brush", function () {
// get the current time extent of viewport
// var extent = viewport.extent();
var extent = d3.event.selection || x.range();
console.log(extent);
startTimeViewport = extent[0];
endTimeViewport = extent[1];
intervalViewport = endTimeViewport.getTime() - startTimeViewport.getTime();
offsetViewport = startTimeViewport.getTime() - startTime.getTime();
// handle invisible viewport
if (intervalViewport == 0) {
intervalViewport = maxSeconds * 1000;
offsetViewport = 0;
}
// update the x domain of the main chart
x.domain((extent === null) ? xNav.domain() : extent);
// update the x axis of the main chart
xAxis.scale(x)(xAxisG);
// update display
refresh();
});
window.viewport = viewport;
// create group and assign to brush
var viewportG = nav.append("g")
.attr("class", "viewport")
.call(viewport)
.selectAll("rect")
.attr("height", heightNav);
// initial invocation
data = initialData || [];
// update display
refresh();
// function to refresh the viz upon changes of the time domain
// (which happens constantly), or after arrival of new data,
// or at init
function refresh() {
// process data to remove too late or too early data items
// (the latter could occur if the chart is stopped, while data
// is being pumped in)
data = data.filter(function(d) {
if (d.time.getTime() > startTime.getTime() &&
d.time.getTime() < endTime.getTime())
return true;
})
// here we bind the new data to the main chart
// note: no key function is used here; therefore the data binding is
// by index, which effectivly means that available DOM elements
// are associated with each item in the available data array, from
// first to last index; if the new data array contains fewer elements
// than the existing DOM elements, the LAST DOM elements are removed;
// basically, for each step, the data items "walks" leftward (each data
// item occupying the next DOM element to the left);
// This data binding is very different from one that is done with a key
// function; in such a case, a data item stays "resident" in the DOM
// element, and such DOM element (with data) would be moved left, until
// the x position is to the left of the chart, where the item would be
// exited
var updateSel = barG.selectAll(".bar")
.data(data);
// remove items
updateSel.exit().remove();
// append items
updateSel.enter().append("rect")
.attr("class", "bar")
.attr("id", function() {
return "bar-" + barId++;
})
.attr("shape-rendering", "crispEdges");
// update items
updateSel
.attr("x", function(d) { return Math.round(x(d.time) - barWidth); })
.attr("y", function(d) { return y(d.value); })
.attr("width", barWidth)
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return d.color == undefined ? "black" : d.color; })
//.style("stroke", "none")
//.style("stroke-width", "1px")
//.style("stroke-opacity", 0.5)
.style("fill-opacity", 1);
// also, bind data to nav chart
// first remove current paths
navG.selectAll("path").remove();
// then append area path...
navG.append('path')
.attr('class', 'area')
.attr('d', navArea(data));
// ...and line path
navG.append('path')
.attr('class', 'line')
.attr('d', navLine(data));
} // end refreshChart function
// function to keep the chart "moving" through time (right to left)
window.intvl = setInterval(function() {
// get current viewport extent
// var extent = viewport.empty() ? xNav.domain() : viewport.extent();
var extent = (sel === null) ? xNav.domain() : viewport.extent();
window.extent = extent;
var interval = extent[1].getTime() - extent[0].getTime();
var offset = extent[0].getTime() - xNav.domain()[0].getTime();
// compute new nav extents
endTime = new Date();
startTime = new Date(endTime.getTime() - maxSeconds * 1000);
// compute new viewport extents
startTimeViewport = new Date(startTime.getTime() + offset);
endTimeViewport = new Date(startTimeViewport.getTime() + interval);
viewport.extent([startTimeViewport, endTimeViewport])
// update scales
x.domain([startTimeViewport, endTimeViewport]);
xNav.domain([startTime, endTime]);
// update axis
xAxis.scale(x)(xAxisG);
xAxisNav.scale(xNav)(xAxisGNav);
// refresh svg
refresh();
}, 200)
// end setInterval function
return chart;
} // end chart function
// chart getter/setters
// array of inital data
chart.initialData = function(_) {
if (arguments.length == 0) return initialData;
initialData = _;
return chart;
}
// new data item (this most recent item will appear
// on the right side of the chart, and begin moving left)
chart.datum = function(_) {
if (arguments.length == 0) return datum;
datum = _;
data.push(datum);
return chart;
}
// svg width
chart.width = function(_) {
if (arguments.length == 0) return svgWidth;
svgWidth = _;
return chart;
}
// svg height
chart.height = function(_) {
if (arguments.length == 0) return svgHeight;
svgHeight = _;
return chart;
}
// svg border
chart.border = function(_) {
if (arguments.length == 0) return border;
border = _;
return chart;
}
// chart title
chart.title = function(_) {
if (arguments.length == 0) return chartTitle;
chartTitle = _;
return chart;
}
// x axis title
chart.xTitle = function(_) {
if (arguments.length == 0) return xTitle;
xTitle = _;
return chart;
}
// y axis title
chart.yTitle = function(_) {
if (arguments.length == 0) return yTitle;
yTitle = _;
return chart;
}
// bar width
chart.barWidth = function(_) {
if (arguments.length == 0) return barWidth;
barWidth = _;
return chart;
}
// version
chart.version = version;
return chart;
} // end realTimeChart function
https://jsfiddle.net/drhhw8c2/
Thanks in advance for any help!
Finally got a working solution based on Mark's advice: https://jsfiddle.net/drhhw8c2/1/
function realTimeChart() {
var version = "0.1.0",
datum, initialData, data,
maxSeconds = 300, pixelsPerSecond = 10,
svgWidth = 700, svgHeight = 300,
margin = { top: 20, bottom: 20, left: 50, right: 30, topNav: 10, bottomNav: 20 },
dimension = { chartTitle: 20, xAxis: 20, yAxis: 20, xTitle: 20, yTitle: 20, navChart: 70 },
barWidth = 3,
maxY = 100, minY = 0,
chartTitle, yTitle, xTitle,
drawXAxis = true, drawYAxis = true, drawNavChart = true,
border,
selection,
barId = 0;
// create the chart
var chart = function(s) {
selection = s;
if (selection == undefined) {
console.error("selection is undefined");
return;
};
// process titles
chartTitle = chartTitle || "";
xTitle = xTitle || "";
yTitle = yTitle || "";
// compute component dimensions
var chartTitleDim = chartTitle == "" ? 0 : dimension.chartTitle;
var xTitleDim = xTitle == "" ? 0 : dimension.xTitle;
var yTitleDim = yTitle == "" ? 0 : dimension.yTitle;
var xAxisDim = !drawXAxis ? 0 : dimension.xAxis;
var yAxisDim = !drawYAxis ? 0 : dimension.yAxis;
var navChartDim = !drawNavChart ? 0 : dimension.navChart;
// compute chart dimension and offset
var marginTop = margin.top + chartTitleDim;
var height = svgHeight - marginTop - margin.bottom - chartTitleDim - xTitleDim - xAxisDim - navChartDim + 30;
var heightNav = navChartDim - margin.topNav - margin.bottomNav;
var marginTopNav = svgHeight - margin.bottom - heightNav - margin.topNav;
var width = svgWidth - margin.left - margin.right;
var widthNav = width;
// append the svg
var svg = selection.append("svg")
.attr("width", svgWidth)
.attr("height", svgHeight)
.style("border", function(d) {
if (border) return "1px solid lightgray";
else return null;
});
// create main group and translate
var main = svg.append("g")
.attr("transform", "translate (" + margin.left + "," + marginTop + ")");
// define clip-path
main.append("defs").append("clipPath")
.attr("id", "myClip")
.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height);
// create chart background
main.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.style("fill", "#f5f5f5");
// note that two groups are created here, the latter assigned to barG;
// the former will contain a clip path to constrain objects to the chart area;
// no equivalent clip path is created for the nav chart as the data itself
// is clipped to the full time domain
var barG = main.append("g")
.attr("class", "barGroup")
.attr("transform", "translate(0, 0)")
.attr("clip-path", "url(#myClip")
.append("g");
// add group for x axis
var xAxisG = main.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
// add group for y axis
var yAxisG = main.append("g")
.attr("class", "y axis");
// in x axis group, add x axis title
xAxisG.append("text")
.attr("class", "title")
.attr("x", width / 2)
.attr("y", 25)
.attr("dy", ".71em")
.text(function(d) {
var text = xTitle == undefined ? "" : xTitle;
return text;
});
// in y axis group, add y axis title
yAxisG.append("text")
.attr("class", "title")
.attr("transform", "rotate(-90)")
.attr("x", - height / 2)
.attr("y", -35)
.attr("dy", ".71em")
.text(function(d) {
var text = yTitle == undefined ? "" : yTitle;
return text;
});
// in main group, add chart title
main.append("text")
.attr("class", "chartTitle")
.attr("x", width / 2)
.attr("y", -20)
.attr("dy", ".71em")
.text(function(d) {
var text = chartTitle == undefined ? "" : chartTitle;
return text;
});
// define main chart scales
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleLinear().domain([minY, maxY]).range([height, 0]);
// define main chart axis
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y);
// add nav chart
var nav = svg.append("g")
.attr("transform", "translate (" + margin.left + "," + marginTopNav + ")");
// add nav background
nav.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", heightNav)
.style("fill", "#F5F5F5")
.style("shape-rendering", "crispEdges")
.attr("transform", "translate(0, 0)");
// add group to hold line and area paths
var navG = nav.append("g")
.attr("class", "nav");
// add group to hold nav x axis
var xAxisGNav = nav.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + heightNav + ")");
// define nav scales
var xNav = d3.scaleTime().range([0, widthNav]);
var yNav = d3.scaleLinear().domain([minY, maxY]).range([heightNav, 0]);
// define nav axis
var xAxisNav = d3.axisBottom(xNav);
// define function that will draw the nav area chart
var navArea = d3.area()
.x(function (d) { return xNav(d.time); })
.y1(function (d) { return yNav(d.value); })
.y0(heightNav);
// define function that will draw the nav line chart
var navLine = d3.line()
.x(function (d) { return xNav(d.time); })
.y(function (d) { return yNav(d.value); });
// compute initial time domains...
var ts = new Date().getTime();
// first, the full time domain
var endTime = new Date(ts);
var startTime = new Date(endTime.getTime() - maxSeconds * 1000);
var interval = endTime.getTime() - startTime.getTime();
// then the viewport time domain (what's visible in the main chart
// and the viewport in the nav chart)
var endTimeViewport = new Date(ts);
var startTimeViewport = new Date(endTime.getTime() - width / pixelsPerSecond * 1000);
var intervalViewport = endTimeViewport.getTime() - startTimeViewport.getTime();
var offsetViewport = startTimeViewport.getTime() - startTime.getTime();
// set the scale domains for main and nav charts
x.domain([startTimeViewport, endTimeViewport]);
xNav.domain([startTime, endTime]);
// update axis with modified scale
xAxis.scale(x)(xAxisG);
yAxis.scale(y)(yAxisG);
xAxisNav.scale(xNav)(xAxisGNav);
// create brush (moveable, changable rectangle that determines
// the time domain of main chart)
var sel;
var viewport = d3.brushX()
// .x(xNav)
// .extent([startTimeViewport, endTimeViewport])
.extent([ [0,0], [widthNav,heightNav] ])
.on("brush end", function () {
// get the current time extent of viewport
var extent = d3.event.selection || xNav.range();
// var extent = d3.event.selection || x.range();
// console.log(extent);
startTimeViewport = xNav.invert(extent[0]);
endTimeViewport = xNav.invert(extent[1]);
console.log(startTimeViewport.getMinutes() + ':' + startTimeViewport.getSeconds() + ' - ' + endTimeViewport.getMinutes() + ':' + endTimeViewport.getSeconds());
intervalViewport = endTimeViewport.getTime() - startTimeViewport.getTime();
offsetViewport = startTimeViewport.getTime() - startTime.getTime();
// handle invisible viewport
if (intervalViewport == 0) {
intervalViewport = maxSeconds * 1000;
offsetViewport = 0;
}
// update the x domain of the main chart
x.domain((extent === null) ? xNav.domain() : extent);
// update the x axis of the main chart
xAxis.scale(x)(xAxisG);/**/
sel = d3.event.selection || xNav.range();
x.domain(sel.map(xNav.invert, xNav));
// update display
refresh();
});
window.viewport = viewport;
// create group and assign to brush
var viewportG = nav.append("g")
.attr("class", "viewport")
.call(viewport)
.selectAll("rect")
.attr("height", heightNav);
// initial invocation
data = initialData || [];
// update display
refresh();
// function to refresh the viz upon changes of the time domain
// (which happens constantly), or after arrival of new data,
// or at init
function refresh() {
// process data to remove too late or too early data items
// (the latter could occur if the chart is stopped, while data
// is being pumped in)
data = data.filter(function(d) {
if (d.time.getTime() > startTime.getTime() &&
d.time.getTime() < endTime.getTime())
return true;
})
// here we bind the new data to the main chart
// note: no key function is used here; therefore the data binding is
// by index, which effectivly means that available DOM elements
// are associated with each item in the available data array, from
// first to last index; if the new data array contains fewer elements
// than the existing DOM elements, the LAST DOM elements are removed;
// basically, for each step, the data items "walks" leftward (each data
// item occupying the next DOM element to the left);
// This data binding is very different from one that is done with a key
// function; in such a case, a data item stays "resident" in the DOM
// element, and such DOM element (with data) would be moved left, until
// the x position is to the left of the chart, where the item would be
// exited
var updateSel = barG.selectAll(".bar")
.data(data);
// remove items
updateSel.exit().remove();
// append items
updateSel.enter().append("rect")
.attr("class", "bar")
.attr("id", function() {
return "bar-" + barId++;
})
.attr("shape-rendering", "crispEdges");
// update items
updateSel
.attr("x", function(d) {
// console.log(x(d.time));
var val = Math.round(x(d.time) - barWidth);
if(val !== val) {
val = 0;
}
return val; })
.attr("y", function(d) { return y(d.value); })
.attr("width", barWidth)
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return d.color == undefined ? "black" : d.color; })
//.style("stroke", "none")
//.style("stroke-width", "1px")
//.style("stroke-opacity", 0.5)
.style("fill-opacity", 1);
// also, bind data to nav chart
// first remove current paths
navG.selectAll("path").remove();
// then append area path...
navG.append('path')
.attr('class', 'area')
.attr('d', navArea(data));
// ...and line path
navG.append('path')
.attr('class', 'line')
.attr('d', navLine(data));
} // end refreshChart function
// function to keep the chart "moving" through time (right to left)
window.intvl = setInterval(function() {
// get current viewport extent
// var extent = viewport.empty() ? xNav.domain() : viewport.extent();
// var extent = (sel === null) ? xNav.domain() : viewport.extent()();
var extent = xNav.domain();
window.extent = extent;
var interval = extent[1] - extent[0];
var offset = extent[0] - xNav.domain()[0];
// var interval = extent[1].getTime() - extent[0].getTime();
// var offset = extent[0].getTime() - xNav.domain()[0].getTime();
// compute new nav extents
endTime = new Date();
startTime = new Date(endTime.getTime() - maxSeconds * 1000);
// compute new viewport extents
startTimeViewport = new Date(startTime.getTime() + offset);
endTimeViewport = new Date(startTimeViewport.getTime() + interval);
viewport.extent([startTimeViewport, endTimeViewport])
// update scales
x.domain([startTimeViewport, endTimeViewport]);
if(sel) {
x.domain(sel.map(xNav.invert, xNav));
}
xNav.domain([startTime, endTime]);
// update axis
xAxis.scale(x)(xAxisG);
xAxisNav.scale(xNav)(xAxisGNav);
// refresh svg
refresh();
}, 200)
// end setInterval function
return chart;
} // end chart function
// chart getter/setters
// array of inital data
chart.initialData = function(_) {
if (arguments.length == 0) return initialData;
initialData = _;
return chart;
}
// new data item (this most recent item will appear
// on the right side of the chart, and begin moving left)
chart.datum = function(_) {
if (arguments.length == 0) return datum;
datum = _;
data.push(datum);
return chart;
}
// svg width
chart.width = function(_) {
if (arguments.length == 0) return svgWidth;
svgWidth = _;
return chart;
}
// svg height
chart.height = function(_) {
if (arguments.length == 0) return svgHeight;
svgHeight = _;
return chart;
}
// svg border
chart.border = function(_) {
if (arguments.length == 0) return border;
border = _;
return chart;
}
// chart title
chart.title = function(_) {
if (arguments.length == 0) return chartTitle;
chartTitle = _;
return chart;
}
// x axis title
chart.xTitle = function(_) {
if (arguments.length == 0) return xTitle;
xTitle = _;
return chart;
}
// y axis title
chart.yTitle = function(_) {
if (arguments.length == 0) return yTitle;
yTitle = _;
return chart;
}
// bar width
chart.barWidth = function(_) {
if (arguments.length == 0) return barWidth;
barWidth = _;
return chart;
}
// version
chart.version = version;
return chart;
} // end realTimeChart function
'use strict';
// mean and deviation for time interval
var meanMs = 1000, // milliseconds
dev = 150;
// define time scale
var timeScale = d3.scaleLinear()
.domain([300, 1700])
.range([300, 1700])
.clamp(true);
// define value scale
var valueScale = d3.scaleLinear()
.domain([0, 1])
.range([30, 95]);
// generate initial data
var normal = d3.randomNormal(1000, 150);
var currMs = new Date().getTime() - 300000 - 4000;
var data = d3.range(300).map(function(d, i, arr) {
var value = valueScale(Math.random()); // random data
//var value = Math.round((d % 60) / 60 * 95); // ramp data
var interval = Math.round(timeScale(normal()));
currMs += interval;
var time = new Date(currMs);
var obj = { interval: interval, value: value, time: time, ts: currMs }
return obj;
})
// create the real time chart
var chart = realTimeChart()
.title("Chart Title")
.yTitle("Y Scale")
.xTitle("X Scale")
.border(true)
.width(600)
.height(290)
.barWidth(1)
.initialData(data);
console.log("Version: ", chart.version);
console.dir("Dir++");
console.trace();
console.warn("warn")
// invoke the chart
var chartDiv = d3.select("#viewDiv").append("div")
.attr("id", "chartDiv")
.call(chart);
// alternative invocation
//chart(chartDiv);
// drive data into the chart roughly every second
// in a normal use case, real time data would arrive through the network or some other mechanism
var d = 0;
function dataGenerator() {
var timeout = Math.round(timeScale(normal()));
setTimeout(function() {
// create new data item
var now = new Date();
var obj = {
value: valueScale(Math.random()), // random data
//value: Math.round((d++ % 60) / 60 * 95), // ramp data
time: now,
color: "red",
ts: now.getTime(),
interval: timeout
};
// send the datum to the chart
chart.datum(obj);
// do forever
dataGenerator();
}, timeout);
}
// start the data generator
dataGenerator();
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];
}
}
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.
I use http://d3pie.org/#docs-settings
But there is no such parameter as the distance from the center to the internal labels.
Can someone tried to do it?
I want to move the internal labels closer to the outer edge of the circle.
Thank you so much.
now so:
need:
You can position the labels by defining a new arc as suggested in https://stackoverflow.com/a/8270668/2314737 and then applying the centroid function.
I defined a new arc newarc with an inner radius equal to 2/3 of the outer radius.
var newarc = d3.svg.arc()
.innerRadius(2 * radius / 3)
.outerRadius(radius);
Here's the JS code:
var width = 300;
var height = 300;
var svg = d3.select("body").append("svg");
svg.attr("width", width)
.attr("height", height);
var dataset = [11, 13, 18, 25, 31];
var radius = width / 2;
var innerRadius = 0;
var arc = d3.svg.arc()
.innerRadius(0)
.outerRadius(radius);
var pie = d3.layout.pie();
var arcs = svg.selectAll("g.arc")
.data(pie(dataset))
.enter()
.append("g")
.attr("class", "arc")
.attr("transform", "translate(" + radius + ", " + radius + ")");
//Draw arc paths
var color = d3.scale.category10();
arcs.append("path")
.attr("fill", function (d, i) {
console.log(d);
return color(i);
})
.attr("stroke", "white")
.attr("d", arc);
var newarc = d3.svg.arc()
.innerRadius(2 * radius / 3)
.outerRadius(radius);
// Place labels
arcs.append("text")
.attr("transform", function (d) {
return "translate(" + newarc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.attr("fill", "white")
.text(function (d) {
return d.value + "%";
});
Here is a working demo: http://jsfiddle.net/user2314737/kvz8uev8/2/
I decided to enroll in another way.
I added my property in the object and function of positioning inner labels in D3pie file d3pie.js
This function is located on the line - 996 d3pie.js
positionLabelGroups: function(pie, section) {
d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
.style("opacity", 0)
.attr("transform", function(d, i) {
var x, y;
if (section === "outer") {
x = pie.outerLabelGroupData[i].x;
y = pie.outerLabelGroupData[i].y;
} else {
var pieCenterCopy = extend(true, {}, pie.pieCenter);
// now recompute the "center" based on the current _innerRadius
if (pie.innerRadius > 0) {
var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
var newCoords = math.translate(pie.pieCenter.x, pie.pieCenter.y, pie.innerRadius, angle);
pieCenterCopy.x = newCoords.x;
pieCenterCopy.y = newCoords.y;
//console.log('i ='+i , 'angle='+angle, 'pieCenterCopy.x='+pieCenterCopy.x, 'pieCenterCopy.y='+pieCenterCopy.y);
}
var dims = helpers.getDimensions(pie.cssPrefix + "labelGroup" + i + "-inner");
var xOffset = dims.w / 2;
var yOffset = dims.h / 4; // confusing! Why 4? should be 2, but it doesn't look right
// ADD VARAIBLE HERE !!! =)
var divisor = pie.options.labels.inner.pieDistanceOfEnd;
x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / divisor;
y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / divisor;
x = x - xOffset;
y = y + yOffset;
}
return "translate(" + x + "," + y + ")";
});
},
I add var divisor = pie.options.labels.inner.pieDistanceOfEnd;
Then I spotted this property devoltnyh the configuration file bhp and passed for plotting parameters.
inner: {
format: "percentage",
hideWhenLessThanPercentage: null,
pieDistanceOfEnd : 1.8
},
Meaning pieDistanceOfEnd: 1 hang tag on the outer radius of the chart
value pieDistanceOfEnd: 1.25 turn them slightly inward ....
You can play these parameters and to achieve the desired option.
In d3pie.js look for the function positionLabelGroups. In this function both labels (outer and inner) are positioned.
To modify the distance from the center you can play with the x,y here:
x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / 1.8;
y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / 1.8;
What I did was decreasing the 1.8 to 1.2 and obtained what youre looking for. Dont know what the other vars do, but you can study the code to figure it out