Search code examples
pythonpython-3.xmatplotlibmarkersubplot

Data value of a variable in each marker within subplots


I'm developing the following function: extract_name_value() that generates a step chart taking the values of a pandas DataFrame in Python, for now it works fine, but I want to add the values of the variable points_axisyvalue or values_list to it in each marker: Script Here

I tried to use the following examples:Data value at each marker, Matplotlib scatter plot with different text at each data point or How to put individual tags for a matplotlib scatter plot?, which would be something like what I want; also I even tried using plt.annotate(), but the data of the values does not come out the way I want it, plus I think it would cover up the graph a lot and not appreciate well. Below I put the code in which I'm using plt.annotate():

    # Function to extract the Name and Value attributes
    def extract_name_value(signals_df, rootXML):
        # print(signals_df)
        names_list = [name for name in signals_df['Name'].unique()]
        num_names_list = len(names_list)
        num_axisx = len(signals_df["Name"])
        values_list = [value for pos, value in enumerate(signals_df["Value"])]
        print(values_list)
        points_axisy = signals_df["Value"]
        print(len(points_axisy))

        colors = ['b', 'g', 'r', 'c', 'm', 'y']
    
        # Creation Graphic
        fig, ax = plt.subplots(nrows=num_names_list, figsize=(20, 30), sharex=True)
        plt.suptitle(f'File XML: {rootXML}', fontsize=16,         fontweight='bold', color='SteelBlue', position=(0.75, 0.95))
        plt.xticks(np.arange(-1, num_axisx), color='SteelBlue', fontweight='bold')
        labels = ['value: {0}'.format(j) for j in values_list]
        print(labels)
        i = 1
        for pos, name in enumerate(names_list):
            # get data
            data = signals_df[signals_df["Name"] == name]["Value"]
            print(data)
            # get color
            j = random.randint(0, len(colors) - 1)
            # get plots by index = pos
            x = np.hstack([-1, data.index.values, len(signals_df) - 1])
            y = np.hstack([0, data.values, data.iloc[-1]])
            ax[pos].plot(x, y, drawstyle='steps-post', marker='o', color=colors[j], linewidth=3)
            ax[pos].set_ylabel(name, fontsize=8, fontweight='bold', color='SteelBlue', rotation=30, labelpad=35)
            ax[pos].yaxis.set_major_formatter(ticker.FormatStrFormatter('%0.1f'))
             ax[pos].yaxis.set_tick_params(labelsize=6)
             ax[pos].grid(alpha=0.4)
             i += 1

             for label, x, y in zip(labels, x, y):
             plt.annotate(label, xy=(x, y), xytext=(-20, 20), textcoords='offset points', ha='right', va='bottom', bbox=dict(boxstyle='round,pad=0.5', fc='yellow', alpha=0.5),
                    arrowprops=dict(arrowstyle='->', connectionstyle='arc3,rad=0'))

        plt.show()

What I get is the annotations spliced and in different positions.

But, What does my code need to show each value at each point?

I've also been trying to use the code from the Matplotlib reference and couldn't get it done: Marker Reference. Thank you very much in advance, any comment helps.


Solution

  • I think it should be quite close to what you are after. I randomly generate data, then annotate it using matplotlib.text. It's not very pretty, you might want to add some padding and more refinements, but I hope it gives a good idea!

    If two points are too close, you might want to annotate one on the left, and the other one on the right, like I am doing for the first point. I have not seen such a situation in the examples that you have given, so it's not handled.

    Function place_label(label, xy, position, ax, pad=0.01) places the label where you want it to be. The rest of the code is demonstrating that it works, using randomly generated data.

    enter image description here

    import random
    import numpy as np
    import matplotlib.pyplot as plt
    
    # function that places the label give the desired position
    def place_label(label, xy, position, ax, pad=0.01):
    
      # annotate in the initial position, xy is the top right corner of the bounding box
      t_ = ax.text(x=xy[0], y=xy[1], s=label, fontsize=16)
    
      # find useful values
      tbb = t_.get_window_extent(renderer=rend)
      abb = ax.get_window_extent(renderer=rend)
      a_xlim, a_ylim = ax.get_xlim(), a_.get_ylim()
    
      # now adjust the position if needed
      new_xy = [xy[0], xy[1]]
    
      relative_width = tbb.width/abb.width * (a_xlim[1] - a_xlim[0])
      pad_x = pad * (a_xlim[1] - a_xlim[0])
      assert(position[0] in ['l', 'c', 'r'])
      if position[0] == 'c':
        new_xy[0] -= relative_width/2
      elif position[0] == 'l':
        new_xy[0] -= relative_width + pad_x
      else:
        new_xy[0] += pad_x
    
      relative_height =  tbb.height/abb.height * (a_ylim[1] - a_ylim[0])
      pad_y = pad * (a_ylim[1] - a_ylim[0])
      assert(position[1] in ['b', 'c', 't'])
      if position[1] == 'c':
        new_xy[1] -= relative_height/2
      elif position[1] == 'b':
        new_xy[1] -= relative_height + pad_y
      else:
        new_xy[1] += pad_y
    
      t_.set_position(new_xy)
    
      return t_
    
    # generate data, plot it and annotate it!
    axes_qty = 9
    axes_gap = 0.035
    
    fig = plt.figure(figsize=(10, 8))
    ax = [plt.axes([axes_gap, axes_gap/2 + i*(1/axes_qty), 1-2*axes_gap, 1/axes_qty-axes_gap]) for i in range(axes_qty)]
    rend = fig.canvas.get_renderer()
    
    for a_ in ax:
      x_ = [random.randint(0, 10) for _ in range(5)]
      x_ = np.unique(x_)
      y_ = [random.randint(0, 12) for _ in x_]
      # as x is shared, we set the limits in advance, otherwise the adjustments won't be accurate
      a_.set_xlim([-0.5, 10.5])
      
      # plotting the data
      data_ = [[x_[0], y_[0]]]
      for i in range(1, len(x_)):
        data_ += [[x_[i-1], y_[i]], [x_[i], y_[i]]]
      a_.plot([d[0] for d in data_], [d[1] for d in data_])
    
      mid_y = 0.5 * (a_.get_ylim()[0] + a_.get_ylim()[1])
    
      # now let's label it
      for i in range(len(x_)):
        # decide what point we annotate
        if i == 0:
          xy = [x_  [0], y_[0]]
        else:
          xy = [x_[i-1], y_[i]]
    
        # decide its position
        position_0 = 'l' if i == 0 else 'r'
        position_1 = 'b' if xy[1] > mid_y else 't'
    
        place_label(label=str(xy[1]), xy=xy, position=position_0+position_1, ax=a_)
    
    plt.show()