Skip to content

Commit 132b5b2

Browse files
committed
add crossline functionality
1 parent 8cc5f55 commit 132b5b2

File tree

7 files changed

+367
-4
lines changed

7 files changed

+367
-4
lines changed

src/components/fx/hover.js

Lines changed: 130 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,13 @@ function _hover(gd, evt, subplot, noHoverEvent) {
232232
xval,
233233
yval,
234234
pointData,
235-
closedataPreviousLength;
235+
closedataPreviousLength,
236+
237+
// crosslinePoints: the set of candidate points we've found to draw crosslines to
238+
crosslinePoints = {
239+
hLinePoint: null,
240+
vLinePoint: null
241+
};
236242

237243
// Figure out what we're hovering on:
238244
// mouse location or user-supplied data
@@ -402,10 +408,60 @@ function _hover(gd, evt, subplot, noHoverEvent) {
402408
hoverData.splice(0, closedataPreviousLength);
403409
distance = hoverData[0].distance;
404410
}
411+
412+
var showSpikes = fullLayout.xaxis && fullLayout.xaxis.showspikes && fullLayout.yaxis && fullLayout.yaxis.showspikes;
413+
var showCrosslines = fullLayout.xaxis && fullLayout.xaxis.showcrossline || fullLayout.yaxis && fullLayout.yaxis.showcrossline;
414+
415+
if(fullLayout._has('cartesian') && showCrosslines && !(showSpikes && hovermode === 'closest')) {
416+
// Now find the points for the crosslines.
417+
if(fullLayout.yaxis.showcrossline) {
418+
crosslinePoints.hLinePoint = findCrosslinePoint(pointData, xval, yval, 'y', crosslinePoints.hLinePoint);
419+
}
420+
if(fullLayout.xaxis.showcrossline) {
421+
crosslinePoints.vLinePoint = findCrosslinePoint(pointData, xval, yval, 'x', crosslinePoints.vLinePoint);
422+
}
423+
}
405424
}
406425

407-
// nothing left: remove all labels and quit
408-
if(hoverData.length === 0) return dragElement.unhoverRaw(gd, evt);
426+
function findCrosslinePoint(pointData, xval, yval, mode, endPoint) {
427+
var resultPoint = endPoint;
428+
pointData.distance = Infinity;
429+
pointData.index = false;
430+
var closestPoints = trace._module.hoverPoints(pointData, xval, yval, mode);
431+
if(closestPoints) {
432+
var closestPt = closestPoints[0];
433+
if(isNumeric(closestPt.x0) && isNumeric(closestPt.y0)) {
434+
var tmpPoint = {
435+
xa: closestPt.xa,
436+
ya: closestPt.ya,
437+
x0: closestPt.x0,
438+
x1: closestPt.x1,
439+
y0: closestPt.y0,
440+
y1: closestPt.y1,
441+
distance: closestPt.distance,
442+
curveNumber: closestPt.trace.index,
443+
pointNumber: closestPt.index
444+
};
445+
if(!resultPoint || (resultPoint.distance > tmpPoint.distance)) {
446+
resultPoint = tmpPoint;
447+
}
448+
}
449+
}
450+
return resultPoint;
451+
}
452+
453+
// if hoverData is empty check for the crosslines to draw and quit if there are none
454+
if(hoverData.length === 0) {
455+
var result = dragElement.unhoverRaw(gd, evt);
456+
if(fullLayout._has('cartesian') && ((crosslinePoints.hLinePoint !== null) || (crosslinePoints.vLinePoint !== null))) {
457+
createCrosslines(crosslinePoints, fullLayout);
458+
}
459+
return result;
460+
}
461+
462+
if(fullLayout._has('cartesian')) {
463+
createCrosslines(crosslinePoints, fullLayout);
464+
}
409465

410466
hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; });
411467

@@ -1089,6 +1145,77 @@ function cleanPoint(d, hovermode) {
10891145
return d;
10901146
}
10911147

