'use strict';
var d3 = require('@plotly/d3');
var Registry = require('../../registry');
var Plots = require('../../plots/plots');
var Lib = require('../../lib');
var strTranslate = Lib.strTranslate;
var Drawing = require('../drawing');
var Color = require('../color');
var Titles = require('../titles');
var Cartesian = require('../../plots/cartesian');
var axisIDs = require('../../plots/cartesian/axis_ids');
var dragElement = require('../dragelement');
var setCursor = require('../../lib/setcursor');
var constants = require('./constants');
module.exports = function(gd) {
var fullLayout = gd._fullLayout;
var rangeSliderData = fullLayout._rangeSliderData;
for(var i = 0; i < rangeSliderData.length; i++) {
var opts = rangeSliderData[i][constants.name];
// fullLayout._uid may not exist when we call makeData
opts._clipId = opts._id + '-' + fullLayout._uid;
}
/*
*
*
* < .... range plot />
*
*
*
*
*
*
*
*
*
*
* ...
*/
function keyFunction(axisOpts) {
return axisOpts._name;
}
var rangeSliders = fullLayout._infolayer
.selectAll('g.' + constants.containerClassName)
.data(rangeSliderData, keyFunction);
// remove exiting sliders and their corresponding clip paths
rangeSliders.exit().each(function(axisOpts) {
var opts = axisOpts[constants.name];
fullLayout._topdefs.select('#' + opts._clipId).remove();
}).remove();
// return early if no range slider is visible
if(rangeSliderData.length === 0) return;
rangeSliders.enter().append('g')
.classed(constants.containerClassName, true)
.attr('pointer-events', 'all');
// for all present range sliders
rangeSliders.each(function(axisOpts) {
var rangeSlider = d3.select(this);
var opts = axisOpts[constants.name];
var oppAxisOpts = fullLayout[axisIDs.id2name(axisOpts.anchor)];
var oppAxisRangeOpts = opts[axisIDs.id2name(axisOpts.anchor)];
// update range
// Expand slider range to the axis range
if(opts.range) {
var rng = Lib.simpleMap(opts.range, axisOpts.r2l);
var axRng = Lib.simpleMap(axisOpts.range, axisOpts.r2l);
var newRng;
if(axRng[0] < axRng[1]) {
newRng = [
Math.min(rng[0], axRng[0]),
Math.max(rng[1], axRng[1])
];
} else {
newRng = [
Math.max(rng[0], axRng[0]),
Math.min(rng[1], axRng[1])
];
}
opts.range = opts._input.range = Lib.simpleMap(newRng, axisOpts.l2r);
}
axisOpts.cleanRange('rangeslider.range');
// update range slider dimensions
var gs = fullLayout._size;
var domain = axisOpts.domain;
opts._width = gs.w * (domain[1] - domain[0]);
var x = Math.round(gs.l + (gs.w * domain[0]));
var y = Math.round(
gs.t + gs.h * (1 - axisOpts._counterDomainMin) +
(axisOpts.side === 'bottom' ? axisOpts._depth : 0) +
opts._offsetShift + constants.extraPad
);
rangeSlider.attr('transform', strTranslate(x, y));
// update data <--> pixel coordinate conversion methods
opts._rl = Lib.simpleMap(opts.range, axisOpts.r2l);
var rl0 = opts._rl[0];
var rl1 = opts._rl[1];
var drl = rl1 - rl0;
opts.p2d = function(v) {
return (v / opts._width) * drl + rl0;
};
opts.d2p = function(v) {
return (v - rl0) / drl * opts._width;
};
if(axisOpts.rangebreaks) {
var rsBreaks = axisOpts.locateBreaks(rl0, rl1);
if(rsBreaks.length) {
var j, brk;
var lBreaks = 0;
for(j = 0; j < rsBreaks.length; j++) {
brk = rsBreaks[j];
lBreaks += (brk.max - brk.min);
}
// TODO fix for reversed-range axes !!!
// compute slope and piecewise offsets
var m2 = opts._width / (rl1 - rl0 - lBreaks);
var _B = [-m2 * rl0];
for(j = 0; j < rsBreaks.length; j++) {
brk = rsBreaks[j];
_B.push(_B[_B.length - 1] - m2 * (brk.max - brk.min));
}
opts.d2p = function(v) {
var b = _B[0];
for(var j = 0; j < rsBreaks.length; j++) {
var brk = rsBreaks[j];
if(v >= brk.max) b = _B[j + 1];
else if(v < brk.min) break;
}
return b + m2 * v;
};
// fill pixel (i.e. 'p') min/max here,
// to not have to loop through the _rangebreaks twice during `p2d`
for(j = 0; j < rsBreaks.length; j++) {
brk = rsBreaks[j];
brk.pmin = opts.d2p(brk.min);
brk.pmax = opts.d2p(brk.max);
}
opts.p2d = function(v) {
var b = _B[0];
for(var j = 0; j < rsBreaks.length; j++) {
var brk = rsBreaks[j];
if(v >= brk.pmax) b = _B[j + 1];
else if(v < brk.pmin) break;
}
return (v - b) / m2;
};
}
}
if(oppAxisRangeOpts.rangemode !== 'match') {
var range0OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[0]);
var range1OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[1]);
var distOppAxis = range1OppAxis - range0OppAxis;
opts.d2pOppAxis = function(v) {
return (v - range0OppAxis) / distOppAxis * opts._height;
};
}
// update inner nodes
rangeSlider
.call(drawBg, gd, axisOpts, opts)
.call(addClipPath, gd, axisOpts, opts)
.call(drawRangePlot, gd, axisOpts, opts)
.call(drawMasks, gd, axisOpts, opts, oppAxisRangeOpts)
.call(drawSlideBox, gd, axisOpts, opts)
.call(drawGrabbers, gd, axisOpts, opts);
// setup drag element
setupDragElement(rangeSlider, gd, axisOpts, opts);
// update current range
setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts);
// title goes next to range slider instead of tick labels, so
// just take it over and draw it from here
if(axisOpts.side === 'bottom') {
Titles.draw(gd, axisOpts._id + 'title', {
propContainer: axisOpts,
propName: axisOpts._name + '.title',
placeholder: fullLayout._dfltTitle.x,
attributes: {
x: axisOpts._offset + axisOpts._length / 2,
y: y + opts._height + opts._offsetShift + 10 + 1.5 * axisOpts.title.font.size,
'text-anchor': 'middle'
}
});
}
});
};
function eventX(event) {
if(typeof event.clientX === 'number') {
return event.clientX;
}
if(event.touches && event.touches.length > 0) {
return event.touches[0].clientX;
}
return 0;
}
function setupDragElement(rangeSlider, gd, axisOpts, opts) {
if(gd._context.staticPlot) return;
var slideBox = rangeSlider.select('rect.' + constants.slideBoxClassName).node();
var grabAreaMin = rangeSlider.select('rect.' + constants.grabAreaMinClassName).node();
var grabAreaMax = rangeSlider.select('rect.' + constants.grabAreaMaxClassName).node();
function mouseDownHandler() {
var event = d3.event;
var target = event.target;
var startX = eventX(event);
var offsetX = startX - rangeSlider.node().getBoundingClientRect().left;
var minVal = opts.d2p(axisOpts._rl[0]);
var maxVal = opts.d2p(axisOpts._rl[1]);
var dragCover = dragElement.coverSlip();
this.addEventListener('touchmove', mouseMove);
this.addEventListener('touchend', mouseUp);
dragCover.addEventListener('mousemove', mouseMove);
dragCover.addEventListener('mouseup', mouseUp);
function mouseMove(e) {
var clientX = eventX(e);
var delta = +clientX - startX;
var pixelMin, pixelMax, cursor;
switch(target) {
case slideBox:
cursor = 'ew-resize';
if(minVal + delta > axisOpts._length || maxVal + delta < 0) {
return;
}
pixelMin = minVal + delta;
pixelMax = maxVal + delta;
break;
case grabAreaMin:
cursor = 'col-resize';
if(minVal + delta > axisOpts._length) {
return;
}
pixelMin = minVal + delta;
pixelMax = maxVal;
break;
case grabAreaMax:
cursor = 'col-resize';
if(maxVal + delta < 0) {
return;
}
pixelMin = minVal;
pixelMax = maxVal + delta;
break;
default:
cursor = 'ew-resize';
pixelMin = offsetX;
pixelMax = offsetX + delta;
break;
}
if(pixelMax < pixelMin) {
var tmp = pixelMax;
pixelMax = pixelMin;
pixelMin = tmp;
}
opts._pixelMin = pixelMin;
opts._pixelMax = pixelMax;
setCursor(d3.select(dragCover), cursor);
setDataRange(rangeSlider, gd, axisOpts, opts);
}
function mouseUp() {
dragCover.removeEventListener('mousemove', mouseMove);
dragCover.removeEventListener('mouseup', mouseUp);
this.removeEventListener('touchmove', mouseMove);
this.removeEventListener('touchend', mouseUp);
Lib.removeElement(dragCover);
}
}
rangeSlider.on('mousedown', mouseDownHandler);
rangeSlider.on('touchstart', mouseDownHandler);
}
function setDataRange(rangeSlider, gd, axisOpts, opts) {
function clamp(v) {
return axisOpts.l2r(Lib.constrain(v, opts._rl[0], opts._rl[1]));
}
var dataMin = clamp(opts.p2d(opts._pixelMin));
var dataMax = clamp(opts.p2d(opts._pixelMax));
window.requestAnimationFrame(function() {
Registry.call('_guiRelayout', gd, axisOpts._name + '.range', [dataMin, dataMax]);
});
}
function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts) {
var hw2 = constants.handleWidth / 2;
function clamp(v) {
return Lib.constrain(v, 0, opts._width);
}
function clampOppAxis(v) {
return Lib.constrain(v, 0, opts._height);
}
function clampHandle(v) {
return Lib.constrain(v, -hw2, opts._width + hw2);
}
var pixelMin = clamp(opts.d2p(axisOpts._rl[0]));
var pixelMax = clamp(opts.d2p(axisOpts._rl[1]));
rangeSlider.select('rect.' + constants.slideBoxClassName)
.attr('x', pixelMin)
.attr('width', pixelMax - pixelMin);
rangeSlider.select('rect.' + constants.maskMinClassName)
.attr('width', pixelMin);
rangeSlider.select('rect.' + constants.maskMaxClassName)
.attr('x', pixelMax)
.attr('width', opts._width - pixelMax);
if(oppAxisRangeOpts.rangemode !== 'match') {
var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1]));
var pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0]));
rangeSlider.select('rect.' + constants.maskMinOppAxisClassName)
.attr('x', pixelMin)
.attr('height', pixelMinOppAxis)
.attr('width', pixelMax - pixelMin);
rangeSlider.select('rect.' + constants.maskMaxOppAxisClassName)
.attr('x', pixelMin)
.attr('y', pixelMaxOppAxis)
.attr('height', opts._height - pixelMaxOppAxis)
.attr('width', pixelMax - pixelMin);
rangeSlider.select('rect.' + constants.slideBoxClassName)
.attr('y', pixelMinOppAxis)
.attr('height', pixelMaxOppAxis - pixelMinOppAxis);
}
// add offset for crispier corners
// https://github.com/plotly/plotly.js/pull/1409
var offset = 0.5;
var xMin = Math.round(clampHandle(pixelMin - hw2)) - offset;
var xMax = Math.round(clampHandle(pixelMax - hw2)) + offset;
rangeSlider.select('g.' + constants.grabberMinClassName)
.attr('transform', strTranslate(xMin, offset));
rangeSlider.select('g.' + constants.grabberMaxClassName)
.attr('transform', strTranslate(xMax, offset));
}
function drawBg(rangeSlider, gd, axisOpts, opts) {
var bg = Lib.ensureSingle(rangeSlider, 'rect', constants.bgClassName, function(s) {
s.attr({
x: 0,
y: 0,
'shape-rendering': 'crispEdges'
});
});
var borderCorrect = (opts.borderwidth % 2) === 0 ?
opts.borderwidth :
opts.borderwidth - 1;
var offsetShift = -opts._offsetShift;
var lw = Drawing.crispRound(gd, opts.borderwidth);
bg.attr({
width: opts._width + borderCorrect,
height: opts._height + borderCorrect,
transform: strTranslate(offsetShift, offsetShift),
'stroke-width': lw
})
.call(Color.stroke, opts.bordercolor)
.call(Color.fill, opts.bgcolor);
}
function addClipPath(rangeSlider, gd, axisOpts, opts) {
var fullLayout = gd._fullLayout;
var clipPath = Lib.ensureSingleById(fullLayout._topdefs, 'clipPath', opts._clipId, function(s) {
s.append('rect').attr({ x: 0, y: 0 });
});
clipPath.select('rect').attr({
width: opts._width,
height: opts._height
});
}
function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
var calcData = gd.calcdata;
var rangePlots = rangeSlider.selectAll('g.' + constants.rangePlotClassName)
.data(axisOpts._subplotsWith, Lib.identity);
rangePlots.enter().append('g')
.attr('class', function(id) { return constants.rangePlotClassName + ' ' + id; })
.call(Drawing.setClipUrl, opts._clipId, gd);
rangePlots.order();
rangePlots.exit().remove();
var mainplotinfo;
rangePlots.each(function(id, i) {
var plotgroup = d3.select(this);
var isMainPlot = (i === 0);
var oppAxisOpts = axisIDs.getFromId(gd, id, 'y');
var oppAxisName = oppAxisOpts._name;
var oppAxisRangeOpts = opts[oppAxisName];
var mockFigure = {
data: [],
layout: {
xaxis: {
type: axisOpts.type,
domain: [0, 1],
range: opts.range.slice(),
calendar: axisOpts.calendar
},
width: opts._width,
height: opts._height,
margin: { t: 0, b: 0, l: 0, r: 0 }
},
_context: gd._context
};
if(axisOpts.rangebreaks) {
mockFigure.layout.xaxis.rangebreaks = axisOpts.rangebreaks;
}
mockFigure.layout[oppAxisName] = {
type: oppAxisOpts.type,
domain: [0, 1],
range: oppAxisRangeOpts.rangemode !== 'match' ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(),
calendar: oppAxisOpts.calendar
};
if(oppAxisOpts.rangebreaks) {
mockFigure.layout[oppAxisName].rangebreaks = oppAxisOpts.rangebreaks;
}
Plots.supplyDefaults(mockFigure);
var xa = mockFigure._fullLayout.xaxis;
var ya = mockFigure._fullLayout[oppAxisName];
xa.clearCalc();
xa.setScale();
ya.clearCalc();
ya.setScale();
var plotinfo = {
id: id,
plotgroup: plotgroup,
xaxis: xa,
yaxis: ya,
isRangePlot: true
};
if(isMainPlot) mainplotinfo = plotinfo;
else {
plotinfo.mainplot = 'xy';
plotinfo.mainplotinfo = mainplotinfo;
}
Cartesian.rangePlot(gd, plotinfo, filterRangePlotCalcData(calcData, id));
});
}
function filterRangePlotCalcData(calcData, subplotId) {
var out = [];
for(var i = 0; i < calcData.length; i++) {
var calcTrace = calcData[i];
var trace = calcTrace[0].trace;
if(trace.xaxis + trace.yaxis === subplotId) {
out.push(calcTrace);
}
}
return out;
}
function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisRangeOpts) {
var maskMin = Lib.ensureSingle(rangeSlider, 'rect', constants.maskMinClassName, function(s) {
s.attr({
x: 0,
y: 0,
'shape-rendering': 'crispEdges'
});
});
maskMin
.attr('height', opts._height)
.call(Color.fill, constants.maskColor);
var maskMax = Lib.ensureSingle(rangeSlider, 'rect', constants.maskMaxClassName, function(s) {
s.attr({
y: 0,
'shape-rendering': 'crispEdges'
});
});
maskMax
.attr('height', opts._height)
.call(Color.fill, constants.maskColor);
// masks used for oppAxis zoom
if(oppAxisRangeOpts.rangemode !== 'match') {
var maskMinOppAxis = Lib.ensureSingle(rangeSlider, 'rect', constants.maskMinOppAxisClassName, function(s) {
s.attr({
y: 0,
'shape-rendering': 'crispEdges'
});
});
maskMinOppAxis
.attr('width', opts._width)
.call(Color.fill, constants.maskOppAxisColor);
var maskMaxOppAxis = Lib.ensureSingle(rangeSlider, 'rect', constants.maskMaxOppAxisClassName, function(s) {
s.attr({
y: 0,
'shape-rendering': 'crispEdges'
});
});
maskMaxOppAxis
.attr('width', opts._width)
.style('border-top', constants.maskOppBorder)
.call(Color.fill, constants.maskOppAxisColor);
}
}
function drawSlideBox(rangeSlider, gd, axisOpts, opts) {
if(gd._context.staticPlot) return;
var slideBox = Lib.ensureSingle(rangeSlider, 'rect', constants.slideBoxClassName, function(s) {
s.attr({
y: 0,
cursor: constants.slideBoxCursor,
'shape-rendering': 'crispEdges'
});
});
slideBox.attr({
height: opts._height,
fill: constants.slideBoxFill
});
}
function drawGrabbers(rangeSlider, gd, axisOpts, opts) {
//
var grabberMin = Lib.ensureSingle(rangeSlider, 'g', constants.grabberMinClassName);
var grabberMax = Lib.ensureSingle(rangeSlider, 'g', constants.grabberMaxClassName);
//
var handleFixAttrs = {
x: 0,
width: constants.handleWidth,
rx: constants.handleRadius,
fill: Color.background,
stroke: Color.defaultLine,
'stroke-width': constants.handleStrokeWidth,
'shape-rendering': 'crispEdges'
};
var handleDynamicAttrs = {
y: Math.round(opts._height / 4),
height: Math.round(opts._height / 2),
};
var handleMin = Lib.ensureSingle(grabberMin, 'rect', constants.handleMinClassName, function(s) {
s.attr(handleFixAttrs);
});
handleMin.attr(handleDynamicAttrs);
var handleMax = Lib.ensureSingle(grabberMax, 'rect', constants.handleMaxClassName, function(s) {
s.attr(handleFixAttrs);
});
handleMax.attr(handleDynamicAttrs);
//
var grabAreaFixAttrs = {
width: constants.grabAreaWidth,
x: 0,
y: 0,
fill: constants.grabAreaFill,
cursor: !gd._context.staticPlot ? constants.grabAreaCursor : undefined,
};
var grabAreaMin = Lib.ensureSingle(grabberMin, 'rect', constants.grabAreaMinClassName, function(s) {
s.attr(grabAreaFixAttrs);
});
grabAreaMin.attr('height', opts._height);
var grabAreaMax = Lib.ensureSingle(grabberMax, 'rect', constants.grabAreaMaxClassName, function(s) {
s.attr(grabAreaFixAttrs);
});
grabAreaMax.attr('height', opts._height);
}