Skip to content
Music Information Retrieval Computational Musicology Data Visualization

Visualizing Musical Analysis Results: Plotting Motif Distributions and Timelines

Oriol Colomé Font
10 min read

Introduction

Once you’ve identified motifs in a musical score, the next crucial step is visualization. Effective visualizations transform raw data into insights, revealing patterns, distributions, and relationships that might not be apparent from numbers alone.

In computational musicology, visualization serves multiple purposes:

  • Exploratory analysis: Understanding the structure of your data
  • Pattern discovery: Identifying recurring motifs and their characteristics
  • Communication: Presenting findings to other researchers or musicians
  • Validation: Verifying that your analysis methods are working correctly

This post explores visualization techniques for musical motif analysis, building on the motif identification methods we discussed previously.

The Visualization Toolkit

Our visualization approach uses three key functions:

  1. Distribution plots: Understanding motif length patterns
  2. Timeline visualizations: Seeing when and where motifs occur
  3. Color mapping: Distinguishing different motifs visually

Let’s examine each in detail.

Function 1: Plotting Motif Distributions

def plot_motif_distribution(
    motifs: List[Any],
    title: str = "Motif Distribution",
    output_path: Optional[str] = None,
) -> None:
    if not motifs:
        raise ValueError("Motifs list cannot be empty")

    lengths = [len(m) for m in motifs]

    plt.figure(figsize=(10, 6))
    plt.hist(lengths, bins="auto")
    plt.title(title)
    plt.xlabel("Motif Length")
    plt.ylabel("Frequency")

    if output_path:
        plt.savefig(output_path)
    plt.close()

What This Function Does

The plot_motif_distribution function creates a histogram showing how many motifs exist at each length. This simple visualization reveals important characteristics:

  • Most common motif length: Which length appears most frequently?
  • Length diversity: Does the piece use motifs of many different lengths, or focus on a few?
  • Distribution shape: Are motifs evenly distributed, or clustered around certain lengths?

Understanding the Output

A typical distribution plot might show:

  • A peak at length 3-4: Short motifs are building blocks
  • Fewer longer motifs: Longer sequences are less common
  • Tails at both ends: Very short (2 notes) or very long (8+ notes) motifs are rare

Enhanced Version

Here’s an enhanced version with more detail:

def plot_motif_distribution_enhanced(
    motifs: List[Any],
    title: str = "Motif Distribution",
    output_path: Optional[str] = None,
    show_stats: bool = True,
) -> None:
    """Enhanced version with statistics and styling."""
    if not motifs:
        raise ValueError("Motifs list cannot be empty")

    lengths = [len(m) for m in motifs]
    
    fig, ax = plt.subplots(figsize=(12, 6))
    
    # Create histogram
    n, bins, patches = ax.hist(lengths, bins="auto", edgecolor='black', alpha=0.7)
    
    # Color bars by frequency
    max_freq = max(n)
    for i, (bar, freq) in enumerate(zip(patches, n)):
        bar.set_facecolor(plt.cm.viridis(freq / max_freq))
    
    # Add statistics
    if show_stats:
        mean_len = np.mean(lengths)
        median_len = np.median(lengths)
        ax.axvline(mean_len, color='red', linestyle='--', linewidth=2, label=f'Mean: {mean_len:.2f}')
        ax.axvline(median_len, color='blue', linestyle='--', linewidth=2, label=f'Median: {median_len:.2f}')
        ax.legend()
    
    ax.set_title(title, fontsize=14, fontweight='bold')
    ax.set_xlabel("Motif Length (number of notes)", fontsize=12)
    ax.set_ylabel("Frequency", fontsize=12)
    ax.grid(axis='y', alpha=0.3)
    
    plt.tight_layout()
    
    if output_path:
        plt.savefig(output_path, dpi=300, bbox_inches='tight')
    plt.close()

Function 2: Plotting Motif Timelines

def plot_motif_timeline(
    score: music21.stream.Score,
    motifs: List[List[music21.note.Note]],
    output_path: Optional[str] = None,
) -> None:

    if not motifs:
        raise ValueError("Motifs list cannot be empty")
    if not score.parts:
        raise ValueError("Score must have at least one part")

    plt.figure(figsize=(15, 8))
    # Implementation would depend on how you want to visualize the timeline
    # This is a placeholder for the actual visualization logic

    if output_path:
        plt.savefig(output_path)
    plt.close()

Why Timeline Visualization Matters

