|
<!DOCTYPE html> |
|
|
|
<head> |
|
<meta charset="utf-8"> |
|
<title>Brushable bar chart - Horizontal - V</title> |
|
|
|
<!-- Google fonts --> |
|
<link href='https://fonts.googleapis.com/css?family=Open+Sans:300,400' rel='stylesheet' type='text/css'> |
|
|
|
<!-- |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
--> |
|
<script src="https://unpkg.com/d3@4.11.0/build/d3.min.js"></script> |
|
|
|
<style> |
|
body { |
|
font-size: 10px; |
|
font-family: 'Open Sans', sans-serif; |
|
font-weight: 400; |
|
text-align: center; |
|
} |
|
|
|
#title { |
|
font-size: 20px; |
|
padding-bottom: 10px; |
|
padding-top: 20px; |
|
font-weight: 300; |
|
} |
|
|
|
.y.axis line { |
|
fill: none; |
|
} |
|
|
|
.x.axis line { |
|
fill: none; |
|
stroke: #e0e0e0; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.axis path { |
|
display: none; |
|
} |
|
|
|
.brush .selection { |
|
fill-opacity: .125; |
|
shape-rendering: crispEdges; |
|
} |
|
|
|
.selection { |
|
stroke: #7A7A7A; |
|
stroke-width: 2px; |
|
} |
|
|
|
.handle--custom { |
|
fill: #7A7A7A; |
|
fill-opacity: 1; |
|
stroke: #7A7A7A; |
|
stroke-width: 2px; |
|
} |
|
|
|
.bar { |
|
shape-rendering: crispEdges; |
|
} |
|
</style> |
|
|
|
</head> |
|
|
|
<body> |
|
|
|
<div id="title">Brushable horizontal bar chart - V</div> |
|
<div id="chart"></div> |
|
|
|
<script> |
|
var data = [], |
|
svg, |
|
defs, |
|
gBrush, |
|
brush, |
|
handle, |
|
main_xScale, |
|
mini_xScale, |
|
main_yScale, |
|
mini_yScale, |
|
main_yZoom, |
|
main_xAxis, |
|
main_yAxis, |
|
mini_width, |
|
mini_height, |
|
brushExtent, |
|
textScale; |
|
|
|
init(); |
|
|
|
function init() { |
|
|
|
//Create the random data |
|
for (var i = 0; i < 40; i++) { |
|
var my_object = {}; |
|
my_object.key = i; |
|
my_object.country = makeWord(); |
|
my_object.value = Math.floor(Math.random() * 600); |
|
data.push(my_object); |
|
} //for i |
|
data.sort(function(a, b) { |
|
return b.value - a.value; |
|
}); |
|
|
|
///////////////////////////////////////////////////////////// |
|
///////////////// Set-up SVG and wrappers /////////////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
//Added only for the mouse wheel |
|
var zoomer = d3.zoom() |
|
.on("zoom", null); |
|
|
|
var main_margin = { |
|
top: 10, |
|
right: 10, |
|
bottom: 30, |
|
left: 100 |
|
}, |
|
main_width = 500 - main_margin.left - main_margin.right, |
|
main_height = 400 - main_margin.top - main_margin.bottom; |
|
|
|
var mini_margin = { |
|
top: 10, |
|
right: 10, |
|
bottom: 30, |
|
left: 10 |
|
}; |
|
mini_height = 400 - mini_margin.top - mini_margin.bottom; |
|
mini_width = 100 - mini_margin.left - mini_margin.right; |
|
|
|
svg = d3.select("#chart").append("svg") |
|
.attr("class", "svgWrapper") |
|
.attr("width", main_width + main_margin.left + main_margin.right + mini_width + mini_margin.left + mini_margin.right) |
|
.attr("height", main_height + main_margin.top + main_margin.bottom) |
|
.call(zoomer) |
|
.on("wheel.zoom", scroll); |
|
|
|
var mainGroup = svg.append("g") |
|
.attr("class", "mainGroupWrapper") |
|
.attr("transform", "translate(" + main_margin.left + "," + main_margin.top + ")") |
|
.append("g") //another one for the clip path - due to not wanting to clip the labels |
|
.attr("clip-path", "url(#clip)") |
|
.style("clip-path", "url(#clip)") |
|
.attr("class", "mainGroup"); |
|
|
|
var miniGroup = svg.append("g") |
|
.attr("class", "miniGroup") |
|
.attr("transform", "translate(" + (main_margin.left + main_width + main_margin.right + mini_margin.left) + "," + mini_margin.top + ")"); |
|
|
|
var brushGroup = svg.append("g") |
|
.attr("class", "brushGroup") |
|
.attr("transform", "translate(" + (main_margin.left + main_width + main_margin.right + mini_margin.left) + "," + mini_margin.top + ")"); |
|
|
|
///////////////////////////////////////////////////////////// |
|
////////////////////// Initiate scales ////////////////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
main_xScale = d3.scaleLinear().range([0, main_width]); |
|
mini_xScale = d3.scaleLinear().range([0, mini_width]); |
|
|
|
// divide space into equal sized bands |
|
main_yScale = d3.scaleBand().range([0, main_height]).padding(0.4).paddingOuter(0); |
|
mini_yScale = d3.scaleBand().range([0, mini_height]).padding(0.4).paddingOuter(0); |
|
|
|
//Based on the idea from: http://stackoverflow.com/questions/21485339/d3-brushing-on-grouped-bar-chart |
|
main_yZoom = d3.scaleLinear() |
|
.range([0, main_height]) |
|
.domain([0, main_height]); |
|
|
|
//Create x axis object |
|
main_xAxis = d3.axisBottom() |
|
.scale(main_xScale) |
|
.ticks(4) |
|
//.tickSize(0) |
|
.tickSizeOuter(0); |
|
|
|
//Add group for the x axis |
|
d3.select(".mainGroupWrapper") |
|
.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(" + 0 + "," + (main_height + 5) + ")"); |
|
|
|
//Create y axis object |
|
main_yAxis = d3.axisLeft() |
|
.scale(main_yScale) |
|
.tickSize(0) |
|
.tickSizeOuter(0); |
|
|
|
//Add group for the y axis |
|
mainGroup.append("g") |
|
.attr("class", "y axis") |
|
.attr("transform", "translate(-5,0)"); |
|
|
|
///////////////////////////////////////////////////////////// |
|
/////////////////////// Update scales /////////////////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
//Update the scales |
|
main_xScale.domain([0, d3.max(data, function(d) { |
|
return d.value; |
|
})]); |
|
mini_xScale.domain([0, d3.max(data, function(d) { |
|
return d.value; |
|
})]); |
|
main_yScale.domain(data.map(function(d) { |
|
return d.country; |
|
})); |
|
mini_yScale.domain(data.map(function(d) { |
|
return d.country; |
|
})); |
|
|
|
//Create the visual part of the y axis |
|
d3.select(".mainGroup").select(".y.axis").call(main_yAxis); |
|
|
|
///////////////////////////////////////////////////////////// |
|
///////////////////// Label axis scales ///////////////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
textScale = d3.scaleLinear() |
|
.domain([15, 50]) |
|
.range([12, 6]) |
|
.clamp(true); |
|
|
|
/////////////////////////////////////////////////////////////////////////// |
|
/////////////////// Create a rainbow gradient - for fun /////////////////// |
|
/////////////////////////////////////////////////////////////////////////// |
|
|
|
defs = svg.append("defs") |
|
|
|
//Create two separate gradients for the main and mini bar - just because it looks fun |
|
createGradient("gradient-rainbow-main", "60%"); |
|
createGradient("gradient-rainbow-mini", "13%"); |
|
|
|
//Add the clip path for the main bar chart |
|
defs.append("clipPath") |
|
.attr("id", "clip") |
|
.append("rect") |
|
.attr("x", -main_margin.left) |
|
.attr("width", main_width + main_margin.left) |
|
.attr("height", main_height); |
|
|
|
///////////////////////////////////////////////////////////// |
|
/////////////// Create brush mini window /////////////////// |
|
//////////////////////////////////////////////////////////// |
|
|
|
//What should the first extent of the brush become - a bit arbitrary this |
|
brushExtent = Math.max(1, Math.min(20, Math.round(data.length * 0.2))); |
|
|
|
brush = d3.brushY() |
|
.extent([ |
|
[0, 0], |
|
[mini_width, mini_height] |
|
]) |
|
.on("brush", brushmove) |
|
|
|
//Set up the visual part of the brush |
|
gBrush = d3.select(".brushGroup").append("g") |
|
.attr("class", "brush") |
|
.call(brush); |
|
|
|
handle = gBrush.selectAll(".handle--custom") |
|
.data([{ |
|
type: "n" |
|
}, { |
|
type: "s" |
|
}]) |
|
.enter().append("path") |
|
.attr("class", "handle--custom") |
|
.attr("cursor", "ew-resize") |
|
.attr("d", d3.symbol().type(d3.symbolTriangle).size(20)) |
|
.attr("transform", function(d,i) { |
|
return i ? "translate(" + (mini_width/2) + "," + (mini_yScale(data[brushExtent].country)+4) + ") rotate(180)" : "translate(" + (mini_width/2) + "," + -4 + ") rotate(0)";}); |
|
|
|
///////////////////////////////////////////////////////////// |
|
/////////////// Set-up the mini bar chart /////////////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
//The mini brushable bar |
|
//DATA JOIN |
|
var mini_bar = d3.select(".miniGroup").selectAll(".bar") |
|
.data(data, function(d) { |
|
return d.key; |
|
}); |
|
|
|
//UDPATE |
|
mini_bar |
|
.attr("width", function(d) { |
|
return mini_xScale(d.value); |
|
}) |
|
.attr("y", function(d, i) { |
|
return mini_yScale(d.country); |
|
}) |
|
.attr("height", mini_yScale.bandwidth()); |
|
|
|
//ENTER |
|
mini_bar.enter().append("rect") |
|
.attr("class", "bar") |
|
.attr("x", 0) |
|
.attr("width", function(d) { |
|
return mini_xScale(d.value); |
|
}) |
|
.attr("y", function(d, i) { |
|
return mini_yScale(d.country); |
|
}) |
|
.attr("height", mini_yScale.bandwidth()) |
|
.style("fill", "url(#gradient-rainbow-mini)"); |
|
|
|
//EXIT |
|
mini_bar.exit() |
|
.remove(); |
|
|
|
//Start the brush |
|
gBrush.call(brush.move, [mini_yScale(data[0].country), mini_yScale(data[brushExtent].country)]) |
|
.selectAll(".overlay") |
|
//On a click recenter the brush window |
|
.each(function(d) { |
|
d.type = "selection"; |
|
}) // Treat overlay interaction as move. |
|
.on("mousedown touchstart", brushcenter); // Recenter before brushing. |
|
|
|
} //init |
|
|
|
//Function runs on a brush move - to update the big bar chart |
|
function update() { |
|
|
|
///////////////////////////////////////////////////////////// |
|
////////// Update the bars of the main bar chart //////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
//DATA JOIN |
|
var bar = d3.select(".mainGroup").selectAll(".bar") |
|
.data(data, function(d) { |
|
return d.key; |
|
}); |
|
|
|
//UPDATE |
|
bar |
|
.attr("y", function(d, i) { |
|
return main_yScale(d.country); |
|
}) |
|
.attr("height", main_yScale.bandwidth()) |
|
.attr("x", 0) |
|
.transition().duration(50) |
|
.attr("width", function(d) { |
|
return main_xScale(d.value); |
|
}); |
|
|
|
//ENTER |
|
bar.enter().append("rect") |
|
.attr("class", "bar") |
|
.style("fill", "url(#gradient-rainbow-main)") |
|
.attr("y", function(d, i) { |
|
return main_yScale(d.country); |
|
}) |
|
.attr("height", main_yScale.bandwidth()) |
|
.attr("x", 0) |
|
.transition().duration(50) |
|
.attr("width", function(d) { |
|
return main_xScale(d.value); |
|
}); |
|
|
|
//EXIT |
|
bar.exit().remove(); |
|
|
|
} //update |
|
|
|
///////////////////////////////////////////////////////////// |
|
////////////////////// Brush functions ////////////////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
//First function that runs on a brush move |
|
function brushmove() { |
|
|
|
//var extent = brush.extent(); |
|
var extent = d3.brushSelection(gBrush.node()); |
|
|
|
//Which bars are still "selected" |
|
var selected = mini_yScale.domain() |
|
.filter(function(d) { |
|
return (extent[0] - mini_yScale.bandwidth() + 1e-2 <= mini_yScale(d)) && (mini_yScale(d) <= extent[1] - 1e-2); |
|
}); |
|
//Update the colors of the mini chart - Make everything outside the brush grey |
|
d3.select(".miniGroup").selectAll(".bar") |
|
.style("fill", function(d, i) { |
|
return selected.indexOf(d.country) > -1 ? "url(#gradient-rainbow-mini)" : "#e0e0e0"; |
|
}); |
|
|
|
//Update the label size |
|
d3.selectAll(".y.axis text") |
|
.style("font-size", textScale(selected.length)); |
|
|
|
|
|
//Update cursor handle |
|
handle.attr("display", null).attr("transform", function(d, i) { |
|
return i ? "translate(" + (mini_width/2) + "," + (extent[1]+4) + ") rotate(180)" : "translate(" + (mini_width/2) + "," + (extent[0]-4) + ") rotate(0)";}); |
|
|
|
///////////////////////////////////////////////////////////// |
|
///////////////////// Update the axes /////////////////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
//Reset the part that is visible on the big chart |
|
var originalRange = main_yZoom.range(); |
|
main_yZoom.domain(extent); |
|
|
|
//Update the domain of the x & y scale of the big bar chart |
|
main_yScale.domain(data.map(function(d) { |
|
return d.country; |
|
})); |
|
|
|
main_yScale.range([ |
|
main_yZoom(originalRange[0]), main_yZoom(originalRange[1]) |
|
]).padding(0.4).paddingOuter(0); |
|
|
|
//Update the y axis of the big chart |
|
d3.select(".mainGroup") |
|
.select(".y.axis") |
|
.call(main_yAxis); |
|
|
|
//Find the new max of the bars to update the x scale |
|
var newMaxXScale = d3.max(data, function(d) { |
|
return selected.indexOf(d.country) > -1 ? d.value : 0; |
|
}); |
|
main_xScale.domain([0, newMaxXScale]); |
|
|
|
//Update the x axis of the big chart |
|
d3.select(".mainGroupWrapper") |
|
.select(".x.axis") |
|
.transition().duration(50) |
|
.call(main_xAxis); |
|
|
|
//Update the big bar chart |
|
update(); |
|
|
|
} //brushmove |
|
|
|
///////////////////////////////////////////////////////////// |
|
////////////////////// Click functions ////////////////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
//Based on http://bl.ocks.org/mbostock/6498000 |
|
//What to do when the user clicks on another location along the brushable bar chart |
|
function brushcenter() { |
|
// Use a fixed width when recentering. |
|
var dy = mini_yScale(data[brushExtent].country) - mini_yScale(data[0].country), |
|
cy = d3.mouse(this)[1], // position of slider |
|
y0 = cy - dy / 2, |
|
y1 = cy + dy / 2; |
|
d3.select(this.parentNode).call( |
|
brush.move, y1 > mini_height ? [mini_height - dy, mini_height] : y0 < 0 ? [0, dy] : [y0, y1]); |
|
} |
|
|
|
///////////////////////////////////////////////////////////// |
|
///////////////////// Scroll functions ////////////////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
function scroll() { |
|
|
|
//Mouse scroll on the mini chart |
|
var extent = d3.brushSelection(gBrush.node()), |
|
size = extent[1] - extent[0], |
|
range = mini_yScale.range(), |
|
y0 = d3.min(range), |
|
y1 = d3.max(range) + mini_yScale.bandwidth(), |
|
dy = d3.event.deltaY, |
|
topSection; |
|
|
|
if (extent[0] - dy < y0) { |
|
topSection = y0; |
|
} else if (extent[1] - dy > y1) { |
|
topSection = y1 - size; |
|
} else { |
|
topSection = extent[0] - dy; |
|
} |
|
|
|
//Make sure the page doesn't scroll as well |
|
d3.event.stopPropagation(); |
|
d3.event.preventDefault(); |
|
|
|
gBrush |
|
.call(brush.move, [topSection, topSection + size]); |
|
|
|
} //scroll |
|
|
|
///////////////////////////////////////////////////////////// |
|
///////////////////// Helper functions ////////////////////// |
|
///////////////////////////////////////////////////////////// |
|
|
|
//Create a gradient |
|
function createGradient(idName, endPerc) { |
|
|
|
var coloursRainbow = ["#EFB605", "#E9A501", "#E48405", "#E34914", "#DE0D2B", "#CF003E", "#B90050", "#A30F65", "#8E297E", "#724097", "#4F54A8", "#296DA4", "#0C8B8C", "#0DA471", "#39B15E", "#7EB852"]; |
|
|
|
defs.append("linearGradient") |
|
.attr("id", idName) |
|
.attr("gradientUnits", "userSpaceOnUse") |
|
.attr("x1", "0%").attr("y1", "0%") |
|
.attr("x2", endPerc).attr("y2", "0%") |
|
.selectAll("stop") |
|
.data(coloursRainbow) |
|
.enter().append("stop") |
|
.attr("offset", function(d, i) { |
|
return i / (coloursRainbow.length - 1); |
|
}) |
|
.attr("stop-color", function(d) { |
|
return d; |
|
}); |
|
} //createGradient |
|
|
|
//Function to generate random strings of 5 letters - for the demo only |
|
function makeWord() { |
|
var possible_UC = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; |
|
var text = possible_UC.charAt(Math.floor(Math.random() * possible_UC.length)); |
|
|
|
var possible_LC = "abcdefghijklmnopqrstuvwxyz"; |
|
|
|
for (var i = 0; i < 5; i++) |
|
text += possible_LC.charAt(Math.floor(Math.random() * possible_LC.length)); |
|
|
|
return text; |
|
} //makeWord |
|
</script> |
|
|
|
</body> |
|
|
|
</html> |