Skip to content

Commit

Permalink
Merge pull request #336 from plotly/feature-range-slider
Browse files Browse the repository at this point in the history
Feature: range-slider first implementation
  • Loading branch information
mdtusz committed Mar 30, 2016
2 parents be03333 + 271589c commit 826ed94
Show file tree
Hide file tree
Showing 16 changed files with 1,032 additions and 34 deletions.
52 changes: 52 additions & 0 deletions src/components/rangeslider/attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var colorAttributes = require('../color/attributes');

module.exports = {
bgcolor: {
valType: 'color',
dflt: colorAttributes.background,
role: 'style',
description: 'Sets the background color of the range slider.'
},
bordercolor: {
valType: 'color',
dflt: colorAttributes.defaultLine,
role: 'style',
description: 'Sets the border color of the range slider.'
},
borderwidth: {
valType: 'integer',
dflt: 0,
role: 'style',
description: 'Sets the border color of the range slider.'
},
thickness: {
valType: 'number',
dflt: 0.15,
min: 0,
max: 1,
role: 'style',
description: [
'The height of the range slider as a fraction of the',
'total plot area height.'
].join(' ')
},
visible: {
valType: 'boolean',
dflt: true,
role: 'info',
description: [
'Determines whether or not the range slider will be visible.',
'If visible, perpendicular axes will be set to `fixedrange`'
].join(' ')
}
};
248 changes: 248 additions & 0 deletions src/components/rangeslider/create_slider.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';


var Plotly = require('../../plotly');
var Lib = require('../../lib');

var svgNS = require('../../constants/xmlns_namespaces').svg;

var helpers = require('./helpers');
var rangePlot = require('./range_plot');


