|
<!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 = 5; |
|
var MAX_LABEL_LENGTH = 5; |
|
var MIN_LABEL_LENGTH = 5; |
|
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> |