Skip to content

Commit 710d2d6

Browse files
committed
alter logic for where spikes start and end
- toaxis always goes from the point to the axis, even for free axes - across spans all counteraxes with subplots for this axis, and extends to the free axis if it exists - fixed a bug with positioning after scrolling gd within a container - also short-circuited calculations we don't need
1 parent 37b77fe commit 710d2d6

File tree

2 files changed

+121
-78
lines changed

2 files changed

+121
-78
lines changed

src/plots/cartesian/axes.js

+65-17
Original file line numberDiff line numberDiff line change
@@ -1632,7 +1632,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
16321632
// set scaling to pixels
16331633
ax.setScale();
16341634

1635-
var axletter = axid.charAt(0),
1635+
var axLetter = axid.charAt(0),
16361636
counterLetter = axes.counterLetter(axid),
16371637
vals = axes.calcTicks(ax),
16381638
datafn = function(d) { return [d.text, d.x, ax.mirror].join('_'); },
@@ -1646,7 +1646,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
16461646
gridWidth = Drawing.crispRound(gd, ax.gridwidth, 1),
16471647
zeroLineWidth = Drawing.crispRound(gd, ax.zerolinewidth, gridWidth),
16481648
tickWidth = Drawing.crispRound(gd, ax.tickwidth, 1),
1649-
sides, transfn, tickpathfn,
1649+
sides, transfn, tickpathfn, subplots,
16501650
i;
16511651

16521652
if(ax._counterangle && ax.ticks === 'outside') {
@@ -1656,7 +1656,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
16561656
}
16571657

