【问题标题】:Matplotlib axis with two scales shared originMatplotlib 轴具有两个比例共享原点
【发布时间】:2012-05-15 22:55:57
【问题描述】:

我需要两个在 Matplotlib 中覆盖两个具有不同 Y 轴刻度的数据集。数据包含正值和负值。我希望两个轴共享一个原点,但 Matplotlib 默认情况下不对齐两个刻度。

import numpy as np
import matplotlib.pyplot as plt

fig = plt.figure()
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()

ax1.bar(range(6), (2, -2, 1, 0, 0, 0))
ax2.plot(range(6), (0, 2, 8, -2, 0, 0))
plt.show()

我想可以使用.get_ylim().set_ylim() 执行一些计算,两个对齐两个比例。有更简单的解决方案吗?

【问题讨论】:

    标签: matplotlib scale axis


    【解决方案1】:

    这可能不是您想要的,但这帮助我将整数排列在两个不同的垂直轴上:

    ax1.set_ylim(0,4000)
    ax2.set_ylim(0,120)
    ax2.set_yticks(np.linspace(ax2.get_yticks()[0], ax2.get_yticks()[-1], len(ax1.get_yticks())))
    
    

    【讨论】:

      【解决方案2】:

      我需要对齐两个子图,但不需要对齐它们的零点。其他解决方案对我来说不太奏效。

      我的程序的主要代码如下所示。子图未对齐。此外,我只更改 align_yaxis 函数并保持所有其他代码相同。

      import matplotlib.pyplot as plt
      
      def align_yaxis(ax1, v1, ax2, v2):
        return 0
      
      x  = range(10)
      y1 = [3.2, 1.3, -0.3, 0.4, 2.3, -0.9, 0.2, 0.1, 1.3, -3.4]
      y2, s = [], 100
      for i in y1:
          s *= 1 + i/100
          y2.append(s)
      
      fig = plt.figure()
      ax1 = fig.add_subplot()
      ax2 = ax1.twinx()
      
      ax1.axhline(y=0, color='k', linestyle='-', linewidth=0.5)
      ax1.bar(x, y1, color='tab:blue')
      ax2.plot(x, y2, color='tab:red')
      
      fig.tight_layout()
      align_yaxis(ax1, 0, ax2, 100)
      plt.show()
      

      Picture of not aligned subplots

      使用@HYRY 的解决方案,我得到了对齐的子图,但第二个子图不在图中。你看不到它。

      def align_yaxis(ax1, v1, ax2, v2):
          """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
          _, y1 = ax1.transData.transform((0, v1))
          _, y2 = ax2.transData.transform((0, v2))
          inv = ax2.transData.inverted()
          _, dy = inv.transform((0, 0)) - inv.transform((0, y1-y2))
          miny, maxy = ax2.get_ylim()
          ax2.set_ylim(miny+dy, maxy+dy)
      

      Picture without second subplot

      使用@drevicko 的解决方案,我也得到了对齐的情节。但是现在第一个子图已经不存在了,第一个 Y 轴很奇怪。

      def align_yaxis(ax1, v1, ax2, v2):
          """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
          _, y1 = ax1.transData.transform((0, v1))
          _, y2 = ax2.transData.transform((0, v2))
          adjust_yaxis(ax2,(y1-y2)/2,v2)
          adjust_yaxis(ax1,(y2-y1)/2,v1)
      
      def adjust_yaxis(ax,ydif,v):
          """shift axis ax by ydiff, maintaining point v at the same location"""
          inv = ax.transData.inverted()
          _, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
          miny, maxy = ax.get_ylim()
          miny, maxy = miny - v, maxy - v
          if -miny>maxy or (-miny==maxy and dy > 0):
              nminy = miny
              nmaxy = miny*(maxy+dy)/(miny+dy)
          else:
              nmaxy = maxy
              nminy = maxy*(miny+dy)/(maxy+dy)
          ax.set_ylim(nminy+v, nmaxy+v)
      
      

      Picture without firstsubplot

      所以我稍微调整了@drevicko 的解决方案,得到了我想要的。

      def align_yaxis(ax1, v1, ax2, v2):
          """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
          _, y1 = ax1.transData.transform((0, v1))
          _, y2 = ax2.transData.transform((0, v2))
          adjust_yaxis(ax1,(y2 - y1)/2,v1)
          adjust_yaxis(ax2,(y1 - y2)/2,v2)
      
      def adjust_yaxis(ax,ydif,v):
          """shift axis ax by ydiff, maintaining point v at the same location"""
          inv = ax.transData.inverted()
          _, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
          miny, maxy = ax.get_ylim()
      
          nminy = miny - v + dy - abs(dy)
          nmaxy = maxy - v + dy + abs(dy)
          ax.set_ylim(nminy+v, nmaxy+v)
      

      Subplots as I've expected them to look

      【讨论】:

        【解决方案3】:

        @Tim 的解决方案适用于两个以上的轴:

        import numpy as np
        
        def align_yaxis(axes): 
            y_lims = np.array([ax.get_ylim() for ax in axes])
        
            # force 0 to appear on all axes, comment if don't need
            y_lims[:, 0] = y_lims[:, 0].clip(None, 0)
            y_lims[:, 1] = y_lims[:, 1].clip(0, None)
        
            # normalize all axes
            y_mags = (y_lims[:,1] - y_lims[:,0]).reshape(len(y_lims),1)
            y_lims_normalized = y_lims / y_mags
        
            # find combined range
            y_new_lims_normalized = np.array([np.min(y_lims_normalized), np.max(y_lims_normalized)])
        
            # denormalize combined range to get new axes
            new_lims = y_new_lims_normalized * y_mags
            for i, ax in enumerate(axes):
                ax.set_ylim(new_lims[i])    
        

        【讨论】:

          【解决方案4】:

          我已经从上面编写了一个解决方案,它将对齐任意数量的轴:

          def align_yaxis_np(axes):
              """Align zeros of the two axes, zooming them out by same ratio"""
              axes = np.array(axes)
              extrema = np.array([ax.get_ylim() for ax in axes])
          
              # reset for divide by zero issues
              for i in range(len(extrema)):
                  if np.isclose(extrema[i, 0], 0.0):
                      extrema[i, 0] = -1
                  if np.isclose(extrema[i, 1], 0.0):
                      extrema[i, 1] = 1
          
              # upper and lower limits
              lowers = extrema[:, 0]
              uppers = extrema[:, 1]
          
              # if all pos or all neg, don't scale
              all_positive = False
              all_negative = False
              if lowers.min() > 0.0:
                  all_positive = True
          
              if uppers.max() < 0.0:
                  all_negative = True
          
              if all_negative or all_positive:
                  # don't scale
                  return
          
              # pick "most centered" axis
              res = abs(uppers+lowers)
              min_index = np.argmin(res)
          
              # scale positive or negative part
              multiplier1 = abs(uppers[min_index]/lowers[min_index])
              multiplier2 = abs(lowers[min_index]/uppers[min_index])
          
              for i in range(len(extrema)):
                  # scale positive or negative part based on which induces valid
                  if i != min_index:
                      lower_change = extrema[i, 1] * -1*multiplier2
                      upper_change = extrema[i, 0] * -1*multiplier1
                      if upper_change < extrema[i, 1]:
                          extrema[i, 0] = lower_change
                      else:
                          extrema[i, 1] = upper_change
          
                  # bump by 10% for a margin
                  extrema[i, 0] *= 1.1
                  extrema[i, 1] *= 1.1
          
              # set axes limits
              [axes[i].set_ylim(*extrema[i]) for i in range(len(extrema))]
          

          4 个随机序列的示例(您可以在 4 组单独的 y 轴标签上看到离散范围):

          【讨论】:

            【解决方案5】:

            这里的其他答案似乎过于复杂,不一定适用于所有场景(例如 ax1 全部为负,ax2 全部为正)。有两种简单的方法总是有效的:

            1. 始终在两个 y 轴的图表中间放置 0
            2. 有点花哨,有点保留正负比,见下文
            def align_yaxis(ax1, ax2):
                y_lims = numpy.array([ax.get_ylim() for ax in [ax1, ax2]])
            
                # force 0 to appear on both axes, comment if don't need
                y_lims[:, 0] = y_lims[:, 0].clip(None, 0)
                y_lims[:, 1] = y_lims[:, 1].clip(0, None)
            
                # normalize both axes
                y_mags = (y_lims[:,1] - y_lims[:,0]).reshape(len(y_lims),1)
                y_lims_normalized = y_lims / y_mags
            
                # find combined range
                y_new_lims_normalized = numpy.array([numpy.min(y_lims_normalized), numpy.max(y_lims_normalized)])
            
                # denormalize combined range to get new axes
                new_lim1, new_lim2 = y_new_lims_normalized * y_mags
                ax1.set_ylim(new_lim1)
                ax2.set_ylim(new_lim2)
            

            【讨论】:

              【解决方案6】:

              在绘制以下两个点序列时,@drevicko 的回答对我来说失败了:

              l1 = [0.03, -0.6, 1, 0.05]
              l2 = [0.8,  0.9,  1,  1.1]
              fig, ax1 = plt.subplots()
              ax1.plot(l1)
              ax2 = ax1.twinx()
              ax2.plot(l2, color='r')
              align_yaxis(ax1, 0, ax2, 0)
              

              ...所以这是我的版本:

              def align_yaxis(ax1, ax2):
                  """Align zeros of the two axes, zooming them out by same ratio"""
                  axes = (ax1, ax2)
                  extrema = [ax.get_ylim() for ax in axes]
                  tops = [extr[1] / (extr[1] - extr[0]) for extr in extrema]
                  # Ensure that plots (intervals) are ordered bottom to top:
                  if tops[0] > tops[1]:
                      axes, extrema, tops = [list(reversed(l)) for l in (axes, extrema, tops)]
              
                  # How much would the plot overflow if we kept current zoom levels?
                  tot_span = tops[1] + 1 - tops[0]
              
                  b_new_t = extrema[0][0] + tot_span * (extrema[0][1] - extrema[0][0])
                  t_new_b = extrema[1][1] - tot_span * (extrema[1][1] - extrema[1][0])
                  axes[0].set_ylim(extrema[0][0], b_new_t)
                  axes[1].set_ylim(t_new_b, extrema[1][1])
              

              原则上,对齐零(或其他提供的解决方案接受的其他值)有无限不同的可能性:无论您在 y 轴上放置零的任何位置,都可以缩放两个系列中的每一个以使其适合。我们只是选择这样的位置,使得在变换之后,两者覆盖相同高度的垂直间隔。 或者换句话说,与非对齐图相比,我们将它们最小化为相同的因子。 (这意味着 0 位于图的一半:例如,如果一个图全为负数而另一个全为正数,则会发生这种情况。)

              Numpy 版本:

              def align_yaxis_np(ax1, ax2):
                  """Align zeros of the two axes, zooming them out by same ratio"""
                  axes = np.array([ax1, ax2])
                  extrema = np.array([ax.get_ylim() for ax in axes])
                  tops = extrema[:,1] / (extrema[:,1] - extrema[:,0])
                  # Ensure that plots (intervals) are ordered bottom to top:
                  if tops[0] > tops[1]:
                      axes, extrema, tops = [a[::-1] for a in (axes, extrema, tops)]
              
                  # How much would the plot overflow if we kept current zoom levels?
                  tot_span = tops[1] + 1 - tops[0]
              
                  extrema[0,1] = extrema[0,0] + tot_span * (extrema[0,1] - extrema[0,0])
                  extrema[1,0] = extrema[1,1] + tot_span * (extrema[1,0] - extrema[1,1])
                  [axes[i].set_ylim(*extrema[i]) for i in range(2)]
              

              【讨论】:

                【解决方案7】:

                为了确保保持 y 边界(因此没有数据点被移出绘图),并平衡两个 y 轴的调整,我对@HYRY 的回答做了一些补充:

                def align_yaxis(ax1, v1, ax2, v2):
                    """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
                    _, y1 = ax1.transData.transform((0, v1))
                    _, y2 = ax2.transData.transform((0, v2))
                    adjust_yaxis(ax2,(y1-y2)/2,v2)
                    adjust_yaxis(ax1,(y2-y1)/2,v1)
                
                def adjust_yaxis(ax,ydif,v):
                    """shift axis ax by ydiff, maintaining point v at the same location"""
                    inv = ax.transData.inverted()
                    _, dy = inv.transform((0, 0)) - inv.transform((0, ydif))
                    miny, maxy = ax.get_ylim()
                    miny, maxy = miny - v, maxy - v
                    if -miny>maxy or (-miny==maxy and dy > 0):
                        nminy = miny
                        nmaxy = miny*(maxy+dy)/(miny+dy)
                    else:
                        nmaxy = maxy
                        nminy = maxy*(miny+dy)/(maxy+dy)
                    ax.set_ylim(nminy+v, nmaxy+v)
                

                【讨论】:

                • 能否请您在 if/else 语句中添加 cmets。我发现这种方法仍然会切断数据。
                • 很难在没有看到您的数据的情况下做到这一点(从而找出数据被切断的原因)。你能提供更多信息吗?也许调试并建议编辑? (在这里联系我,以防您进行编辑,以便我接受 - 代码编辑通常不被接受!)
                • 谢谢,今天晚些时候我会整理一个可重现的例子。如果您能解释 if/else 和重新缩放的逻辑,那就太好了
                • 好的,if 本质上是确定minymaxy 的绝对值是否更大(abs(miny) 只有在为负时才会更大)。换句话说,它离 0 点更远(嗯,实际上是 v-point,因为你可以在某个值处对齐 v)。
                • @devicko :这里很难展示一个可重现的例子,所以我创建了一个新问题stackoverflow.com/questions/51766031/…
                【解决方案8】:

                使用 align_yaxis() 函数:

                import numpy as np
                import matplotlib.pyplot as plt
                
                def align_yaxis(ax1, v1, ax2, v2):
                    """adjust ax2 ylimit so that v2 in ax2 is aligned to v1 in ax1"""
                    _, y1 = ax1.transData.transform((0, v1))
                    _, y2 = ax2.transData.transform((0, v2))
                    inv = ax2.transData.inverted()
                    _, dy = inv.transform((0, 0)) - inv.transform((0, y1-y2))
                    miny, maxy = ax2.get_ylim()
                    ax2.set_ylim(miny+dy, maxy+dy)
                
                
                fig = plt.figure()
                ax1 = fig.add_subplot(111)
                ax2 = ax1.twinx()
                
                ax1.bar(range(6), (2, -2, 1, 0, 0, 0))
                ax2.plot(range(6), (0, 2, 8, -2, 0, 0))
                
                align_yaxis(ax1, 0, ax2, 0)
                plt.show()
                

                【讨论】:

                • 这怎么可能是公认的答案?它几乎可以保证削减数据。
                猜你喜欢
                • 2023-03-14
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2010-11-27
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多