This is a very direct follow-up on this question.
Using matplotlib, I'd like to be able to place a sort of "highlighting bar" over a range of data markers that I know will all be in a straight horizontal line.
This bar/rectangle should be slightly taller than the markers and contain them, something like this for the three markers below:
In order to be a sensible highlighting bar, it needs to have the following two traits:
- If the plot is panned, the bar moves with the markers (so it always covers them).
- If the plot is zoomed, the bar's display height doesn't change (so it always is slightly taller than the markers).
If it is helpful to know, these markers have no meaningful y values (they are plotted all at y=-1), only meaningful x values. Therefore, the height of the bar is meaningless in data coordinates; it merely needs to be always just tall enough to enclose the markers.
Great question! This was a good challenge and requires a combination of things to achieve.
Firstly, we need to invent a transform which will return the device coordinates of a pre-defined value plus an offset based on the given point. For instance, if we know we want the bar to be at x_pt, y_pt, then the transform should represent (in pseudo code):
def transform(x, y):return x_pt_in_device + x, y_pt_in_device + y
Once we have done this, we could use this transform to draw a box of 20 pixels around a fixed data point. However, you only want to draw a box of fixed pixel height in the y direction, but in the x direction you would like standard data scaling.
Therefore, we need to create a blended transform which can transform the x and y coordinates independently. The whole code to do what you are asking:
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import matplotlib.path as mpath
import matplotlib.transforms as mtransimport numpy as npclass FixedPointOffsetTransform(mtrans.Transform):"""Always returns the same transformed point plusthe given point in device coordinates as an offset."""def __init__(self, trans, fixed_point):mtrans.Transform.__init__(self)self.input_dims = self.output_dims = 2self.trans = transself.fixed_point = np.array(fixed_point).reshape(1, 2)def transform(self, values):fp = self.trans.transform(self.fixed_point)values = np.array(values)if values.ndim == 1:return fp.flatten() + valueselse:return fp + valuesplt.scatter([3.1, 3.2, 3.4, 5], [2, 2, 2, 6])ax = plt.gca()
fixed_pt_trans = FixedPointOffsetTransform(ax.transData, (0, 2))xdata_yfixed = mtrans.blended_transform_factory(ax.transData, fixed_pt_trans)x = [3.075, 3.425] # x range of box (in data coords)
height = 20 # of box in device coords (pixels)
path = mpath.Path([[x[0], -height], [x[1], -height],[x[1], height], [x[0], height],[x[0], -height]])
patch = mpatches.PathPatch(path, transform=xdata_yfixed,facecolor='red', edgecolor='black',alpha=0.4, zorder=0)
ax.add_patch(patch)plt.show()