@domoinc/multiline-chart
Version:
MultiLineChart - Domo Widget
520 lines (446 loc) • 13.7 kB
JavaScript
/*
This function adds scrolling to svg elements
Every call to .sync() will add syncronized scrolling to that element
.syncX() and .syncY() will limit scrolling on the element to only
horizontal or vertical
Usage:
var scroll = d3.svgScroll()
.viewHeight(1000)
.viewWidth(500)
.sync(element)
.sync(element2)
.syncY(element3);
Note that this function currently does not add a clipping path to the element
*/
d3.scroll = function() {
var atEdge = {};
var viewHeight = 582;
var viewWidth = 1064;
var contentHeight = 582;
var contentWidth = 1064;
var draggable = true;
var sync = [];
var syncX = [];
var syncY = [];
var currentX = 0;
var currentY = 0;
var enabled = true;
var introConfig;
var introHasRun = false;
var preventDefaultFlag = false;
var normalizedScroll = (function() {
var normalScalar = {};
var init = {};
function validProp(event, props) {
for (var i = 0; i < props.length; i++) {
if (event[props[i]]) return event[props[i]];
}
}
function getNormalizedDelta(coord) {
normalScalar[coord] = 0;
init[coord] = true;
var props = ['delta' + coord.toUpperCase(), 'wheelDelta' + coord.toUpperCase()]
return function(event, sensitivity) {
var delta = validProp(event, props);
if (!delta) return 0;
if (init[coord]) {
normalScalar[coord] = (Math.abs(delta) || 1);
delete init[coord]
}
/*
Trackpad input appears to sometimes be an integer while scroll wheel input is always a decimal.
Integer input is consistent across browsers so simply return the original delta.
*/
if (delta % 1 === 0) {
return -delta;
}
if (!sensitivity) sensitivity = 4;
return -sensitivity * delta / normalScalar[coord];
}
}
return {
dx: getNormalizedDelta('x'),
dy: getNormalizedDelta('y')
}
})();
function getScrollAmount(event, sensitivity) {
var elem;
var bBox;
var dx, dy;
if (d3.event.type !== 'drag') {
if (d3.event.stopPropagation) {
d3.event.stopPropagation();
}
dx = normalizedScroll.dx(d3.event, sensitivity);
dy = normalizedScroll.dy(d3.event, sensitivity);
} else {
dx = d3.event.dx;
dy = d3.event.dy;
}
var x = 0;
var y = 0;
if (contentWidth > viewWidth) {
x = currentX + dx;
atEdge.left = x > 0 ? true : false;
atEdge.right = x < viewWidth - contentWidth ? true : false;
}
if (contentHeight > viewHeight) {
y = currentY + dy;
atEdge.top = y > 0 ? true : false;
atEdge.bottom = y < viewHeight - contentHeight ? true : false;
}
svgScroll.dispatch.scrolling(atEdge);
if ((!atEdge.top && !atEdge.bottom) || preventDefaultFlag) {
preventDefault(event);
}
return {
x: x,
y: y
};
}
function calcScrollAmountThenApply(modifier, sensitivity) {
return function(event) {
var amount = getScrollAmount(event, sensitivity);
if (modifier) {
amount = modifier(amount);
}
scrollInBounds(amount);
}
}
function scrollInBounds(amount) {
if (!amount) {
amount = {
x: currentX,
y: currentY
};
}
svgScroll({
x: Math.min(0, Math.max(amount.x, viewWidth - contentWidth)),
y: Math.min(0, Math.max(amount.y, viewHeight - contentHeight))
});
}
function svgScroll(amount) {
if (!enabled) return;
var isSvg;
var element;
currentX = amount.x;
currentY = amount.y;
for (var elem in sync) {
element = sync[elem];
isSvg = /svg/.test(element.node()
.namespaceURI);
if (isSvg) {
element.attr("transform", "translate(" + amount.x + ", " + amount.y + ")");
} else {
element.style("transform", "translate(" + amount.x + "px, " + amount.y + "px)");
element.style("-webkit-transform", "translate(" + amount.x + "px, " + amount.y + "px)");
element.style("-ms-transform", "translate(" + amount.x + "px, " + amount.y + "px)");
}
}
for (var elem in syncX) {
element = syncX[elem];
isSvg = /svg/.test(element.node()
.namespaceURI);
if (isSvg) {
element.attr("transform", "translate(" + amount.x + ", 0)");
} else {
element.style("transform", "translate(" + amount.x + "px, 0px)");
element.style("-webkit-transform", "translate(" + amount.x + "px, 0px)");
element.style("-ms-transform", "translate(" + amount.x + "px, 0px)");
}
}
for (var elem in syncY) {
element = syncY[elem];
isSvg = /svg/.test(element.node()
.namespaceURI);
if (isSvg) {
element.attr("transform", "translate(0, " + amount.y + ")");
} else {
element.style("transform", "translate(0px, " + amount.y + "px)");
element.style("-webkit-transform", "translate(0px, " + amount.y + "px)");
element.style("-ms-transform", "translate(0px, " + amount.y + "px)");
}
}
}
svgScroll.preventDefault = function(flag) {
preventDefaultFlag = (flag === true);
return svgScroll;
};
function preventDefault(e) {
// e = e || window.event;
// e = e || d3.event;
e = d3.event;
if (e.preventDefault) {
e.preventDefault();
}
e.returnValue = false;
}
svgScroll.dispatch = d3.dispatch('scrolling');
svgScroll.introAnimation = function(config) {
introConfig = config;
return svgScroll;
}
svgScroll.run = function(config) {
config = config ? config : introConfig;
var isSvg;
var syncElements = svgScroll.sync();
var amount = {};
var element;
var htmlAttrs = {};
var currentHtmlAttrs = {};
var translate;
var translateTransition;
var axis = config.axis ? config.axis : 'default';
var directionY = config.directionY ? config.directionY : 'top';
var directionX = config.directionX ? config.directionX : 'left';
var duration = config.duration ? config.duration : 1000;
var delay = config.delay ? config.delay : 0;
introHasRun = config.runOnce !== undefined ? config.runOnce : introHasRun;
if (!introHasRun) {
switch (axis) {
case 'x':
syncElements = syncElements.concat(svgScroll.syncX());
amount.x = -(contentWidth - viewWidth);
amount.x = viewWidth < contentWidth ? amount.x : 0;
amount.y = 0;
if (directionX === 'right') {
translate = 'translate(0,0)';
translateTransition = 'translate(' + amount.x + ',0)';
} else {
translate = 'translate(' + amount.x + ',0)';
translateTransition = 'translate(0, 0)';
amount.x = 0;
}
break;
case 'y':
syncElements = syncElements.concat(svgScroll.syncY());
amount.x = 0;
amount.y = viewHeight - contentHeight;
amount.y = viewHeight < contentHeight ? amount.y : 0;
if (directionY === 'top') {
translateTransition = 'translate(0,0)';
translate = "translate(0, " + amount.y + ")"
amount.y = 0;
} else {
translateTransition = 'translate(0,' + amount.y + ')';
translate = "translate(0,0)"
}
break;
default:
syncElements = syncElements.concat(svgScroll.syncY())
.concat(svgScroll.syncX());
amount.x = directionX === 'right' ? -(viewWidth - contentWidth) : viewWidth - contentWidth;
amount.x = viewWidth < contentWidth ? amount.x : 0;
amount.y = directionY === 'bottom' ? -(viewHeight - contentHeight) : viewHeight - contentHeight;
amount.y = viewHeight < contentHeight ? amount.y : 0;
}
htmlAttrs[directionX] = amount.x;
htmlAttrs[directionY] = amount.y;
syncElements.map(function(selection) {
selection.each(function(d, i) {
element = d3.select(this);
isSvg = /svg/.test(this.namespaceURI);
if (isSvg) {
element.attr("transform", translate);
element.transition()
.delay(delay)
.duration(duration)
.attr("transform", translateTransition)
.each('end', function() {
svgScroll({
x: amount.x,
y: amount.y
})
});
} else {
currentHtmlAttrs = element.attr();
element.attr(htmlAttrs)
element.attr(currentHtmlAttrs)
.transition()
.delay(delay)
.duration(duration)
.each('end', function() {
svgScroll({
x: amount.x,
y: amount.y
})
});
}
})
})
introHasRun = true;
}
}
svgScroll.viewHeight = function(x) {
if (!arguments.length) return viewHeight;
viewHeight = x;
return svgScroll;
};
svgScroll.viewWidth = function(x) {
if (!arguments.length) return viewWidth;
viewWidth = x;
return svgScroll;
};
svgScroll.contentHeight = function(x) {
if (!arguments.length) return contentHeight;
contentHeight = x;
var allSync = sync.concat(syncX)
.concat(syncY);
for (var elem in allSync) {
var element = allSync[elem];
var rect = element.selectAll('rect.scrollBackground')
.attr({
height: contentHeight
})
}
return svgScroll;
};
svgScroll.contentWidth = function(x) {
if (!arguments.length) return contentWidth;
contentWidth = x;
var allSync = sync.concat(syncX)
.concat(syncY);
for (var elem in allSync) {
var element = allSync[elem];
var rect = element.selectAll('rect.scrollBackground')
.attr({
width: contentWidth
})
}
return svgScroll;
};
svgScroll.draggable = function(d) {
draggable = d;
return svgScroll;
};
var syncAxis = {
x: {
array: syncX,
scrollAmountModifier: function(amount) {
amount.y = currentY;
return amount;
}
},
y: {
array: syncY,
scrollAmountModifier: function(amount) {
amount.x = currentX;
return amount;
}
},
both: {
array: sync,
scrollAmountModifier: null
}
}
function syncInit(axis) {
return function(x, sensitivity) {
if (!arguments.length) return syncAxis[axis].array;
syncAxis[axis].array.push(x);
var listener = calcScrollAmountThenApply(
syncAxis[axis].scrollAmountModifier,
sensitivity
);
//If user scrolls on mouse then call scroll function
x.on('wheel', listener);
// For Safari
x.on('mousewheel', listener);
// allow drag event for mobile devices
if (draggable) {
x.call(d3.behavior.drag().on('drag', listener));
}
svgScroll.resize();
return svgScroll;
}
}
svgScroll.sync = syncInit('both');
svgScroll.syncX = syncInit('x');
svgScroll.syncY = syncInit('y');
svgScroll.resize = function() {
var allSync = sync.concat(syncX)
.concat(syncY);
var bBox;
var elem;
var element;
var elemWidth = 0;
var elemHeight = 0;
var minX = 0;
var minY = 0;
var maxBBox = {
x: 0,
y: 0,
width: 0,
height: 0
};
for (elem in allSync) {
element = allSync[elem];
var isSvg = /svg/.test(element.node()
.namespaceURI);
if (isSvg) {
element.select('rect.scrollBackground')
.remove();
bBox = element.node()
.getBBox();
if (bBox.x < maxBBox.x) maxBBox.x = bBox.x;
if (bBox.y < maxBBox.y) maxBBox.y = bBox.y;
if (bBox.width > maxBBox.width) maxBBox.width = bBox.width;
if (bBox.height > maxBBox.height) maxBBox.height = bBox.height;
} else {
element.select('div.scrollBackground')
.remove();
elemWidth = parseFloat(window.getComputedStyle(element.node())
.width, 10);
elemHeight = parseFloat(window.getComputedStyle(element.node())
.height, 10);
if (elemWidth > maxBBox.width) maxBBox.width = elemWidth;
if (elemHeight > maxBBox.height) maxBBox.height = elemHeight;
}
}
contentHeight = maxBBox.height;
contentWidth = maxBBox.width;
scrollInBounds();
for (elem in allSync) {
element = allSync[elem];
isSvg = /svg/.test(element.node()
.namespaceURI);
if (isSvg) {
var rect = element.selectAll('rect.scrollBackground')
.data([0])
.enter()
.insert('rect', ':first-child')
.classed('scrollBackground', true)
.attr(maxBBox)
.style({
'opacity': 0,
'pointer-events': 'all'
});
} else {
var rect = element.selectAll('div.scrollBackground')
.data([0])
.enter()
.insert('div', ':first-child')
.classed('scrollBackground', true)
.style({
opacity: 0,
display: 'block',
position: 'absolute',
width: maxBBox.width + 'px',
height: maxBBox.height + 'px',
top: maxBBox.y + 'px',
left: maxBBox.x + 'px',
'pointer-events': 'none'
});
}
}
return svgScroll;
};
svgScroll.enable = function(x) {
if (!arguments.length) {
return enabled;
}
enabled = x;
return svgScroll;
}
return svgScroll;
}
d3.svgScroll = d3.scroll;