Built with blockbuilder.org
forked from cdagli's block: fresh block
forked from cdagli's block: Scroll Bar Chart (Using Zoom)
license: mit |
Built with blockbuilder.org
forked from cdagli's block: fresh block
forked from cdagli's block: Scroll Bar Chart (Using Zoom)
<!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> | |
'use strict'; | |
var DATA_COUNT = 50; | |
var MAX_LABEL_LENGTH = 10; | |
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 maxLabelLength = null; | |
var marginModifier = null; | |
var margin = null; | |
var marginOverview = null; | |
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) { | |
maxLabelLength = d3.max(data.map(function (d) { return d.name.length })); | |
marginModifier = maxLabelLength < MAX_LABEL_LENGTH_ALLOWED ? maxLabelLength : MAX_LABEL_LENGTH_ALLOWED; | |
margin = { | |
top: 50, | |
right: 30, | |
bottom: 40 + marginModifier * MARGIN_MODIFIER_CONSTANT, | |
left: 80 | |
}; | |
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 = 450 - 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 yMinValue = d3.min(data, function (d) { | |
return d.value; | |
}); | |
var yMaxValue = d3.max(data, function (d) { | |
return d.value; | |
}); | |
var y = d3.scale.linear() | |
.domain([yMinValue > 0 ? 0 : yMinValue, yMaxValue < 0 ? 0 : yMaxValue]) | |
.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 - margin.right, | |
"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 = 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 = []; | |
console.log('allowedMultipliers: ', allowedMultipliers) | |
console.log('yTicks: ', yTicks) | |
console.log('maxMultiplier: ',maxMultiplier) | |
console.log('newStepSize: ', newStepSize) | |
console.log('tickMultiplier: ', tickMultiplier) | |
console.log('tickStartValue: ', tickStartValue) | |
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] | |
console.log(newTicks) | |
return newTicks; | |
function calculateFactors(num) { | |
var arr = [] | |
for (var 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> |