16581658
// positioning arguments for x vs y axes
1659-
if(axletter === 'x') {
1659+
if(axLetter === 'x') {
16601660
sides = ['bottom', 'top'];
16611661
transfn = function(d) {
16621662
return 'translate(' + ax.l2p(d.x) + ',0)';
@@ -1669,7 +1669,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
16691669
else return 'M0,' + shift + 'v' + len;
16701670
};
16711671
}
1672-
else if(axletter === 'y') {
1672+
else if(axLetter === 'y') {
16731673
sides = ['left', 'right'];
16741674
transfn = function(d) {
16751675
return 'translate(0,' + ax.l2p(d.x) + ')';
@@ -1690,7 +1690,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
16901690
// which direction do the side[0], side[1], and free ticks go?
16911691
// then we flip if outside XOR y axis
16921692
ticksign = [-1, 1, axside === sides[1] ? 1 : -1];
1693-
if((ax.ticks !== 'inside') === (axletter === 'x')) {
1693+
if((ax.ticks !== 'inside') === (axLetter === 'x')) {
16941694
ticksign = ticksign.map(function(v) { return -v; });
16951695
}
16961696

@@ -1724,12 +1724,12 @@ axes.doTicks = function(gd, axid, skipTitle) {
17241724
var tickLabels = container.selectAll('g.' + tcls).data(vals, datafn);
17251725
if(!ax.showticklabels || !isNumeric(position)) {
17261726
tickLabels.remove();
1727-
drawAxTitle(axid);
1727+
drawAxTitle();
17281728
return;
17291729
}
17301730

17311731
var labelx, labely, labelanchor, labelpos0, flipit;
1732-
if(axletter === 'x') {
1732+
if(axLetter === 'x') {
17331733
flipit = (axside === 'bottom') ? 1 : -1;
17341734
labelx = function(d) { return d.dx + labelShift * flipit; };
17351735
labelpos0 = position + (labelStandoff + pad) * flipit;
@@ -1845,7 +1845,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
18451845
// check for auto-angling if x labels overlap
18461846
// don't auto-angle at all for log axes with
18471847
// base and digit format
1848-
if(axletter === 'x' && !isNumeric(ax.tickangle) &&
1848+
if(axLetter === 'x' && !isNumeric(ax.tickangle) &&
18491849
(ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')) {
18501850
var lbbArray = [];
18511851
tickLabels.each(function(d) {
@@ -1890,12 +1890,59 @@ axes.doTicks = function(gd, axid, skipTitle) {
18901890
// (so it can move out of the way if needed)
18911891
// TODO: separate out scoot so we don't need to do
18921892
// a full redraw of the title (mostly relevant for MathJax)
1893-
drawAxTitle(axid);
1893+
drawAxTitle();
18941894
return axid + ' done';
18951895
}
18961896

18971897
function calcBoundingBox() {
1898-
ax._boundingBox = container.node().getBoundingClientRect();
1898+
var bBox = container.node().getBoundingClientRect();
1899+
var gdBB = gd.getBoundingClientRect();
1900+
1901+
/*
1902+
* the way we're going to use this, the positioning that matters
1903+
* is relative to the origin of gd. This is important particularly
1904+
* if gd is scrollable, and may have been scrolled between the time
1905+
* we calculate this and the time we use it
1906+
*/
1907+
var bbFinal = ax._boundingBox = {
1908+
width: bBox.width,
1909+
height: bBox.height,
1910+
left: bBox.left - gdBB.left,
1911+
right: bBox.right - gdBB.left,
1912+
top: bBox.top - gdBB.top,
1913+
bottom: bBox.bottom - gdBB.top
1914+
};
1915+
1916+
/*
1917+
* for spikelines: what's the full domain of positions in the
1918+
* opposite direction that are associated with this axis?
1919+
* This means any axes that we make a subplot with, plus the
1920+
* position of the axis itself if it's free.
1921+
*/
1922+
if(subplots) {
1923+
var fullRange = ax._counterSpan = [Infinity, -Infinity];
1924+
1925+
for(i = 0; i < subplots.length; i++) {
1926+
var subplot = fullLayout._plots[subplots[i]];
1927+
var counterAxis = subplot[(axLetter === 'x') ? 'yaxis' : 'xaxis'];
1928+
1929+
extendRange(fullRange, [
1930+
counterAxis._offset,
1931+
counterAxis._offset + counterAxis._length
1932+
]);
1933+
}
1934+
1935+
if(ax.anchor === 'free') {
1936+
extendRange(fullRange, (axLetter === 'x') ?
1937+
[ax._boundingBox.bottom, ax._boundingBox.top] :
1938+
[ax._boundingBox.right, ax._boundingBox.left]);
1939+
}
1940+
}
1941+
1942+
function extendRange(range, newRange) {
1943+
range[0] = Math.min(range[0], newRange[0]);
1944+
range[1] = Math.max(range[1], newRange[1]);
1945+
}
18991946
}
19001947

19011948
var done = Lib.syncOrAsync([
@@ -1907,7 +1954,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
19071954
return done;
19081955
}
19091956

1910-
function drawAxTitle(axid) {
1957+
function drawAxTitle() {
19111958
if(skipTitle) return;
19121959

19131960
// now this only applies to regular cartesian axes; colorbars and
@@ -1978,16 +2025,16 @@ axes.doTicks = function(gd, axid, skipTitle) {
19782025

19792026
function traceHasBarsOrFill(trace, subplot) {
19802027
if(trace.visible !== true || trace.xaxis + trace.yaxis !== subplot) return false;
1981-
if(Registry.traceIs(trace, 'bar') && trace.orientation === {x: 'h', y: 'v'}[axletter]) return true;
1982-
return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axletter;
2028+
if(Registry.traceIs(trace, 'bar') && trace.orientation === {x: 'h', y: 'v'}[axLetter]) return true;
2029+
return trace.fill && trace.fill.charAt(trace.fill.length - 1) === axLetter;
19832030
}
19842031

19852032
function drawGrid(plotinfo, counteraxis, subplot) {
19862033
var gridcontainer = plotinfo.gridlayer,
19872034
zlcontainer = plotinfo.zerolinelayer,
1988-
gridvals = plotinfo['hidegrid' + axletter] ? [] : valsClipped,
2035+
gridvals = plotinfo['hidegrid' + axLetter] ? [] : valsClipped,
19892036
gridpath = ax._gridpath ||
1990-
'M0,0' + ((axletter === 'x') ? 'v' : 'h') + counteraxis._length,
2037+
'M0,0' + ((axLetter === 'x') ? 'v' : 'h') + counteraxis._length,
19912038
grid = gridcontainer.selectAll('path.' + gcls)
19922039
.data((ax.showgrid === false) ? [] : gridvals, datafn);
19932040
grid.enter().append('path').classed(gcls, 1)
@@ -2041,12 +2088,13 @@ axes.doTicks = function(gd, axid, skipTitle) {
20412088
return drawLabels(ax._axislayer, ax._pos);
20422089
}
20432090
else {
2044-
var alldone = axes.getSubplots(gd, ax).map(function(subplot) {
2091+
subplots = axes.getSubplots(gd, ax);
2092+
var alldone = subplots.map(function(subplot) {
20452093
var plotinfo = fullLayout._plots[subplot];
20462094

20472095
if(!fullLayout._has('cartesian')) return;
20482096

2049-
var container = plotinfo[axletter + 'axislayer'],
2097+
var container = plotinfo[axLetter + 'axislayer'],
20502098

20512099
// [bottom or left, top or right, free, main]
20522100
linepositions = ax._linepositions[subplot] || [],

src/plots/cartesian/graph_interact.js

+56-61
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
var d3 = require('d3');
1313
var isNumeric = require('fast-isnumeric');
14+
var tinycolor = require('tinycolor2');
1415

1516
var Lib = require('../../lib');
1617
var Events = require('../../lib/events');
@@ -849,64 +850,45 @@ fx.loneUnhover = function(containerOrSelection) {
849850
d3.select(containerOrSelection);
850851

851852
selection.selectAll('g.hovertext').remove();
852-
selection.selectAll('line.spikeline').remove();
853-
selection.selectAll('circle.spikeline').remove();
853+
selection.selectAll('.spikeline').remove();
854854
};
855855

856856
function createSpikelines(hoverData, opts) {
857-
var hovermode = opts.hovermode,
858-
container = opts.container,
859-
outerContainer = opts.outerContainer;
860-
if(hovermode !== 'closest') return;
861-
var c0 = hoverData[0],
862-
x = (c0.x0 + c0.x1) / 2,
863-
y = (c0.y0 + c0.y1) / 2,
864-
xOffset = c0.xa._offset,
865-
yOffset = c0.ya._offset,
866-
xPoint = xOffset + x,
867-
yPoint = yOffset + y,
868-
xSide = c0.xa.side,
869-
ySide = c0.ya.side,
870-
xLength = c0.xa._length,
871-
yLength = c0.ya._length,
872-
xEdge = c0.ya._boundingBox.left + (ySide === 'left' ? c0.ya._boundingBox.width : 0),
873-
yEdge = c0.xa._boundingBox.top + (xSide === 'top' ? c0.xa._boundingBox.height : 0),
874-
xFreeBase = xOffset + (ySide === 'right' ? xLength : 0),
875-
yFreeBase = yOffset + (xSide === 'top' ? yLength : 0),
876-
xAnchoredBase = xEdge - outerContainer.node().offsetLeft,
877-
yAnchoredBase = yEdge - outerContainer.node().offsetTop,
878-
xBase = c0.ya.anchor === 'free' ? xFreeBase : xAnchoredBase,
879-
yBase = c0.xa.anchor === 'free' ? yFreeBase : yAnchoredBase,
880-
contrastColor = Color.combine(opts.fullLayout.plot_bgcolor, opts.fullLayout.paper_bgcolor),
881-
xColor = c0.xa.spikecolor ? c0.xa.spikecolor : (
882-
tinycolor.readability(c0.color, contrastColor) < 1.5 ? (
883-
tinycolor(c0.color).getBrightness() > 128 ? '#000' : Color.background) :
884-
c0.color),
885-
yColor = c0.ya.spikecolor ? c0.ya.spikecolor : (
886-
tinycolor.readability(c0.color, contrastColor) < 1.5 ? (
887-
tinycolor(c0.color).getBrightness() > 128 ? '#000' : Color.background) :
888-
c0.color),
889-
xThickness = c0.xa.spikethickness,
890-
yThickness = c0.ya.spikethickness,
891-
xDash = Drawing.dashStyle(c0.xa.spikedash, xThickness),
892-
yDash = Drawing.dashStyle(c0.xa.spikedash, yThickness),
893-
xMarker = c0.xa.spikemode.indexOf('marker') !== -1,
894-
yMarker = c0.ya.spikemode.indexOf('marker') !== -1,
895-
xSpikeLine = c0.xa.spikemode.indexOf('toaxis') !== -1 || c0.xa.spikemode.indexOf('across') !== -1,
896-
ySpikeLine = c0.ya.spikemode.indexOf('toaxis') !== -1 || c0.ya.spikemode.indexOf('across') !== -1,
897-
xEndSpike = c0.xa.spikemode.indexOf('across') !== -1 ?
898-
(ySide === 'left' ? xBase + xLength : xBase - xLength) :
899-
xPoint,
900-
yEndSpike = c0.ya.spikemode.indexOf('across') !== -1 ?
901-
(xSide === 'bottom' ? yBase - yLength : yBase + yLength) :
902-
yPoint;
857+
var hovermode = opts.hovermode;
858+
var container = opts.container;
859+
var c0 = hoverData[0];
860+
var xa = c0.xa;
861+
var ya = c0.ya;
862+
var showX = xa.showspikes;
863+
var showY = ya.showspikes;
903864

904865
// Remove old spikeline items
905-
container.selectAll('line.spikeline').remove();
906-
container.selectAll('circle.spikeline').remove();
866+
container.selectAll('.spikeline').remove();
867+
868+
if(hovermode !== 'closest' || !(showX || showY)) return;
869+
870+
var fullLayout = opts.fullLayout;
871+
var xPoint = xa._offset + (c0.x0 + c0.x1) / 2;
872+
var yPoint = ya._offset + (c0.y0 + c0.y1) / 2;
873+
var contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor);
874+
var dfltDashColor = tinycolor.readability(c0.color, contrastColor) < 1.5 ?
875+
Color.contrast(contrastColor) : c0.color;
876+
877+
if(showY) {
878+
var yMode = ya.spikemode;
879+
var yThickness = ya.spikethickness;
880+
var yColor = ya.spikecolor || dfltDashColor;
881+
var yBB = ya._boundingBox;
882+
var xEdge = ((yBB.left + yBB.right) / 2) < xPoint ? yBB.right : yBB.left;
883+
884+
if(yMode.indexOf('toaxis') !== -1 || yMode.indexOf('across') !== -1) {
885+
var xBase = xEdge;
886+
var xEndSpike = xPoint;
887+
if(yMode.indexOf('across') !== -1) {
888+
xBase = ya._counterSpan[0];
889+
xEndSpike = ya._counterSpan[1];
890+
}
907891

908-
if(c0.ya.showspikes) {
909-
if(ySpikeLine) {
910892
// Background horizontal Line (to y-axis)
911893
container.append('line')
912894
.attr({
@@ -929,16 +911,16 @@ function createSpikelines(hoverData, opts) {
929911
'y2': yPoint,
930912
'stroke-width': yThickness,
931913
'stroke': yColor,
932-
'stroke-dasharray': yDash
914+
'stroke-dasharray': Drawing.dashStyle(ya.spikedash, yThickness)
933915
})
934916
.classed('spikeline', true)
935917
.classed('crisp', true);
936918
}
937919
// Y axis marker
938-
if(yMarker) {
920+
if(yMode.indexOf('marker') !== -1) {
939921
container.append('circle')
940922
.attr({
941-
'cx': xAnchoredBase + (ySide !== 'right' ? yThickness : -yThickness),
923+
'cx': xEdge + (ya.side !== 'right' ? yThickness : -yThickness),
942924
'cy': yPoint,
943925
'r': yThickness,
944926
'fill': yColor
@@ -947,9 +929,22 @@ function createSpikelines(hoverData, opts) {
947929
}
948930
}
949931

950-
if(c0.xa.showspikes) {
951-
if(xSpikeLine) {
952-
// Background vertical line (to x-axis)
932+
if(showX) {
933+
var xMode = xa.spikemode;
934+
var xThickness = xa.spikethickness;
935+
var xColor = xa.spikecolor || dfltDashColor;
936+
var xBB = xa._boundingBox;
937+
var yEdge = ((xBB.top + xBB.bottom) / 2) < yPoint ? xBB.bottom : xBB.top;
938+
939+
if(xMode.indexOf('toaxis') !== -1 || xMode.indexOf('across') !== -1) {
940+
var yBase = yEdge;
941+
var yEndSpike = yPoint;
942+
if(xMode.indexOf('across') !== -1) {
943+
yBase = xa._counterSpan[0];
944+
yEndSpike = xa._counterSpan[1];
945+
}
946+
947+
// Background vertical line (to x-axis)
953948
container.append('line')
954949
.attr({
955950
'x1': xPoint,
@@ -971,18 +966,18 @@ function createSpikelines(hoverData, opts) {
971966
'y2': yEndSpike,
972967
'stroke-width': xThickness,
973968
'stroke': xColor,
974-
'stroke-dasharray': xDash
969+
'stroke-dasharray': Drawing.dashStyle(xa.spikedash, xThickness)
975970
})
976971
.classed('spikeline', true)
977972
.classed('crisp', true);
978973
}
979974

980975
// X axis marker
981-
if(xMarker) {
976+
if(xMode.indexOf('marker') !== -1) {
982977
container.append('circle')
983978
.attr({
984979
'cx': xPoint,
985-
'cy': yAnchoredBase - (xSide !== 'top' ? xThickness : -xThickness),
980+
'cy': yEdge - (xa.side !== 'top' ? xThickness : -xThickness),
986981
'r': xThickness,
987982
'fill': xColor
988983
})

0 commit comments

Comments
 (0)