# CHAPTER 5
Line Charts and Trend Visualization
#
1. Chapter Introduction
#
Line charts are the most common business visualization — perfect for showing how metrics change over time. Revenue trends, website traffic, stock prices, and KPI trajectories all live in line charts.
2. Basic to Advanced Line Charts
#
python
{ copied = true; setTimeout(() => copied = false, 2000) })" class="text-slate-400 hover:text-white p-1 rounded hover:bg-slate-800 transition relative flex items-center justify-center" title="Copy code">
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
# Sample: Monthly website traffic
months = pd.date_range ('2024-01', periods=12, freq='MS')
traffic = [45000 , 48000 , 52000 , 51000 , 58000 , 63000 ,
61000 , 65000 , 70000 , 68000 , 72000 , 78000 ]
fig, axes = plt.subplots (2 , 2 , figsize=(14 , 10 ))
# Chart 1: Basic line
axes[0,0].plot (months, traffic, color='#2196F3', linewidth=2)
axes[0,0].set_title ('Basic Line Chart')
# Chart 2: Styled with markers
axes[0,1 ].plot (months, traffic, 'o-', color='#E91E63', linewidth=2,
markersize=8 , markerfacecolor='white', markeredgewidth=2,
markeredgecolor='#E91E63', label='Monthly Traffic')
axes[0,1 ].fill_between (months, traffic, alpha=0.1 , color='#E91E63')
axes[0,1 ].set_title ('Line with Markers and Fill')
axes[0,1 ].legend ()
# Chart 3: With trend line
traffic_arr = np.array (traffic)
x_num = np.arange (len (traffic))
z = np.polyfit (x_num, traffic_arr, 1 )
p = np.poly1d (z)
axes[1 ,0].plot (months, traffic, 'o-', color='#4CAF50', linewidth=2, label='Actual')
axes[1 ,0].plot (months, p (x_num), '--', color='#FF5722', linewidth=2, label=f'Trend (+{z[0]:.0f}/month)')
axes[1 ,0].set_title ('With Linear Trend Line')
axes[1 ,0].legend ()
# Chart 4: With annotations
axes[1 ,1 ].plot (months, traffic, 'o-', color='#9C27B0', linewidth=2)
peak_idx = traffic.index (max (traffic))
axes[1 ,1 ].annotate (f'Peak: {max(traffic):,}',
xy=(months[peak_idx], max (traffic)),
xytext=(months[peak_idx-3 ], max (traffic) + 3000 ),
arrowprops=dict (arrowstyle='->', color='gray'))
axes[1 ,1 ].set_title ('With Peak Annotation')
for ax in axes.flatten ():
ax.xaxis.set_major_formatter (plt.matplotlib.dates.DateFormatter ('%b'))
ax.grid (True , alpha=0.3 )
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.suptitle ('Line Chart Variations', fontsize=14, fontweight='bold')
plt.tight_layout ()
plt.savefig ('line_variations.png', dpi=150)
plt.show ()
3. Multi-Line Comparison Charts
#
python
{ copied = true; setTimeout(() => copied = false, 2000) })" class="text-slate-400 hover:text-white p-1 rounded hover:bg-slate-800 transition relative flex items-center justify-center" title="Copy code">
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
# Multi-product revenue comparison
products = {
'Product A': [42000, 45000, 51000, 48000, 53000, 62000, 58000, 61000, 67000, 72000, 89000, 95000],
'Product B': [28000, 31000, 35000, 33000, 38000, 42000, 39000, 43000, 47000, 51000, 60000, 68000],
'Product C': [15000, 16000, 18000, 17000, 19000, 22000, 21000, 24000, 26000, 28000, 32000, 36000]
}
colors = ['#1565C0', '#2E7D32', '#E65100']
months_str = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']
fig, ax = plt.subplots (figsize=(12 , 6 ))
for (product, values), color in zip (products.items (), colors):
ax.plot (months_str, values, 'o-', color=color, linewidth=2.5,
markersize=6 , label=f'{product} (Dec: ${values[-1]:,.0f})')
# Add a target line
ax.axhline (y=50000 , color='red', linestyle=':', linewidth=1.5, label='Target: $50K')
ax.set_title ('Product Revenue Comparison — 2024', fontsize=14, fontweight='bold')
ax.set_xlabel ('Month')
ax.set_ylabel ('Revenue ($)')
ax.yaxis.set_major_formatter (plt.FuncFormatter (lambda x, _: f'${x/1000:.0f}K'))
ax.legend (loc='upper left', framealpha=0.8)
ax.grid (True , alpha=0.3 )
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout ()
plt.savefig ('multi_line.png', dpi=150)
plt.show ()
4. Mini Project: Monthly Revenue Tracker
#
python
{ copied = true; setTimeout(() => copied = false, 2000) })" class="text-slate-400 hover:text-white p-1 rounded hover:bg-slate-800 transition relative flex items-center justify-center" title="Copy code">
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
np.random.seed (42 )
months = pd.date_range ('2024-01', periods=12, freq='MS')
revenue = np.array ([95000 , 88000 , 102000 , 97000 , 115000 , 128000 ,
120000 , 132000 , 140000 , 135000 , 155000 , 175000 ])
target = np.full (12 , 120000 )
ma3 = pd.Series (revenue).rolling (3 ).mean ().values # 3-month moving average
fig, (ax1, ax2) = plt.subplots (2 , 1 , figsize=(12 , 10 ), sharex=True )
# Top: Revenue vs Target
ax1.fill_between (months, revenue, target,
where=(revenue >= target), alpha=0.2 , color='green', label='Above target')
ax1.fill_between (months, revenue, target,
where=(revenue < target), alpha=0.2 , color='red', label='Below target')
ax1.plot (months, revenue, 'o-', color='#1565C0', linewidth=2.5, markersize=8, label='Revenue')
ax1.plot (months, target, '--', color='gray', linewidth=1.5, label='Target ($120K)')
ax1.plot (months, ma3, '-', color='#FF9800', linewidth=2, alpha=0.8, label='3-Month MA')
ax1.set_title ('Revenue vs Target with Moving Average', fontsize=13, fontweight='bold')
ax1.set_ylabel ('Revenue ($)')
ax1.yaxis.set_major_formatter (plt.FuncFormatter (lambda x, _: f'${x/1000:.0f}K'))
ax1.legend (loc='upper left')
ax1.grid (True , alpha=0.3 )
ax1.spines['top'].set_visible(False)
ax1.spines['right'].set_visible(False)
# Bottom: Month-over-month change
mom_change = np.diff (revenue, prepend=revenue[0])
colors = ['#4CAF50' if c >= 0 else '#F44336' for c in mom_change]
ax2.bar (months, mom_change, color=colors, alpha=0.8 , width=20 )
ax2.axhline (y=0, color='black', linewidth=0.8)
ax2.set_title ('Month-over-Month Change', fontsize=13, fontweight='bold')
ax2.set_ylabel ('Change ($)')
ax2.yaxis.set_major_formatter (plt.FuncFormatter (lambda x, _: f'${x:+,.0f}'))
ax2.xaxis.set_major_formatter (plt.matplotlib.dates.DateFormatter ('%b'))
ax2.grid (True , alpha=0.3 )
ax2.spines['top'].set_visible(False)
ax2.spines['right'].set_visible(False)
total = revenue.sum ()
avg = revenue.mean ()
fig.suptitle (f'Revenue Tracker 2024 | Total: ${total/1e6:.2f}M | Avg: ${avg:,.0f}/month',
fontsize=14 , fontweight='bold')
plt.tight_layout ()
plt.savefig ('revenue_tracker.png', dpi=150, bbox_inches='tight')
plt.show ()
5. Common Mistakes
#
Too many lines: More than 5-6 lines in one chart creates spaghetti plots. Use small multiples or highlight only key lines.
Not starting Y-axis at zero for percentage data: Truncated Y-axes can make small changes look dramatic — a 2% change can look like 100% with a truncated axis.
6. MCQs
#
Question 1
Line chart is best for?
A
Proportions
B
Trends and changes over continuous time
C
Comparisons —
Check Answer
Question 2
np.polyfit(x, y, 1) calculates?
A
Moving average
B
Linear regression coefficients for trend line
C
Correlation —
Check Answer
Question 3
fillbetween(x, y1, y2, where=cond) fills?
A
Always
B
Only where condition is True
C
Nothing —
Check Answer
Question 4
3-month moving average is calculated with?
A
np.mean()
B
pd.Series.rolling(3).mean()
C
np.cumsum() —
Check Answer
Question 5
sharex=True in subplots?
A
Copies data
B
Shares X-axis between subplot panels
C
Same limits —
Check Answer
Question 6
Too many lines on one chart causes?
A
Faster rendering
B
"Spaghetti chart" — hard to distinguish individual lines
C
Better insights —
Check Answer
Question 7
ax.xaxis.setmajorformatter(DateFormatter('%b')) formats dates as?
A
Full date
B
Month abbreviation (Jan, Feb)
C
Year —
Check Answer
Question 8
markerfacecolor='white' creates?
A
Invisible markers
B
Hollow marker style (white center, colored border)
C
Filled markers —
Check Answer
Question 9
Month-over-month (MoM) change is calculated with?
A
cumsum()
B
np.diff()
C
pctchange() for % —
Check Answer
Question 10
Reference line for target/threshold uses?
A
ax.plot()
B
ax.axhline(y=target)
C
ax.bar() —
Check Answer
7. Interview Questions
#
Q: When would you use a line chart vs a bar chart for monthly data?
Q: How do you add a trend line to a line chart in Matplotlib?
8. Summary
#
Line charts communicate time-based trends instantly. Key enhancements: trend lines (
np.polyfit), moving averages (
.rolling()), fill_between for above/below target areas, and annotations for key points. Limit to 5-6 lines per chart. Always label the Y-axis clearly with units.
9. Next Chapter Recommendation
#
In
Chapter 6: Bar Charts and Categorical Data , we master vertical, horizontal, grouped, and stacked bar charts for categorical comparison.