module.exports = function createSlider(gd, minStart, maxStart) {
var fullLayout = gd._fullLayout,
sliderContainer = fullLayout._infolayer.selectAll('g.range-slider'),
options = fullLayout.xaxis.rangeslider,
width = fullLayout._size.w,
height = (fullLayout.height - fullLayout.margin.b - fullLayout.margin.t) * options.thickness,
handleWidth = 2,
offsetShift = Math.floor(options.borderwidth / 2),
x = fullLayout.margin.l,
y = fullLayout.height - height - fullLayout.margin.b;

minStart = minStart || 0;
maxStart = maxStart || width;

var slider = document.createElementNS(svgNS, 'g');
helpers.setAttributes(slider, {
'class': 'range-slider',
'data-min': minStart,
'data-max': maxStart,
'pointer-events': 'all',
'transform': 'translate(' + x + ',' + y + ')'
});


var sliderBg = document.createElementNS(svgNS, 'rect'),
borderCorrect = options.borderwidth % 2 === 0 ? options.borderwidth : options.borderwidth - 1;
helpers.setAttributes(sliderBg, {
'fill': options.bgcolor,
'stroke': options.bordercolor,
'stroke-width': options.borderwidth,
'height': height + borderCorrect,
'width': width + borderCorrect,
'transform': 'translate(-' + offsetShift + ', -' + offsetShift + ')',
'shape-rendering': 'crispEdges'
});


var maskMin = document.createElementNS(svgNS, 'rect');
helpers.setAttributes(maskMin, {
'x': 0,
'width': minStart,
'height': height,
'fill': 'rgba(0,0,0,0.4)'
});


var maskMax = document.createElementNS(svgNS, 'rect');
helpers.setAttributes(maskMax, {
'x': maxStart,
'width': width - maxStart,
'height': height,
'fill': 'rgba(0,0,0,0.4)'
});


var grabberMin = document.createElementNS(svgNS, 'g'),
grabAreaMin = document.createElementNS(svgNS, 'rect'),
handleMin = document.createElementNS(svgNS, 'rect');
helpers.setAttributes(grabberMin, { 'transform': 'translate(' + (minStart - handleWidth - 1) + ')' });
helpers.setAttributes(grabAreaMin, {
'width': 10,
'height': height,
'x': -6,
'fill': 'transparent',
'cursor': 'col-resize'
});
helpers.setAttributes(handleMin, {
'width': handleWidth,
'height': height / 2,
'y': height / 4,
'rx': 1,
'fill': 'white',
'stroke': '#666',
'shape-rendering': 'crispEdges'
});
helpers.appendChildren(grabberMin, [handleMin, grabAreaMin]);


var grabberMax = document.createElementNS(svgNS, 'g'),
grabAreaMax = document.createElementNS(svgNS, 'rect'),
handleMax = document.createElementNS(svgNS, 'rect');
helpers.setAttributes(grabberMax, { 'transform': 'translate(' + maxStart + ')' });
helpers.setAttributes(grabAreaMax, {
'width': 10,
'height': height,
'x': -2,
'fill': 'transparent',
'cursor': 'col-resize'
});
helpers.setAttributes(handleMax, {
'width': handleWidth,
'height': height / 2,
'y': height / 4,
'rx': 1,
'fill': 'white',
'stroke': '#666',
'shape-rendering': 'crispEdges'
});
helpers.appendChildren(grabberMax, [handleMax, grabAreaMax]);


var slideBox = document.createElementNS(svgNS, 'rect');
helpers.setAttributes(slideBox, {
'x': minStart,
'width': maxStart - minStart,
'height': height,
'cursor': 'ew-resize',
'fill': 'transparent'
});


slider.addEventListener('mousedown', function(event) {
var target = event.target,
startX = event.clientX,
offsetX = startX - slider.getBoundingClientRect().left,
minVal = slider.getAttribute('data-min'),
maxVal = slider.getAttribute('data-max');

window.addEventListener('mousemove', mouseMove);
window.addEventListener('mouseup', mouseUp);

function mouseMove(e) {
var delta = +e.clientX - startX;

switch(target) {
case slideBox:
slider.style.cursor = 'ew-resize';
setPixelRange(+maxVal + delta, +minVal + delta);
break;

case grabAreaMin:
slider.style.cursor = 'col-resize';
setPixelRange(+minVal + delta, +maxVal);
break;

case grabAreaMax:
slider.style.cursor = 'col-resize';
setPixelRange(+minVal, +maxVal + delta);
break;

default:
slider.style.cursor = 'ew-resize';
setPixelRange(offsetX, offsetX + delta);
break;
}
}

function mouseUp() {
window.removeEventListener('mousemove', mouseMove);
window.removeEventListener('mouseup', mouseUp);
slider.style.cursor = 'auto';
}
});


function setRange(min, max) {
min = min || -Infinity;
max = max || Infinity;

var rangeMin = fullLayout.xaxis.range[0],
rangeMax = fullLayout.xaxis.range[1],
range = rangeMax - rangeMin,
pixelMin = (min - rangeMin) / range * width,
pixelMax = (max - rangeMin) / range * width;

setPixelRange(pixelMin, pixelMax);
}


function setPixelRange(min, max) {

min = Lib.constrain(min, 0, width);
max = Lib.constrain(max, 0, width);

if(max < min) {
var temp = max;
max = min;
min = temp;
}

helpers.setAttributes(slider, {
'data-min': min,
'data-max': max
});

helpers.setAttributes(slideBox, {
'x': min,
'width': max - min
});

helpers.setAttributes(maskMin, { 'width': min });
helpers.setAttributes(maskMax, {
'x': max,
'width': width - max
});

helpers.setAttributes(grabberMin, { 'transform': 'translate(' + (min - handleWidth - 1) + ')' });
helpers.setAttributes(grabberMax, { 'transform': 'translate(' + max + ')' });

// call to set range on plot here
var rangeMin = fullLayout.xaxis.range[0],
rangeMax = fullLayout.xaxis.range[1],
range = rangeMax - rangeMin,
dataMin = min / width * range + rangeMin,
dataMax = max / width * range + rangeMin;

Plotly.relayout(gd, 'xaxis.range', [dataMin, dataMax]);
}


var rangePlots = rangePlot(gd, width, height);

helpers.appendChildren(slider, [
sliderBg,
rangePlots,
maskMin,
maskMax,
slideBox,
grabberMin,
grabberMax
]);

sliderContainer.data([0])
.enter().append(function() {
options.setRange = setRange;
return slider;
});
};
18 changes: 18 additions & 0 deletions src/components/rangeslider/data_processors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Lib = require('../../lib');

module.exports = {
'linear': function(val) { return val; },
'log': function(val) { return Math.log(val)/Math.log(10); },
'date': function(val) { return Lib.dateTime2ms(val); },
'category': function(_, i) { return i; }
};
41 changes: 41 additions & 0 deletions src/components/rangeslider/defaults.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var Lib = require('../../lib');
var attributes = require('./attributes');


module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, axName, counterAxes) {

if(!layoutIn[axName].rangeslider) return;

var containerIn = typeof layoutIn[axName].rangeslider === 'object' ?
layoutIn[axName].rangeslider : {},
containerOut = layoutOut[axName].rangeslider = {};

function coerce(attr, dflt) {
return Lib.coerce(containerIn, containerOut,
attributes, attr, dflt);
}

coerce('visible');
coerce('thickness');
coerce('bgcolor');
coerce('bordercolor');
coerce('borderwidth');

if(containerOut.visible) {
counterAxes.forEach(function(ax) {
var opposing = layoutOut[ax] || {};
opposing.fixedrange = true;
layoutOut[ax] = opposing;
});
}
};
24 changes: 24 additions & 0 deletions src/components/rangeslider/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright 2012-2016, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

exports.setAttributes = function setAttributes(el, attributes) {
for(var key in attributes) {
el.setAttribute(key, attributes[key]);
}
};


exports.appendChildren = function appendChildren(el, children) {
for(var i = 0; i < children.length; i++) {
if(children[i]) {
el.appendChild(children[i]);
}
}
};
Loading

0 comments on commit 826ed94

Please sign in to comment.