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.
forked from MartynJones87's block: Grouped Bar Chart With Panning And Zooming Via Brushing
forked from anonymous's block: Grouped Bar Chart With Panning And Zooming Via Brushing
xxxxxxxxxx
<meta charset="utf-8">
<head>
<script src="https://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 https://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>
Modified http://d3js.org/d3.v3.min.js to a secure url
https://d3js.org/d3.v3.min.js