Matplotlib: Cursor snap to plotted data with datetime axis

﹥>﹥吖頭↗ 提交于 2019-12-04 12:23:40

问题


I have a plot of 3 data sets that have datetime objetcs on the x axis. I want to have a cursor that snaps to the data and shows the precise x and y value.

I already have a "snap to cursor", but that only works for scalar x axes. Can anyone help me to modify the snap to cursor so that it works for datetime x axes as well?

Here are my data plots:

import numpy as np
import matplotlib.pyplot as plot
import matplotlib.ticker as mticker
import matplotlib.dates as dates
import datetime
import Helpers

fig = plot.figure(1)
DAU = (  2,  20,  25,  60, 190, 210,  18, 196, 212)
WAU = ( 50, 160, 412, 403, 308, 379, 345, 299, 258)
MAU = (760, 620, 487, 751, 612, 601, 546, 409, 457)

firstDay = datetime.datetime(2012,1,15)

#create an array with len(DAU) entries from given starting day
dayArray = [firstDay + datetime.timedelta(days = i) for i in xrange(len(DAU))]

line1 = plot.plot(dayArray, DAU, 'o-', color = '#336699')
line2 = plot.plot(dayArray, WAU, 'o-', color = '#993333')
line3 = plot.plot(dayArray, MAU, 'o-', color = '#89a54e')

ax = plot.subplot(111)
dateLocator   = mticker.MultipleLocator(2)
dateFormatter = dates.DateFormatter('%d.%m.%Y')
ax.xaxis.set_major_locator(dateLocator)
ax.xaxis.set_major_formatter(dateFormatter)
fig.autofmt_xdate(rotation = 90, ha = 'center')

yMax = max(np.max(DAU), np.max(WAU), np.max(MAU))
yLimit = 100 - (yMax % 100) + yMax
plot.yticks(np.arange(0, yLimit + 1, 100))

plot.title('Active users', weight = 'bold')
plot.grid(True, axis = 'both')
plot.subplots_adjust(bottom = 0.2)
plot.subplots_adjust(right = 0.82)

legend = plot.legend((line1[0], line2[0], line3[0]),
                 ('DAU',
                 'WAU',
                 'MAU'),
                 'upper left',
                 bbox_to_anchor = [1, 1],
                 shadow = True)

frame = legend.get_frame()
frame.set_facecolor('0.80')
for t in legend.get_texts():
    t.set_fontsize('small')

#THIS DOES NOT WORK
cursor = Helpers.SnaptoCursor(ax, dayArray, DAU, 'euro daily')
plot.connect('motion_notify_event', cursor.mouse_move)

plot.show()

And this is my module "Helper" that contains the "SnaptoCursor" class: (I got the basic SnaptoCursor class from somewhere else and modified it a little bit)

from __future__ import print_function
import numpy as np
import matplotlib.pyplot as plot

def minsec(sec, unused):
    """
    Returns a string of the input seconds formatted as mm'ss''.
    """
    minutes = sec // 60
    sec = sec - minutes * 60
    return '{0:02d}\'{1:02d}\'\''.format(int(minutes), int(sec))

class SnaptoCursor():
    """
    A cursor with crosshair snaps to the nearest x point.
    For simplicity, I'm assuming x is sorted.
    """
    def __init__(self, ax, x, y, formatting, z = None):
        """
        ax: plot axis
        x: plot spacing
        y: plot data
        formatting: string flag for desired formatting
        z: optional second plot data
        """
        self.ax = ax
        self.lx = ax.axhline(color = 'k')  #the horiz line
        self.ly = ax.axvline(color = 'k')  #the vert line
        self.x = x
        self.y = y
        self.z = z
        # text location in axes coords
        self.txt = ax.text(0.6, 0.9, '', transform = ax.transAxes)
        self.formatting = formatting

    def format(self, x, y):
        if self.formatting == 'minsec':
            return 'x={0:d}, y='.format(x) + minsec(y, 0)

        elif self.formatting == 'daily euro':
            return u'day {0:d}: {1:.2f}€'.format(x, y)

    def mouse_move(self, event):
        if not event.inaxes: return

        mouseX, mouseY = event.xdata, event.ydata

        #searchsorted: returns an index or indices that suggest where x should be inserted
        #so that the order of the list self.x would be preserved
        indx = np.searchsorted(self.x, [mouseX])[0]

        mouseX = self.x[indx]
        #if z wasn't defined
        if self.z == None:
            mouseY = self.y[indx]
        #if z was defined: compare the distance between mouse and the two plots y and z
        #and use the nearest one
        elif abs(mouseY - self.y[indx]) < abs(mouseY - self.z[indx]):
            mouseY = self.y[indx]
        else:
            mouseY = self.z[indx]

        #update the line positions
        self.lx.set_ydata(mouseY)
        self.ly.set_xdata(mouseX)

        self.txt.set_text(self.format(mouseX, mouseY))
        plot.draw()

