Skip to content

Commit

Permalink
Merge pull request #2364 from TomDemulierChevret/rangeslider-allow-zo…
Browse files Browse the repository at this point in the history
…om-on-oppaxis

Rangeslider allow zoom on oppaxis
  • Loading branch information
alexcjohnson authored Mar 7, 2018
2 parents 2ff1c85 + 4f54f62 commit beaec8d
Show file tree
Hide file tree
Showing 14 changed files with 1,118 additions and 620 deletions.
8 changes: 4 additions & 4 deletions src/components/rangeslider/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,22 @@ module.exports = {
valType: 'color',
dflt: colorAttributes.background,
role: 'style',
editType: 'calc',
editType: 'plot',
description: 'Sets the background color of the range slider.'
},
bordercolor: {
valType: 'color',
dflt: colorAttributes.defaultLine,
role: 'style',
editType: 'calc',
editType: 'plot',
description: 'Sets the border color of the range slider.'
},
borderwidth: {
valType: 'integer',
dflt: 0,
min: 0,
role: 'style',
editType: 'calc',
editType: 'plot',
description: 'Sets the border color of the range slider.'
},
autorange: {
Expand Down Expand Up @@ -73,7 +73,7 @@ module.exports = {
min: 0,
max: 1,
role: 'style',
editType: 'calc',
editType: 'plot',
description: [
'The height of the range slider as a fraction of the',
'total plot area height.'
Expand Down
4 changes: 4 additions & 0 deletions src/components/rangeslider/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,13 @@ module.exports = {
grabAreaMaxClassName: 'rangeslider-grabarea-max',
handleMaxClassName: 'rangeslider-handle-max',

maskMinOppAxisClassName: 'rangeslider-mask-min-opp-axis',
maskMaxOppAxisClassName: 'rangeslider-mask-max-opp-axis',

// style constants

maskColor: 'rgba(0,0,0,0.4)',
maskOppAxisColor: 'rgba(0,0,0,0.2)',

slideBoxFill: 'transparent',
slideBoxCursor: 'ew-resize',
Expand Down
39 changes: 38 additions & 1 deletion src/components/rangeslider/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

var Lib = require('../../lib');
var attributes = require('./attributes');
var oppAxisAttrs = require('./oppaxis_attributes');
var axisIds = require('../../plots/cartesian/axis_ids');

module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
if(!layoutIn[axName].rangeslider) return;
Expand All @@ -27,6 +29,10 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
return Lib.coerce(containerIn, containerOut, attributes, attr, dflt);
}

function coerceRange(rangeContainerIn, rangeContainerOut, attr, dflt) {
return Lib.coerce(rangeContainerIn, rangeContainerOut, oppAxisAttrs, attr, dflt);
}

var visible = coerce('visible');
if(!visible) return;

Expand All @@ -35,9 +41,40 @@ module.exports = function handleDefaults(layoutIn, layoutOut, axName) {
coerce('borderwidth');
coerce('thickness');

coerce('autorange', !axOut.isValidRange(containerIn.range));
axOut._rangesliderAutorange = coerce('autorange', !axOut.isValidRange(containerIn.range));
coerce('range');

var subplots = layoutOut._subplots;
if(subplots) {
var yIds = subplots.cartesian
.filter(function(subplotId) {
return subplotId.substr(0, subplotId.indexOf('y')) === axisIds.name2id(axName);
})
.map(function(subplotId) {
return subplotId.substr(subplotId.indexOf('y'), subplotId.length);
});
var yNames = Lib.simpleMap(yIds, axisIds.id2name);
for(var i = 0; i < yNames.length; i++) {
var yName = yNames[i];

var rangeContainerIn = containerIn[yName] || {};
var rangeContainerOut = containerOut[yName] = {};

var yAxOut = layoutOut[yName];

var rangemodeDflt;
if(rangeContainerIn.range && yAxOut.isValidRange(rangeContainerIn.range)) {
rangemodeDflt = 'fixed';
}

var rangeMode = coerceRange(rangeContainerIn, rangeContainerOut, 'rangemode', rangemodeDflt);
if(rangeMode !== 'match') {
coerceRange(rangeContainerIn, rangeContainerOut, 'range', yAxOut.range.slice());
}
yAxOut._rangesliderAutorange = (rangeMode === 'auto');
}
}

// to map back range slider (auto) range
containerOut._input = containerIn;
};
79 changes: 72 additions & 7 deletions src/components/rangeslider/draw.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ module.exports = function(gd) {
// for all present range sliders
rangeSliders.each(function(axisOpts) {
var rangeSlider = d3.select(this),
opts = axisOpts[constants.name];
opts = axisOpts[constants.name],
oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)],
oppAxisRangeOpts = opts[Axes.id2name(axisOpts.anchor)];

// update range
// Expand slider range to the axis range
Expand Down Expand Up @@ -141,21 +143,31 @@ module.exports = function(gd) {

opts._rl = [range0, range1];

if(oppAxisRangeOpts.rangemode !== 'match') {
var range0OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[0]),
range1OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[1]),
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)
.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);
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
Expand Down Expand Up @@ -284,13 +296,17 @@ function setDataRange(rangeSlider, gd, axisOpts, opts) {
});
}

function setPixelRange(rangeSlider, gd, axisOpts, opts) {
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);
}
Expand All @@ -309,6 +325,26 @@ function setPixelRange(rangeSlider, gd, axisOpts, opts) {
.attr('x', pixelMax)
.attr('width', opts._width - pixelMax);

if(oppAxisRangeOpts.rangemode !== 'match') {
var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])),
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;
Expand Down Expand Up @@ -391,7 +427,8 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
isMainPlot = (i === 0);

var oppAxisOpts = Axes.getFromId(gd, id, 'y'),
oppAxisName = oppAxisOpts._name;
oppAxisName = oppAxisOpts._name,
oppAxisRangeOpts = opts[oppAxisName];

var mockFigure = {
data: [],
Expand All @@ -412,7 +449,7 @@ function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
mockFigure.layout[oppAxisName] = {
type: oppAxisOpts.type,
domain: [0, 1],
range: oppAxisOpts.range.slice(),
range: oppAxisRangeOpts.rangemode !== 'match' ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(),
calendar: oppAxisOpts.calendar
};

Expand Down Expand Up @@ -453,7 +490,7 @@ function filterRangePlotCalcData(calcData, subplotId) {
return out;
}

function drawMasks(rangeSlider, gd, axisOpts, opts) {
function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisRangeOpts) {
var maskMin = rangeSlider.selectAll('rect.' + constants.maskMinClassName)
.data([0]);

Expand All @@ -477,6 +514,34 @@ function drawMasks(rangeSlider, gd, axisOpts, opts) {
maskMax
.attr('height', opts._height)
.call(Color.fill, constants.maskColor);

// masks used for oppAxis zoom
if(oppAxisRangeOpts.rangemode !== 'match') {
var maskMinOppAxis = rangeSlider.selectAll('rect.' + constants.maskMinOppAxisClassName)
.data([0]);

maskMinOppAxis.enter().append('rect')
.classed(constants.maskMinOppAxisClassName, true)
.attr('y', 0)
.attr('shape-rendering', 'crispEdges');

maskMinOppAxis
.attr('width', opts._width)
.call(Color.fill, constants.maskOppAxisColor);

var maskMaxOppAxis = rangeSlider.selectAll('rect.' + constants.maskMaxOppAxisClassName)
.data([0]);

maskMaxOppAxis.enter().append('rect')
.classed(constants.maskMaxOppAxisClassName, true)
.attr('y', 0)
.attr('shape-rendering', 'crispEdges');

maskMaxOppAxis
.attr('width', opts._width)
.style('border-top', constants.maskOppBorder)
.call(Color.fill, constants.maskOppAxisColor);
}
}

function drawSlideBox(rangeSlider, gd, axisOpts, opts) {
Expand Down
10 changes: 9 additions & 1 deletion src/components/rangeslider/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@

'use strict';

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

module.exports = {
moduleType: 'component',
name: 'rangeslider',

schema: {
subplots: {
xaxis: {rangeslider: require('./attributes')}
xaxis: {
rangeslider: Lib.extendFlat({}, attrs, {
yaxis: oppAxisAttrs
})
}
}
},

Expand Down
45 changes: 45 additions & 0 deletions src/components/rangeslider/oppaxis_attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Copyright 2012-2018, 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';

module.exports = {
// not really a 'subplot' attribute container,
// but this is the flag we use to denote attributes that
// support yaxis, yaxis2, yaxis3, ... counters
_isSubplotObj: true,

rangemode: {
valType: 'enumerated',
values: ['auto', 'fixed', 'match'],
dflt: 'match',
role: 'style',
editType: 'calc',
description: [
'Determines whether or not the range of this axis in',
'the rangeslider use the same value than in the main plot',
'when zooming in/out.',
'If *auto*, the autorange will be used.',
'If *fixed*, the `range` is used.',
'If *match*, the current range of the corresponding y-axis on the main subplot is used.'
].join(' ')
},
range: {
valType: 'info_array',
role: 'style',
items: [
{valType: 'any', editType: 'plot'},
{valType: 'any', editType: 'plot'}
],
editType: 'plot',
description: [
'Sets the range of this axis for the rangeslider.'
].join(' ')
},
editType: 'calc'
};
22 changes: 19 additions & 3 deletions src/plots/cartesian/autorange.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,11 @@ function makePadFn(ax) {
}

function doAutoRange(ax) {
ax.setScale();
if(!ax._length) ax.setScale();

// TODO do we really need this?
var hasDeps = (ax._min && ax._max && ax._min.length && ax._max.length);
var axIn;

if(ax.autorange && hasDeps) {
ax.range = getAutoRange(ax);
Expand All @@ -188,14 +189,29 @@ function doAutoRange(ax) {
// doAutoRange will get called on fullLayout,
// but we want to report its results back to layout

var axIn = ax._input;
axIn = ax._input;
axIn.range = ax.range.slice();
axIn.autorange = ax.autorange;
}

if(ax._anchorAxis && ax._anchorAxis.rangeslider) {
var axeRangeOpts = ax._anchorAxis.rangeslider[ax._name];
if(axeRangeOpts) {
if(axeRangeOpts.rangemode === 'auto') {
if(hasDeps) {
axeRangeOpts.range = getAutoRange(ax);
} else {
axeRangeOpts.range = ax._rangeInitial ? ax._rangeInitial.slice() : ax.range.slice();
}
}
}
axIn = ax._anchorAxis._input;
axIn.rangeslider[ax._name] = Lib.extendFlat({}, axeRangeOpts);
}
}

function needsAutorange(ax) {
return ax.autorange || !!(ax.rangeslider || {}).autorange;
return ax.autorange || ax._rangesliderAutorange;
}

/*
Expand Down
6 changes: 6 additions & 0 deletions src/plots/cartesian/axis_defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ module.exports = function handleAxisDefaults(containerIn, containerOut, coerce,

var autoRange = coerce('autorange', !containerOut.isValidRange(containerIn.range));

// both x and y axes may need autorange done just for the range slider's purposes
// the logic is complicated to figure this out later, particularly for y axes since
// the settings can be spread out in the x axes... so instead we'll collect them
// during supplyDefaults
containerOut._rangesliderAutorange = false;

if(autoRange) coerce('rangemode');

coerce('range');
Expand Down
Binary file added test/image/baselines/range_slider_rangemode.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit beaec8d

Please sign in to comment.