matplotlib – ¿Expandir la línea con el ancho especificado en la unidad de datos?

Mi pregunta es un poco similar a esta pregunta que dibuja una línea con el ancho dado en las coordenadas de datos . Lo que hace que mi pregunta sea un poco más desafiante es que, a diferencia de la pregunta vinculada, el segmento que deseo expandir tiene una orientación aleatoria.

Digamos si el segmento de línea va de (0, 10) a (10, 10) , y deseo expandirlo a un ancho de 6 . Entonces es simplemente

 x = [0, 10] y = [10, 10] ax.fill_between(x, y - 3, y + 3) 

Sin embargo, mi segmento de línea es de orientación aleatoria . Es decir, no es necesariamente a lo largo del eje x o del eje y. Tiene una cierta pendiente .

Un segmento de línea s se define como una lista de sus puntos de inicio y final: [(x1, y1), (x2, y2)] .

Ahora deseo expandir el segmento de línea a un ancho determinado w . Se espera que la solución funcione para un segmento de línea en cualquier orientación. ¿Como hacer esto?

ACTUALIZACIÓN: plt.plot(x, y, linewidth=6.0) no puede hacer el truco, porque quiero que mi ancho esté en la misma unidad que mis datos.

Solo para agregar a la respuesta anterior (todavía no puedo comentar), aquí hay una función que automatiza este proceso sin la necesidad de ejes iguales o el valor heurístico de 0.8 para las tags. Los límites de datos y el tamaño del eje deben fijarse y no cambiarse después de llamar a esta función.

 def linewidth_from_data_units(linewidth, axis, reference='y'): """ Convert a linewidth in data units to linewidth in points. Parameters ---------- linewidth: float Linewidth in data units of the respective reference-axis axis: matplotlib axis The axis which is used to extract the relevant transformation data (data limits and size must not change afterwards) reference: string The axis that is taken as a reference for the data width. Possible values: 'x' and 'y'. Defaults to 'y'. Returns ------- linewidth: float Linewidth in points """ fig = axis.get_figure() if reference == 'x': length = fig.bbox_inches.width * axis.get_position().width value_range = np.diff(axis.get_xlim()) elif reference == 'y': length = fig.bbox_inches.height * axis.get_position().height value_range = np.diff(axis.get_ylim()) # Convert length to points length *= 72 # Scale linewidth to value range return linewidth * (length / value_range) 

Explicación:

  • Configure la figura con una altura conocida y haga que la escala de los dos ejes sea igual (de lo contrario, la idea de “coordenadas de datos” no se aplica). Asegúrese de que las proporciones de la figura coincidan con las proporciones esperadas de los ejes x e y .

  • Calcule la altura de toda la figura point_hei (incluidos los márgenes) en unidades de puntos multiplicando pulgadas por 72

  • Asigne manualmente el rango y yrange del eje yrange (puede hacer esto trazando primero una serie “ficticia” y luego consultando el eje del trazado para obtener los límites y superiores e inferiores).

  • Proporcione el ancho de la línea que desee en unidades de datos linewid

  • Calcule lo que serían esas unidades en puntos pointlinewid mientras ajusta los márgenes. En una gráfica de un solo cuadro, la gráfica es el 80% de la altura de la imagen completa.

  • Dibuje las líneas, utilizando un estilo de tapa que no rellene los extremos de la línea (tiene un gran efecto en estos tamaños de línea grandes)

Voilà? (Nota: esto debe generar la imagen adecuada en el archivo guardado, pero no hay garantías si cambia el tamaño de una ventana de trazado).

 import matplotlib.pyplot as plt rez=600 wid=8.0 # Must be proportional to x and y limits below hei=6.0 fig = plt.figure(1, figsize=(wid, hei)) sp = fig.add_subplot(111) # # plt.figure.tight_layout() # fig.set_autoscaley_on(False) sp.set_xlim([0,4000]) sp.set_ylim([0,3000]) plt.axes().set_aspect('equal') # line is in points: 72 points per inch point_hei=hei*72 xval=[100,1300,2200,3000,3900] yval=[10,200,2500,1750,1750] x1,x2,y1,y2 = plt.axis() yrange = y2 - y1 # print yrange linewid = 500 # in data units # For the calculation below, you have to adjust width by 0.8 # because the top and bottom 10% of the figure are labels & axis pointlinewid = (linewid * (point_hei/yrange)) * 0.8 # corresponding width in pts plt.plot(xval,yval,linewidth = pointlinewid,color="blue",solid_capstyle="butt") # just for fun, plot the half-width line on top of it plt.plot(xval,yval,linewidth = pointlinewid/2,color="red",solid_capstyle="butt") plt.savefig('mymatplot2.png',dpi=rez) 

