Another one about spikes, as you can see in the gif the spikes disappear if there are no traces and I would like them to remain visible.
In order to have the spikes always visible, I had to resort to modifying the Plotly.js file again. Other options were that instead of making the graph invisible, I could change its opacity or its line width to 0 or have a second graph with these properties, but neither of the two options seemed good or optimal to me, so while reviewing the code I found the createSpikeline function and this is how it looks after modifying it.
function createSpikelines(gd, closestPoints, opts) {
var container = opts.container;
var fullLayout = opts.fullLayout;
var gs = fullLayout._size;
var evt = opts.event;
var showY = !!closestPoints.hLinePoint;
var showX = !!closestPoints.vLinePoint;
var xa, ya;
var subplot = evt.target.attributes["data-subplot"].value
// Remove old spikeline items
container.selectAll('.spikeline').remove();
// if (!(showX || showY)) return;
var contrastColor = Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor);
// Horizontal line (to y-axis)
if (showY) {
var hLinePoint = closestPoints.hLinePoint;
var hLinePointX, hLinePointY;
xa = hLinePoint && hLinePoint.xa;
ya = hLinePoint && hLinePoint.ya;
var ySnap = ya.spikesnap;
if (ySnap === 'cursor') {
hLinePointX = evt.pointerX;
hLinePointY = evt.pointerY;
} else {
hLinePointX = xa._offset + hLinePoint.x;
hLinePointY = ya._offset + hLinePoint.y;
}
var dfltHLineColor = tinycolor.readability(hLinePoint.color, contrastColor) < 1.5 ? Color.contrast(contrastColor) : hLinePoint.color;
var yMode = ya.spikemode;
var yThickness = ya.spikethickness;
var yColor = ya.spikecolor || dfltHLineColor;
var xEdge = Axes.getPxPosition(gd, ya);
var xBase, xEndSpike;
if (yMode.indexOf('toaxis') !== -1 || yMode.indexOf('across') !== -1) {
if (yMode.indexOf('toaxis') !== -1) {
xBase = xEdge;
xEndSpike = hLinePointX;
}
if (yMode.indexOf('across') !== -1) {
var xAcross0 = ya._counterDomainMin;
var xAcross1 = ya._counterDomainMax;
if (ya.anchor === 'free') {
xAcross0 = Math.min(xAcross0, ya.position);
xAcross1 = Math.max(xAcross1, ya.position);
}
xBase = gs.l + xAcross0 * gs.w;
xEndSpike = gs.l + xAcross1 * gs.w;
}
// Foreground horizontal line (to y-axis)
container.insert('line', ':first-child').attr({
x1: xBase,
x2: xEndSpike,
y1: hLinePointY,
y2: hLinePointY,
'stroke-width': yThickness,
stroke: yColor,
'stroke-dasharray': Drawing.dashStyle(ya.spikedash, yThickness)
}).classed('spikeline', true).classed('crisp', true);
// Background horizontal Line (to y-axis)
// container.insert('line', ':first-child').attr({
// x1: xBase,
// x2: xEndSpike,
// y1: hLinePointY,
// y2: hLinePointY,
// 'stroke-width': yThickness + 2,
// stroke: contrastColor
// }).classed('spikeline', true).classed('crisp', true);
}
// Y axis marker
if (yMode.indexOf('marker') !== -1) {
container.insert('circle', ':first-child').attr({
cx: xEdge + (ya.side !== 'right' ? yThickness : -yThickness),
cy: hLinePointY,
r: yThickness,
fill: yColor
}).classed('spikeline', true);
}
}
else {
ya = fullLayout._plots[subplot].yaxis;
var xEdge = Axes.getPxPosition(gd, ya);
var xBase, xEndSpike;
if (ya.spikemode.indexOf('toaxis') !== -1 || ya.spikemode.indexOf('across') !== -1) {
if (ya.spikemode.indexOf('toaxis') !== -1) {
xBase = xEdge;
xEndSpike = evt.pointerX;
}
if (ya.spikemode.indexOf('across') !== -1) {
var xAcross0 = ya._counterDomainMin;
var xAcross1 = ya._counterDomainMax;
if (ya.anchor === 'free') {
xAcross0 = Math.min(xAcross0, ya.position);
xAcross1 = Math.max(xAcross1, ya.position);
}
xBase = gs.l + xAcross0 * gs.w;
xEndSpike = gs.l + xAcross1 * gs.w;
}
// Foreground horizontal line (to y-axis)
container.insert('line', ':first-child').attr({
x1: xBase,
x2: xEndSpike,
y1: evt.pointerY,
y2: evt.pointerY,
'stroke-width': ya.spikethickness,
stroke: ya.spikecolor,
'stroke-dasharray': Drawing.dashStyle(ya.spikedash, ya.spikethickness)
}).classed('spikeline', true).classed('crisp', true);
}
if (ya.spikemode.indexOf('marker') !== -1) {
container.insert('circle', ':first-child').attr({
cx: xEdge + (ya.side !== 'right' ? ya.spikethickness : -ya.spikethickness),
cy: evt.pointerY,
r: ya.spikethickness,
fill: ya.spikecolor
}).classed('spikeline', true);
}
}
if (showX) {
var vLinePoint = closestPoints.vLinePoint;
var vLinePointX, vLinePointY;
xa = vLinePoint && vLinePoint.xa;
ya = vLinePoint && vLinePoint.ya;
var xSnap = xa.spikesnap;
if (xSnap === 'cursor') {
vLinePointX = evt.pointerX;
vLinePointY = evt.pointerY;
} else {
vLinePointX = xa._offset + vLinePoint.x;
vLinePointY = ya._offset + vLinePoint.y;
}
var dfltVLineColor = tinycolor.readability(vLinePoint.color, contrastColor) < 1.5 ? Color.contrast(contrastColor) : vLinePoint.color;
var xMode = xa.spikemode;
var xThickness = xa.spikethickness;
var xColor = xa.spikecolor || dfltVLineColor;
var yEdge = Axes.getPxPosition(gd, xa);
var yBase, yEndSpike;
if (xMode.indexOf('toaxis') !== -1 || xMode.indexOf('across') !== -1) {
if (xMode.indexOf('toaxis') !== -1) {
yBase = yEdge;
yEndSpike = vLinePointY;
}
if (xMode.indexOf('across') !== -1) {
var yAcross0 = xa._counterDomainMin;
var yAcross1 = xa._counterDomainMax;
if (xa.anchor === 'free') {
yAcross0 = Math.min(yAcross0, xa.position);
yAcross1 = Math.max(yAcross1, xa.position);
}
yBase = gs.t + (1 - yAcross1) * gs.h;
yEndSpike = gs.t + (1 - yAcross0) * gs.h;
}
// Foreground vertical line (to x-axis)
container.insert('line', ':first-child').attr({
x1: vLinePointX,
x2: vLinePointX,
y1: yBase,
y2: yEndSpike,
'stroke-width': xThickness,
stroke: xColor,
'stroke-dasharray': Drawing.dashStyle(xa.spikedash, xThickness)
}).classed('spikeline', true).classed('crisp', true);
// Background vertical line (to x-axis)
// container.insert('line', ':first-child').attr({
// x1: vLinePointX,
// x2: vLinePointX,
// y1: yBase,
// y2: yEndSpike,
// 'stroke-width': xThickness + 2,
// stroke: contrastColor
// }).classed('spikeline', true).classed('crisp', true);
}
// X axis marker
if (xMode.indexOf('marker') !== -1) {
container.insert('circle', ':first-child').attr({
cx: vLinePointX,
cy: yEdge - (xa.side !== 'top' ? xThickness : -xThickness),
r: xThickness,
fill: xColor
}).classed('spikeline', true);
}
}
else {
xa = fullLayout._plots[subplot].xaxis
var yEdge = Axes.getPxPosition(gd, xa);
var yBase, yEndSpike;
if (xa.spikemode.indexOf('toaxis') !== -1 || xa.spikemode.indexOf('across') !== -1) {
if (xa.spikemode.indexOf('toaxis') !== -1) {
yBase = yEdge;
yEndSpike = evt.pointerY;
}
if (xa.spikemode.indexOf('across') !== -1) {
var yAcross0 = xa._counterDomainMin;
var yAcross1 = xa._counterDomainMax;
if (xa.anchor === 'free') {
yAcross0 = Math.min(yAcross0, xa.position);
yAcross1 = Math.max(yAcross1, xa.position);
}
yBase = gs.t + (1 - yAcross1) * gs.h;
yEndSpike = gs.t + (1 - yAcross0) * gs.h;
}
// Foreground vertical line (to x-axis)
container.insert('line', ':first-child').attr({
x1: evt.pointerX,
x2: evt.pointerX,
y1: yBase,
y2: yEndSpike,
'stroke-width': xa.spikethickness,
stroke: xa.spikecolor,
'stroke-dasharray': Drawing.dashStyle(xa.spikedash, xa.spikethickness)
}).classed('spikeline', true).classed('crisp', true);
}
// X axis marker
if (xa.spikemode.indexOf('marker') !== -1) {
container.insert('circle', ':first-child').attr({
cx: evt.pointerX,
cy: yEdge - (xa.side !== 'top' ? xa.spikethickness : -xa.spikethickness),
r: xa.spikethickness,
fill: xa.spikecolor
}).classed('spikeline', true);
}
}
}
In addition to changing the condition that is in the first call to the function
if (hasCartesian && (spikePoints.hLinePoint !== null || spikePoints.vLinePoint !== null))
for
if (hasCartesian)
And this is the result