Timeline visualizations answer critical questions:

  • When do motifs appear? Are they clustered at the beginning, end, or distributed throughout?
  • How do motifs relate to structure? Do they align with section markers (like “Tawchiya”, “Mshalia” in Arab-Andalusian music)?
  • Are there patterns? Do certain motifs always appear together or in sequence?

Complete Implementation

Here’s a complete implementation based on the Arab-Andalusian music analysis:

def plot_motif_timeline(
    score: music21.stream.Score,
    motif_counts: dict,
    text_expressions: dict = None,
    filename: str = "",
    output_path: Optional[str] = None,
) -> None:

    if not motif_counts:
        raise ValueError("Motif counts dictionary cannot be empty")
    
    measures = score.parts[0].getElementsByClass('Measure')
    total_measures = len(measures)
    unique_motifs = list(motif_counts.keys())
    
    # Calculate max count for consistent y-axis
    max_count = max(max(counts.values()) if isinstance(counts, dict) else 0 
                    for counts in motif_counts.values())
    
    # Create subplots for each motif
    n_motifs = len(unique_motifs)
    fig, axs = plt.subplots(n_motifs, 1, figsize=(14, 3 * n_motifs), sharex=True)
    
    if n_motifs == 1:
        axs = [axs]
    
    palette = plt.cm.Pastel2.colors
    
    for i, (motif, counts) in enumerate(motif_counts.items()):
        # Create measure-by-measure count array
        measure_counts = [counts.get(m, 0) for m in range(1, total_measures + 1)]
        
        # Plot as bar chart
        axs[i].bar(range(1, total_measures + 1), measure_counts, 
                   color=palette[i % len(palette)], width=1.0)
        
        # Add section markers if provided
        if text_expressions:
            for measure_num, label in text_expressions.items():
                axs[i].axvline(x=measure_num, color='black', 
                              linestyle='--', linewidth=0.75, alpha=0.7)
        
        # Styling
        axs[i].set_title(f'Cento: {motif}', fontsize=11, fontweight='bold')
        axs[i].set_ylabel('Counts', fontsize=10)
        axs[i].set_ylim(0, max_count)
        axs[i].set_yticks(np.arange(0, max_count + 1, 1))
        axs[i].grid(axis='y', linestyle='--', alpha=0.5)
        axs[i].set_xticks(np.arange(20, total_measures, 20))
    
    # Common x-axis label
    axs[-1].set_xlabel('Measure Number', fontsize=12)
    
    # Overall title
    plt.suptitle(f'Distribution of Centos across {filename}', 
                 fontsize=14, fontweight='bold', y=0.995)
    
    plt.tight_layout(rect=[0, 0, 1, 0.98])
    
    if output_path:
        plt.savefig(output_path, dpi=300, bbox_inches='tight')
    plt.close()

Real-World Example

In the Arab-Andalusian motif analysis, this visualization revealed:

Motif Distribution - Btaihi Iraq Ajam

This plot shows:

  • Five different centos (motifs) being tracked
  • Vertical dashed lines marking structural sections (Tawchiya, Mshalia, Futintu, etc.)
  • Bar heights indicating how many times each motif appears in each measure
  • Patterns: Some motifs cluster around certain sections, others are more evenly distributed

The visualization makes it immediately clear that:

  • The (B, A, G) cento appears most frequently (91 occurrences total)
  • Motifs are distributed throughout the piece, not just at the beginning
  • Section markers often align with changes in motif density

Function 3: Creating Color Maps

def create_colormap(n_motifs: int) -> np.ndarray:

    if n_motifs < 1:
        raise ValueError("Number of motifs must be positive")

    return plt.cm.viridis(np.linspace(0, 1, n_motifs))

Why Color Mapping Matters

When visualizing multiple motifs simultaneously, color helps:

  • Distinguish patterns: Each motif gets a unique, distinguishable color
  • Maintain consistency: The same motif always uses the same color across visualizations
  • Improve readability: Good color choices make complex plots easier to interpret

Choosing the Right Colormap

Different colormaps serve different purposes:

1. Categorical Colormaps (for distinct motifs)

def create_categorical_colormap(n_motifs: int) -> np.ndarray:
    """Use distinct colors for each motif."""
    if n_motifs <= 10:
        # Use qualitative colormap for small numbers
        return plt.cm.tab10(np.linspace(0, 1, n_motifs))
    else:
        # Use Set3 for more colors
        return plt.cm.Set3(np.linspace(0, 1, n_motifs))

2. Sequential Colormaps (for ordered data)