introduzca la descripción de la imagen aquí

El siguiente código es un ejemplo genérico sobre cómo hacer un trazado de línea en matplotlib usando coordenadas de datos como ancho de línea. Hay dos soluciones; uno usando devoluciones de llamada, uno usando subclasificación de Line2D.

Utilizando callbacks.

Se implementa como una clase data_linewidth_plot que se puede llamar con una firma muy cerca del comando normal plt.plot ,

 l = data_linewidth_plot(x, y, ax=ax, label='some line', linewidth=1, alpha=0.4) 

donde ax es los ejes para trazar. El argumento ax se puede omitir, cuando solo existe un argumento secundario en la figura. El argumento de linewidth se interpreta en (y-) unidades de datos.

Otras características:

  1. Es independiente de las ubicaciones de la subplot, los márgenes o el tamaño de la figura.
  2. Si la relación de aspecto es desigual, utiliza coordenadas de datos y como el ancho de línea.
  3. También se encarga de que el controlador de leyenda esté correctamente configurado (es posible que desee tener una línea enorme en la ttwig, pero ciertamente no en la leyenda).
  4. Es compatible con los cambios en el tamaño de la figura, el zoom o los eventos de panorámica , ya que se encarga de redimensionar el ancho de línea en dichos eventos.

Aquí está el código completo.

 import matplotlib.pyplot as plt class data_linewidth_plot(): def __init__(self, x, y, **kwargs): self.ax = kwargs.pop("ax", plt.gca()) self.fig = self.ax.get_figure() self.lw_data = kwargs.pop("linewidth", 1) self.lw = 1 self.fig.canvas.draw() self.ppd = 72./self.fig.dpi self.trans = self.ax.transData.transform self.linehandle, = self.ax.plot([],[],**kwargs) if "label" in kwargs: kwargs.pop("label") self.line, = self.ax.plot(x, y, **kwargs) self.line.set_color(self.linehandle.get_color()) self._resize() self.cid = self.fig.canvas.mpl_connect('draw_event', self._resize) def _resize(self, event=None): lw = ((self.trans((1, self.lw_data))-self.trans((0, 0)))*self.ppd)[1] if lw != self.lw: self.line.set_linewidth(lw) self.lw = lw self._redraw_later() def _redraw_later(self): self.timer = self.fig.canvas.new_timer(interval=10) self.timer.single_shot = True self.timer.add_callback(lambda : self.fig.canvas.draw_idle()) self.timer.start() fig1, ax1 = plt.subplots() #ax.set_aspect('equal') #<-not necessary ax1.set_ylim(0,3) x = [0,1,2,3] y = [1,1,2,2] # plot a line, with 'linewidth' in (y-)data coordinates. l = data_linewidth_plot(x, y, ax=ax1, label='some 1 data unit wide line', linewidth=1, alpha=0.4) plt.legend() # <- legend possible plt.show() 

introduzca la descripción de la imagen aquí

(Actualicé el código para usar un temporizador para volver a dibujar el canvas, debido a este problema )

Subclase Line2D

La solución anterior tiene algunos inconvenientes. Requiere un temporizador y devoluciones de llamada para actualizarse al cambiar los límites de los ejes o el tamaño de la figura. La siguiente es una solución sin tales necesidades. Utilizará una propiedad dinámica para calcular siempre el ancho de línea en puntos desde el ancho de línea deseado en coordenadas de datos sobre la marcha. Es mucho más corto que el anterior. Un inconveniente aquí es que una leyenda debe crearse manualmente a través de un proxyartist.

 import matplotlib.pyplot as plt from matplotlib.lines import Line2D class LineDataUnits(Line2D): def __init__(self, *args, **kwargs): _lw_data = kwargs.pop("linewidth", 1) super().__init__(*args, **kwargs) self._lw_data = _lw_data def _get_lw(self): if self.axes is not None: ppd = 72./self.axes.figure.dpi trans = self.axes.transData.transform return ((trans((1, self._lw_data))-trans((0, 0)))*ppd)[1] else: return 1 def _set_lw(self, lw): self._lw_data = lw _linewidth = property(_get_lw, _set_lw) fig, ax = plt.subplots() #ax.set_aspect('equal') # <-not necessary, if not given, y data is assumed ax.set_xlim(0,3) ax.set_ylim(0,3) x = [0,1,2,3] y = [1,1,2,2] line = LineDataUnits(x, y, linewidth=1, alpha=0.4) ax.add_line(line) ax.legend([Line2D([],[], linewidth=3, alpha=0.4)], ['some 1 data unit wide line']) # <- legend possible via proxy artist plt.show()