"""
Module: visualizations.analysis_results_graphs.py
=================================================
This module, part of the `visualizations` package, provides a suite of methods for generating visualizations from
the analysis results of musical scores.
The purpose of these functions is to create comprehensive and understandable visualizations of the musical structures
and characteristics that emerge from the analysis of musical scores.
"""
import matplotlib.pyplot as plt
import seaborn as sns
from iMaT.src.cli.menu_constructors import display_menu_print_textblock
from iMaT.src.utils.error_handling import handle_error
[docs]def map_analysis_functions_to_display_functions(analysis_func: callable):
"""
Returns the corresponding results display function for a provided analysis function.
Parameters
----------
analysis_func : callable
The analysis function for which the corresponding results display function is to be returned.
Returns
-------
function
The corresponding results display function.
Raises
------
ValueError
If the provided analysis function is not mapped to a results display function.
"""
try:
function_mapping = {
'analysis_number_of_rests_per_rest_duration': display_analysis_number_of_rests_per_rest_duration,
'analysis_number_of_intervals_per_type': display_analysis_number_of_intervals_per_type,
'analysis_number_of_intervals_per_type_with_direction': display_analysis_number_of_intervals_per_type_with_direction,
'analysis_number_of_sound_events_per_pitch': display_analysis_number_of_sound_events_per_pitch,
'analysis_number_of_sound_events_per_pitch_class': display_analysis_number_of_sound_events_per_pitch_class,
'analysis_number_of_sound_events_per_tone_duration': display_analysis_number_of_sound_events_per_tone_duration,
'analysis_number_of_sound_events_per_metrical_position': display_analysis_number_of_sound_events_per_metrical_position,
'analysis_number_of_pitches_per_tone_duration': display_analysis_number_of_pitches_per_tone_duration,
'analysis_number_of_pitches_per_metrical_position': display_analysis_number_of_pitches_per_metrical_position,
'analysis_number_of_pitches_per_offset': display_analysis_number_of_pitches_per_offset,
'analysis_number_of_pitch_classes_per_tone_duration': display_analysis_number_of_pitch_classes_per_tone_duration,
'analysis_number_of_pitch_classes_per_metrical_position': display_analysis_number_of_pitch_classes_per_metrical_position,
'analysis_number_of_pitch_classes_per_offset': display_analysis_number_of_pitch_classes_per_offset,
'analysis_advanced_compare_pitches_and_pitch_classes_per_duration': display_advanced_pitch_class_duration_analysis
}
function_name = analysis_func.__name__
display_results_func = function_mapping[function_name]
return display_results_func
except KeyError:
text_dict = {
"menu_displayed_text": [
"-- Attention --",
f"\nFor the chosen analysis function is no display function available. Please see below for possible "
f"reasons:",
"<To continue, please press Enter>",
["", "Error Messages"]
],
"menu_entries_text": [
["Function in Question:", f"'{function_name}'."],
["Possible Reasons", "Not all analysis functions necessarily have a corresponding display function. "
"The display functions are intended to provide visual representations of the "
"analysis results, but for some analyses, that generate only very limited data, "
"the numerical or tabular results shown in the previous window may be sufficient."],
["Possible Solution", "Check the function mapping or the available display functions in the code. "
"It's also possible that a display function for the given analysis function has "
"not been implemented yet. Feel free to contribute your own display function!"],
]
}
display_menu_print_textblock(text_dict)
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_rests_per_rest_duration(analysis_results, identifier):
"""
Display a bar diagram of the number of rests per rest duration.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Create the plot
plt.figure(figsize=(10, 8))
sns.barplot(x='Rest Duration', y='Count', data=analysis_results, color='b')
# Rotate x-axis labels for better readability
plt.xticks(rotation=90)
# Add labels and title
plt.title(f'Number of Rests per Rest Duration for {identifier}')
plt.ylabel('Count')
plt.xlabel('Rest Duration')
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_intervals_per_type(analysis_results, identifier):
"""
Display a bar diagram of the number of interval types.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Create the plot
plt.figure(figsize=(10, 8))
sns.barplot(x='Interval Type', y='Count', data=analysis_results, color='b')
# Rotate x-axis labels for better readability
plt.xticks(rotation=90)
# Add labels and title
plt.title(f'Number of Interval Types for {identifier}')
plt.ylabel('Count')
plt.xlabel('Interval Type')
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_intervals_per_type_with_direction(analysis_results, identifier):
"""
Display a bar diagram of the number of ascending and descending interval types.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Create the plot
plt.figure(figsize=(10, 8))
sns.barplot(x='Interval', y='Count', data=analysis_results, color='b')
# Rotate x-axis labels for better readability
plt.xticks(rotation=90)
# Add labels and title
plt.title(f'Number of Ascending and Descending Interval Types for {identifier}')
plt.ylabel('Count')
plt.xlabel('Interval')
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_sound_events_per_pitch(analysis_results, identifier):
"""
Display a bar diagram of the number of sound events per pitch.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Create the plot
plt.figure(figsize=(10, 8))
sns.barplot(x='Pitch Name', y='Count', data=analysis_results, color='b')
# Rotate x-axis labels for better readability
plt.xticks(rotation=90)
# Add labels and title
plt.title(f'Number of Sound Events Per Pitch for {identifier}')
plt.ylabel('Count')
plt.xlabel('Pitch Name')
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_sound_events_per_pitch_class(analysis_results, identifier):
"""
Display a bar diagram of the number of sound events per pitch class.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Create the plot
plt.figure(figsize=(10, 8))
sns.barplot(x='Pitch Class Name', y='Count', data=analysis_results, color='b')
# Add labels and title
plt.title(f'Number of Sound Events Per Pitch Class for {identifier}')
plt.ylabel('Count')
plt.xlabel('Pitch Class Name')
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_sound_events_per_tone_duration(analysis_results, identifier):
"""
Display a bar diagram of the number of sound events per tone duration.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Create the plot
plt.figure(figsize=(10, 8))
sns.barplot(x='Tone Duration', y='Count', data=analysis_results, color='b')
# Add labels and title
plt.title(f'Number of Sound Events Per Tone Duration for {identifier}')
plt.ylabel('Count')
plt.xlabel('Tone Duration')
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_sound_events_per_metrical_position(analysis_results, identifier):
"""
Display a bar diagram of the number of sound events per metrical position.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Create the plot
plt.figure(figsize=(10, 8))
sns.barplot(x='Metrical Position', y='Count', data=analysis_results, color='b')
# Add labels and title
plt.title(f'Number of Sound Events Per Metrical Position for {identifier}')
plt.ylabel('Count')
plt.xlabel('Metrical Position')
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_pitches_per_tone_duration(analysis_results, identifier):
"""
Display a matrix plot of the number of pitches per duration.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Pivot the DataFrame to create a matrix format for the heatmap
df_pivot = analysis_results.pivot(index='Duration', columns='MIDI Pitches', values='Count')
# Create the plot
plt.figure(figsize=(10, 8))
sns.heatmap(df_pivot, cmap="YlGnBu", linewidths=.5)
# Add labels and title
plt.title(f'Pitches Over Durations for {identifier}')
plt.ylabel('Duration')
plt.xlabel('MIDI Pitches')
# Invert the y-axis
plt.gca().invert_yaxis()
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_pitches_per_metrical_position(analysis_results, identifier):
"""
Display a matrix plot of the number of pitches per metrical position.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Pivot the DataFrame to create a matrix format for the heatmap
df_pivot = analysis_results.pivot(index='Metrical Position', columns='MIDI Pitches', values='Count')
# Create the plot
plt.figure(figsize=(10, 8))
sns.heatmap(df_pivot, cmap="YlGnBu", linewidths=.5)
# Add labels and title
plt.title(f'Pitches Over Metrical Positions for {identifier}')
plt.ylabel('Metrical Position')
plt.xlabel('MIDI Pitches')
# Invert the y-axis
plt.gca().invert_yaxis()
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_pitches_per_offset(analysis_results, identifier):
"""
Display a matrix plot of the number of pitches per offset.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Pivot the DataFrame to create a matrix format for the heatmap
df_pivot = analysis_results.pivot(index='Offset', columns='MIDI Pitches', values='Count')
# Create the plot
plt.figure(figsize=(10, 8))
sns.heatmap(df_pivot, cmap="YlGnBu", linewidths=.5)
# Add labels and title
plt.title(f'Pitches Over Offsets for {identifier}')
plt.ylabel('Offset')
plt.xlabel('MIDI Pitches')
# Invert the y-axis
plt.gca().invert_yaxis()
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_pitch_classes_per_tone_duration(analysis_results, identifier):
"""
Display a matrix plot of the number of pitch classes per duration.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Pivot the DataFrame to create a matrix format for the heatmap
df_pivot = analysis_results.pivot(index='Duration', columns='Pitch Class', values='Count')
# Create the plot
plt.figure(figsize=(10, 8))
sns.heatmap(df_pivot, cmap="YlGnBu", linewidths=.5)
# Add labels and title
plt.title(f'Pitch Classes Over Duration for {identifier}')
plt.ylabel('Duration (quarter length)')
plt.xlabel('Pitch Class')
# Invert the y-axis
plt.gca().invert_yaxis()
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_pitch_classes_per_metrical_position(analysis_results, identifier):
"""
Display a matrix plot of the number of pitch classes per metrical position.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Pivot the DataFrame to create a matrix format for the heatmap
df_pivot = analysis_results.pivot(index='Metrical Position', columns='Pitch Class', values='Count')
# Create the plot
plt.figure(figsize=(10, 8))
sns.heatmap(df_pivot, cmap="YlGnBu", linewidths=.5)
# Add labels and title
plt.title(f'Pitch Classes Over Metrical Positions for {identifier}')
plt.ylabel('Metrical Position')
plt.xlabel('Pitch Class')
# Invert the y-axis
plt.gca().invert_yaxis()
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_analysis_number_of_pitch_classes_per_offset(analysis_results, identifier):
"""
Display a matrix plot of the number of pitch classes per offset.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Pivot the DataFrame to create a matrix format for the heatmap
df_pivot = analysis_results.pivot(index='Offset', columns='Pitch Class', values='Count')
# Create the plot
plt.figure(figsize=(10, 8))
sns.heatmap(df_pivot, cmap="YlGnBu", linewidths=.5)
# Add labels and title
plt.title(f'Pitch Classes Over Offsets for {identifier}')
plt.ylabel('Offset')
plt.xlabel('Pitch Class')
# Invert the y-axis
plt.gca().invert_yaxis()
# Display the plot
plt.show()
except Exception as e:
handle_error(e)
[docs]def display_advanced_pitch_class_duration_analysis(analysis_results, identifier):
"""
Display a series of bar plots for the advanced pitch and pitch class duration analysis.
Parameters
----------
analysis_results : pd.DataFrame
The DataFrame with the results of the analysis.
identifier : str
The identifier string to be added to the plot title.
"""
try:
# Generate subplots
fig, axs = plt.subplots(3, 1, figsize=(10, 12), sharex=True)
# Plot data
sns.barplot(x='Duration', y='Pitches Count', data=analysis_results, ax=axs[0], color='b')
sns.barplot(x='Duration', y='Pitch Classes Count', data=analysis_results, ax=axs[1], color='g')
ratio_plot = sns.barplot(x='Duration', y='Pitch to Pitch Class Ratio', data=analysis_results, ax=axs[2],
color='r')
# Apply transparency to bars below 1
for patch in ratio_plot.patches:
current_value = patch.get_height()
if current_value < 1:
patch.set_alpha(0.5)
# Set labels and titles
axs[0].set_ylabel('Pitches Count')
axs[0].set_title(f'Number of Pitches per Duration for {identifier}')
axs[1].set_ylabel('Pitch Classes Count')
axs[1].set_title(f'Number of Pitch Classes per Duration for {identifier}')
axs[2].set_ylabel('Pitch to Pitch Class Ratio')
axs[2].set_title(f'Pitch to Pitch Class Ratio per Duration for {identifier}')
# Set y-axis limit for the ratio plot to start at 0.75
axs[2].set_ylim(0.75, None)
# Add a horizontal line at y=1
axs[2].axhline(1, color='white', linestyle='--')
# Set x-axis labels to show duration values and rotate for better readability
for ax in axs:
ax.set_xticklabels(analysis_results['Duration'], rotation=90)
# Display the plots
plt.tight_layout()
plt.show()
except Exception as e:
handle_error(e)