'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); }