def create_sequential_colormap(n_motifs: int) -> np.ndarray:
    """Use sequential colors if motifs have an inherent order."""
    return plt.cm.viridis(np.linspace(0, 1, n_motifs))

3. Custom Color Schemes

def create_musical_colormap(n_motifs: int) -> np.ndarray:
    """Create a custom color scheme for musical visualization."""
    # Define a palette that works well for music
    musical_colors = [
        '#1f77b4',  # Blue
        '#ff7f0e',  # Orange
        '#2ca02c',  # Green
        '#d62728',  # Red
        '#9467bd',  # Purple
        '#8c564b',  # Brown
        '#e377c2',  # Pink
        '#7f7f7f',  # Gray
        '#bcbd22',  # Olive
        '#17becf',  # Cyan
    ]
    
    if n_motifs <= len(musical_colors):
        return np.array([plt.colors.to_rgba(c) for c in musical_colors[:n_motifs]])
    else:
        # Interpolate for more motifs
        return plt.cm.tab20(np.linspace(0, 1, n_motifs))

Accessibility Considerations

When choosing colors, consider:

  • Colorblind-friendly palettes: Use tools like ColorBrewer or matplotlib’s built-in accessible colormaps
  • Contrast: Ensure colors are distinguishable even in grayscale
  • Cultural associations: Be aware that colors may have different meanings in different contexts
def create_accessible_colormap(n_motifs: int) -> np.ndarray:
    """Create a colorblind-friendly colormap."""
    # Use ColorBrewer qualitative palette
    accessible_colors = [
        '#a6cee3', '#1f78b4', '#b2df8a', '#33a02c',
        '#fb9a99', '#e31a1c', '#fdbf6f', '#ff7f00',
        '#cab2d6', '#6a3d9a', '#ffff99', '#b15928'
    ]
    
    if n_motifs <= len(accessible_colors):
        return np.array([plt.colors.to_rgba(c) for c in accessible_colors[:n_motifs]])
    else:
        # Fall back to Set3 which is generally accessible
        return plt.cm.Set3(np.linspace(0, 1, n_motifs))

Advanced Visualization Techniques

1. Heatmaps for Multiple Motifs

Instead of separate subplots, you can visualize all motifs in a single heatmap:

def plot_motif_heatmap(
    score: music21.stream.Score,
    motif_counts: dict,
    text_expressions: dict = None,
    output_path: Optional[str] = None,
) -> None:
    """Create a heatmap showing all motifs across measures."""
    import seaborn as sns
    
    measures = score.parts[0].getElementsByClass('Measure')
    total_measures = len(measures)
    unique_motifs = list(motif_counts.keys())
    
    # Create matrix: rows = motifs, columns = measures
    matrix = np.zeros((len(unique_motifs), total_measures))
    
    for i, motif in enumerate(unique_motifs):
        counts = motif_counts[motif]
        for j in range(total_measures):
            matrix[i, j] = counts.get(j + 1, 0)
    
    # Create heatmap
    plt.figure(figsize=(16, max(6, len(unique_motifs) * 0.8)))
    sns.heatmap(matrix,
                xticklabels=range(1, total_measures + 1, 20),
                yticklabels=[str(m) for m in unique_motifs],
                cmap='YlOrRd',
                cbar_kws={'label': 'Occurrences'})
    
    plt.xlabel('Measure Number', fontsize=12)
    plt.ylabel('Motif', fontsize=12)
    plt.title('Motif Distribution Heatmap', fontsize=14, fontweight='bold')
    plt.tight_layout()
    
    if output_path:
        plt.savefig(output_path, dpi=300, bbox_inches='tight')
    plt.close()

2. Interactive Visualizations

For web-based exploration, consider interactive libraries:

# Example using plotly (requires: pip install plotly)
def plot_motif_timeline_interactive(
    score: music21.stream.Score,
    motif_counts: dict,
    output_path: Optional[str] = None,
) -> None:
    """Create an interactive timeline using plotly."""
    import plotly.graph_objects as go
    from plotly.subplots import make_subplots
    
    measures = score.parts[0].getElementsByClass('Measure')
    total_measures = len(measures)
    
    fig = make_subplots(
        rows=len(motif_counts),
        cols=1,
        shared_xaxes=True,
        vertical_spacing=0.05
    )
    
    for i, (motif, counts) in enumerate(motif_counts.items(), 1):
        measure_counts = [counts.get(m, 0) for m in range(1, total_measures + 1)]
        fig.add_trace(
            go.Bar(x=list(range(1, total_measures + 1)),
                   y=measure_counts,
                   name=str(motif)),
            row=i, col=1
        )
    
    fig.update_layout(height=300 * len(motif_counts),
                     title_text="Interactive Motif Timeline")
    
    if output_path:
        fig.write_html(output_path)
    else:
        fig.show()

