Skip to content

Instantly share code, notes, and snippets.

@cdagli
Last active January 29, 2018 13:26
Show Gist options
  • Save cdagli/abc191522fb381b6ccb229714f890cf3 to your computer and use it in GitHub Desktop.
Save cdagli/abc191522fb381b6ccb229714f890cf3 to your computer and use it in GitHub Desktop.
Scroll Bar Chart (29.12.2017) (Towards Reusable Charts)
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;
}
.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-value
{
text-align: left;
margin-top: 5px;
}
</style>
</head>
<body>
<!DOCTYPE html>
<input name="updateButton"
type="button"
value="Random Data"
onclick="changeData()" />
<input name="updateButton"
type="button"
value="Random Height"
onclick="changeHeight()" />
<div class='chart1'></div>
<div style="height: 50px; width: 100%; background: gray; color: white; padding: 50px">Some random div</div>
<div class='chart2'></div>
<div style="height: 50px; width: 100%; background: gray; color: white; padding: 50px">More random div</div>
<script>
'use strict';
var DATA_COUNT = 30;
var MAX_LABEL_LENGTH = 5;
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;
var data = null;
var chart1Options = {
color: 'orange',
height: 550,
width: 650,
xAxisLabel: 'Name',
yAxisLabel: 'Value'
};
var chart1 = ChartFactory().createNew('.chart1', 1, chart1Options, data);
changeData();
var chart2Options = {
xAxisLabel: 'Name',
yAxisLabel: 'Value'
}
var chart2 = ChartFactory().createNew('.chart2', 2, chart2Options);
data = generateData(MAX_LABEL_LENGTH, MIN_LABEL_LENGTH);
chart2.data(data).height(500).width(600);
function changeHeight() {
chart1.height(Math.random() * 400 + 250);
}
function changeData() {
data = generateData(MAX_LABEL_LENGTH, MIN_LABEL_LENGTH);
chart1.data(data);
}
setInterval(function () {
data = generateData(MAX_LABEL_LENGTH, MIN_LABEL_LENGTH);
chart2.data(data).height(Math.random() * 400 + 250).width(Math.random() * 400 + 250);
}, 1000);
function generateData(maxLabelLength, minLabelLength) {
var data = [];
var dataCount = Math.floor(Math.random() * DATA_COUNT + 5);
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() * 6000 * 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 ChartFactory() {
function Chart(element, chartId, options, data) {
if (!options) {
options = {
color: 'steelblue'
};
}
if (!data) {
data = [];
}
var DEFAULT_HEIGHT_IN_PIXELS = 300;
var DEFAULT_WIDTH_IN_PIXELS = 300;
var DEFAULT_HEIGHT_OVERVIEW_IN_PIXELS = 75;
var DEFAULT_MARGIN = {
bottom: 80,
left: 80,
right: 30,
top: 20
};
var DEFAULT_PADDING = {
bottom: 0,
left: 0,
right: 0,
top: 0
};
var bars = null;
var barColor = options.color;
var barsGroup = null;
var barPadding = null;
var barWidth = null;
var chart = null;
var chartClipPath = null;
var defs = null;
var height = null;
var heightOverview = null;
var horizontalGridLinesGroup = null;
var horizontalGridLines = null;
var innerHeight = null;
var innerWidth = null;
var margin = null;
var marginModifier = null;
var maxLabelLength = null;
var outerHeight = null;
var outerWidth = null;
var overviewGroup = null;
var overviewBarsGroup = null;
var overviewVisible = null;
var overviewRect = null;
var dragHandle = null;
var dragHandleWidth = null;
var padding = null;
var svg = null;
var tooltipDiv = null;
var tooltipGuideline = null;
var width = null;
var x = null;
var xAxis = null;
var xAxisClipPath = null;
var xAxisGroup = null;
var xAxisLabel = null;
var xOverview = null;
var y = null;
var yAxis = null;
var yAxisLabel = null;
var xAxisTickLabels = null;
var yAxisGroup = null;
var yOverview = null;
var yTickValues = null;
var zoom = null;
margin = Object.assign({}, DEFAULT_MARGIN);
padding = Object.assign({}, DEFAULT_PADDING);
width = null;
height = null;
heightOverview = DEFAULT_HEIGHT_OVERVIEW_IN_PIXELS;
outerWidth = options.width || DEFAULT_WIDTH_IN_PIXELS;
outerHeight = options.height || DEFAULT_HEIGHT_IN_PIXELS;
innerHeight = null;
innerWidth = null;
render(element);
if (data.length) {
redraw(data);
}
function render(element) {
innerWidth = outerWidth - margin.left - margin.right;
innerHeight = outerHeight - margin.top - margin.bottom;
width = innerWidth - padding.left - padding.right;
height = innerHeight - padding.top - padding.bottom;
svg = d3.select(element)
.append('svg');
chart = svg.append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
defs = chart.append('defs');
chartClipPath = defs.append('clipPath').attr('id', 'chart-' + chartId + '-clip-path').append('rect');
xAxisClipPath = defs.append('clipPath').attr('id', 'x-axis' + chartId + '-clip-path').append('rect');
tooltipDiv = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
tooltipGuideline = chart.append('line');
xAxisGroup = chart.append('g').attr('class', 'x-axis').attr('clip-path', 'url(#x-axis' + chartId + '-clip-path)');
xAxisLabel = chart.append('text')
.attr('text-anchor', 'middle');
yAxisLabel = chart.append('text')
.attr('text-anchor', 'middle');
horizontalGridLinesGroup = chart.append('g');
barsGroup = chart.append('g');
barsGroup.attr('clip-path', 'url(#chart-' + chartId + '-clip-path)');
xAxisTickLabels = xAxisGroup.append('g')
.attr('class', 'x axis')
.style('text-anchor', 'end')
yAxisGroup = chart.append('g').attr('class', 'y axis');
overviewGroup = chart.append('g').append('g');
overviewBarsGroup = overviewGroup.append('g');
overviewRect = overviewGroup.append('rect');
dragHandle = overviewGroup.append('rect')
.attr('class', 'drag-handle')
.style('fill', barColor)
.style('opacity', .3);
zoom = d3.behavior.zoom().scaleExtent([1, 1]);
}
Chart.prototype.height = function (value) {
if (!arguments.length) {
return outerHeight;
}
outerHeight = value;
innerHeight = outerHeight - margin.top - margin.bottom;
height = innerHeight - padding.top - padding.bottom;
if (typeof redraw === 'function') {
redraw();
}
return this;
};
Chart.prototype.width = function (value) {
if (!arguments.length) {
return outerWidth;
}
outerWidth = value;
innerWidth = outerWidth - margin.left - margin.right;
width = innerWidth - padding.left - padding.right;
if (typeof redraw === 'function') {
redraw();
}
return this;
};
Chart.prototype.data = function (value) {
if (!arguments.length) {
return data;
}
data = value;
if (typeof redraw === 'function') {
redraw();
}
return this;
};
function setXScale() {
x = d3.scale.ordinal()
.domain(data.map(function (d) {
return d.name;
}))
.range(data.map(function (d, i) {
return i * (barWidth + barPadding);
}));
xAxis = d3.svg.axis()
.scale(x)
.orient('bottom')
xOverview = d3.scale.ordinal()
.domain(data.map(function (d) {
return d.name;
}))
.rangeBands([0, width], barPadding / barWidth, 0);
}
function setYScale() {
var yMinValue = d3.min(data, function (d) {
return d.value;
});
var yMaxValue = d3.max(data, function (d) {
return d.value;
});
y = d3.scale.linear()
.domain([yMinValue > 0 ? 0 : yMinValue, yMaxValue < 0 ? 0 : yMaxValue])
.range([height, 0]).nice();
yTickValues = generateYAxisTicksProportionalToHeight(data, height, y.ticks());
yAxis = d3.svg.axis()
.scale(y)
.tickValues(yTickValues)
.orient('left');
yOverview = d3.scale.linear().range([heightOverview, 0]);
yOverview.domain(y.domain());
}
function adjustSVGHeight() {
maxLabelLength = d3.max(data.map(function (d) { return d.name.length; }));
marginModifier = maxLabelLength < MAX_LABEL_LENGTH_ALLOWED ? maxLabelLength : MAX_LABEL_LENGTH_ALLOWED;
margin.bottom = DEFAULT_MARGIN.bottom + marginModifier * MARGIN_MODIFIER_CONSTANT,
innerWidth = outerWidth - margin.left - margin.right;
innerHeight = outerHeight - margin.top - margin.bottom;
width = innerWidth - padding.left - padding.right;
height = innerHeight - padding.top - padding.bottom;
var totalHeight = outerHeight + (overviewVisible ? heightOverview : 0);
svg.attr('width', outerWidth)
.attr('height', totalHeight);
}
function showTooltip(data) {
tooltipDiv.style('opacity', .9);
tooltipDiv.html('<div class="tooltip-name" style="color:' + options.color + '">' + 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({
'stroke': 'gray',
'stroke-dasharray': '3,3',
'stroke-width': '1px',
'x1': 0,
'x2': width,
'y1': y(data.value),
'y2': y(data.value),
})
.style('display', 'block');
}
function hideTooltip() {
tooltipDiv.style('opacity', 0);
tooltipGuideline.style('display', 'none');
}
function calculateBarWidth() {
var calculatedBarWidth = width / data.length;
if (calculatedBarWidth > MIN_BAR_WIDTH) {
barWidth = calculatedBarWidth * 90 / 100;
barPadding = calculatedBarWidth * 10 / 100;
overviewVisible = false;
} else {
barWidth = MIN_BAR_WIDTH;
barPadding = MIN_BAR_PADDING;
overviewVisible = true;
}
}
function redraw() {
resetDrag();
calculateBarWidth();
adjustSVGHeight();
setXScale();
setYScale();
var labelRotationValues = calculateRotationDegree(barWidth, data);
xAxisClipPath.attr('width', width).attr('height', height + margin.bottom);
chartClipPath.attr('width', width).attr('height', height);
xAxisTickLabels.transition()
.attr('transform', 'translate(' + (barWidth + barPadding) / 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;
}
});
yAxisGroup.transition().call(yAxis);
horizontalGridLines = horizontalGridLinesGroup.selectAll('line.horizontalGrid').data(yTickValues);
horizontalGridLines.exit().remove();
horizontalGridLines.enter()
.append('line')
.attr(
{
'class': 'horizontalGrid',
'fill': 'none',
'opacity': function (d) { return d === 0 ? 1 : .1; },
'shape-rendering': 'crispEdges',
'stroke': 'black',
'stroke-width': '1px',
'x1': 0,
'x2': width,
'y1': function (d) { return y(d); },
'y2': function (d) { return y(d); }
});
horizontalGridLines.data(yTickValues)
.transition()
.attr(
{
'class': 'horizontalGrid',
'fill': 'none',
'opacity': function (d) { return d === 0 ? 1 : .1; },
'shape-rendering': 'crispEdges',
'stroke': 'black',
'stroke-width': '1px',
'x1': 0,
'x2': width,
'y1': function (d) { return y(d); },
'y2': function (d) { return y(d); }
});
xAxisLabel
.transition()
.attr('transform', 'translate(' + (width / 2) + ',' + (outerHeight - margin.bottom + labelRotationValues.oppositeLength) + ')')
.text(options.xAxisLabel);
yAxisLabel
.transition()
.attr('transform', 'translate(' + -margin.left / 2 + ',' + (height / 2) + ')rotate(-90)')
.text(options.yAxisLabel);
bars = barsGroup.selectAll('.bar')
.data(data);
bars.exit().remove();
bars.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', barWidth)
.style('fill', barColor)
.on('mouseenter', showTooltip.bind(this))
.on('touchstart', showTooltip.bind(this))
.on('mouseleave', hideTooltip.bind(this))
.on('touchend', hideTooltip.bind(this));
bars.data(data)
.transition()
.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', barWidth);
if (overviewVisible) {
overviewGroup
.attr('width', width)
.attr('height', heightOverview)
.attr('visibility', 'visible');
var subBarYValue = function (datum) {
return height + labelRotationValues.oppositeLength + heightOverview / 2 + (datum.value > 0 ? yOverview(datum.value) : yOverview(0));
};
var overviewBars = overviewBarsGroup.selectAll('.subBar')
.data(data);
overviewBars.exit().remove();
overviewBars.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: subBarYValue
})
.style('fill', barColor);
overviewBars
.transition()
.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: subBarYValue
});
overviewRect.attr('y', height + heightOverview / 2 + labelRotationValues.oppositeLength)
.attr('width', width)
.attr('height', heightOverview)
.style('opacity', '0')
.style('cursor', 'pointer').on('click', click);
dragHandleWidth = (width / (barWidth) * (xOverview.rangeBand()));
dragHandle
.transition()
.attr('x', 0)
.attr('y', height + heightOverview / 2 + labelRotationValues.oppositeLength)
.attr('height', heightOverview)
.attr('width', dragHandleWidth)
.attr('pointer-events', 'all')
.attr('cursor', 'ew-resize')
dragHandle.call(d3.behavior.drag().on('drag', drag));
}
else {
overviewGroup.attr('visibility', 'hidden');
}
}
function drag() {
var nx = d3.event.dx;
var dragHandleX = parseFloat(dragHandle.attr('x')) + nx;
var customScale = d3.scale.linear().domain([0, width]).range([0, ((barWidth + barPadding) * data.length)]);
var transformX = customScale(dragHandleX);
var xEndValue = customScale(xOverview(data[data.length - 1].name)) - customScale(dragHandleWidth) + barWidth;
if (transformX < xEndValue && transformX >= 0) {
dragHandle.attr('x', dragHandleX);
bars.attr('transform', 'translate(' + -transformX + ',0)');
chart.select('.x.axis').attr('transform', 'translate(' + (-transformX + (barWidth + barPadding) / 2) + ',' + (height) + ')');
chart.select('.y.axis').call(yAxis);
zoom.translate([transformX, 0]);
}
}
function resetDrag() {
var customScale = d3.scale.linear().domain([0, width]).range([0, ((barWidth + barPadding) * data.length)])
var transformX = customScale(0);
if (bars) {
bars.attr('transform', 'translate(' + transformX + ',0)');
chart.select('.x.axis').attr('transform', 'translate(' + (transformX) + ',' + (height) + ')');
chart.select('.y.axis').call(yAxis);
zoom.translate([0, 0]);
}
}
function click() {
var newX = null;
var dragHandleX = null;
var customScale = d3.scale.linear().domain([0, width]).range([0, ((barWidth + barPadding) * data.length)])
dragHandleX = (d3.event.x - margin.left) - dragHandleWidth / 2;
newX = customScale(dragHandleX);
if (dragHandleX > width - dragHandleWidth) {
newX = customScale(width - dragHandleWidth);
dragHandleX = width - dragHandleWidth;
} else if (dragHandleX - (dragHandleWidth / 2) < 0) {
newX = 0;
dragHandleX = 0;
}
dragHandle.transition().attr('x', dragHandleX);
bars.transition().duration(300).attr('transform', 'translate(' + (-newX) + ',0)');
chart.transition().duration(300).select('.x.axis').attr('transform', 'translate(' + -(newX - (barWidth + barPadding) / 2) + ',' + (height) + ')');
chart.select('.y.axis').call(yAxis);
zoom.translate([-newX, 0]);
}
}
function createNew(element, chartId, options, data) {
return new Chart(element, chartId, options, data);
}
return {
createNew: createNew
};
}
function calculateRotationDegree(barWidth, data) {
var CHARACTER_LENGTH_IN_PIXELS = 5;
var MIN_OPPOSITE_LENGTH_IN_PIXELS = 20;
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 oppositeLength = Math.sqrt(Math.pow(hypotenuseLength, 2) - Math.pow(adjacentLength, 2));
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 {
labelXDistance: xDistanceScale(rotationDegree),
labelYDistance: yDistanceScale(rotationDegree),
oppositeLength: isNaN(oppositeLength) ? MIN_OPPOSITE_LENGTH_IN_PIXELS : (oppositeLength + MIN_OPPOSITE_LENGTH_IN_PIXELS),
rotationDegree: rotationDegree
};
}
function generateYAxisTicksProportionalToHeight(data, height, yTicks) {
var MINIMUM_TICK_HEIGHT_IN_PIXELS = 27;
var stepSize = Math.abs(yTicks[1] - yTicks[0]);
var lastTick = yTicks[yTicks.length - 1];
var maxMultiplier = lastTick === 0 ? 1 : lastTick / 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;
});
newTicks[0] = yTicks[0]
newTicks[newTicks.length - 1] = yTicks[yTicks.length - 1]
return newTicks;
function calculateFactors(num) {
var arr = [];
for (var i = 1; i <= num; i++) {
if (num % i == 0) {
arr.push(i);
}
}
return arr;
}
}
</script>
</body>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment