在matplotlib中自定义绘图线型

5 投票
2 回答
3445 浏览
提问于 2025-04-17 13:30

我想用 matplotlib 来画一个图,图上有一些点附近有空白的线条,像这个例子一样:

graph.png
(来源: simplystatistics.org)

我知道有一个 set_dashes 的函数,但这个函数只能从起点开始设置周期性的虚线,不能控制虚线在终点的样子。

编辑:我找到了一种变通的方法,但结果出来的图只是一些普通的线条,并不是一个整体的对象。而且这个方法还用了另一个库 pandas,奇怪的是,结果并不是我想要的——我希望每段线的间隔是一样的,但它们的间隔似乎和线的长度有关。

import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd

def my_plot(X,Y):
  df = pd.DataFrame({
    'x': X,
    'y': Y,
  })
  roffset = 0.1
  df['x_diff'] = df['x'].diff()
  df['y_diff'] = df['y'].diff()

  df['length'] = np.sqrt(df['x_diff']**2 + df['y_diff']**2)
  aoffset = df['length'].mean()*roffset

  # this is to drop values with negative magnitude
  df['length_'] = df['length'][df['length']>2*aoffset]-2*aoffset 

  df['x_start'] = df['x']             -aoffset*(df['x_diff']/df['length'])
  df['x_end']   = df['x']-df['x_diff']+aoffset*(df['x_diff']/df['length'])
  df['y_start'] = df['y']             -aoffset*(df['y_diff']/df['length'])
  df['y_end']   = df['y']-df['y_diff']+aoffset*(df['y_diff']/df['length'])

  ax = plt.gca()
  d = {}
  idf = df.dropna().index
  for i in idf:
    line, = ax.plot(
      [df['x_start'][i], df['x_end'][i]],
      [df['y_start'][i], df['y_end'][i]],
      linestyle='-', **d)
    d['color'] = line.get_color()
  ax.plot(df['x'], df['y'], marker='o', linestyle='', **d)

fig = plt.figure(figsize=(8,6))
axes = plt.subplot(111)
X = np.linspace(0,2*np.pi, 8)
Y = np.sin(X)
my_plot(X,Y)
plt.show()

enter image description here

2 个回答

4

你可以考虑在你的标记周围加一个厚厚的白色边框吗?这不是一种自定义的线条样式,而是一种简单的方法,可以达到类似的效果:

y = np.random.randint(1,9,15)

plt.plot(y,'o-', color='black', ms=10, mew=5, mec='white')
plt.ylim(0,10)

这里插入图片描述

这里的关键是这些参数:

  • mec='white',标记的边缘颜色是白色
  • ms=10,标记的大小是10个点(这个比较大)
  • mew=5,标记的边缘宽度是5个点,所以实际上这些点的大小是10-5=5个点。
4

好的,我找到了一种还算满意的解决办法。虽然有点啰嗦,也有点小技巧,但它确实能工作!这个方法在每个点周围提供了一个固定的显示偏移,即使在进行缩放、平移等互动操作时,它也能保持这个偏移不变。

它的原理是为图表中的每条线创建一个自定义的 matplotlib.transforms.Transform 对象。虽然这个方法比较慢,但这种类型的图表通常不会用到成百上千个点,所以我觉得性能问题不是特别重要。

理想情况下,所有这些线段应该合并成一条“绘图线”,不过现在这样也还挺适合我的需求。

import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt

class MyTransform(mpl.transforms.Transform):
  input_dims = 2
  output_dims = 2
  def __init__(self, base_point, base_transform, offset, *kargs, **kwargs):
    self.base_point = base_point
    self.base_transform = base_transform
    self.offset = offset
    super(mpl.transforms.Transform, self).__init__(*kargs, **kwargs)
  def transform_non_affine(self, values):
    new_base_point = self.base_transform.transform(self.base_point)
    t = mpl.transforms.Affine2D().translate(-new_base_point[0], -new_base_point[1])
    values = t.transform(values)
    x = values[:, 0:1]
    y = values[:, 1:2]
    r = np.sqrt(x**2+y**2)
    new_r = r-self.offset
    new_r[new_r<0] = 0.0
    new_x = new_r/r*x
    new_y = new_r/r*y
    return t.inverted().transform(np.concatenate((new_x, new_y), axis=1))

def my_plot(X,Y):
  ax = plt.gca()
  line, = ax.plot(X, Y, marker='o', linestyle='')
  color = line.get_color()

  size = X.size
  for i in range(1,size):
    mid_x = (X[i]+X[i-1])/2
    mid_y = (Y[i]+Y[i-1])/2

    # this transform takes data coords and returns display coords
    t = ax.transData

    # this transform takes display coords and 
    # returns them shifted by `offset' towards `base_point'
    my_t = MyTransform(base_point=(mid_x, mid_y), base_transform=t, offset=10)

    # resulting combination of transforms
    t_end = t + my_t

    line, = ax.plot(
      [X[i-1], X[i]],
      [Y[i-1], Y[i]],
      linestyle='-', color=color)
    line.set_transform(t_end)

fig = plt.figure(figsize=(8,6))
axes = plt.subplot(111)

X = np.linspace(0,2*np.pi, 8)
Y = np.sin(X)
my_plot(X,Y)
plt.show()

在这里输入图片描述

撰写回答