How to Create a Multi-Line Chart in Matplotlib

Multi-line charts are the workhorse visualization for comparing trends across different categories, tracking multiple time series, or displaying related metrics on a shared timeline. You'll use them...

Key Insights

  • Multi-line charts excel at comparing trends across categories or time periods, but readability degrades rapidly beyond 5-7 lines—consider small multiples or interactive visualizations for larger datasets.
  • Matplotlib’s plt.plot() can be called repeatedly to layer lines, but pandas’ DataFrame.plot() offers a more concise approach when working with structured data.
  • Proper line differentiation requires more than just color—combine line styles, markers, and widths to ensure accessibility and clarity, especially for colorblind users or printed materials.

Introduction & Setup

Multi-line charts are the workhorse visualization for comparing trends across different categories, tracking multiple time series, or displaying related metrics on a shared timeline. You’ll use them to compare sales across regions, track multiple stock prices, or monitor system metrics over time. The key advantage is immediate visual comparison—viewers can spot correlations, divergences, and relative magnitudes at a glance.

Before diving in, ensure you have the necessary libraries installed:

pip install matplotlib pandas numpy

Here are the essential imports for working with multi-line charts:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from datetime import datetime, timedelta

# Optional: Set style for better-looking charts
plt.style.use('seaborn-v0_8-darkgrid')

Creating a Basic Multi-Line Chart

The fundamental approach to multi-line charts in Matplotlib is straightforward: call plt.plot() multiple times before displaying the figure. Each call adds another line to the same axes.

# Generate sample data
months = np.arange(1, 13)
product_a_sales = np.array([120, 135, 145, 160, 155, 170, 185, 190, 200, 210, 225, 240])
product_b_sales = np.array([80, 85, 95, 100, 110, 115, 125, 135, 145, 150, 160, 170])
product_c_sales = np.array([200, 195, 190, 185, 180, 175, 170, 165, 160, 155, 150, 145])

# Create the chart
plt.figure(figsize=(10, 6))
plt.plot(months, product_a_sales, label='Product A')
plt.plot(months, product_b_sales, label='Product B')
plt.plot(months, product_c_sales, label='Product C')

plt.xlabel('Month')
plt.ylabel('Sales (units)')
plt.title('Monthly Sales Comparison by Product')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

The label parameter is crucial—it populates the legend automatically when you call plt.legend(). Without labels, your viewers won’t know which line represents which data series.

Customizing Line Styles and Colors

Default styling works for quick analysis, but production visualizations need deliberate visual design. Matplotlib provides extensive customization through line style parameters.

plt.figure(figsize=(12, 6))

# Explicit color and style control
plt.plot(months, product_a_sales, 
         color='#2E86AB',           # Hex color
         linestyle='-',              # Solid line
         linewidth=2.5,
         marker='o',                 # Circle markers
         markersize=6,
         label='Product A')

plt.plot(months, product_b_sales,
         color='#A23B72',
         linestyle='--',             # Dashed line
         linewidth=2,
         marker='s',                 # Square markers
         markersize=5,
         label='Product B')

plt.plot(months, product_c_sales,
         color='#F18F01',
         linestyle=':',              # Dotted line
         linewidth=2,
         marker='^',                 # Triangle markers
         markersize=6,
         alpha=0.7,                  # Transparency
         label='Product C')

plt.xlabel('Month', fontsize=12)
plt.ylabel('Sales (units)', fontsize=12)
plt.title('Monthly Sales with Custom Styling', fontsize=14, fontweight='bold')
plt.legend(loc='best', framealpha=0.9)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Common linestyle options include:

  • '-' or 'solid': Solid line (default)
  • '--' or 'dashed': Dashed line
  • '-.' or 'dashdot': Dash-dot pattern
  • ':' or 'dotted': Dotted line

Marker options are extensive: 'o' (circle), 's' (square), '^' (triangle up), 'v' (triangle down), 'D' (diamond), '*' (star), and many more.

Working with DataFrames

When your data lives in a pandas DataFrame—which it should for any serious analysis—you have two approaches: iterate through columns with plt.plot(), or use the DataFrame’s built-in plotting method.

# Create a DataFrame with time series data
dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='M')
df = pd.DataFrame({
    'Product A': np.random.randint(100, 250, len(dates)),
    'Product B': np.random.randint(80, 180, len(dates)),
    'Product C': np.random.randint(150, 220, len(dates))
}, index=dates)

# Method 1: Traditional approach
plt.figure(figsize=(12, 6))
for column in df.columns:
    plt.plot(df.index, df[column], label=column, linewidth=2)
plt.xlabel('Date')
plt.ylabel('Sales (units)')
plt.title('Sales Trends - Traditional Method')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# Method 2: DataFrame.plot() - more concise
ax = df.plot(figsize=(12, 6), 
             linewidth=2,
             title='Sales Trends - DataFrame Method')
ax.set_xlabel('Date')
ax.set_ylabel('Sales (units)')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

The DataFrame.plot() method automatically handles legends, uses the index for x-values, and applies sensible defaults. It returns an axes object that you can further customize. For quick exploratory analysis, this is the fastest route. For publication-quality graphics requiring precise control, the traditional approach offers more flexibility.

Advanced Formatting

Professional visualizations require attention to detail: formatted axes, annotations, and sometimes multiple y-axes for variables with different scales.

# Create sample data with different scales
dates = pd.date_range(start='2024-01-01', periods=12, freq='M')
revenue = np.array([45000, 48000, 52000, 54000, 58000, 62000, 
                    65000, 68000, 71000, 74000, 78000, 82000])
conversion_rate = np.array([2.1, 2.3, 2.5, 2.4, 2.7, 2.9, 
                            3.1, 3.0, 3.2, 3.4, 3.5, 3.6])

fig, ax1 = plt.subplots(figsize=(12, 6))

# Primary y-axis: Revenue
color1 = '#2E86AB'
ax1.set_xlabel('Date', fontsize=12)
ax1.set_ylabel('Revenue ($)', color=color1, fontsize=12)
line1 = ax1.plot(dates, revenue, color=color1, linewidth=2.5, 
                 marker='o', label='Revenue')
ax1.tick_params(axis='y', labelcolor=color1)
ax1.yaxis.set_major_formatter(plt.FuncFormatter(lambda x, p: f'${x/1000:.0f}K'))

# Secondary y-axis: Conversion Rate
ax2 = ax1.twinx()
color2 = '#F18F01'
ax2.set_ylabel('Conversion Rate (%)', color=color2, fontsize=12)
line2 = ax2.plot(dates, conversion_rate, color=color2, linewidth=2.5, 
                 marker='s', linestyle='--', label='Conversion Rate')
ax2.tick_params(axis='y', labelcolor=color2)

# Add annotation for a significant event
peak_idx = np.argmax(revenue)
ax1.annotate('Launch of new campaign',
             xy=(dates[peak_idx], revenue[peak_idx]),
             xytext=(dates[peak_idx-2], revenue[peak_idx] + 5000),
             arrowprops=dict(arrowstyle='->', color='black', lw=1.5),
             fontsize=10,
             bbox=dict(boxstyle='round,pad=0.5', facecolor='yellow', alpha=0.7))

# Combine legends from both axes
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left')

plt.title('Revenue and Conversion Rate Trends', fontsize=14, fontweight='bold', pad=20)
ax1.grid(True, alpha=0.3)
fig.tight_layout()
plt.show()

The dual-axis approach is powerful but dangerous—it can mislead viewers if the scales aren’t carefully chosen. Use it sparingly and only when the variables genuinely have different units or scales that make a shared axis impractical.

Best Practices & Common Pitfalls

Limit the number of lines. Beyond 5-7 lines, charts become cluttered and difficult to interpret. If you need to show more series, consider faceting (small multiples), interactive visualizations, or highlighting a subset while showing others in muted colors.

Use colorblind-friendly palettes. Approximately 8% of men and 0.5% of women have some form of color vision deficiency. Use palettes designed for accessibility:

# Colorblind-friendly palette
colorblind_colors = ['#0173B2', '#DE8F05', '#029E73', '#CC78BC', 
                     '#CA9161', '#949494', '#ECE133']

for i, column in enumerate(df.columns):
    plt.plot(df.index, df[column], 
             color=colorblind_colors[i % len(colorblind_colors)],
             linewidth=2, label=column)

Position legends thoughtfully. The loc='best' parameter attempts automatic placement, but manual positioning often works better. Use loc='upper left', loc='lower right', or place legends outside the plot area with bbox_to_anchor:

plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0)

Optimize for large datasets. Matplotlib can slow down with tens of thousands of points per line. For large datasets:

# Downsample data before plotting
df_resampled = df.resample('W').mean()  # Weekly averages instead of daily

# Or use rasterization for faster rendering
plt.plot(x, y, rasterized=True)

Always include units in axis labels. “Sales” is ambiguous—is it dollars, units, thousands? Write “Sales (thousands USD)” or “Sales (units)” instead.

Save figures at appropriate resolution. For publications or presentations:

plt.savefig('multi_line_chart.png', dpi=300, bbox_inches='tight')

Multi-line charts are deceptively simple but require thoughtful design to communicate effectively. Focus on clarity over complexity, ensure accessibility through color and style choices, and always test your visualizations with representative users before finalizing them.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.