Annotations, labels & ₹ number formatting
A chart is only as good as the story a reader can pick up in five seconds. Clear labels, readable numbers and one or two well-placed annotations do more for comprehension than any colour scheme.
Tick formatters
Raw tick labels such as 2500000 are hard to read. Matplotlib's ticker module lets you control the text of every tick.
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
cities = ["Delhi", "Mumbai", "Chennai", "Kolkata", "Surat"]
revenue = [2_450_000, 3_120_000, 1_780_000, 1_150_000, 940_000]
def lakhs(x, pos):
return f"₹{x / 1e5:.0f}L"
fig, ax = plt.subplots(figsize=(7, 4))
bars = ax.bar(cities, revenue, color="#3b6ea8")
ax.yaxis.set_major_formatter(FuncFormatter(lakhs))
ax.set_ylabel("Revenue")
ax.set_title("Q2 revenue by city")
FuncFormatter receives the tick value and its position and must return a string. For simpler cases StrMethodFormatter("{x:,.0f}") or PercentFormatter(xmax=1) are ready-made.
Indian digit grouping
Python's , format uses Western grouping (2,450,000). Indian readers expect 24,50,000. A small helper does it:
def indian_format(n):
s = str(int(round(n)))
if len(s) <= 3:
return s
head, tail = s[:-3], s[-3:]
groups = []
while len(head) > 2:
groups.insert(0, head[-2:])
head = head[:-2]
if head:
groups.insert(0, head)
return ",".join(groups) + "," + tail
print(indian_format(2450000)) # 24,50,000
| Value | Western | Indian | Short |
|---|---|---|---|
| 1,50,000 | 150,000 | 1,50,000 | ₹1.5L |
| 2,45,00,000 | 24,500,000 | 2,45,00,000 | ₹2.45Cr |
Choose one convention per report and stick to it. If the rupee glyph does not render in your chosen font, fall back to "Rs" rather than showing an empty box.
Direct labels on bars
ax.bar_label() writes values above bars, which often lets you delete the y-axis entirely:
ax.bar_label(bars, labels=[f"₹{v / 1e5:.1f}L" for v in revenue], padding=3)
ax.spines[["top", "right"]].set_visible(False)
Annotating a point
ax.annotate() places text and an optional arrow. xy is the point being described; xytext is where the text sits.
ax.annotate(
"Diwali sale week",
xy=(1, 3_120_000),
xytext=(2, 3_300_000),
arrowprops=dict(arrowstyle="->", color="gray"),
fontsize=9,
)
Use textcoords="offset points" when you want the text a fixed distance (in points) from the data point regardless of axis scale. For shaded periods (a lockdown, a festival season) use ax.axvspan(start, end, alpha=0.15); for targets use ax.axhline(target, linestyle="--").
Key takeaways
- Format ticks with
FuncFormatter; never make readers count zeros. - Label bars directly with
bar_labelwhen exact values matter. - Annotate the one or two points that explain the story, not every point.
- Titles can state the insight: "Mumbai leads Q2 revenue" beats "Revenue by city".
Practice
Plot 12 months of revenue for a Surat textile wholesaler ranging between ₹40L and ₹1.3Cr. Format the y-axis in lakhs, add a dashed target line at ₹1Cr, shade the Oct-Nov festive period and annotate the highest month with an arrow.