Of course this does not work since I am calling the SnaptoCursor with the datetime array "dayArray", which is supposed to be compared to the mouse coordinates later on. And these data types are not comparable.


回答1:


I got it!!!

The problems where these two lines in the init method of the SnaptoCursor class:

self.lx = ax.axhline(color = 'k')  #the horiz line
self.ly = ax.axvline(color = 'k')  #the vert line

They were somehow messing up the datetime x axis (that has ordinals up to 730,000 e.g.), so you just have to initialize the lines' coordinates:

self.lx = ax.axhline(y = min(y), color = 'k')  #the horiz line
self.ly = ax.axvline(x = min(x), color = 'k')  #the vert line

Then it works just fine!

I'll be posting my complete SnaptoCursor class now that I have modified so it accepts individual formatting strings, and it can take up to 3 input data plots - that get snapped to according to your mouse position.

def percent(x, unused):
    """
    Returns a string of the float number x formatted as %.
    """
    return '{0:1.2f}%'.format(x * 100)

def minsec(sec, unused):
    """
    Returns a string of the input seconds formatted as mm'ss''.
    """
    minutes = sec // 60
    sec = sec - minutes * 60
    return '{0:02d}\'{1:02d}\'\''.format(int(minutes), int(sec))

class SnaptoCursor():
    """
    A cursor with crosshair snaps to the nearest x point.
    For simplicity, I'm assuming x is sorted.
    """
    def __init__(self, ax, x, y, formatting, y2 = None, y3 = None):
        """
        ax: plot axis
        x: plot spacing
        y: plot data
        formatting: string flag for desired formatting
        y2: optional second plot data
        y3: optional third plot data
        """
        self.ax = ax
        self.lx = ax.axhline(y = min(y), color = 'k')  #the horiz line
        self.ly = ax.axvline(x = min(x), color = 'k')  #the vert line
        self.x = x
        self.y = y
        self.y2 = y2
        self.y3 = y3
        # text location in axes coords
        self.txt = ax.text(0.6, 0.9, '', transform = ax.transAxes)
        self.formatting = formatting

    def format(self, x, y):
        if self.formatting == 'minsec':
            return 'x={0:d}, y='.format(x) + minsec(y, 0)

        if self.formatting == 'decimal':
            return 'x={0:d}, y={1:d}'.format(x, int(y))

        elif self.formatting == 'date decimal':
            return 'x={0:%d.%m.%Y}, y={1:d}'.format(x, int(y))

        elif self.formatting == 'decimal percent':
            return 'x={0:d}, y={1:d}%'.format(x, int(y * 100))

        elif self.formatting == 'float':
            return 'x={0:d}, y={1:.2f}'.format(x, y)

        elif self.formatting == 'float percent':
            return 'x={0:d}, y='.format(x) + percent(y, 0)

        elif self.formatting == 'daily euro':
            return u'day {0:d}: {1:.2f}€'.format(x, y)

    def mouse_move(self, event):
        if not event.inaxes:
            return

        mouseX, mouseY = event.xdata, event.ydata
        if type(self.x[0]) == datetime.datetime:
            mouseX = dates.num2date(int(mouseX)).replace(tzinfo = None)

        #searchsorted: returns an index or indices that suggest where mouseX should be inserted
        #so that the order of the list self.x would be preserved
        indx = np.searchsorted(self.x, [mouseX])[0]

        #if indx is out of bounds
        if indx >= len(self.x):
            indx = len(self.x) - 1

        #if y2 wasn't defined
        if self.y2 == None:
            mouseY = self.y[indx]

        #if y2 was defined AND y3 wasn't defined
        elif self.y3 == None: 
            if abs(mouseY - self.y[indx]) < abs(mouseY - self.y2[indx]):
                mouseY = self.y[indx]
            else:
                mouseY = self.y2[indx]

        #if y2 AND y3 were defined
        elif abs(mouseY - self.y2[indx]) < abs(mouseY - self.y[indx]):
            if abs(mouseY - self.y2[indx]) < abs(mouseY - self.y3[indx]):
                mouseY = self.y2[indx]
            else:
                mouseY = self.y3[indx]
        #lastly, compare y with y3
        elif abs(mouseY - self.y[indx]) < abs(mouseY - self.y3[indx]):
            mouseY = self.y[indx]
        else:
            mouseY = self.y3[indx]

        #update the line positions
        self.lx.set_ydata(mouseY)
        self.ly.set_xdata(mouseX)

        self.txt.set_text(self.format(mouseX, mouseY))
        plot.draw()


来源:https://stackoverflow.com/questions/14338051/matplotlib-cursor-snap-to-plotted-data-with-datetime-axis

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!