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> |