Skip to content

Instantly share code, notes, and snippets.

@cdagli
Created January 24, 2018 14:39
Show Gist options
  • Save cdagli/6e6221633b9ff7b14e7201460356e130 to your computer and use it in GitHub Desktop.
Save cdagli/6e6221633b9ff7b14e7201460356e130 to your computer and use it in GitHub Desktop.
Scroll Bar Chart (24.12.2017) - With Rotation
license: mit
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="http://d3js.org/d3.v3.min.js"></script>
<style>
.bar {
fill: steelblue;
}
.subBar{
fill: steelblue;
}
.axis text {
font: 10px sans-serif;
user-select: none;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.x.axis path {
display: none;
}
rect.mover {
fill: lightSteelBlue;
fill-opacity: .5;
}
.brush .extent {
stroke: #fff;
fill-opacity: .125;
shape-rendering: crispEdges;
}
.tooltip {
position: absolute;
pointer-events: none;
padding: 12px;
background: white;
border: 1px solid gray;
border-radius: 4px;
}
.tooltip-name
{
text-align: center;
color: steelblue;
}
.tooltip-value
{
text-align: left;
margin-top: 5px;
}
</style>
</head>
<body>
<!DOCTYPE html>
<label for="dataCount"
style="display: inline-block; width: 100px;">
Data Count
</label>
<input type="number" min="0" max="360" step="5" value="0" id="dataCount">
<div class='chart'></div>
<script>
var DATA_COUNT = 30;
var MAX_LABEL_LENGTH = 50;
var MIN_LABEL_LENGTH = 50;
var MAX_LABEL_LENGTH_ALLOWED = 27;
var MARGIN_MODIFIER_CONSTANT = 4;
var MIN_BAR_WIDTH = 20;
var MIN_BAR_PADDING = 5;
// when the input range changes update value
d3.select("#dataCount").on("input", function () {
console.log('Rendering chart: ', this.value);
});
var data = generateData(DATA_COUNT, MAX_LABEL_LENGTH, MIN_LABEL_LENGTH);
renderChart(data);
// setInterval(function(){
// d3.select(".chart").selectAll("svg").remove();
// var data = generateData(DATA_COUNT, MAX_LABEL_LENGTH, MIN_LABEL_LENGTH);
// renderChart(data)}, 1000)
function generateData(dataCount, maxLabelLength, minLabelLength) {
var data = [];
for (var i = 0; i < dataCount; i++) {
var datum = {};
var plusOrMinus = Math.random() < 0.5 ? -1 : 1;
datum.name = stringGen(minLabelLength, maxLabelLength);
datum.value = Math.floor(Math.random() * 600 * plusOrMinus);
data.push(datum);
}
function stringGen(minLength, maxLength) {
var text = "";
var charset = "abcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < getRandomArbitrary(minLength, maxLength); i++) {
text += charset.charAt(Math.floor(Math.random() * charset.length));
}
return text;
}
function getRandomArbitrary(min, max) {
return Math.round(Math.random() * (max - min) + min);
}
return data;
}
function renderChart(data) {
var maxLabelLength = d3.max(data.map(function (d) { return d.name.length }));
var marginModifier = maxLabelLength < MAX_LABEL_LENGTH_ALLOWED ? maxLabelLength : MAX_LABEL_LENGTH_ALLOWED;
var margin = {
top: 50,
right: 30,
bottom: 40 + marginModifier * MARGIN_MODIFIER_CONSTANT,
left: 80
};
var marginOverview = {
top: 50 + marginModifier * MARGIN_MODIFIER_CONSTANT,
right: 30,
bottom: 0,
left: 30
};
var width = 650 - margin.left - margin.right;
var heightOverview = 75;
var height = 550 - margin.top - margin.bottom;
var barWidth = width / data.length;
var overviewVisible = true;
if (barWidth > MIN_BAR_WIDTH) {
MIN_BAR_WIDTH = barWidth * 90 / 100;
MIN_BAR_PADDING = barWidth * 10 / 100;
overviewVisible = false;
}
var labelRotationValues = calculateRotationDegree(MIN_BAR_WIDTH, data);
var x = d3.scale.ordinal()
.domain(data.map(function (d) {
return d.name;
}))
.range(data.map(function (d, i) {
return i * (MIN_BAR_WIDTH + MIN_BAR_PADDING);
}));
var y = d3.scale.linear()
.domain([d3.min(data, function (d) {
return d.value;
}), Math.abs(d3.max(data, function (d) {
return d.value;
}))])
.range([height, 0]).nice();
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom")
var yTickValues = generateYAxisTicksProportionalToHeight(data, height);
var yAxis = d3.svg.axis()
.scale(y)
.tickValues(yTickValues)
.orient("left");
var svg = d3.select(".chart")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom + heightOverview + marginOverview.top + marginOverview.bottom);
var chart = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var defs = chart.append("defs");
defs.append("clipPath").attr('id', 'chart-clip-path').append('rect')
.attr('width', width) //Set the width of the clipping area
.attr('height', height); // set the height of the clipping area
defs.append("clipPath").attr('id', 'x-axis-clip-path').append('rect')
.attr('width', width) //Set the width of the clipping area
.attr('height', height + margin.bottom); // set the height of the clipping area
var horizontalGridLine = chart.selectAll("line.horizontalGrid").data(yTickValues).enter()
.append("line")
.attr(
{
"class": "horizontalGrid",
"x1": 0,
"x2": width,
"y1": function (d) { return y(d); },
"y2": function (d) { return y(d); },
"fill": "none",
"shape-rendering": "crispEdges",
"stroke": "black",
"stroke-width": "1px",
"opacity": function (d) { return d === 0 ? 1 : .1; },
});
var barsGroup = chart.append('g');
barsGroup.attr('clip-path', 'url(#chart-clip-path)');
var xAxisGroup = chart.append("g").attr('class', 'x-axis')
var xAxisLabels = xAxisGroup.append('g')
.attr("class", "x axis")
.attr("transform", "translate(" + (MIN_BAR_WIDTH + MIN_BAR_PADDING) / 2 + "," + height + ")")
.call(xAxis)
.selectAll("text")
.attr("y", labelRotationValues.labelYDistance)
.attr("x", labelRotationValues.labelXDistance)
.attr("dy", 0)
.attr("transform", "rotate(" + labelRotationValues.rotationDegree + ")")
.style("text-anchor", "end")
.text(function (d) {
if (d.length > MAX_LABEL_LENGTH_ALLOWED)
return d.substring(0, MAX_LABEL_LENGTH_ALLOWED) + '...';
else
return d;
});
xAxisGroup.attr('clip-path', 'url(#x-axis-clip-path)');
var yAxisGroup = chart.append("g").attr("class", "y axis")
yAxisGroup.call(yAxis);
var tooltipDiv = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
var tooltipGuideline = chart.append("line");
var bars = barsGroup.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", function (d) {
return x(d.name);
})
.attr("y", function (d) {
return d.value > 0 ? y(d.value) : y(0);
})
.attr("height", function (d) {
return Math.abs(y(d.value) - y(0));
})
.attr("width", MIN_BAR_WIDTH)
.on("mouseenter", showTooltip)
.on("touchstart", showTooltip)
.on("mouseleave", hideTooltip)
.on("touchend", hideTooltip);
var xAxisLabel = chart.append("text")
.attr("text-anchor", "middle") // this makes it easy to centre the text as the transform is applied to the anchor
.attr("transform", "translate(" + (width / 2) + "," + (height + marginOverview.top) + ")") // centre below axis
.text("Name");
var yAxisLabel = chart.append("text")
.attr("text-anchor", "middle") // this makes it easy to centre the text as the transform is applied to the anchor
.attr("transform", "translate(" + -margin.left / 2 + "," + (height / 2) + ")rotate(-90)") // text is drawn off the screen top left, move down and out and rotate
.text("Value");
if (overviewVisible) {
var zoom = d3.behavior.zoom().scaleExtent([1, 1]);
var xOverview = d3.scale.ordinal()
.domain(data.map(function (d) {
return d.name;
}))
.rangeBands([0, width], MIN_BAR_PADDING / MIN_BAR_WIDTH, 0);
var yOverview = d3.scale.linear().range([heightOverview, 0]);
yOverview.domain(y.domain());
var overviewGroup = chart.append('g')
.attr('width', width)
.attr('height', heightOverview);
var subBars = overviewGroup.append('g').selectAll('.subBar')
.data(data)
subBars.enter().append("rect")
.classed('subBar', true)
.attr({
height: function (d) {
return Math.abs(yOverview(d.value) - yOverview(0));
},
width: function (d) {
return xOverview.rangeBand()
},
x: function (d) {
return xOverview(d.name);
},
y: function (d) {
return height + marginOverview.top + (d.value > 0 ? yOverview(d.value) : yOverview(0));
}
});
var overviewRect = overviewGroup.append('rect')
.attr('y', height + marginOverview.top)
.attr('width', width)
.attr('height', heightOverview)
.style("opacity", "0")
.style("cursor", "pointer").on("click", click);
var selectorWidth = (width / (MIN_BAR_WIDTH) * (xOverview.rangeBand()));
var selector = chart.append("rect")
.attr("class", "mover")
.attr("x", 0)
.attr("y", height + marginOverview.top)
.attr("height", heightOverview)
.attr("width", selectorWidth)
.attr("pointer-events", "all")
.attr("cursor", "ew-resize")
.call(d3.behavior.drag().on("drag", drag));
}
function showTooltip(data) {
tooltipDiv.style("opacity", .9);
tooltipDiv.html('<div class="tooltip-name">' + data.name + '</div><div class="tooltip-value">Value:' + data.value + '</div>')
.style("left", (d3.event.pageX + 25) + "px")
.style("top", (d3.event.pageY - 25) + "px");
tooltipGuideline.attr({
"x1": 0,
"x2": width,
"y1": y(data.value),
"y2": y(data.value),
"stroke": "gray",
"stroke-dasharray": "3, 3",
"stroke-width": "1px"
})
.style('display', 'block');
}
function hideTooltip(data) {
tooltipDiv.style("opacity", 0);
tooltipGuideline.style('display', 'none');
}
function calculateRotationDegree(barWidth, data) {
var CHARACTER_LENGTH_IN_PIXELS = 5;
var xTickLengths = data.map(function (d) { return d.name.length });
var maxLabelLength = d3.max(xTickLengths);
var hypotenuseLength = maxLabelLength * CHARACTER_LENGTH_IN_PIXELS;
var adjacentLength = barWidth / 2;
var rotationDegree = null;
if (adjacentLength > hypotenuseLength) {
rotationDegree = 0
} else {
rotationDegree = -1 * Math.acos(adjacentLength / hypotenuseLength) * (180 / Math.PI);
}
var yDistanceScale = d3.scale.pow().exponent(1.9).range([20, 0]).domain([0, -90]);
var xDistanceScale = d3.scale.pow().exponent(0.1).range([25, -10]).domain([0, -90]);
return {
rotationDegree: rotationDegree,
labelYDistance: yDistanceScale(rotationDegree),
labelXDistance: xDistanceScale(rotationDegree)
};
}
function generateYAxisTicksProportionalToHeight(data, height) {
var MINIMUM_TICK_HEIGHT_IN_PIXELS = 27;
var yTicks = y.ticks();
var yData = data.map(function (d) { return d.value });
var stepSize = yTicks[1] - yTicks[0];
var maxMultiplier = yTicks[yTicks.length - 1] / stepSize;
var yTickSpaceInPixels = height / yTicks.length;
var allowedMultipliers = calculateFactors(maxMultiplier)
var yTickScale = d3.scale.quantize().range(allowedMultipliers.reverse()).domain([0, MINIMUM_TICK_HEIGHT_IN_PIXELS]);
var calculatedMultiplier = yTickScale(yTickSpaceInPixels);
var newStepSize = calculatedMultiplier * stepSize;
var tickMultiplier = Math.floor(yTicks[0] / newStepSize);
var tickStartValue = tickMultiplier * newStepSize;
var newTicks = [];
yTicks.forEach(function (tick) {
var tickValueNewIndex = Math.floor((tick / newStepSize)) - tickMultiplier;
var tickValue = tickStartValue + ((tickValueNewIndex) * newStepSize);
newTicks[tickValueNewIndex] = tickValue;
});
return newTicks;
function calculateFactors(num) {
var arr = []
for (i = 1; i <= num; i++) {
if (num % i == 0) {
arr.push(i)
}
}
return arr;
}
}
function click() {
var newX = null;
var selectorX = null;
var customScale = d3.scale.linear().domain([0, width]).range([0, ((MIN_BAR_WIDTH + MIN_BAR_PADDING) * data.length)])
selectorX = (d3.event.x - marginOverview.left) - selectorWidth / 2;
newX = customScale(selectorX);
if (selectorX > width - selectorWidth) {
newX = customScale(width - selectorWidth);
selectorX = width - selectorWidth;
} else if (selectorX - (selectorWidth / 2) < 0) {
newX = 0;
selectorX = 0
}
selector.transition().attr("x", selectorX)
bars.transition().duration(300).attr("transform", "translate(" + (-newX) + ",0)");
chart.transition().duration(300).select(".x.axis").attr("transform", "translate(" + -(newX - (MIN_BAR_WIDTH + MIN_BAR_PADDING) / 2) + "," + (height) + ")");
chart.select(".y.axis").call(yAxis);
var transformX = (-(d3.event.x - selectorWidth) * ((MIN_BAR_WIDTH + MIN_BAR_PADDING) * data.length) / width);
zoom.translate([-newX, 0])
}
function drag() {
var nx = d3.event.dx;
var t = zoom.translate(),
tx = t[0],
ty = t[1];
var selectorX = parseFloat(selector.attr("x")) + nx
var customScale = d3.scale.linear().domain([0, width]).range([0, ((MIN_BAR_WIDTH + MIN_BAR_PADDING) * data.length)])
var transformX = customScale(selectorX)
var xEndValue = customScale(xOverview(data[data.length - 1].name)) - customScale(selectorWidth) + MIN_BAR_WIDTH
if (transformX < xEndValue && transformX >= 0) {
selector.attr("x", selectorX)
bars.attr("transform", "translate(" + -transformX + ",0)");
chart.select(".x.axis").attr("transform", "translate(" + (-transformX + (MIN_BAR_WIDTH + MIN_BAR_PADDING) / 2) + "," + (height) + ")");
chart.select(".y.axis").call(yAxis);
zoom.translate([transformX, 0])
}
}
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment