Annotate plot with ordered pair of cartesian coordinates via Python and MatPlotLib library

173 views Asked by At

Looking for a solution to properly annotate a subplot with an ordered pair of cartesian coordinates.

My figure is a bar graph of total product quantities with a line graph of the average price for the given products. For additional reference, please see the figure at the end of this article: https://medium.com/swlh/product-sales-analysis-using-python-863b29026957

Please note, I have two vertical axes where:

  • y1 = total quantity of a given product
  • y2 = average price of a given product
  • y1 & y2 share an x-axis of product categories

Rather than plotting labels "(x, y)", my goal is to plot labels for (y1, y2), i.e. "(qty, price)".

The current error that I am running into is that the list elements in my variable, label, are not recognized as "subscriptable objects". I am under the impression that the solution is to convert each element of my list into a string, but I am not positive.

df =

Products Quantity Price
Product1 10 100.00
Product2 15 200.00
Product3 20 150.00
Product2 30 200.00
Product3 50 150.00

Attempt

quantity = df.groupby("Products")["Quantity"].sum()
price = df.groupby("Products")["Price"].mean()

fig, ax1 = plt.subplots()
ax2 = ax1.twinx()

ax1.bar(Products, quantity, color='.8', alpha =.8)
ax2.plot(Products, price, 'bo-')

ax1.set_xlabel('', fontweight='bold')
ax1.set_ylabel('Quantity', color = 'k', fontweight='bold')
ax2.set_ylabel('Price $', color = 'b', fontweight='bold')
ax1.set_xticklabels(Products, rotation=45, size = 8)

y1 = [i for i in quantity]
y2 = [j for j in price]

label = []
for x, y in zip(y1,y2):
    label.append(f"({x:.2f},{y:.2f})")

for i, label in enumerate(labels):
    plt.annotate(label, xy=(x[i], y[i]), xytext=(5, 5),
    textcoords='offset points', ha='left', va='bottom')
plt.show()

Trouble Area

#can't find a method to convert my list elements from float to string values *inline* with label.append()
label = []
for x, y in zip(y1,y2):
    label.append(f"({x:.2f},{y:.2f})")

I feel like I am looking for a solution similar to either:

  1. https://www.tutorialspoint.com/how-to-annotate-several-points-with-one-text-in-matplotlib
  2. https://queirozf.com/entries/add-labels-and-text-to-matplotlib-plots-annotation-examples
1

There are 1 answers

3
JohanC On BEST ANSWER

There are a few misunderstandings in the code:

  • When working with ax1 and ax2, it is recommended to use matplotlib's object-oriented interface everywhere. plt.annotate(...) will plot on the "current ax", while ax1.annotate(...) will plot on ax1.
  • In the newest matplotlib versions, to plot on ax1, the x-coordinate can be given as a string (the name of the product), and the y-coordinate as the numeric quantity.
  • Creating the label to plot at the same time as calling the annotation can make the code easier to read.
  • To create a loop, in Python it is highly recommended to try to avoid enumerate(...) and indexing. Loops are clearer using zip to directly get the list elements.
  • To rotate ticks, ax.tick_params(...) will leave the existing labels untouched.
  • ax1.margins(y=...) can make more free space for the labels.
import matplotlib.pyplot as plt
import pandas as pd

df = pd.DataFrame({'Products': ['Product1', 'Product2', 'Product3', 'Product2', 'Product3'],
                   'Quantity': [10, 15, 20, 30, 50],
                   'Price': [100, 200, 150, 200, 150]})
quantity = df.groupby("Products")["Quantity"].sum()
price = df.groupby("Products")["Price"].mean()
Products = quantity.index

fig, ax1 = plt.subplots()
ax2 = ax1.twinx()

ax1.bar(Products, quantity, color='.8', alpha=.8)
ax2.plot(Products, price, 'bo-')

ax1.set_xlabel('', fontweight='bold')
ax1.set_ylabel('Quantity', color='k', fontweight='bold')
ax2.set_ylabel('Price $', color='b', fontweight='bold')
ax1.tick_params(axis='x', rotation=45, labelsize=8)

for prod, quant, prc in zip(Products, quantity, price):
    ax1.annotate(f'{quant:.2f}, {prc:.2f}', xy=(prod, quant), xytext=(0, 5),
                 textcoords='offset points', ha='center', va='bottom')
ax1.margins(y=0.2)  # more space on top of the bars
plt.tight_layout()
plt.show()

annotating using strings as coordinate