Real-World Examples from Research

Let’s look at actual visualizations from the Arab-Andalusian motif analysis:

Example 1: Btaihi Iraq Ajam

Btaihi Iraq Ajam Motif Distribution

This visualization shows the distribution of five melodic centos across 979 measures. Key observations:

  • Cento (B, A, G) appears most frequently (91 times)
  • Cento (F, E, D) is nearly as common (88 times)
  • Motifs are distributed throughout, with some clustering around section markers
  • The longer cento (G, F#, E, D) appears less frequently (16 times), as expected

Example 2: Bassit Iraq Ajam

Bassit Iraq Ajam Motif Distribution

This score shows a different pattern:

  • Similar centos but with different frequencies
  • Different structural markers (visible as vertical lines)
  • Variations in how motifs cluster around sections

Example 3: Multiple Quddam Versions

Quddam Iraq Ajam

Comparing different versions of the same piece (Quddam) reveals:

  • Consistency: Same centos appear across versions
  • Variation: Different frequencies and distributions
  • Structure: How the same musical material is organized differently

Best Practices for Musical Visualization

1. Choose Appropriate Scales

# For timeline plots, use measure numbers, not time
# This aligns with how musicians think about structure
ax.set_xlabel('Measure Number')  # ✅ Good
ax.set_xlabel('Time (seconds)')   # ❌ Less useful for structural analysis

2. Include Contextual Markers

# Add section markers, key changes, tempo markings
for section, measure in text_expressions.items():
    ax.axvline(x=measure, color='black', linestyle='--', 
              label=section, alpha=0.5)

3. Make Plots Reproducible

# Set random seed for any stochastic elements
np.random.seed(42)

# Use consistent styling
plt.style.use('seaborn-v0_8-whitegrid')  # or your preferred style

4. Export at High Resolution

# For publications, use high DPI
plt.savefig(output_path, dpi=300, bbox_inches='tight')

5. Consider Your Audience

  • Musicians: Focus on measure numbers, section labels, musical terminology
  • Researchers: Include statistical measures, confidence intervals, error bars
  • General audience: Simplify, add explanations, use intuitive color schemes

Common Pitfalls and Solutions

Problem: Overlapping Elements

Solution: Use transparency and adjust spacing

ax.bar(x, y, alpha=0.7, width=0.8)  # Transparency helps
plt.tight_layout()  # Adjust spacing automatically

Problem: Too Many Motifs

Solution: Group or filter

# Show only top N motifs
top_motifs = sorted(motif_counts.items(), 
                   key=lambda x: sum(x[1].values()), 
                   reverse=True)[:5]

Problem: Inconsistent Colors

Solution: Create a color mapping dictionary

motif_colors = {motif: color for motif, color in 
                zip(unique_motifs, create_colormap(len(unique_motifs)))}
# Use motif_colors[motif] consistently across all plots

Integration with Analysis Pipeline

Here’s how visualization fits into a complete analysis workflow:

# 1. Load and analyze
score = load_score("Btaihi_Iraq_Ajam.mxl")
melody = extract_melody(score)
motifs = identify_motifs(melody, min_length=3, max_length=5)

# 2. Process and count
motif_counts = identify_unique_motifs(melody, normalize_octave=True)
text_expressions = extract_text_expressions(score)

# 3. Visualize
plot_motif_distribution(motifs, 
                       title="Motif Length Distribution",
                       output_path="plots/distribution.png")

plot_motif_timeline(score, motif_counts, text_expressions,
                    filename="Btaihi_Iraq_Ajam",
                    output_path="plots/timeline.png")

Conclusion

Effective visualization is essential for computational musicology research. The functions we’ve explored provide:

  • Distribution analysis: Understanding motif characteristics
  • Temporal visualization: Seeing patterns across time/structure
  • Color mapping: Distinguishing multiple motifs clearly

These visualizations transform raw motif data into insights about musical structure, style, and development. Combined with the motif identification methods from the previous post, you have a complete toolkit for computational motif analysis.

For complete implementations and interactive notebooks, see the Arab-Andalusian Motif Development Analysis and the GitHub repository.

Further Reading


This post is part of a series on computational musicology and music information retrieval. Read the previous post on motif identification and explore the interactive notebook for hands-on examples.

Share this post

Related Posts