Built with blockbuilder.org
forked from cdagli's block: fresh block
forked from cdagli's block: Scroll Bar Chart (Using Zoom)
forked from cdagli's block: Scroll Bar Chart (24.12.2017)
license: mit |
Built with blockbuilder.org
forked from cdagli's block: fresh block
forked from cdagli's block: Scroll Bar Chart (Using Zoom)
forked from cdagli's block: Scroll Bar Chart (24.12.2017)
<!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> |