Skip to content

Instantly share code, notes, and snippets.

@kkaiser
Created November 9, 2017 13:54
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 kkaiser/87892fed3ffc5f13cdd9869e5acdead4 to your computer and use it in GitHub Desktop.
Save kkaiser/87892fed3ffc5f13cdd9869e5acdead4 to your computer and use it in GitHub Desktop.
d3.v4 port of "Brushable Horizontal Bar Chart - V" (nbremer)
height: 540
<!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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment