How can I move the spike to the nearest data of an OHLC candle?

I bring a new request (challenge for me) related to the spikes, this time what I am looking for is a behavior of the spikes similar to one of the options that TV brings which is the Magneto mode that when activated makes the spikes move to the closest data in the graph being at a certain distance, the closest thing I have achieved in Plotly is configuring the hovermode to “x” and the spikedistace to -1 in the layout and the spikesnap in xaxis and yaxis to “data”, the vertical spike (xaxis) works well but not the horizontal one since the spike always moves to the data regardless of the distance and what I am looking for is that this only happens when being at a distance of 50 pixels, this could be solved by calculating the distance of the pointer with respect to the data and changing the value of the spikesnap of the yaxis between “data” and “cursor” when the mouse pointer is inside or outside the distance of 50 pixels but things get more complicated since TV’s Magneto mode detects What point is the pointer closest to an OHLC candle? That is, if the pointer is closer to the OPEN value, the spike moves to that coordinate. If the pointer is closer to LOW, the spike moves to that value, but Plotly only detects the CLOSE. In addition, constantly making calls to Plotly.update to change the spikesnap value does not seem optimal to me. In the meantime, it is time to rework the code in Plotly.js, hoping that in future versions they can add this feature.

1 Like

I leave you the changes to make in the Plotly.js file to have this functionality in your ohlc or candlestick graphs, to start add the value “magnet” to the spikesnap property, we look for the following structure and modify it by adding the value “magnet”

  spikesnap: {
    valType: 'enumerated',
    values: ['data', 'cursor', 'hovered data', 'magnet'],
    dflt: 'hovered data',
    editType: 'none'
  }

We create a new property in the layout that will be used to establish the distance in pixels so that the spikes are attracted to the data, which we call magnetdistance, which we create and register in the following way, we look for the spikedistance value until we find the following structure

  spikedistance: {
    valType: 'integer',
    min: -1,
    dflt: -1,
    editType: 'none'
  }

In that area we will find the declaration of the layout properties, at the end of which we add the following

  magnetdistance: {
    valType: 'integer',
    min: 0,
    dflt: 0,
    editType: 'none'
  }

Now we need to make this property appear in the fullLayout, so we look for the following line of code

coerce('spikedistance');

and below we add the following

coerce('magnetdistance');

Now that we have this property we need to be able to pass it into the parameters of the function that calculates the closest point, so we look for the word “function _hover” and go down until we find the following code

  var hoverdistance = fullLayout.hoverdistance;
  if (hoverdistance === -1) hoverdistance = Infinity;
  var spikedistance = fullLayout.spikedistance;
  if (spikedistance === -1) spikedistance = Infinity;

Below we add the following

var magnetdistance = fullLayout.magnetdistance;

Now we look for the following lines inside the findHoverPoints function

        maxHoverDistance: hoverdistance,
        maxSpikeDistance: spikedistance,

And we add the following below

maxMagnetDistance: magnetdistance,

The following is an extra, you can add it or not, when we have several traces on the same axes the function that searches for the closest data does it on all the traces that are visible on the same axes, in my case I only need it to search in the main candle chart, any other indicator chart that is added later I am not interested in the spikes moving (attracting) towards their data, the problem is that there is no way to disable it, so I had to create another property for the traces which I call spikeshover, also I feel that the function that does the data search work does not work well or at least that is what it seemed to me.

First we look for where all the traces properties are declared, then we look for the word hoverinfo until we find the following code

  hoverinfo: {
    valType: 'flaglist',
    flags: ['x', 'y', 'z', 'text', 'name'],
    extras: ['all', 'none', 'skip'],
    arrayOk: true,
    dflt: 'all',
    editType: 'none'
  },

There all the properties of the traces are declared, at the end we add the following

  spikeshover: {
    valType: 'boolean',
    dflt: true,
    editType: 'none'
  },

Now we need this property to appear in the fullData of the strokes, we look for the following line of code

coerce('uirevision', layout.uirevision);

And we add the following line below

coerce('spikeshover', layout.spikeshover);

In the findHoverPoints function we change the following condition to skip the strokes that have the spikeshover property to false and not search them for data

if (trace.visible !== true || trace._length === 0 || !trace.spikeshover) continue;

After all the above we are ready to add the code that does the magic and have that attraction effect as if it were a magnet, now to finish we look for the _getClosestPoint function and add at the end of it and before the return the following code

  if(["ohlc", "candlestick"].includes(type)) {
    if(ya.spikesnap === 'magnet') {
      if(pointData.distance !== Infinity) {
        var yPos = ya.c2p(yval, true);
        var ohlcPos = [
          ya.c2p(trace.open[pointData.index]), 
          ya.c2p(trace.high[pointData.index]),
          ya.c2p(trace.low[pointData.index]),
          ya.c2p(trace.close[pointData.index])
        ]
        var ohlcDist = ohlcPos.map(p => Math.abs(p - yPos))
        var minDist = Math.min(...ohlcDist)

        if(minDist < pointData.maxMagnetDistance)
          pointData.ySpike = ohlcPos[ohlcDist.findIndex(p => p === minDist)]
        else
          pointData.ySpike = yPos
      }
    }
  }

And this is the result

spikes