Source code for music21.graph.utilities

# ------------------------------------------------------------------------------
# Name:         graph/utilities.py
# Purpose:      Functions for finding external modules, manipulating colors, etc.
#
# Authors:      Christopher Ariza
#               Michael Scott Asato Cuthbert
#
# Copyright:    Copyright © 2009-2023 Michael Scott Asato Cuthbert
# License:      BSD, see license.txt
# ------------------------------------------------------------------------------
'''
Functions for finding external modules, converting colors to Matplotlib colors, etc.
'''
from __future__ import annotations

from collections import namedtuple
import typing as t
import unittest
import webcolors  # type: ignore  # no typing in module

# TODO: Move _missingImport to environment or common so this is unnecessary.
from music21.base import _missingImport
from music21 import common
from music21 import environment
from music21 import exceptions21
from music21 import pitch

environLocal = environment.Environment('graph.utilities')


ExtendedModules = namedtuple('ExtendedModules',
                             ['matplotlib', 'Axes3D', 'collections', 'patches', 'plt'])


[docs] def getExtendedModules(): ''' this is done inside a function, so that the slow import of matplotlib is not done in ``from music21 import *`` unless it's actually needed. Returns a namedtuple: (matplotlib, Axes3D, collections, patches, plt) ''' if 'matplotlib' in _missingImport: raise GraphException( 'could not find matplotlib, graphing is not allowed') # pragma: no cover import matplotlib # type: ignore # eventually: https://pypi.org/project/data-science-types/ # backend can be configured from config file, matplotlibrc, # but an early test broke all processing # matplotlib.use('WXAgg') try: from mpl_toolkits.mplot3d import Axes3D # type: ignore except ImportError: # pragma: no cover Axes3D = None environLocal.warn( 'mpl_toolkits.mplot3d.Axes3D could not be imported -- likely cause is an ' + 'old version of six.py (< 1.9.0) on your system somewhere' ) from matplotlib import collections from matplotlib import patches # from matplotlib.colors import colorConverter import matplotlib.pyplot as plt # type: ignore return ExtendedModules(matplotlib, Axes3D, collections, patches, plt)
# ------------------------------------------------------------------------------ class GraphException(exceptions21.Music21Exception): pass class PlotStreamException(exceptions21.Music21Exception): pass
[docs] def accidentalLabelToUnicode(label): ''' Changes a label possibly containing a modifier such as "-" or "#" into a unicode string. >>> graph.utilities.accidentalLabelToUnicode('B-4') 'B♭4' Since matplotlib's default fonts do not support double sharps or double flats, etc. these are converted as best we can. >>> graph.utilities.accidentalLabelToUnicode('B--4') 'B♭♭4' ''' if not isinstance(label, str): return label for modifier, unicodeAcc in pitch.unicodeFromModifier.items(): if modifier != '' and modifier in label and modifier in ('-', '#'): # ideally eventually matplotlib will do the other accidentals label = label.replace(modifier, unicodeAcc) break return label
[docs] def getColor(color): ''' Convert any specification of a color to a hexadecimal color used by matplotlib. >>> graph.utilities.getColor('red') '#ff0000' >>> graph.utilities.getColor('r') '#ff0000' >>> graph.utilities.getColor('Steel Blue') '#4682b4' >>> graph.utilities.getColor('#f50') '#ff5500' >>> graph.utilities.getColor([0.5, 0.5, 0.5]) '#808080' >>> graph.utilities.getColor(0.8) '#cccccc' >>> graph.utilities.getColor([0.8]) '#cccccc' >>> graph.utilities.getColor([255, 255, 255]) '#ffffff' Invalid colors raise GraphExceptions: >>> graph.utilities.getColor('l') Traceback (most recent call last): music21.graph.utilities.GraphException: invalid color abbreviation: l >>> graph.utilities.getColor('chalkywhitebutsortofgreenish') Traceback (most recent call last): music21.graph.utilities.GraphException: invalid color name: chalkywhitebutsortofgreenish >>> graph.utilities.getColor(True) Traceback (most recent call last): music21.graph.utilities.GraphException: invalid color specification: True ''' # expand a single value to three if common.isNum(color): color = [color, color, color] if isinstance(color, str): if color[0] == '#': # assume is hex # this will expand three-value codes, and check for badly # formed codes return webcolors.normalize_hex(color) color = color.lower().replace(' ', '') # check for one character matplotlib colors if len(color) == 1: colorMap = {'b': 'blue', 'g': 'green', 'r': 'red', 'c': 'cyan', 'm': 'magenta', 'y': 'yellow', 'k': 'black', 'w': 'white'} try: color = colorMap[color] except KeyError: raise GraphException(f'invalid color abbreviation: {color}') try: return webcolors.name_to_hex(color) except ValueError: # no color match raise GraphException(f'invalid color name: {color}') elif common.isListLike(color): percent = False for sub in color: if sub < 1: percent = True break if percent: if len(color) == 1: color = [color[0], color[0], color[0]] # convert to 0 100% values as strings with % symbol colorStrList = t.cast(tuple[str, str, str], tuple(str(x * 100) + '%' for x in color)) return webcolors.rgb_percent_to_hex(colorStrList) else: # assume integers return webcolors.rgb_to_hex(tuple(color)) raise GraphException(f'invalid color specification: {color}')
class Test(unittest.TestCase): def testColors(self): self.assertEqual(getColor([0.5, 0.5, 0.5]), '#808080') self.assertEqual(getColor(0.5), '#808080') self.assertEqual(getColor(255), '#ffffff') self.assertEqual(getColor('Steel Blue'), '#4682b4') if __name__ == '__main__': # sys.arg test options will be used in mainTest() import music21 music21.mainTest(Test) # TestExternal, 'noDocTest') #, runTest='testGetPlotsToMakeA')