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_label when 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.

B

Written by Bablu Kumar Jha

Founder, BeeProton AI · Mumbai, Maharashtra, India