Skip to content

Instantly share code, notes, and snippets.

@MartynJones87
Last active July 12, 2016 10:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save MartynJones87/7db0d637e178e7204c0a to your computer and use it in GitHub Desktop.
Save MartynJones87/7db0d637e178e7204c0a to your computer and use it in GitHub Desktop.
Grouped Bar Chart With Panning And Zooming Via Brushing

This grouped bar chart uses brushes along both the x and y axes to allow for zooming and panning behaviour with a visual cue to the user informing them which sections of each axis they are currently viewing.

It is based around multiple examples, mainly:

It adds the extra ability from the Focus+Context via Brushing brushing example of allowing panning and zooming along an ordinal axis, with credit to AmeliaBR from this StackOverflow answer.

The inspiration for creating this was to achieve a similar experience and functionality to that of Telerik's Silverlight RadChartView component.

Country Year Cost
England 2011 30
England 2012 35
England 2013 45
England 2014 50
Wales 2010 43
Wales 2012 27
Wales 2013 29
Wales 2014 28
Scotland 2010 73
Scotland 2011 73
Scotland 2013 27
Scotland 2014 28
Ireland 2010 18
Ireland 2011 28
Ireland 2012 53
Ireland 2014 16
<!DOCTYPE html>
<meta charset="utf-8">
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<style>
g.axis path, g.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
g.brush rect.extent {
fill-opacity: 0.2;
}
.resize path {
fill-opacity: 0.2;
}
</style>
<body>
<div id="chart"></div>
<script>
var nestedData;
var main_margin = {
top: 25,
right: 80,
bottom: 60,
left: 70
},
width = 900 - main_margin.left - main_margin.right,
mini_x_height = 10;
main_height = 525 - main_margin.top - main_margin.bottom,
mini_x_margin = {
top: main_height,
right: main_margin.right + 10,
bottom: main_margin.bottom,
left: main_margin.left + 10
},
mini_x_width = 900 - mini_x_margin.left - mini_x_margin.right,
mini_y_margin = {
top: main_margin.top + 10,
right: 0,
bottom: main_margin.bottom + 10,
left: 0
},
mini_y_width = 10,
mini_y_height = 525 - mini_y_margin.top - mini_y_margin.bottom;
var color = d3.scale.category10();
// x0 is the groups scale on the x axis.
var main_x0 = d3.scale.ordinal().rangeRoundBands([0, width], 0.2);
var mini_x0 = d3.scale.ordinal().rangeRoundBands([0, width], 0.2);
var main_xZoom = d3.scale.linear()
.range([0, width])
.domain([0, width]);
// x1 is the series scale on the x axis.
var main_x1 = d3.scale.ordinal();
var mini_x1 = d3.scale.ordinal();
// y is the value scale on the y axis.
var main_y0 = d3.scale.linear().range([main_height, 0]);
var mini_y0 = d3.scale.linear().range([mini_y_height, 0]);
var main_xAxis = d3.svg.axis()
.scale(main_x0)
.orient("bottom");
var mini_xAxis = d3.svg.axis()
.scale(mini_x0)
.orient("bottom");
var main_yAxis = d3.svg.axis()
.scale(main_y0)
.orient("left");
var mini_yAxis = d3.svg.axis()
.scale(mini_y0)
.orient("left");
var svg = d3.select("#chart").append("svg")
.attr("width", width + main_margin.left + main_margin.right)
.attr("height", main_height + main_margin.top + main_margin.bottom);
var main = svg.append("g")
.attr("transform", "translate(" + main_margin.left + "," + main_margin.top + ")");
main.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", width)
.attr("height", main_height + mini_x_height + main_margin.bottom);
var mini_x = svg.append("g")
.attr("transform", "translate(" + mini_x_margin.left + "," + (main_margin.top + main_height) + ")")
.attr("width", mini_x_width);
var mini_y = svg.append("g")
.attr("width", mini_y_width)
.attr("transform", "translate(" + (main_margin.left - mini_y_width) + ", " + mini_y_margin.top + ")")
.attr("height", mini_y_height);
d3.csv("data.csv", function(error, data) {
var seriesValues = d3.set(data.map(function(x){return x.Year;})).values().sort(d3.ascending);
nestedData = d3.nest()
.key(function(d) { return d.Country;})
.sortKeys(d3.ascending)
.sortValues(function(a, b) { return a.Year - b.Year; })
.entries(data);
var groupValues = d3.set(data.map(function (x) { return x.Country; })).values();
// Define the axis domains
main_x0.domain(groupValues);
mini_x0.domain(groupValues);
main_x1.domain(seriesValues).rangeRoundBands([0, main_x0.rangeBand()], 0);
mini_x1.domain(seriesValues).rangeRoundBands([0, main_x0.rangeBand()], 0);
main_y0.domain([0, d3.max(nestedData, function (d) { return d3.max(d.values, function (d) { return d.Cost; }); })]);
mini_y0.domain(main_y0.domain());
var xBrush = d3.svg.brush().x(mini_x0).on("brush", xBrushed);
var yBrush = d3.svg.brush().y(mini_y0).on("brush", yBrushed);
// Add the x axis
main.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + (main_height + mini_x_height) + ")")
.attr("clip-path", "url(#clip)")
.call(main_xAxis)
.selectAll(".tick text")
.call(wrap, main_x0.rangeBand());
// Add the y axis
main.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + (-mini_y_width) + ", 0)")
.call(main_yAxis)
.append("text")
.attr("transform", "rotate(-90), translate(" + -(main_height / 2) + ", " + -(mini_y_width + main_margin.left - 20) + ")")
.attr("dy", ".71em")
.style("text-anchor", "middle")
.text("Cost");
var x_arc = d3.svg.arc()
.outerRadius(mini_x_height / 2)
.startAngle(0)
.endAngle(function(d, i) {
return i ? -Math.PI : Math.PI;
});
var brush_x_grab = mini_x.append("g")
.attr("class", "x brush")
.call(xBrush);
brush_x_grab.selectAll(".resize").append("path")
.attr("transform", "translate(0," + mini_x_height / 2 + ")")
.attr("d", x_arc);
brush_x_grab.selectAll("rect").attr("height", mini_x_height);
var y_arc = d3.svg.arc()
.outerRadius(mini_y_width / 2)
.startAngle(-(Math.PI/2))
.endAngle(function(d, i) {
return i ? -((3 * Math.PI) / 2) : ((Math.PI) / 2);
});
var brush_y_grab = mini_y.append("g")
.attr("class", "y brush")
.call(yBrush);
brush_y_grab.selectAll(".resize").append("path")
.attr("transform", "translate(" + (mini_y_width / 2) + ", 0)")
.attr("d", y_arc);
brush_y_grab.selectAll("rect").attr("width", mini_y_width);
// Create the main bars
var bar = main.selectAll(".bars")
.data(nestedData)
.enter().append("g")
.attr("clip-path", "url(#clip)")
.attr("class", function(d) {
return d.key + "-group bar";
});
bar.selectAll("rect")
.data(function(d) {
return d.values;
})
.enter().append("rect")
.attr("class", function(d) {
return d.Year;
})
.attr("transform", function(d) {
return "translate(" + main_x0(d.Country) + ",0)";
})
.attr("width", function(d) {
return main_x1.rangeBand();
})
.attr("x", function(d) {
return main_x1(d.Year);
})
.attr("y", function(d) {
return main_y0(d.Cost);
})
.attr("height", function(d) {
return main_height - main_y0(d.Cost);
})
.style("fill", function (d) {
return color(d.Year);
});
// Draws the series items onto a legend
var legend = svg.selectAll(".legend")
.data(seriesValues.slice())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function (d, i) { return "translate(50," + (main_margin.top + (i * 20)) + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function (d) { return d; });
// Called to re-draw the bars on the main chart when the brush on the x axis
// has been altered.
function xBrushed() {
var originalRange = main_xZoom.range();
main_xZoom.domain(xBrush.empty() ? originalRange : xBrush.extent());
main_x0.rangeRoundBands([main_xZoom(originalRange[0]), main_xZoom(originalRange[1])], .1);
main_x1.rangeRoundBands([0, main_x0.rangeBand()], 0);
bar.selectAll("rect")
.attr("transform", function (d) {
return "translate(" + main_x0(d.Country) + ",0)";
})
.attr("width", function (d) {
return main_x1.rangeBand();
})
.attr("x", function (d) {
return main_x1(d.Year);
});
main.select("g.x.axis").call(main_xAxis).selectAll(".tick text").call(wrap, main_x0.rangeBand());
};
// Called to re-draw the bars on the main chart when the
// brush on the y axis has been altered.
function yBrushed() {
main_y0.domain(yBrush.empty() ? mini_y0.domain() : yBrush.extent());
bar.selectAll("rect")
.attr("y", function(d) {
return main_y0(d.Cost);
})
.attr("height", function(d) {
return main_height - main_y0(d.Cost);
});
main.select("g.y.axis").call(main_yAxis);
};
// This comes from the example at http://bl.ocks.org/mbostock/7555321
// for wrapping long axis tick labels
function wrap(text, width) {
text.each(function () {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
};
// Set the initial brush selections.
// svg.select(".x.brush").call(xBrush.extent(main_xZoom.domain()));
svg.select(".x.brush").call(xBrush.extent([0,610]));
svg.select(".y.brush").call(yBrush.extent(mini_y0.domain()));
// Forces a refresh of the brushes and main chart based
// on the selected extents.
xBrushed();
yBrushed();
});
</script>
@arvindsrivathsanr
Copy link

Thanks! Was of great help..

@MounikaMylaram
Copy link

MounikaMylaram commented Jul 12, 2016

How can i Implement this for grouped category bar chart .here is the link to plunker what i wrote.but i didn't get.https://plnkr.co/edit/JaeWfMK1j80v3Dbsro7C?p=preview

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment