-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Introduce cartesian axis breaks #4614
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
4f6ea7e
introduce axis breaks attributes
etpinard e9cfe04
add axis breaks default logic
etpinard baf753a
add ax.maskBreaks method
etpinard 259eafa
implement axis breaks setConvert logic
etpinard eca3da7
adapt autorange routine for axis breaks
etpinard 6bec94d
adapt calcTicks for axis breaks
etpinard fe80cad
adapt dragbox logic for axis breaks
etpinard d425373
do not show zeroline when it falls inside an axis break
etpinard 5f2fbe0
add axis breaks mocks
etpinard b5aaf92
add TODO for better "first tick" algo on date axes
etpinard 187c93a
fix typo in comment
etpinard ebca01b
fix axis breaks + rangeslider behavior
etpinard 3957d95
during l2p(v) when v falls into breaks, pick offset closest to it
etpinard 76a265e
fix typo in break `bounds` description
etpinard e00af90
Handle breaks on date axes only for now
etpinard 53196e5
simplify logic - breaks are on date axes only
archmoj 493bb4e
Handle axis breaks on reversed ranges
etpinard 7080f90
replace 'spread' -> 'size' in attr descriptions
etpinard dcceb76
fix %H maskBreaks for values greater but on the same hour
etpinard 28c328d
increase dtick on axes with breaks ...
etpinard 432f0d0
Merge branch 'master' into axis-breaks
etpinard 40d57fa
fix scale when panning on breaks
archmoj 1b55b42
fix 'increase dtick' lgoic for cases with dtick value starting 'M'
etpinard 49b4053
set scattergl and splom traces to visible:false on axis with breaks
etpinard File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,10 @@ var numConstants = require('../../constants/numerical'); | |
var FP_SAFE = numConstants.FP_SAFE; | ||
var BADNUM = numConstants.BADNUM; | ||
var LOG_CLIP = numConstants.LOG_CLIP; | ||
var ONEDAY = numConstants.ONEDAY; | ||
var ONEHOUR = numConstants.ONEHOUR; | ||
var ONEMIN = numConstants.ONEMIN; | ||
var ONESEC = numConstants.ONESEC; | ||
|
||
var constants = require('./constants'); | ||
var axisIds = require('./axis_ids'); | ||
|
@@ -170,14 +174,73 @@ module.exports = function setConvert(ax, fullLayout) { | |
if(isNumeric(v)) return +v; | ||
} | ||
|
||
function l2p(v) { | ||
// include 2 fractional digits on pixel, for PDF zooming etc | ||
function _l2p(v, m, b) { return d3.round(b + m * v, 2); } | ||
|
||
function _p2l(px, m, b) { return (px - b) / m; } | ||
|
||
var l2p = function l2p(v) { | ||
if(!isNumeric(v)) return BADNUM; | ||
return _l2p(v, ax._m, ax._b); | ||
}; | ||
|
||
// include 2 fractional digits on pixel, for PDF zooming etc | ||
return d3.round(ax._b + ax._m * v, 2); | ||
} | ||
var p2l = function(px) { | ||
return _p2l(px, ax._m, ax._b); | ||
}; | ||
|
||
function p2l(px) { return (px - ax._b) / ax._m; } | ||
if(ax.breaks) { | ||
if(axLetter === 'y') { | ||
l2p = function(v) { | ||
if(!isNumeric(v)) return BADNUM; | ||
if(!ax._breaks.length) return _l2p(v, ax._m, ax._b); | ||
|
||
var b = ax._B[0]; | ||
for(var i = 0; i < ax._breaks.length; i++) { | ||
var brk = ax._breaks[i]; | ||
if(v <= brk.min) b = ax._B[i + 1]; | ||
else if(v > brk.max) break; | ||
} | ||
return _l2p(v, -ax._m2, b); | ||
}; | ||
p2l = function(px) { | ||
if(!isNumeric(px)) return BADNUM; | ||
if(!ax._breaks.length) return _p2l(px, ax._m, ax._b); | ||
|
||
var b = ax._B[0]; | ||
for(var i = 0; i < ax._breaks.length; i++) { | ||
var brk = ax._breaks[i]; | ||
if(px >= brk.pmin) b = ax._B[i + 1]; | ||
else if(px < brk.pmax) break; | ||
} | ||
return _p2l(px, -ax._m2, b); | ||
}; | ||
} else { | ||
l2p = function(v) { | ||
if(!isNumeric(v)) return BADNUM; | ||
if(!ax._breaks.length) return _l2p(v, ax._m, ax._b); | ||
|
||
var b = ax._B[0]; | ||
for(var i = 0; i < ax._breaks.length; i++) { | ||
var brk = ax._breaks[i]; | ||
if(v >= brk.max) b = ax._B[i + 1]; | ||
else if(v < brk.min) break; | ||
} | ||
return _l2p(v, ax._m2, b); | ||
}; | ||
p2l = function(px) { | ||
if(!isNumeric(px)) return BADNUM; | ||
if(!ax._breaks.length) return _p2l(px, ax._m, ax._b); | ||
|
||
var b = ax._B[0]; | ||
for(var i = 0; i < ax._breaks.length; i++) { | ||
var brk = ax._breaks[i]; | ||
if(px >= brk.pmax) b = ax._B[i + 1]; | ||
else if(px < brk.pmin) break; | ||
} | ||
return _p2l(px, ax._m2, b); | ||
}; | ||
} | ||
} | ||
|
||
// conversions among c/l/p are fairly simple - do them together for all axis types | ||
ax.c2l = (ax.type === 'log') ? toLog : ensureNumber; | ||
|
@@ -486,6 +549,51 @@ module.exports = function setConvert(ax, fullLayout) { | |
ax._b = -ax._m * rl0; | ||
} | ||
|
||
// set of "N" disjoint breaks inside the range | ||
ax._breaks = []; | ||
// length of these breaks in value space | ||
ax._lBreaks = 0; | ||
// l2p slope (same for all intervals) | ||
ax._m2 = 0; | ||
// set of l2p offsets (one for each of the (N+1) piecewise intervals) | ||
ax._B = []; | ||
archmoj marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if(ax.breaks) { | ||
var i, brk; | ||
|
||
ax._breaks = ax.locateBreaks(rl0, rl1); | ||
|
||
if(ax._breaks.length) { | ||
for(i = 0; i < ax._breaks.length; i++) { | ||
brk = ax._breaks[i]; | ||
ax._lBreaks += (brk.max - brk.min); | ||
} | ||
|
||
ax._m2 = ax._length / (rl1 - rl0 - ax._lBreaks); | ||
|
||
if(axLetter === 'y') { | ||
ax._breaks.reverse(); | ||
// N.B. top to bottom (negative coord, positive px direction) | ||
ax._B.push(ax._m2 * rl1); | ||
} else { | ||
ax._B.push(-ax._m2 * rl0); | ||
} | ||
|
||
for(i = 0; i < ax._breaks.length; i++) { | ||
brk = ax._breaks[i]; | ||
ax._B.push(ax._B[ax._B.length - 1] - ax._m2 * (brk.max - brk.min)); | ||
} | ||
|
||
// fill pixel (i.e. 'p') min/max here, | ||
// to not have to loop through the _breaks twice during `p2l` | ||
for(i = 0; i < ax._breaks.length; i++) { | ||
brk = ax._breaks[i]; | ||
brk.pmin = l2p(brk.min); | ||
brk.pmax = l2p(brk.max); | ||
} | ||
} | ||
} | ||
|
||
if(!isFinite(ax._m) || !isFinite(ax._b) || ax._length < 0) { | ||
fullLayout._replotting = false; | ||
throw new Error('Something went wrong with axis scaling'); | ||
|
@@ -565,6 +673,141 @@ module.exports = function setConvert(ax, fullLayout) { | |
} | ||
return v; | ||
}; | ||
|
||
ax.locateBreaks = function(r0, r1) { | ||
var i, bnds, b0, b1; | ||
|
||
var breaksOut = []; | ||
if(!ax.breaks) return breaksOut; | ||
|
||
var breaksIn; | ||
if(ax.type === 'date') { | ||
breaksIn = ax.breaks.slice().sort(function(a, b) { | ||
if(a.pattern === '%w' && b.pattern === '%H') return -1; | ||
else if(b.pattern === '%w' && a.pattern === '%H') return 1; | ||
return 0; | ||
}); | ||
} else { | ||
breaksIn = ax.breaks; | ||
} | ||
|
||
var addBreak = function(min, max) { | ||
min = Lib.constrain(min, r0, r1); | ||
max = Lib.constrain(max, r0, r1); | ||
if(min === max) return; | ||
|
||
var isNewBreak = true; | ||
for(var j = 0; j < breaksOut.length; j++) { | ||
var brkj = breaksOut[j]; | ||
if(min > brkj.max || max < brkj.min) { | ||
// potentially a new break | ||
} else { | ||
if(min < brkj.min) { | ||
brkj.min = min; | ||
} | ||
if(max > brkj.max) { | ||
brkj.max = max; | ||
} | ||
isNewBreak = false; | ||
} | ||
} | ||
if(isNewBreak) { | ||
breaksOut.push({min: min, max: max}); | ||
} | ||
}; | ||
|
||
for(i = 0; i < breaksIn.length; i++) { | ||
var brk = breaksIn[i]; | ||
|
||
if(brk.enabled) { | ||
var op = brk.operation; | ||
var op0 = op.charAt(0); | ||
var op1 = op.charAt(1); | ||
|
||
if(brk.bounds) { | ||
if(brk.pattern) { | ||
bnds = Lib.simpleMap(brk.bounds, cleanNumber); | ||
if(bnds[0] === bnds[1] && op === '()') continue; | ||
|
||
// r0 value as date | ||
var r0Date = new Date(r0); | ||
// r0 value for break pattern | ||
var r0Pattern; | ||
// delta between r0 and first break in break pattern values | ||
var r0PatternDelta; | ||
// delta between break bounds in ms | ||
var bndDelta; | ||
// step in ms between breaks | ||
var step; | ||
// tracker to position bounds | ||
var t; | ||
|
||
switch(brk.pattern) { | ||
case '%w': | ||
b0 = bnds[0] + (op0 === '(' ? 1 : 0); | ||
b1 = bnds[1]; | ||
r0Pattern = r0Date.getUTCDay(); | ||
r0PatternDelta = b0 - r0Pattern; | ||
bndDelta = (b1 >= b0 ? b1 - b0 : (b1 + 7) - b0) * ONEDAY; | ||
if(op1 === ']') bndDelta += ONEDAY; | ||
step = 7 * ONEDAY; | ||
|
||
t = r0 + r0PatternDelta * ONEDAY - | ||
r0Date.getUTCHours() * ONEHOUR - | ||
r0Date.getUTCMinutes() * ONEMIN - | ||
r0Date.getUTCSeconds() * ONESEC - | ||
r0Date.getUTCMilliseconds(); | ||
break; | ||
case '%H': | ||
b0 = bnds[0]; | ||
b1 = bnds[1]; | ||
r0Pattern = r0Date.getUTCHours(); | ||
r0PatternDelta = b0 - r0Pattern; | ||
bndDelta = (b1 >= b0 ? b1 - b0 : (b1 + 24) - b0) * ONEHOUR; | ||
step = ONEDAY; | ||
|
||
t = r0 + r0PatternDelta * ONEHOUR - | ||
r0Date.getUTCMinutes() * ONEMIN - | ||
r0Date.getUTCSeconds() * ONESEC - | ||
r0Date.getUTCMilliseconds(); | ||
break; | ||
} | ||
|
||
while(t <= r1) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we add a finite test for |
||
// TODO we need to remove decimal (most often found | ||
// in auto ranges) for this to work correctly, | ||
// should this be Math.floor, Math.ceil or | ||
// Math.round ?? | ||
addBreak(Math.floor(t), Math.floor(t + bndDelta)); | ||
t += step; | ||
} | ||
} else { | ||
bnds = Lib.simpleMap(brk.bounds, ax.r2l); | ||
if(bnds[0] <= bnds[1]) { | ||
b0 = bnds[0]; | ||
b1 = bnds[1]; | ||
} else { | ||
b0 = bnds[1]; | ||
b1 = bnds[0]; | ||
} | ||
addBreak(b0, b1); | ||
} | ||
} else { | ||
var vals = Lib.simpleMap(brk.values, ax.d2c); | ||
for(var j = 0; j < vals.length; j++) { | ||
b0 = vals[j]; | ||
b1 = b0 + brk.dvalue; | ||
addBreak(b0, b1); | ||
} | ||
} | ||
} | ||
} | ||
|
||
breaksOut.sort(function(a, b) { return a.min - b.min; }); | ||
|
||
return breaksOut; | ||
}; | ||
|
||
// makeCalcdata: takes an x or y array and converts it | ||
// to a position on the axis object "ax" | ||
// inputs: | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.