1148+
function createCrosslines(hoverData, fullLayout) {
1149+
var showXSpikeline = fullLayout.xaxis && fullLayout.xaxis.showspikes;
1150+
var showYSpikeline = fullLayout.yaxis && fullLayout.yaxis.showspikes;
1151+
var showH = fullLayout.yaxis && fullLayout.yaxis.showcrossline;
1152+
var showV = fullLayout.xaxis && fullLayout.xaxis.showcrossline;
1153+
var container = fullLayout._hoverlayer;
1154+
var hovermode = fullLayout.hovermode;
1155+
if(!(showV || showH) || (showXSpikeline && showYSpikeline && hovermode === 'closest')) return;
1156+
var hLinePoint,
1157+
vLinePoint,
1158+
xa,
1159+
ya,
1160+
hLinePointY,
1161+
vLinePointX;
1162+
1163+
// Remove old crossline items
1164+
container.selectAll('.crossline').remove();
1165+
1166+
var contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor);
1167+
var dfltCrosslineColor = Color.contrast(contrastColor);
1168+
1169+
// do not draw a crossline if there is a spikeline
1170+
if(showV && !(showXSpikeline && hovermode === 'closest')) {
1171+
vLinePoint = hoverData.vLinePoint;
1172+
xa = vLinePoint.xa;
1173+
vLinePointX = xa._offset + (vLinePoint.x0 + vLinePoint.x1) / 2;
1174+
1175+
var xThickness = xa.crosslinethickness;
1176+
var xDash = xa.crosslinedash;
1177+
var xColor = xa.crosslinecolor || dfltCrosslineColor;
1178+
1179+
// Foreground vertical line (to x-axis)
1180+
container.insert('line', ':first-child')
1181+
.attr({
1182+
'x1': vLinePointX,
1183+
'x2': vLinePointX,
1184+
'y1': xa._counterSpan[0],
1185+
'y2': xa._counterSpan[1],
1186+
'stroke-width': xThickness,
1187+
'stroke': xColor,
1188+
'stroke-dasharray': Drawing.dashStyle(xDash, xThickness)
1189+
})
1190+
.classed('crossline', true)
1191+
.classed('crisp', true);
1192+
}
1193+
1194+
if(showH && !(showYSpikeline && hovermode === 'closest')) {
1195+
hLinePoint = hoverData.hLinePoint;
1196+
ya = hLinePoint.ya;
1197+
hLinePointY = ya._offset + (hLinePoint.y0 + hLinePoint.y1) / 2;
1198+
1199+
var yThickness = ya.crosslinethickness;
1200+
var yDash = ya.crosslinedash;
1201+
var yColor = ya.crosslinecolor || dfltCrosslineColor;
1202+
1203+
// Foreground horizontal line (to y-axis)
1204+
container.insert('line', ':first-child')
1205+
.attr({
1206+
'x1': ya._counterSpan[0],
1207+
'x2': ya._counterSpan[1],
1208+
'y1': hLinePointY,
1209+
'y2': hLinePointY,
1210+
'stroke-width': yThickness,
1211+
'stroke': yColor,
1212+
'stroke-dasharray': Drawing.dashStyle(yDash, yThickness)
1213+
})
1214+
.classed('crossline', true)
1215+
.classed('crisp', true);
1216+
}
1217+
}
1218+
10921219
function createSpikelines(hoverData, opts) {
10931220
var hovermode = opts.hovermode;
10941221
var container = opts.container;

src/components/fx/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ function loneUnhover(containerOrSelection) {
5959

6060
selection.selectAll('g.hovertext').remove();
6161
selection.selectAll('.spikeline').remove();
62+
selection.selectAll('.crossline').remove();
6263
}
6364

6465
// helpers for traces that use Fx.loneHover

src/plot_api/plot_api.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ Plotly.plot = function(gd, data, layout, config) {
166166
// save initial show spikes once per graph
167167
if(graphWasEmpty) Plotly.Axes.saveShowSpikeInitial(gd);
168168

169+
// save initial show crosslines once per graph
170+
if(graphWasEmpty) Plotly.Axes.saveShowCrosslineInitial(gd);
171+
169172
// prepare the data and find the autorange
170173

171174
// generate calcdata, if we need to

src/plots/cartesian/axes.js

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,30 @@ axes.saveShowSpikeInitial = function(gd, overwrite) {
423423
return hasOneAxisChanged;
424424
};
425425

426+
// save a copy of the initial crossline visibility
427+
axes.saveShowCrosslineInitial = function(gd, overwrite) {
428+
var axList = axes.list(gd, '', true),
429+
hasOneAxisChanged = false;
430+
431+
for(var i = 0; i < axList.length; i++) {
432+
var ax = axList[i];
433+
434+
var isNew = (ax._showCrosslineInitial === undefined);
435+
var hasChanged = (
436+
isNew || !(
437+
ax.showcrossline === ax._showcrossline
438+
)
439+
);
440+
441+
if((isNew) || (overwrite && hasChanged)) {
442+
ax._showCrosslineInitial = ax.showcrossline;
443+
hasOneAxisChanged = true;
444+
}
445+
446+
}
447+
return hasOneAxisChanged;
448+
};
449+
426450
// axes.expand: if autoranging, include new data in the outer limits
427451
// for this axis
428452
// data is an array of numbers (ie already run through ax.d2c)
@@ -2048,7 +2072,7 @@ axes.doTicks = function(gd, axid, skipTitle) {
20482072
top: pos,
20492073
bottom: pos,
20502074
left: ax._offset,
2051-
rigth: ax._offset + ax._length,
2075+
right: ax._offset + ax._length,
20522076
width: ax._length,
20532077
height: 0
20542078
};

src/plots/cartesian/layout_attributes.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,28 @@ module.exports = {
387387
'plotted on'
388388
].join(' ')
389389
},
390+
showcrossline: {
391+
valType: 'boolean',
392+
dflt: false,
393+
role: 'style',
394+
editType: 'none',
395+
description: 'Determines whether or not crossline are drawn for this axis.'
396+
},
397+
crosslinecolor: {
398+
valType: 'color',
399+
dflt: null,
400+
role: 'style',
401+
editType: 'none',
402+
description: 'Sets the crossline color. If undefined, will use the contrast to background color'
403+
},
404+
crosslinethickness: {
405+
valType: 'number',
406+
dflt: 2,
407+
role: 'style',
408+
editType: 'none',
409+
description: 'Sets the width (in px) of the zero line.'
410+
},
411+
crosslinedash: extendFlat({}, dash, {dflt: 'solid', editType: 'none'}),
390412
tickfont: fontAttrs({
391413
editType: 'ticks',
392414
description: 'Sets the tick font.'

src/plots/cartesian/layout_defaults.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
139139

140140
handleAxisDefaults(axLayoutIn, axLayoutOut, coerce, defaultOptions, layoutOut);
141141

142+
var showCrossline = coerce('showcrossline');
143+
if(showCrossline) {
144+
coerce('crosslinecolor');
145+
coerce('crosslinethickness');
146+
coerce('crosslinedash');
147+
}
148+
142149
var showSpikes = coerce('showspikes');
143150
if(showSpikes) {
144151
coerce('spikecolor');

0 commit comments

Comments
 (0)